Merge remote-tracking branch 'upstream/develop' into inverted

This commit is contained in:
Michael 2020-01-12 18:18:45 +00:00
commit 6ccf038053
55 changed files with 1834 additions and 709 deletions

View File

@ -1,6 +1,6 @@
-- ------------------------------------------
-- Friendica 2020.03-dev (Dalmatian Bellflower)
-- DB_UPDATE_VERSION 1329
-- DB_UPDATE_VERSION 1330
-- ------------------------------------------

View File

@ -17,22 +17,24 @@ namespace Friendica\Model\Storage;
```php
interface IStorage
{
public static function get($ref);
public static function put($data, $ref = "");
public static function delete($ref);
public static function getOptions();
public static function saveOptions($data);
public function get(string $reference);
public function put(string $data, string $reference = '');
public function delete(string $reference);
public function getOptions();
public function saveOptions(array $data);
public function __toString();
public static function getName();
}
```
- `get($ref)` returns data pointed by `$ref`
- `put($data, $ref)` saves data in `$data` to position `$ref`, or a new position if `$ref` is empty.
- `delete($ref)` delete data pointed by `$ref`
- `get(string $reference)` returns data pointed by `$reference`
- `put(string $data, string $reference)` saves data in `$data` to position `$reference`, or a new position if `$reference` is empty.
- `delete(string $reference)` delete data pointed by `$reference`
Each storage backend can have options the admin can set in admin page.
- `getOptions()` returns an array with details about each option to build the interface.
- `saveOptions($data)` get `$data` from admin page, validate it and save it.
- `saveOptions(array $data)` get `$data` from admin page, validate it and save it.
The array returned by `getOptions()` is defined as:
@ -84,11 +86,38 @@ See doxygen documentation of `IStorage` interface for details about each method.
Each backend must be registered in the system when the plugin is installed, to be aviable.
`Friendica\Core\StorageManager::register($name, $class)` is used to register the backend class.
The `$name` must be univocal and will be shown to admin.
`DI::facStorage()->register(string $class)` is used to register the backend class.
When the plugin is uninstalled, registered backends must be unregistered using
`Friendica\Core\StorageManager::unregister($class)`.
`DI::facStorage()->unregister(string $class)`.
You have to register a new hook in your addon, listening on `storage_instance(App $a, array $data)`.
In case `$data['name']` is your storage class name, you have to instance a new instance of your `Friendica\Model\Storage\IStorage` class.
Set the instance of your class as `$data['storage']` to pass it back to the backend.
This is necessary because it isn't always clear, if you need further construction arguments.
## Adding tests
**Currently testing is limited to core Friendica only, this shows theoretically how tests should work in the future**
Each new Storage class should be added to the test-environment at [Storage Tests](https://github.com/friendica/friendica/tree/develop/tests/src/Model/Storage/).
Add a new test class which's naming convention is `StorageClassTest`, which extend the `StorageTest` in the same directory.
Override the two necessary instances:
```php
use Friendica\Model\Storage\IStorage;
abstract class StorageTest
{
// returns an instance of your newly created storage class
abstract protected function getInstance();
// Assertion for the option array you return for your new StorageClass
abstract protected function assertOption(IStorage $storage);
}
```
## Example
@ -112,60 +141,91 @@ use Friendica\Core\L10n;
class SampleStorageBackend implements IStorage
{
public static function get($ref)
const NAME = 'Sample Storage';
/** @var Config\IConfiguration */
private $config;
/** @var L10n\L10n */
private $l10n;
/**
* SampleStorageBackend constructor.
* @param Config\IConfiguration $config The configuration of Friendica
*
* You can add here every dynamic class as dependency you like and add them to a private field
* Friendica automatically creates these classes and passes them as argument to the constructor
*/
public function __construct(Config\IConfiguration $config, L10n\L10n $l10n)
{
// we return alwais the same image data. Which file we load is defined by
$this->config = $config;
$this->l10n = $l10n;
}
public function get(string $reference)
{
// we return always the same image data. Which file we load is defined by
// a config key
$filename = Config::get("storage", "samplestorage", "sample.jpg");
$filename = $this->config->get('storage', 'samplestorage', 'sample.jpg');
return file_get_contents($filename);
}
public static function put($data, $ref = "")
public function put(string $data, string $reference = '')
{
if ($ref === "") {
$ref = "sample";
if ($reference === '') {
$reference = 'sample';
}
// we don't save $data !
return $ref;
return $reference;
}
public static function delete($ref)
public function delete(string $reference)
{
// we pretend to delete the data
return true;
}
public static function getOptions()
public function getOptions()
{
$filename = Config::get("storage", "samplestorage", "sample.jpg");
$filename = $this->config->get('storage', 'samplestorage', 'sample.jpg');
return [
"filename" => [
"input", // will use a simple text input
L10n::t("The file to return"), // the label
'filename' => [
'input', // will use a simple text input
$this->l10n->t('The file to return'), // the label
$filename, // the current value
L10n::t("Enter the path to a file"), // the help text
// no extra data for "input" type..
$this->l10n->t('Enter the path to a file'), // the help text
// no extra data for 'input' type..
],
];
}
public static function saveOptions($data)
public function saveOptions(array $data)
{
// the keys in $data are the same keys we defined in getOptions()
$newfilename = trim($data["filename"]);
$newfilename = trim($data['filename']);
// this function should always validate the data.
// in this example we check if file exists
if (!file_exists($newfilename)) {
// in case of error we return an array with
// ["optionname" => "error message"]
return ["filename" => "The file doesn't exists"];
// ['optionname' => 'error message']
return ['filename' => 'The file doesn\'t exists'];
}
Config::set("storage", "samplestorage", $newfilename);
$this->config->set('storage', 'samplestorage', $newfilename);
// no errors, return empty array
return [];
}
public function __toString()
{
return self::NAME;
}
public static function getName()
{
return self::NAME;
}
}
```
@ -182,23 +242,59 @@ The file is `addon/samplestorage/samplestorage.php`
* Author: Alice <https://alice.social/~alice>
*/
use Friendica\Core\StorageManager;
use Friendica\Addon\samplestorage\SampleStorageBackend;
use Friendica\DI;
function samplestorage_install()
{
// on addon install, we register our class with name "Sample Storage".
// note: we use `::class` property, which returns full class name as string
// this save us the problem of correctly escape backslashes in class name
StorageManager::register("Sample Storage", SampleStorageBackend::class);
DI::storageManager()->register(SampleStorageBackend::class);
}
function samplestorage_unistall()
{
// when the plugin is uninstalled, we unregister the backend.
StorageManager::unregister("Sample Storage");
DI::storageManager()->unregister(SampleStorageBackend::class);
}
function samplestorage_storage_instance(\Friendica\App $a, array $data)
{
if ($data['name'] === SampleStorageBackend::getName()) {
// instance a new sample storage instance and pass it back to the core for usage
$data['storage'] = new SampleStorageBackend(DI::config(), DI::l10n(), DI::cache());
}
}
```
**Theoretically - until tests for Addons are enabled too - create a test class with the name `addon/tests/SampleStorageTest.php`:
```php
use Friendica\Model\Storage\IStorage;
use Friendica\Test\src\Model\Storage\StorageTest;
class SampleStorageTest extends StorageTest
{
// returns an instance of your newly created storage class
protected function getInstance()
{
// create a new SampleStorageBackend instance with all it's dependencies
// Have a look at DatabaseStorageTest or FilesystemStorageTest for further insights
return new SampleStorageBackend();
}
// Assertion for the option array you return for your new StorageClass
protected function assertOption(IStorage $storage)
{
$this->assertEquals([
'filename' => [
'input',
'The file to return',
'sample.jpg',
'Enter the path to a file'
],
], $storage->getOptions());
}
}
```

View File

@ -706,6 +706,14 @@ Here is a complete list of all hook callbacks with file locations (as of 24-Sep-
Hook::callAll('page_header', DI::page()['nav']);
Hook::callAll('nav_info', $nav);
### src/Core/Authentication.php
Hook::callAll('logged_in', $a->user);
### src/Core/StorageManager
Hook::callAll('storage_instance', $data);
### src/Worker/Directory.php
Hook::callAll('globaldir_update', $arr);

View File

@ -425,6 +425,10 @@ Eine komplette Liste aller Hook-Callbacks mit den zugehörigen Dateien (am 01-Ap
Hook::callAll('logged_in', $a->user);
### src/Core/StorageManager
Hook::callAll('storage_instance', $data);
### src/Worker/Directory.php
Hook::callAll('globaldir_update', $arr);

View File

@ -427,8 +427,16 @@ function events_content(App $a)
// Passed parameters overrides anything found in the DB
if (in_array($mode, ['edit', 'new', 'copy'])) {
$share_checked = '';
$share_disabled = '';
if (empty($orig_event)) {
$orig_event = User::getById(local_user(), ['allow_cid', 'allow_gid', 'deny_cid', 'deny_gid']);;
} elseif ($orig_event['allow_cid'] !== '<' . local_user() . '>'
|| $orig_event['allow_gid']
|| $orig_event['deny_cid']
|| $orig_event['deny_gid']) {
$share_checked = ' checked="checked" ';
}
// In case of an error the browser is redirected back here, with these parameters filled in with the previous values
@ -450,20 +458,8 @@ function events_content(App $a)
$cid = !empty($orig_event) ? $orig_event['cid'] : 0;
$uri = !empty($orig_event) ? $orig_event['uri'] : '';
$sh_disabled = '';
$sh_checked = '';
if (!empty($orig_event)
&& ($orig_event['allow_cid'] !== '<' . local_user() . '>'
|| $orig_event['allow_gid']
|| $orig_event['deny_cid']
|| $orig_event['deny_gid']))
{
$sh_checked = ' checked="checked" ';
}
if ($cid || $mode === 'edit') {
$sh_disabled = 'disabled="disabled"';
$share_disabled = 'disabled="disabled"';
}
$sdt = !empty($orig_event) ? $orig_event['start'] : 'now';
@ -547,8 +543,8 @@ function events_content(App $a)
'$t_orig' => $t_orig,
'$summary' => ['summary', L10n::t('Title:'), $t_orig, '', '*'],
'$sh_text' => L10n::t('Share this event'),
'$share' => ['share', L10n::t('Share this event'), $sh_checked, '', $sh_disabled],
'$sh_checked' => $sh_checked,
'$share' => ['share', L10n::t('Share this event'), $share_checked, '', $share_disabled],
'$sh_checked' => $share_checked,
'$nofinish' => ['nofinish', L10n::t('Finish date/time is not known or not relevant'), $n_checked],
'$adjust' => ['adjust', L10n::t('Adjust for viewer timezone'), $a_checked],
'$preview' => L10n::t('Preview'),

View File

@ -6,7 +6,7 @@
use Friendica\App;
use Friendica\Content\ForumManager;
use Friendica\Content\Text\BBCode;
use Friendica\Core\Cache;
use Friendica\Core\Cache\Cache;
use Friendica\Core\Config;
use Friendica\Core\Hook;
use Friendica\Core\L10n;
@ -197,7 +197,7 @@ function ping_init(App $a)
}
$cachekey = "ping_init:".local_user();
$ev = Cache::get($cachekey);
$ev = DI::cache()->get($cachekey);
if (is_null($ev)) {
$ev = q(
"SELECT type, start, adjust FROM `event`
@ -208,7 +208,7 @@ function ping_init(App $a)
DBA::escape(DateTimeFormat::utcNow())
);
if (DBA::isResult($ev)) {
Cache::set($cachekey, $ev, Cache::HOUR);
DI::cache()->set($cachekey, $ev, Cache::HOUR);
}
}

View File

@ -6,13 +6,12 @@
use Friendica\App;
use Friendica\Content\Text\BBCode;
use Friendica\Core\Cache;
use Friendica\Core\Config;
use Friendica\Core\Logger;
use Friendica\Core\Protocol;
use Friendica\Core\Renderer;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Protocol\PortableContact;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Strings;
@ -255,10 +254,10 @@ function poco_init(App $a) {
if (isset($contact['account-type'])) {
$contact['contact-type'] = $contact['account-type'];
}
$about = Cache::get("about:" . $contact['updated'] . ":" . $contact['nurl']);
$about = DI::cache()->get("about:" . $contact['updated'] . ":" . $contact['nurl']);
if (is_null($about)) {
$about = BBCode::convert($contact['about'], false);
Cache::set("about:" . $contact['updated'] . ":" . $contact['nurl'], $about);
DI::cache()->set("about:" . $contact['updated'] . ":" . $contact['nurl'], $about);
}
// Non connected persons can only see the keywords of a Diaspora account

View File

@ -13,6 +13,19 @@ class Storage extends \Asika\SimpleConsole\Console
{
protected $helpOptions = ['h', 'help', '?'];
/** @var StorageManager */
private $storageManager;
/**
* @param StorageManager $storageManager
*/
public function __construct(StorageManager $storageManager, array $argv = [])
{
parent::__construct($argv);
$this->storageManager = $storageManager;
}
protected function getHelp()
{
$help = <<<HELP
@ -69,11 +82,11 @@ HELP;
protected function doList()
{
$rowfmt = ' %-3s | %-20s';
$current = StorageManager::getBackend();
$current = $this->storageManager->getBackend();
$this->out(sprintf($rowfmt, 'Sel', 'Name'));
$this->out('-----------------------');
$isregisterd = false;
foreach (StorageManager::listBackends() as $name => $class) {
foreach ($this->storageManager->listBackends() as $name => $class) {
$issel = ' ';
if ($current === $class) {
$issel = '*';
@ -100,14 +113,14 @@ HELP;
}
$name = $this->args[1];
$class = StorageManager::getByName($name);
$class = $this->storageManager->getByName($name);
if ($class === '') {
$this->out($name . ' is not a registered backend.');
return -1;
}
if (!StorageManager::setBackend($class)) {
if (!$this->storageManager->setBackend($class)) {
$this->out($class . ' is not a valid backend storage class.');
return -1;
}
@ -130,11 +143,11 @@ HELP;
$tables = [$table];
}
$current = StorageManager::getBackend();
$current = $this->storageManager->getBackend();
$total = 0;
do {
$moved = StorageManager::move($current, $tables, $this->getOption('n', 5000));
$moved = $this->storageManager->move($current, $tables, $this->getOption('n', 5000));
if ($moved) {
$this->out(date('[Y-m-d H:i:s] ') . sprintf('Moved %d files', $moved));
}

View File

@ -10,7 +10,7 @@ use DOMNode;
use DOMText;
use DOMXPath;
use Exception;
use Friendica\Core\Cache;
use Friendica\Core\Cache\Cache;
use Friendica\Core\Config;
use Friendica\Core\Hook;
use Friendica\Core\L10n;
@ -66,7 +66,7 @@ class OEmbed
if (DBA::isResult($oembed_record)) {
$json_string = $oembed_record['content'];
} else {
$json_string = Cache::get($cache_key);
$json_string = DI::cache()->get($cache_key);
}
// These media files should now be caught in bbcode.php
@ -125,7 +125,7 @@ class OEmbed
$cache_ttl = Cache::FIVE_MINUTES;
}
Cache::set($cache_key, $json_string, $cache_ttl);
DI::cache()->set($cache_key, $json_string, $cache_ttl);
}
if ($oembed->type == 'error') {

View File

@ -10,7 +10,6 @@ use DOMXPath;
use Exception;
use Friendica\Content\OEmbed;
use Friendica\Content\Smilies;
use Friendica\Core\Cache;
use Friendica\Core\Config;
use Friendica\Core\Hook;
use Friendica\Core\L10n;
@ -1070,7 +1069,7 @@ class BBCode
private static function removePictureLinksCallback($match)
{
$cache_key = 'remove:' . $match[1];
$text = Cache::get($cache_key);
$text = DI::cache()->get($cache_key);
if (is_null($text)) {
$a = DI::app();
@ -1112,7 +1111,7 @@ class BBCode
}
}
}
Cache::set($cache_key, $text);
DI::cache()->set($cache_key, $text);
}
return $text;
@ -1143,7 +1142,7 @@ class BBCode
}
$cache_key = 'clean:' . $match[1];
$text = Cache::get($cache_key);
$text = DI::cache()->get($cache_key);
if (!is_null($text)) {
return $text;
}
@ -1194,7 +1193,7 @@ class BBCode
}
}
}
Cache::set($cache_key, $text);
DI::cache()->set($cache_key, $text);
return $text;
}

View File

@ -2,10 +2,8 @@
namespace Friendica\Content\Widget;
use Friendica\Core\Cache;
use Friendica\Core\L10n;
use Friendica\Core\Renderer;
use Friendica\Database\DBA;
use Friendica\Model\Term;
/**

View File

@ -1,102 +0,0 @@
<?php
/**
* @file src/Core/Cache.php
*/
namespace Friendica\Core;
use Friendica\Core\Cache\Cache as CacheClass;
use Friendica\DI;
/**
* @brief Class for storing data for a short time
*/
class Cache
{
/** @deprecated Use CacheClass::MONTH */
const MONTH = CacheClass::MONTH;
/** @deprecated Use CacheClass::WEEK */
const WEEK = CacheClass::WEEK;
/** @deprecated Use CacheClass::DAY */
const DAY = CacheClass::DAY;
/** @deprecated Use CacheClass::HOUR */
const HOUR = CacheClass::HOUR;
/** @deprecated Use CacheClass::HALF_HOUR */
const HALF_HOUR = CacheClass::HALF_HOUR;
/** @deprecated Use CacheClass::QUARTER_HOUR */
const QUARTER_HOUR = CacheClass::QUARTER_HOUR;
/** @deprecated Use CacheClass::FIVE_MINUTES */
const FIVE_MINUTES = CacheClass::FIVE_MINUTES;
/** @deprecated Use CacheClass::MINUTE */
const MINUTE = CacheClass::MINUTE;
/** @deprecated Use CacheClass::INFINITE */
const INFINITE = CacheClass::INFINITE;
/**
* @brief Returns all the cache keys sorted alphabetically
*
* @param string $prefix Prefix of the keys (optional)
*
* @return array Empty if the driver doesn't support this feature
* @throws \Exception
*/
public static function getAllKeys($prefix = null)
{
return DI::cache()->getAllKeys($prefix);
}
/**
* @brief Fetch cached data according to the key
*
* @param string $key The key to the cached data
*
* @return mixed Cached $value or "null" if not found
* @throws \Exception
*/
public static function get($key)
{
return DI::cache()->get($key);
}
/**
* @brief Put data in the cache according to the key
*
* The input $value can have multiple formats.
*
* @param string $key The key to the cached data
* @param mixed $value The value that is about to be stored
* @param integer $duration The cache lifespan
*
* @return bool
* @throws \Exception
*/
public static function set($key, $value, $duration = CacheClass::MONTH)
{
return DI::cache()->set($key, $value, $duration);
}
/**
* @brief Delete a value from the cache
*
* @param string $key The key to the cached data
*
* @return bool
* @throws \Exception
*/
public static function delete($key)
{
return DI::cache()->delete($key);
}
/**
* @brief Remove outdated data from the cache
*
* @param boolean $outdated just remove outdated values
*
* @return bool
* @throws \Exception
*/
public static function clear($outdated = true)
{
return DI::cache()->clear($outdated);
}
}

View File

@ -2,7 +2,7 @@
namespace Friendica\Core\Lock;
use Friendica\Core\Cache;
use Friendica\Core\Cache\Cache;
use Friendica\Database\Database;
use Friendica\Util\DateTimeFormat;

View File

@ -2,8 +2,12 @@
namespace Friendica\Core;
use Friendica\Database\DBA;
use Friendica\Model\Storage\IStorage;
use Exception;
use Friendica\Core\Config\IConfiguration;
use Friendica\Core\L10n\L10n;
use Friendica\Database\Database;
use Friendica\Model\Storage;
use Psr\Log\LoggerInterface;
/**
@ -14,59 +18,146 @@ use Friendica\Model\Storage\IStorage;
*/
class StorageManager
{
private static $default_backends = [
'Filesystem' => \Friendica\Model\Storage\Filesystem::class,
'Database' => \Friendica\Model\Storage\Database::class,
// Default tables to look for data
const TABLES = ['photo', 'attach'];
// Default storage backends
const DEFAULT_BACKENDS = [
Storage\Filesystem::NAME => Storage\Filesystem::class,
Storage\Database::NAME => Storage\Database::class,
];
private static $backends = [];
private $backends = [];
private static function setup()
/**
* @var Storage\IStorage[] A local cache for storage instances
*/
private $backendInstances = [];
/** @var Database */
private $dba;
/** @var IConfiguration */
private $config;
/** @var LoggerInterface */
private $logger;
/** @var L10n */
private $l10n;
/** @var Storage\IStorage */
private $currentBackend;
/**
* @param Database $dba
* @param IConfiguration $config
* @param LoggerInterface $logger
* @param L10n $l10n
*/
public function __construct(Database $dba, IConfiguration $config, LoggerInterface $logger, L10n $l10n)
{
if (count(self::$backends) == 0) {
self::$backends = Config::get('storage', 'backends', self::$default_backends);
}
$this->dba = $dba;
$this->config = $config;
$this->logger = $logger;
$this->l10n = $l10n;
$this->backends = $config->get('storage', 'backends', self::DEFAULT_BACKENDS);
$currentName = $this->config->get('storage', 'name', '');
$this->currentBackend = $this->getByName($currentName);
}
/**
* @brief Return current storage backend class
*
* @return string
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @return Storage\IStorage|null
*/
public static function getBackend()
public function getBackend()
{
return Config::get('storage', 'class', '');
return $this->currentBackend;
}
/**
* @brief Return storage backend class by registered name
*
* @param string $name Backend name
* @return string Empty if no backend registered at $name exists
* @param string|null $name Backend name
* @param boolean $userBackend Just return instances in case it's a user backend (e.g. not SystemResource)
*
* @return Storage\IStorage|null null if no backend registered at $name
*
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function getByName($name)
public function getByName(string $name = null, $userBackend = true)
{
self::setup();
return self::$backends[$name] ?? '';
// If there's no cached instance create a new instance
if (!isset($this->backendInstances[$name])) {
// If the current name isn't a valid backend (or the SystemResource instance) create it
if ($this->isValidBackend($name, $userBackend)) {
switch ($name) {
// Try the filesystem backend
case Storage\Filesystem::getName():
$this->backendInstances[$name] = new Storage\Filesystem($this->config, $this->logger, $this->l10n);
break;
// try the database backend
case Storage\Database::getName():
$this->backendInstances[$name] = new Storage\Database($this->dba, $this->logger, $this->l10n);
break;
// at least, try if there's an addon for the backend
case Storage\SystemResource::getName():
$this->backendInstances[$name] = new Storage\SystemResource();
break;
default:
$data = [
'name' => $name,
'storage' => null,
];
Hook::callAll('storage_instance', $data);
if (($data['storage'] ?? null) instanceof Storage\IStorage) {
$this->backendInstances[$data['name'] ?? $name] = $data['storage'];
} else {
return null;
}
break;
}
} else {
return null;
}
}
return $this->backendInstances[$name];
}
/**
* Checks, if the storage is a valid backend
*
* @param string|null $name The name or class of the backend
* @param boolean $userBackend True, if just user backend should get returned (e.g. not SystemResource)
*
* @return boolean True, if the backend is a valid backend
*/
public function isValidBackend(string $name = null, bool $userBackend = true)
{
return array_key_exists($name, $this->backends) ||
(!$userBackend && $name === Storage\SystemResource::getName());
}
/**
* @brief Set current storage backend class
*
* @param string $class Backend class name
* @return bool
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @param string $name Backend class name
*
* @return boolean True, if the set was successful
*/
public static function setBackend($class)
public function setBackend(string $name = null)
{
if (!in_array('Friendica\Model\Storage\IStorage', class_implements($class))) {
if (!$this->isValidBackend($name)) {
return false;
}
Config::set('storage', 'class', $class);
if ($this->config->set('storage', 'name', $name)) {
$this->currentBackend = $this->getByName($name);
return true;
} else {
return false;
}
}
/**
@ -74,42 +165,63 @@ class StorageManager
*
* @return array
*/
public static function listBackends()
public function listBackends()
{
self::setup();
return self::$backends;
return $this->backends;
}
/**
* @brief Register a storage backend class
* Register a storage backend class
*
* You have to register the hook "storage_instance" as well to make this class work!
*
* @param string $name User readable backend name
* @param string $class Backend class name
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*
* @return boolean True, if the registration was successful
*/
public static function register($name, $class)
public function register(string $class)
{
/// @todo Check that $class implements IStorage
self::setup();
self::$backends[$name] = $class;
Config::set('storage', 'backends', self::$backends);
}
if (is_subclass_of($class, Storage\IStorage::class)) {
/** @var Storage\IStorage $class */
$backends = $this->backends;
$backends[$class::getName()] = $class;
if ($this->config->set('storage', 'backends', $backends)) {
$this->backends = $backends;
return true;
} else {
return false;
}
} else {
return false;
}
}
/**
* @brief Unregister a storage backend class
*
* @param string $name User readable backend name
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @param string $class Backend class name
*
* @return boolean True, if unregistering was successful
*/
public static function unregister($name)
public function unregister(string $class)
{
self::setup();
unset(self::$backends[$name]);
Config::set('storage', 'backends', self::$backends);
if (is_subclass_of($class, Storage\IStorage::class)) {
/** @var Storage\IStorage $class */
unset($this->backends[$class::getName()]);
if ($this->currentBackend instanceof $class) {
$this->config->set('storage', 'name', null);
$this->currentBackend = null;
}
return $this->config->set('storage', 'backends', $this->backends);
} else {
return false;
}
}
/**
* @brief Move up to 5000 resources to storage $dest
@ -117,64 +229,60 @@ class StorageManager
* Copy existing data to destination storage and delete from source.
* This method cannot move to legacy in-table `data` field.
*
* @param string $destination Storage class name
* @param array|null $tables Tables to look in for resources. Optional, defaults to ['photo', 'attach']
* @param Storage\IStorage $destination Destination storage class name
* @param array $tables Tables to look in for resources. Optional, defaults to ['photo', 'attach']
* @param int $limit Limit of the process batch size, defaults to 5000
*
* @return int Number of moved resources
* @throws \Exception
* @throws Storage\StorageException
* @throws Exception
*/
public static function move($destination, $tables = null, $limit = 5000)
public function move(Storage\IStorage $destination, array $tables = self::TABLES, int $limit = 5000)
{
if (empty($destination)) {
throw new \Exception('Can\'t move to NULL storage backend');
}
if (is_null($tables)) {
$tables = ['photo', 'attach'];
if ($destination === null) {
throw new Storage\StorageException('Can\'t move to NULL storage backend');
}
$moved = 0;
foreach ($tables as $table) {
// Get the rows where backend class is not the destination backend class
$resources = DBA::select(
$resources = $this->dba->select(
$table,
['id', 'data', 'backend-class', 'backend-ref'],
['`backend-class` IS NULL or `backend-class` != ?', $destination],
['`backend-class` IS NULL or `backend-class` != ?', $destination::getName()],
['limit' => $limit]
);
while ($resource = DBA::fetch($resources)) {
while ($resource = $this->dba->fetch($resources)) {
$id = $resource['id'];
$data = $resource['data'];
/** @var IStorage $backendClass */
$backendClass = $resource['backend-class'];
$backendRef = $resource['backend-ref'];
if (!empty($backendClass)) {
Logger::log("get data from old backend " . $backendClass . " : " . $backendRef);
$data = $backendClass::get($backendRef);
$source = $this->getByName($resource['backend-class']);
$sourceRef = $resource['backend-ref'];
if (!empty($source)) {
$this->logger->info('Get data from old backend.', ['oldBackend' => $source, 'oldReference' => $sourceRef]);
$data = $source->get($sourceRef);
}
Logger::log("save data to new backend " . $destination);
/** @var IStorage $destination */
$ref = $destination::put($data);
Logger::log("saved data as " . $ref);
$this->logger->info('Save data to new backend.', ['newBackend' => $destination]);
$destinationRef = $destination->put($data);
$this->logger->info('Saved data.', ['newReference' => $destinationRef]);
if ($ref !== '') {
Logger::log("update row");
if (DBA::update($table, ['backend-class' => $destination, 'backend-ref' => $ref, 'data' => ''], ['id' => $id])) {
if (!empty($backendClass)) {
Logger::log("delete data from old backend " . $backendClass . " : " . $backendRef);
$backendClass::delete($backendRef);
if ($destinationRef !== '') {
$this->logger->info('update row');
if ($this->dba->update($table, ['backend-class' => $destination, 'backend-ref' => $destinationRef, 'data' => ''], ['id' => $id])) {
if (!empty($source)) {
$this->logger->info('Delete data from old backend.', ['oldBackend' => $source, 'oldReference' => $sourceRef]);
$source->delete($sourceRef);
}
$moved++;
}
}
}
DBA::close($resources);
$this->dba->close($resources);
}
return $moved;
}
}

View File

@ -7,6 +7,7 @@ use Friendica\Database\DBA;
use Friendica\Database\DBStructure;
use Friendica\DI;
use Friendica\Util\Strings;
use Friendica\Core\Cache\Cache;
class Update
{

View File

@ -27,6 +27,7 @@ use Psr\Log\LoggerInterface;
* @method static Core\L10n\L10n l10n()
* @method static Core\Process process()
* @method static Core\Session\ISession session()
* @method static Core\StorageManager storageManager()
* @method static Database\Database dba()
* @method static Factory\Mastodon\Account mstdnAccount()
* @method static Factory\Mastodon\FollowRequest mstdnFollowRequest()
@ -34,6 +35,7 @@ use Psr\Log\LoggerInterface;
* @method static Model\User\Cookie cookie()
* @method static Model\Notify notify()
* @method static Repository\Introduction intro()
* @method static Model\Storage\IStorage storage()
* @method static Protocol\Activity activity()
* @method static Util\ACLFormatter aclFormatter()
* @method static Util\DateTimeFormat dtFormat()
@ -64,12 +66,14 @@ abstract class DI
'lock' => Core\Lock\ILock::class,
'process' => Core\Process::class,
'session' => Core\Session\ISession::class,
'storageManager' => Core\StorageManager::class,
'dba' => Database\Database::class,
'mstdnAccount' => Factory\Mastodon\Account::class,
'mstdnFollowRequest' => Factory\Mastodon\FollowRequest::class,
'mstdnRelationship' => Factory\Mastodon\Relationship::class,
'cookie' => Model\User\Cookie::class,
'notify' => Model\Notify::class,
'storage' => Model\Storage\IStorage::class,
'intro' => Repository\Introduction::class,
'activity' => Protocol\Activity::class,
'aclFormatter' => Util\ACLFormatter::class,

View File

@ -6,12 +6,10 @@
*/
namespace Friendica\Model;
use Friendica\Core\StorageManager;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\Database\DBStructure;
use Friendica\DI;
use Friendica\Model\Storage\IStorage;
use Friendica\Object\Image;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Mimetype;
@ -146,7 +144,8 @@ class Attach
*/
public static function getData($item)
{
if ($item['backend-class'] == '') {
$backendClass = DI::storageManager()->getByName($photo['backend-class'] ?? '');
if ($backendClass === null) {
// legacy data storage in 'data' column
$i = self::selectFirst(['data'], ['id' => $item['id']]);
if ($i === false) {
@ -154,9 +153,8 @@ class Attach
}
return $i['data'];
} else {
$backendClass = $item['backend-class'];
$backendRef = $item['backend-ref'];
return $backendClass::get($backendRef);
return $backendClass->get($backendRef);
}
}
@ -186,13 +184,8 @@ class Attach
$filesize = strlen($data);
}
/** @var IStorage $backend_class */
$backend_class = StorageManager::getBackend();
$backend_ref = '';
if ($backend_class !== '') {
$backend_ref = $backend_class::put($data);
$backend_ref = DI::storage()->put($data);
$data = '';
}
$hash = System::createGUID(64);
$created = DateTimeFormat::utcNow();
@ -210,7 +203,7 @@ class Attach
'allow_gid' => $allow_gid,
'deny_cid' => $deny_cid,
'deny_gid' => $deny_gid,
'backend-class' => $backend_class,
'backend-class' => (string)DI::storage(),
'backend-ref' => $backend_ref
];
@ -266,10 +259,9 @@ class Attach
$items = self::selectToArray(['backend-class','backend-ref'], $conditions);
foreach($items as $item) {
/** @var IStorage $backend_class */
$backend_class = (string)$item['backend-class'];
if ($backend_class !== '') {
$fields['backend-ref'] = $backend_class::put($img->asString(), $item['backend-ref']);
$backend_class = DI::storageManager()->getByName($item['backend-class'] ?? '');
if ($backend_class !== null) {
$fields['backend-ref'] = $backend_class->put($img->asString(), $item['backend-ref'] ?? '');
} else {
$fields['data'] = $img->asString();
}
@ -299,10 +291,9 @@ class Attach
$items = self::selectToArray(['backend-class','backend-ref'], $conditions);
foreach($items as $item) {
/** @var IStorage $backend_class */
$backend_class = (string)$item['backend-class'];
if ($backend_class !== '') {
$backend_class::delete($item['backend-ref']);
$backend_class = DI::storageManager()->getByName($item['backend-class'] ?? '');
if ($backend_class !== null) {
$backend_class->delete($item['backend-ref'] ?? '');
}
}

View File

@ -1896,6 +1896,14 @@ class Contact
$data = [$contact["photo"], $contact["thumb"], $contact["micro"]];
}
foreach ($data as $image_uri) {
$image_rid = Photo::ridFromURI($image_uri);
if ($image_rid && !Photo::exists(['resource-id' => $image_rid, 'uid' => $uid])) {
Logger::info('Regenerating avatar', ['contact uid' => $uid, 'cid' => $cid, 'missing photo' => $image_rid, 'avatar' => $contact['avatar']]);
$force = true;
}
}
if (($contact["avatar"] != $avatar) || $force) {
$photos = Photo::importProfilePhoto($avatar, $uid, $cid, true);

View File

@ -217,7 +217,7 @@ class GServer
$serverdata = self::analyseRootBody($curlResult, $serverdata, $url);
}
if (!$curlResult->isSuccess() || empty($curlResult->getBody())) {
if (!$curlResult->isSuccess() || empty($curlResult->getBody()) || self::invalidBody($curlResult->getBody())) {
DBA::update('gserver', ['last_failure' => DateTimeFormat::utcNow()], ['nurl' => Strings::normaliseLink($url)]);
return false;
}
@ -1061,6 +1061,7 @@ class GServer
$serverdata['platform'] = 'gnusocial';
// Remove junk that some GNU Social servers return
$serverdata['version'] = str_replace(chr(239) . chr(187) . chr(191), '', $curlResult->getBody());
$serverdata['version'] = str_replace(["\r", "\n", "\t"], '', $serverdata['version']);
$serverdata['version'] = trim($serverdata['version'], '"');
$serverdata['network'] = Protocol::OSTATUS;
return $serverdata;
@ -1070,12 +1071,21 @@ class GServer
$curlResult = Network::curl($url . '/api/statusnet/version.json');
if ($curlResult->isSuccess() && ($curlResult->getBody() != '{"error":"not implemented"}') &&
($curlResult->getBody() != '') && (strlen($curlResult->getBody()) < 30)) {
$serverdata['platform'] = 'statusnet';
// Remove junk that some GNU Social servers return
$serverdata['version'] = str_replace(chr(239).chr(187).chr(191), '', $curlResult->getBody());
$serverdata['version'] = str_replace(["\r", "\n", "\t"], '', $serverdata['version']);
$serverdata['version'] = trim($serverdata['version'], '"');
if (!empty($serverdata['version']) && strtolower(substr($serverdata['version'], 0, 7)) == 'pleroma') {
$serverdata['platform'] = 'pleroma';
$serverdata['version'] = trim(str_ireplace('pleroma', '', $serverdata['version']));
$serverdata['network'] = Protocol::ACTIVITYPUB;
} else {
$serverdata['platform'] = 'statusnet';
$serverdata['network'] = Protocol::OSTATUS;
}
}
return $serverdata;
}
@ -1285,7 +1295,6 @@ class GServer
$serverdata['platform'] = 'diaspora';
$serverdata['network'] = $network = Protocol::DIASPORA;
$serverdata['version'] = $curlResult->getHeader('x-diaspora-version');
} elseif ($curlResult->inHeader('x-friendica-version')) {
$serverdata['platform'] = 'friendica';
$serverdata['network'] = $network = Protocol::DFRN;
@ -1294,6 +1303,19 @@ class GServer
return $serverdata;
}
/**
* Test if the body contains valid content
*
* @param string $body
* @return boolean
*/
private static function invalidBody(string $body)
{
// Currently we only test for a HTML element.
// Possibly we enhance this in the future.
return !strpos($body, '>');
}
/**
* Update the user directory of a given gserver record
*

View File

@ -215,12 +215,10 @@ class Mail
$images = $match[1];
if (count($images)) {
foreach ($images as $image) {
if (!stristr($image, DI::baseUrl() . '/photo/')) {
continue;
$image_rid = Photo::ridFromURI($image);
if (!empty($image_rid)) {
Photo::update(['allow-cid' => '<' . $recipient . '>'], ['resource-id' => $image_rid, 'album' => 'Wall Photos', 'uid' => local_user()]);
}
$image_uri = substr($image, strrpos($image, '/') + 1);
$image_uri = substr($image_uri, 0, strpos($image_uri, '-'));
Photo::update(['allow-cid' => '<' . $recipient . '>'], ['resource-id' => $image_uri, 'album' => 'Wall Photos', 'uid' => local_user()]);
}
}
}

View File

@ -52,12 +52,15 @@ class Nodeinfo
$logger->debug('user statistics', $userStats);
$local_posts = DBA::count('thread', ["`wall` AND NOT `deleted` AND `uid` != 0"]);
$config->set('nodeinfo', 'local_posts', $local_posts);
$logger->debug('thread statistics', ['local_posts' => $local_posts]);
$local_comments = DBA::count('item', ["`origin` AND `id` != `parent` AND NOT `deleted` AND `uid` != 0"]);
$config->set('nodeinfo', 'local_comments', $local_comments);
$logger->debug('item statistics', ['local_comments' => $local_comments]);
$items = DBA::p("SELECT COUNT(*) AS `total`, `gravity` FROM `item` WHERE `origin` AND NOT `deleted` AND `uid` != 0 AND `gravity` IN (?, ?) GROUP BY `gravity`",
GRAVITY_PARENT, GRAVITY_COMMENT);
while ($item = DBA::fetch($items)) {
if ($item['gravity'] == GRAVITY_PARENT) {
$config->set('nodeinfo', 'local_posts', $item['total']);
} elseif ($item['gravity'] == GRAVITY_COMMENT) {
$config->set('nodeinfo', 'local_comments', $item['total']);
}
}
DBA::close($items);
}
}

View File

@ -6,16 +6,15 @@
*/
namespace Friendica\Model;
use Friendica\Core\Cache;
use Friendica\Core\Cache\Cache;
use Friendica\Core\Config;
use Friendica\Core\L10n;
use Friendica\Core\Logger;
use Friendica\Core\StorageManager;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\Database\DBStructure;
use Friendica\DI;
use Friendica\Model\Storage\IStorage;
use Friendica\Model\Storage\SystemResource;
use Friendica\Object\Image;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Images;
@ -172,26 +171,24 @@ class Photo
*/
public static function getImageForPhoto(array $photo)
{
$data = "";
if ($photo["backend-class"] == "") {
$backendClass = DI::storageManager()->getByName($photo['backend-class'] ?? '');
if ($backendClass === null) {
// legacy data storage in "data" column
$i = self::selectFirst(["data"], ["id" => $photo["id"]]);
$i = self::selectFirst(['data'], ['id' => $photo['id']]);
if ($i === false) {
return null;
}
$data = $i["data"];
$data = $i['data'];
} else {
$backendClass = $photo["backend-class"];
$backendRef = $photo["backend-ref"];
$data = $backendClass::get($backendRef);
$backendRef = $photo['backend-ref'] ?? '';
$data = $backendClass->get($backendRef);
}
if ($data === "") {
if (empty($data)) {
return null;
}
return new Image($data, $photo["type"]);
return new Image($data, $photo['type']);
}
/**
@ -223,10 +220,10 @@ class Photo
$values = array_fill(0, count($fields), "");
$photo = array_combine($fields, $values);
$photo["backend-class"] = Storage\SystemResource::class;
$photo["backend-ref"] = $filename;
$photo["type"] = $mimetype;
$photo["cacheable"] = false;
$photo['backend-class'] = SystemResource::NAME;
$photo['backend-ref'] = $filename;
$photo['type'] = $mimetype;
$photo['cacheable'] = false;
return $photo;
}
@ -273,18 +270,17 @@ class Photo
$data = "";
$backend_ref = "";
/** @var IStorage $backend_class */
if (DBA::isResult($existing_photo)) {
$backend_ref = (string)$existing_photo["backend-ref"];
$backend_class = (string)$existing_photo["backend-class"];
$storage = DI::storageManager()->getByName($existing_photo["backend-class"] ?? '');
} else {
$backend_class = StorageManager::getBackend();
$storage = DI::storage();
}
if ($backend_class === "") {
if ($storage === null) {
$data = $Image->asString();
} else {
$backend_ref = $backend_class::put($Image->asString(), $backend_ref);
$backend_ref = $storage->put($Image->asString(), $backend_ref);
}
@ -309,7 +305,7 @@ class Photo
"deny_cid" => $deny_cid,
"deny_gid" => $deny_gid,
"desc" => $desc,
"backend-class" => $backend_class,
"backend-class" => (string)$storage,
"backend-ref" => $backend_ref
];
@ -340,10 +336,9 @@ class Photo
$photos = self::selectToArray(['backend-class', 'backend-ref'], $conditions);
foreach($photos as $photo) {
/** @var IStorage $backend_class */
$backend_class = (string)$photo["backend-class"];
if ($backend_class !== "") {
$backend_class::delete($photo["backend-ref"]);
$backend_class = DI::storageManager()->getByName($photo['backend-class'] ?? '');
if ($backend_class !== null) {
$backend_class->delete($photo["backend-ref"] ?? '');
}
}
@ -370,10 +365,9 @@ class Photo
$photos = self::selectToArray(['backend-class', 'backend-ref'], $conditions);
foreach($photos as $photo) {
/** @var IStorage $backend_class */
$backend_class = (string)$photo["backend-class"];
if ($backend_class !== "") {
$fields["backend-ref"] = $backend_class::put($img->asString(), $photo["backend-ref"]);
$backend_class = DI::storageManager()->getByName($photo['backend-class'] ?? '');
if ($backend_class !== null) {
$fields["backend-ref"] = $backend_class->put($img->asString(), $photo['backend-ref']);
} else {
$fields["data"] = $img->asString();
}
@ -546,7 +540,7 @@ class Photo
$sql_extra = Security::getPermissionsSQLByUserId($uid);
$key = "photo_albums:".$uid.":".local_user().":".remote_user();
$albums = Cache::get($key);
$albums = DI::cache()->get($key);
if (is_null($albums) || $update) {
if (!Config::get("system", "no_count", false)) {
/// @todo This query needs to be renewed. It is really slow
@ -569,7 +563,7 @@ class Photo
DBA::escape(L10n::t("Contact Photos"))
);
}
Cache::set($key, $albums, Cache::DAY);
DI::cache()->set($key, $albums, Cache::DAY);
}
return $albums;
}
@ -582,7 +576,7 @@ class Photo
public static function clearAlbumCache($uid)
{
$key = "photo_albums:".$uid.":".local_user().":".remote_user();
Cache::set($key, null, Cache::DAY);
DI::cache()->set($key, null, Cache::DAY);
}
/**
@ -596,6 +590,25 @@ class Photo
return System::createGUID(32, false);
}
/**
* Extracts the rid from a local photo URI
*
* @param string $image_uri The URI of the photo
* @return string The rid of the photo, or an empty string if the URI is not local
*/
public static function ridFromURI(string $image_uri)
{
if (!stristr($image_uri, DI::baseUrl() . '/photo/')) {
return '';
}
$image_uri = substr($image_uri, strrpos($image_uri, '/') + 1);
$image_uri = substr($image_uri, 0, strpos($image_uri, '-'));
if (!strlen($image_uri)) {
return '';
}
return $image_uri;
}
/**
* Changes photo permissions that had been embedded in a post
*
@ -622,12 +635,8 @@ class Photo
}
foreach ($images as $image) {
if (!stristr($image, DI::baseUrl() . '/photo/')) {
continue;
}
$image_uri = substr($image,strrpos($image,'/') + 1);
$image_uri = substr($image_uri,0, strpos($image_uri,'-'));
if (!strlen($image_uri)) {
$image_rid = self::ridFromURI($image);
if (empty($image_rid)) {
continue;
}
@ -636,7 +645,7 @@ class Photo
$condition = [
'allow_cid' => $srch, 'allow_gid' => '', 'deny_cid' => '', 'deny_gid' => '',
'resource-id' => $image_uri, 'uid' => $uid
'resource-id' => $image_rid, 'uid' => $uid
];
if (!Photo::exists($condition)) {
continue;
@ -646,7 +655,7 @@ class Photo
$fields = ['allow_cid' => $str_contact_allow, 'allow_gid' => $str_group_allow,
'deny_cid' => $str_contact_deny, 'deny_gid' => $str_group_deny];
$condition = ['resource-id' => $image_uri, 'uid' => $uid];
$condition = ['resource-id' => $image_rid, 'uid' => $uid];
Logger::info('Set permissions', ['condition' => $condition, 'permissions' => $fields]);
Photo::update($fields, $condition);
}

View File

@ -10,7 +10,7 @@ use Friendica\Content\ForumManager;
use Friendica\Content\Text\BBCode;
use Friendica\Content\Text\HTML;
use Friendica\Content\Widget\ContactBlock;
use Friendica\Core\Cache;
use Friendica\Core\Cache\Cache;
use Friendica\Core\Config;
use Friendica\Core\Hook;
use Friendica\Core\L10n;
@ -586,7 +586,7 @@ class Profile
$bd_short = L10n::t('F d');
$cachekey = 'get_birthdays:' . local_user();
$r = Cache::get($cachekey);
$r = DI::cache()->get($cachekey);
if (is_null($r)) {
$s = DBA::p(
"SELECT `event`.*, `event`.`id` AS `eid`, `contact`.* FROM `event`
@ -608,7 +608,7 @@ class Profile
);
if (DBA::isResult($s)) {
$r = DBA::toArray($s);
Cache::set($cachekey, $r, Cache::HOUR);
DI::cache()->set($cachekey, $r, Cache::HOUR);
}
}
@ -1066,11 +1066,11 @@ class Profile
// Avoid endless loops
$cachekey = 'zrlInit:' . $my_url;
if (Cache::get($cachekey)) {
if (DI::cache()->get($cachekey)) {
Logger::log('URL ' . $my_url . ' already tried to authenticate.', Logger::DEBUG);
return;
} else {
Cache::set($cachekey, true, Cache::MINUTE);
DI::cache()->set($cachekey, true, Cache::MINUTE);
}
Logger::log('Not authenticated. Invoking reverse magic-auth for ' . $my_url, Logger::DEBUG);

View File

@ -0,0 +1,32 @@
<?php
namespace Friendica\Model\Storage;
use Friendica\Core\L10n\L10n;
use Psr\Log\LoggerInterface;
/**
* A general storage class which loads common dependencies and implements common methods
*/
abstract class AbstractStorage implements IStorage
{
/** @var L10n */
protected $l10n;
/** @var LoggerInterface */
protected $logger;
/**
* @param L10n $l10n
* @param LoggerInterface $logger
*/
public function __construct(L10n $l10n, LoggerInterface $logger)
{
$this->l10n = $l10n;
$this->logger = $logger;
}
public function __toString()
{
return static::getName();
}
}

View File

@ -6,58 +6,100 @@
namespace Friendica\Model\Storage;
use Friendica\Core\Logger;
use Friendica\Core\L10n;
use Friendica\Database\DBA;
use Friendica\Core\L10n\L10n;
use Psr\Log\LoggerInterface;
use Friendica\Database\Database as DBA;
/**
* @brief Database based storage system
*
* This class manage data stored in database table.
*/
class Database implements IStorage
class Database extends AbstractStorage
{
public static function get($ref)
const NAME = 'Database';
/** @var DBA */
private $dba;
/**
* @param DBA $dba
* @param LoggerInterface $logger
* @param L10n $l10n
*/
public function __construct(DBA $dba, LoggerInterface $logger, L10n $l10n)
{
$r = DBA::selectFirst('storage', ['data'], ['id' => $ref]);
if (!DBA::isResult($r)) {
parent::__construct($l10n, $logger);
$this->dba = $dba;
}
/**
* @inheritDoc
*/
public function get(string $reference)
{
$result = $this->dba->selectFirst('storage', ['data'], ['id' => $reference]);
if (!$this->dba->isResult($result)) {
return '';
}
return $r['data'];
return $result['data'];
}
public static function put($data, $ref = '')
/**
* @inheritDoc
*/
public function put(string $data, string $reference = '')
{
if ($ref !== '') {
$r = DBA::update('storage', ['data' => $data], ['id' => $ref]);
if ($r === false) {
Logger::log('Failed to update data with id ' . $ref . ': ' . DBA::errorNo() . ' : ' . DBA::errorMessage());
throw new StorageException(L10n::t('Database storage failed to update %s', $ref));
if ($reference !== '') {
$result = $this->dba->update('storage', ['data' => $data], ['id' => $reference]);
if ($result === false) {
$this->logger->warning('Failed to update data.', ['id' => $reference, 'errorCode' => $this->dba->errorNo(), 'errorMessage' => $this->dba->errorMessage()]);
throw new StorageException($this->l10n->t('Database storage failed to update %s', $reference));
}
return $ref;
return $reference;
} else {
$r = DBA::insert('storage', ['data' => $data]);
if ($r === false) {
Logger::log('Failed to insert data: ' . DBA::errorNo() . ' : ' . DBA::errorMessage());
throw new StorageException(L10n::t('Database storage failed to insert data'));
$result = $this->dba->insert('storage', ['data' => $data]);
if ($result === false) {
$this->logger->warning('Failed to insert data.', ['errorCode' => $this->dba->errorNo(), 'errorMessage' => $this->dba->errorMessage()]);
throw new StorageException($this->l10n->t('Database storage failed to insert data'));
}
return DBA::lastInsertId();
return $this->dba->lastInsertId();
}
}
public static function delete($ref)
/**
* @inheritDoc
*/
public function delete(string $reference)
{
return DBA::delete('storage', ['id' => $ref]);
return $this->dba->delete('storage', ['id' => $reference]);
}
public static function getOptions()
/**
* @inheritDoc
*/
public function getOptions()
{
return [];
}
public static function saveOptions($data)
/**
* @inheritDoc
*/
public function saveOptions(array $data)
{
return [];
}
/**
* @inheritDoc
*/
public static function getName()
{
return self::NAME;
}
}

View File

@ -6,10 +6,10 @@
namespace Friendica\Model\Storage;
use Friendica\Core\Config;
use Friendica\Core\L10n;
use Friendica\Core\Logger;
use Friendica\Core\Config\IConfiguration;
use Friendica\Core\L10n\L10n;
use Friendica\Util\Strings;
use Psr\Log\LoggerInterface;
/**
* @brief Filesystem based storage backend
@ -21,52 +21,72 @@ use Friendica\Util\Strings;
* Each new resource gets a value as reference and is saved in a
* folder tree stucture created from that value.
*/
class Filesystem implements IStorage
class Filesystem extends AbstractStorage
{
const NAME = 'Filesystem';
// Default base folder
const DEFAULT_BASE_FOLDER = 'storage';
private static function getBasePath()
/** @var IConfiguration */
private $config;
/** @var string */
private $basePath;
/**
* Filesystem constructor.
*
* @param IConfiguration $config
* @param LoggerInterface $logger
* @param L10n $l10n
*/
public function __construct(IConfiguration $config, LoggerInterface $logger, L10n $l10n)
{
$path = Config::get('storage', 'filesystem_path', self::DEFAULT_BASE_FOLDER);
return rtrim($path, '/');
parent::__construct($l10n, $logger);
$this->config = $config;
$path = $this->config->get('storage', 'filesystem_path', self::DEFAULT_BASE_FOLDER);
$this->basePath = rtrim($path, '/');
}
/**
* @brief Split data ref and return file path
* @param string $ref Data reference
*
* @param string $reference Data reference
*
* @return string
*/
private static function pathForRef($ref)
private function pathForRef(string $reference)
{
$base = self::getBasePath();
$fold1 = substr($ref, 0, 2);
$fold2 = substr($ref, 2, 2);
$file = substr($ref, 4);
$fold1 = substr($reference, 0, 2);
$fold2 = substr($reference, 2, 2);
$file = substr($reference, 4);
return implode('/', [$base, $fold1, $fold2, $file]);
return implode('/', [$this->basePath, $fold1, $fold2, $file]);
}
/**
* @brief Create dirctory tree to store file, with .htaccess and index.html files
*
* @param string $file Path and filename
*
* @throws StorageException
*/
private static function createFoldersForFile($file)
private function createFoldersForFile(string $file)
{
$path = dirname($file);
if (!is_dir($path)) {
if (!mkdir($path, 0770, true)) {
Logger::log('Failed to create dirs ' . $path);
throw new StorageException(L10n::t('Filesystem storage failed to create "%s". Check you write permissions.', $path));
$this->logger->warning('Failed to create dir.', ['path' => $path]);
throw new StorageException($this->l10n->t('Filesystem storage failed to create "%s". Check you write permissions.', $path));
}
}
$base = self::getBasePath();
while ($path !== $base) {
while ($path !== $this->basePath) {
if (!is_file($path . '/index.html')) {
file_put_contents($path . '/index.html', '');
}
@ -80,9 +100,12 @@ class Filesystem implements IStorage
}
}
public static function get($ref)
/**
* @inheritDoc
*/
public function get(string $reference)
{
$file = self::pathForRef($ref);
$file = $this->pathForRef($reference);
if (!is_file($file)) {
return '';
}
@ -90,27 +113,33 @@ class Filesystem implements IStorage
return file_get_contents($file);
}
public static function put($data, $ref = '')
/**
* @inheritDoc
*/
public function put(string $data, string $reference = '')
{
if ($ref === '') {
$ref = Strings::getRandomHex();
if ($reference === '') {
$reference = Strings::getRandomHex();
}
$file = self::pathForRef($ref);
$file = $this->pathForRef($reference);
self::createFoldersForFile($file);
$this->createFoldersForFile($file);
$r = file_put_contents($file, $data);
if ($r === FALSE) {
Logger::log('Failed to write data to ' . $file);
throw new StorageException(L10n::t('Filesystem storage failed to save data to "%s". Check your write permissions', $file));
if (!file_put_contents($file, $data)) {
$this->logger->warning('Failed to write data.', ['file' => $file]);
throw new StorageException($this->l10n->t('Filesystem storage failed to save data to "%s". Check your write permissions', $file));
}
chmod($file, 0660);
return $ref;
return $reference;
}
public static function delete($ref)
/**
* @inheritDoc
*/
public function delete(string $reference)
{
$file = self::pathForRef($ref);
$file = $this->pathForRef($reference);
// return true if file doesn't exists. we want to delete it: success with zero work!
if (!is_file($file)) {
return true;
@ -118,28 +147,42 @@ class Filesystem implements IStorage
return unlink($file);
}
public static function getOptions()
/**
* @inheritDoc
*/
public function getOptions()
{
return [
'storagepath' => [
'input',
L10n::t('Storage base path'),
self::getBasePath(),
L10n::t('Folder where uploaded files are saved. For maximum security, This should be a path outside web server folder tree')
$this->l10n->t('Storage base path'),
$this->basePath,
$this->l10n->t('Folder where uploaded files are saved. For maximum security, This should be a path outside web server folder tree')
]
];
}
public static function saveOptions($data)
/**
* @inheritDoc
*/
public function saveOptions(array $data)
{
$storagepath = $data['storagepath'] ?? '';
if ($storagepath === '' || !is_dir($storagepath)) {
$storagePath = $data['storagepath'] ?? '';
if ($storagePath === '' || !is_dir($storagePath)) {
return [
'storagepath' => L10n::t('Enter a valid existing folder')
'storagepath' => $this->l10n->t('Enter a valid existing folder')
];
};
Config::set('storage', 'filesystem_path', $storagepath);
$this->config->set('storage', 'filesystem_path', $storagePath);
$this->basePath = $storagePath;
return [];
}
/**
* @inheritDoc
*/
public static function getName()
{
return self::NAME;
}
}

View File

@ -13,25 +13,31 @@ interface IStorage
{
/**
* @brief Get data from backend
* @param string $ref Data reference
*
* @param string $reference Data reference
*
* @return string
*/
public static function get($ref);
public function get(string $reference);
/**
* @brief Put data in backend as $ref. If $ref is not defined a new reference is created.
*
* @param string $data Data to save
* @param string $ref Data referece. Optional.
* @return string Saved data referece
* @param string $reference Data reference. Optional.
*
* @return string Saved data reference
*/
public static function put($data, $ref = "");
public function put(string $data, string $reference = "");
/**
* @brief Remove data from backend
* @param string $ref Data referece
*
* @param string $reference Data reference
*
* @return boolean True on success
*/
public static function delete($ref);
public function delete(string $reference);
/**
* @brief Get info about storage options
@ -71,7 +77,7 @@ interface IStorage
*
* See https://github.com/friendica/friendica/wiki/Quick-Template-Guide
*/
public static function getOptions();
public function getOptions();
/**
* @brief Validate and save options
@ -82,8 +88,19 @@ interface IStorage
*
* Return array must be empty if no error.
*/
public static function saveOptions($data);
public function saveOptions(array $data);
/**
* The name of the backend
*
* @return string
*/
public function __toString();
/**
* The name of the backend
*
* @return string
*/
public static function getName();
}

View File

@ -16,10 +16,15 @@ use \BadMethodCallException;
*/
class SystemResource implements IStorage
{
const NAME = 'SystemResource';
// Valid folders to look for resources
const VALID_FOLDERS = ["images"];
public static function get($filename)
/**
* @inheritDoc
*/
public function get(string $filename)
{
$folder = dirname($filename);
if (!in_array($folder, self::VALID_FOLDERS)) {
@ -31,25 +36,48 @@ class SystemResource implements IStorage
return file_get_contents($filename);
}
public static function put($data, $filename = "")
/**
* @inheritDoc
*/
public function put(string $data, string $filename = '')
{
throw new BadMethodCallException();
}
public static function delete($filename)
public function delete(string $filename)
{
throw new BadMethodCallException();
}
public static function getOptions()
/**
* @inheritDoc
*/
public function getOptions()
{
return [];
}
public static function saveOptions($data)
/**
* @inheritDoc
*/
public function saveOptions(array $data)
{
return [];
}
/**
* @inheritDoc
*/
public function __toString()
{
return self::NAME;
}
/**
* @inheritDoc
*/
public static function getName()
{
return self::NAME;
}
}

View File

@ -4,7 +4,7 @@
*/
namespace Friendica\Model;
use Friendica\Core\Cache;
use Friendica\Core\Cache\Cache;
use Friendica\Core\Logger;
use Friendica\Database\DBA;
use Friendica\DI;
@ -57,7 +57,7 @@ class Term
*/
public static function getGlobalTrendingHashtags(int $period, $limit = 10)
{
$tags = Cache::get('global_trending_tags');
$tags = DI::cache()->get('global_trending_tags');
if (!$tags) {
$tagsStmt = DBA::p("SELECT t.`term`, COUNT(*) AS `score`
@ -84,7 +84,7 @@ class Term
if (DBA::isResult($tagsStmt)) {
$tags = DBA::toArray($tagsStmt);
Cache::set('global_trending_tags', $tags, Cache::HOUR);
DI::cache()->set('global_trending_tags', $tags, Cache::HOUR);
}
}
@ -100,7 +100,7 @@ class Term
*/
public static function getLocalTrendingHashtags(int $period, $limit = 10)
{
$tags = Cache::get('local_trending_tags');
$tags = DI::cache()->get('local_trending_tags');
if (!$tags) {
$tagsStmt = DBA::p("SELECT t.`term`, COUNT(*) AS `score`
@ -129,7 +129,7 @@ class Term
if (DBA::isResult($tagsStmt)) {
$tags = DBA::toArray($tagsStmt);
Cache::set('local_trending_tags', $tags, Cache::HOUR);
DI::cache()->set('local_trending_tags', $tags, Cache::HOUR);
}
}

View File

@ -14,71 +14,196 @@ class Federation extends BaseAdminModule
{
parent::content($parameters);
// get counts on active friendica, diaspora, redmatrix, hubzilla, gnu
// social and statusnet nodes this node is knowing
//
// We are looking for the following platforms in the DB, "Red" should find
// all variants of that platform ID string as the q() function is stripping
// off one % two of them are needed in the query
// Add more platforms if you like, when one returns 0 known nodes it is not
// displayed on the stats page.
$platforms = ['Friendi%%a', 'Diaspora', '%%red%%', 'Hubzilla', 'BlaBlaNet', 'GNU Social', 'StatusNet', 'Mastodon', 'Pleroma', 'socialhome', 'ganggo'];
$colors = [
'Friendi%%a' => '#ffc018', // orange from the logo
'Diaspora' => '#a1a1a1', // logo is black and white, makes a gray
'%%red%%' => '#c50001', // fire red from the logo
'Hubzilla' => '#43488a', // blue from the logo
'BlaBlaNet' => '#3B5998', // blue from the navbar at blablanet-dot-com
'GNU Social' => '#a22430', // dark red from the logo
'StatusNet' => '#789240', // the green from the logo (red and blue have already others
'Mastodon' => '#1a9df9', // blue from the Mastodon logo
'Pleroma' => '#E46F0F', // Orange from the text that is used on Pleroma instances
'socialhome' => '#52056b', // lilac from the Django Image used at the Socialhome homepage
'ganggo' => '#69d7e2', // from the favicon
// get counts on active federation systems this node is knowing
// We list the more common systems by name. The rest is counted as "other"
$systems = [
'Friendica' => ['name' => 'Friendica', 'color' => '#ffc018'], // orange from the logo
'diaspora' => ['name' => 'Diaspora', 'color' => '#a1a1a1'], // logo is black and white, makes a gray
'funkwhale' => ['name' => 'Funkwhale', 'color' => '#4082B4'], // From the homepage
'gnusocial' => ['name' => 'GNU Social/Statusnet', 'color' => '#a22430'], // dark red from the logo
'hubzilla' => ['name' => 'Hubzilla/Red Matrix', 'color' => '#43488a'], // blue from the logo
'mastodon' => ['name' => 'Mastodon', 'color' => '#1a9df9'], // blue from the Mastodon logo
'misskey' => ['name' => 'Misskey', 'color' => '#ccfefd'], // Font color of the homepage
'peertube' => ['name' => 'Peertube', 'color' => '#ffad5c'], // One of the logo colors
'pixelfed' => ['name' => 'Pixelfed', 'color' => '#11da47'], // One of the logo colors
'pleroma' => ['name' => 'Pleroma', 'color' => '#E46F0F'], // Orange from the text that is used on Pleroma instances
'plume' => ['name' => 'Plume', 'color' => '#7765e3'], // From the homepage
'socialhome' => ['name' => 'SocialHome', 'color' => '#52056b'], // lilac from the Django Image used at the Socialhome homepage
'wordpress' => ['name' => 'WordPress', 'color' => '#016087'], // Background color of the homepage
'writefreely' => ['name' => 'WriteFreely', 'color' => '#292929'], // Font color of the homepage
'other' => ['name' => L10n::t('Other'), 'color' => '#F1007E'], // ActivityPub main color
];
$platforms = array_keys($systems);
$counts = [];
foreach ($platforms as $platform) {
$counts[$platform] = [];
}
$total = 0;
$users = 0;
foreach ($platforms as $platform) {
// get a total count for the platform, the name and version of the
// highest version and the protocol tpe
$platformCount = DBA::fetchFirst('SELECT
COUNT(*) AS `total`,
SUM(`registered-users`) AS `users`,
ANY_VALUE(`platform`) AS `platform`,
ANY_VALUE(`network`) AS `network`,
MAX(`version`) AS `version` FROM `gserver`
WHERE `platform` LIKE ?
AND `last_contact` >= `last_failure`
ORDER BY `version` ASC', $platform);
$total += $platformCount['total'];
$users += $platformCount['users'];
$gservers = DBA::p("SELECT COUNT(*) AS `total`, SUM(`registered-users`) AS `users`, `platform`,
ANY_VALUE(`network`) AS `network`, MAX(`version`) AS `version`
FROM `gserver` WHERE `last_contact` >= `last_failure` GROUP BY `platform`");
while ($gserver = DBA::fetch($gservers)) {
$total += $gserver['total'];
$users += $gserver['users'];
// what versions for that platform do we know at all?
// again only the active nodes
$versionCountsStmt = DBA::p('SELECT
COUNT(*) AS `total`,
`version` FROM `gserver`
WHERE `last_contact` >= `last_failure`
AND `platform` LIKE ?
GROUP BY `version`
ORDER BY `version`;', $platform);
$versionCounts = DBA::toArray($versionCountsStmt);
$versionCounts = [];
$versions = DBA::p("SELECT COUNT(*) AS `total`, `version` FROM `gserver`
WHERE `last_contact` >= `last_failure` AND `platform` = ?
GROUP BY `version` ORDER BY `version`", $gserver['platform']);
while ($version = DBA::fetch($versions)) {
$version['version'] = str_replace(["\n", "\r", "\t"], " ", $version['version']);
//
// clean up version numbers
//
// some platforms do not provide version information, add a unkown there
// to the version string for the displayed list.
foreach ($versionCounts as $key => $value) {
if ($versionCounts[$key]['version'] == '') {
$versionCounts[$key] = ['total' => $versionCounts[$key]['total'], 'version' => L10n::t('unknown')];
}
if (in_array($gserver['platform'], ['Red Matrix', 'redmatrix', 'red'])) {
$version['version'] = 'Red ' . $version['version'];
}
// Reformat and compact version numbers
if ($platform == 'Pleroma') {
$versionCounts[] = $version;
}
DBA::close($versions);
$platform = $gserver['platform'];
if ($platform == 'Friendika') {
$platform = 'Friendica';
} elseif (in_array($platform, ['Red Matrix', 'redmatrix', 'red'])) {
$platform = 'hubzilla';
} elseif(stristr($platform, 'pleroma')) {
$platform = 'pleroma';
} elseif(stristr($platform, 'statusnet')) {
$platform = 'gnusocial';
} elseif(stristr($platform, 'wordpress')) {
$platform = 'wordpress';
} elseif (!in_array($platform, $platforms)) {
$platform = 'other';
}
if ($platform != $gserver['platform']) {
if ($platform == 'other') {
$versionCounts = $counts[$platform][1] ?? [];
$versionCounts[] = ['version' => $gserver['platform'] ?: L10n::t('unknown'), 'total' => $gserver['total']];
$gserver['version'] = '';
} else {
$versionCounts = array_merge($versionCounts, $counts[$platform][1] ?? []);
}
$gserver['platform'] = $platform;
$gserver['total'] += $counts[$platform][0]['total'] ?? 0;
$gserver['users'] += $counts[$platform][0]['users'] ?? 0;
}
if ($platform == 'Friendica') {
$versionCounts = self::reformaFriendicaVersions($versionCounts);
} elseif ($platform == 'pleroma') {
$versionCounts = self::reformaPleromaVersions($versionCounts);
} elseif ($platform == 'diaspora') {
$versionCounts = self::reformaDiasporaVersions($versionCounts);
}
$versionCounts = self::sortVersion($versionCounts);
$gserver['platform'] = $systems[$platform]['name'];
$counts[$platform] = [$gserver, $versionCounts, str_replace([' ', '%'], '', $platform), $systems[$platform]['color']];
}
DBA::close($gserver);
// some helpful text
$intro = L10n::t('This page offers you some numbers to the known part of the federated social network your Friendica node is part of. These numbers are not complete but only reflect the part of the network your node is aware of.');
$hint = L10n::t('The <em>Auto Discovered Contact Directory</em> feature is not enabled, it will improve the data displayed here.');
// load the template, replace the macros and return the page content
$t = Renderer::getMarkupTemplate('admin/federation.tpl');
return Renderer::replaceMacros($t, [
'$title' => L10n::t('Administration'),
'$page' => L10n::t('Federation Statistics'),
'$intro' => $intro,
'$hint' => $hint,
'$autoactive' => Config::get('system', 'poco_completion'),
'$counts' => $counts,
'$version' => FRIENDICA_VERSION,
'$legendtext' => L10n::t('Currently this node is aware of %d nodes with %d registered users from the following platforms:', $total, $users),
]);
}
/**
* early friendica versions have the format x.x.xxxx where xxxx is the
* DB version stamp; those should be operated out and versions be combined
*
* @param array $versionCounts list of version numbers
* @return array with cleaned version numbers
*/
private static function reformaFriendicaVersions(array $versionCounts)
{
$newV = [];
$newVv = [];
foreach ($versionCounts as $vv) {
$newVC = $vv['total'];
$newVV = $vv['version'];
$lastDot = strrpos($newVV, '.');
$len = strlen($newVV) - 1;
if (($lastDot == $len - 4) && (!strrpos($newVV, '-rc') == $len - 3)) {
$newVV = substr($newVV, 0, $lastDot);
}
if (isset($newV[$newVV])) {
$newV[$newVV] += $newVC;
} else {
$newV[$newVV] = $newVC;
}
}
foreach ($newV as $key => $value) {
array_push($newVv, ['total' => $value, 'version' => $key]);
}
$versionCounts = $newVv;
return $versionCounts;
}
/**
* in the DB the Diaspora versions have the format x.x.x.x-xx the last
* part (-xx) should be removed to clean up the versions from the "head
* commit" information and combined into a single entry for x.x.x.x
*
* @param array $versionCounts list of version numbers
* @return array with cleaned version numbers
*/
private static function reformaDiasporaVersions(array $versionCounts)
{
$newV = [];
$newVv = [];
foreach ($versionCounts as $vv) {
$newVC = $vv['total'];
$newVV = $vv['version'];
$posDash = strpos($newVV, '-');
if ($posDash) {
$newVV = substr($newVV, 0, $posDash);
}
if (isset($newV[$newVV])) {
$newV[$newVV] += $newVC;
} else {
$newV[$newVV] = $newVC;
}
}
foreach ($newV as $key => $value) {
array_push($newVv, ['total' => $value, 'version' => $key]);
}
$versionCounts = $newVv;
return $versionCounts;
}
/**
* Clean up Pleroma version numbers
*
* @param array $versionCounts list of version numbers
* @return array with cleaned version numbers
*/
private static function reformaPleromaVersions(array $versionCounts)
{
$compacted = [];
foreach ($versionCounts as $key => $value) {
$version = $versionCounts[$key]['version'];
@ -103,57 +228,27 @@ class Federation extends BaseAdminModule
foreach ($compacted as $version => $pl_total) {
$versionCounts[] = ['version' => $version, 'total' => $pl_total];
}
return $versionCounts;
}
// in the DB the Diaspora versions have the format x.x.x.x-xx the last
// part (-xx) should be removed to clean up the versions from the "head
// commit" information and combined into a single entry for x.x.x.x
if ($platform == 'Diaspora') {
$newV = [];
$newVv = [];
foreach ($versionCounts as $vv) {
$newVC = $vv['total'];
$newVV = $vv['version'];
$posDash = strpos($newVV, '-');
if ($posDash) {
$newVV = substr($newVV, 0, $posDash);
/**
* Reformat, sort and compact version numbers
*
* @param array $versionCounts list of version numbers
* @return array with reformatted version numbers
*/
private static function sortVersion(array $versionCounts)
{
//
// clean up version numbers
//
// some platforms do not provide version information, add a unkown there
// to the version string for the displayed list.
foreach ($versionCounts as $key => $value) {
if ($versionCounts[$key]['version'] == '') {
$versionCounts[$key] = ['total' => $versionCounts[$key]['total'], 'version' => L10n::t('unknown')];
}
if (isset($newV[$newVV])) {
$newV[$newVV] += $newVC;
} else {
$newV[$newVV] = $newVC;
}
}
foreach ($newV as $key => $value) {
array_push($newVv, ['total' => $value, 'version' => $key]);
}
$versionCounts = $newVv;
}
// early friendica versions have the format x.x.xxxx where xxxx is the
// DB version stamp; those should be operated out and versions be
// conbined
if ($platform == 'Friendi%%a') {
$newV = [];
$newVv = [];
foreach ($versionCounts as $vv) {
$newVC = $vv['total'];
$newVV = $vv['version'];
$lastDot = strrpos($newVV, '.');
$len = strlen($newVV) - 1;
if (($lastDot == $len - 4) && (!strrpos($newVV, '-rc') == $len - 3)) {
$newVV = substr($newVV, 0, $lastDot);
}
if (isset($newV[$newVV])) {
$newV[$newVV] += $newVC;
} else {
$newV[$newVV] = $newVC;
}
}
foreach ($newV as $key => $value) {
array_push($newVv, ['total' => $value, 'version' => $key]);
}
$versionCounts = $newVv;
}
// Assure that the versions are sorted correctly
@ -172,26 +267,6 @@ class Federation extends BaseAdminModule
$versionCounts[] = $v2[$version];
}
// the 3rd array item is needed for the JavaScript graphs as JS does
// not like some characters in the names of variables...
$counts[$platform] = [$platformCount, $versionCounts, str_replace([' ', '%'], '', $platform), $colors[$platform]];
}
// some helpful text
$intro = L10n::t('This page offers you some numbers to the known part of the federated social network your Friendica node is part of. These numbers are not complete but only reflect the part of the network your node is aware of.');
$hint = L10n::t('The <em>Auto Discovered Contact Directory</em> feature is not enabled, it will improve the data displayed here.');
// load the template, replace the macros and return the page content
$t = Renderer::getMarkupTemplate('admin/federation.tpl');
return Renderer::replaceMacros($t, [
'$title' => L10n::t('Administration'),
'$page' => L10n::t('Federation Statistics'),
'$intro' => $intro,
'$hint' => $hint,
'$autoactive' => Config::get('system', 'poco_completion'),
'$counts' => $counts,
'$version' => FRIENDICA_VERSION,
'$legendtext' => L10n::t('Currently this node is aware of %d nodes with %d registered users from the following platforms:', $total, $users),
]);
return $versionCounts;
}
}

View File

@ -199,15 +199,11 @@ class Site extends BaseAdminModule
$relay_user_tags = !empty($_POST['relay_user_tags']);
$active_panel = (!empty($_POST['active_panel']) ? "#" . Strings::escapeTags(trim($_POST['active_panel'])) : '');
/**
* @var $storagebackend \Friendica\Model\Storage\IStorage
*/
$storagebackend = Strings::escapeTags(trim($_POST['storagebackend'] ?? ''));
// save storage backend form
if (!is_null($storagebackend) && $storagebackend != "") {
if (StorageManager::setBackend($storagebackend)) {
$storage_opts = $storagebackend::getOptions();
if (DI::storageManager()->setBackend($storagebackend)) {
$storage_opts = DI::storage()->getOptions();
$storage_form_prefix = preg_replace('|[^a-zA-Z0-9]|', '', $storagebackend);
$storage_opts_data = [];
foreach ($storage_opts as $name => $info) {
@ -225,7 +221,7 @@ class Site extends BaseAdminModule
unset($name);
unset($info);
$storage_form_errors = $storagebackend::saveOptions($storage_opts_data);
$storage_form_errors = DI::storage()->saveOptions($storage_opts_data);
if (count($storage_form_errors)) {
foreach ($storage_form_errors as $name => $err) {
notice('Storage backend, ' . $storage_opts[$name][1] . ': ' . $err);
@ -235,7 +231,6 @@ class Site extends BaseAdminModule
} else {
info(L10n::t('Invalid storage backend setting value.'));
}
}
// Has the directory url changed? If yes, then resubmit the existing profiles there
if ($global_directory != Config::get('system', 'directory') && ($global_directory != '')) {
@ -530,29 +525,25 @@ class Site extends BaseAdminModule
$optimize_max_tablesize = -1;
}
$storage_backends = StorageManager::listBackends();
/** @var $current_storage_backend \Friendica\Model\Storage\IStorage */
$current_storage_backend = StorageManager::getBackend();
$current_storage_backend = DI::storage();
$available_storage_backends = [];
// show legacy option only if it is the current backend:
// once changed can't be selected anymore
if ($current_storage_backend == '') {
if ($current_storage_backend == null) {
$available_storage_backends[''] = L10n::t('Database (legacy)');
}
foreach ($storage_backends as $name => $class) {
$available_storage_backends[$class] = $name;
foreach (DI::storageManager()->listBackends() as $name => $class) {
$available_storage_backends[$name] = $name;
}
unset($storage_backends);
// build storage config form,
$storage_form_prefix = preg_replace('|[^a-zA-Z0-9]|' ,'', $current_storage_backend);
$storage_form = [];
if (!is_null($current_storage_backend) && $current_storage_backend != '') {
foreach ($current_storage_backend::getOptions() as $name => $info) {
foreach ($current_storage_backend->getOptions() as $name => $info) {
$type = $info[0];
$info[0] = $storage_form_prefix . '_' . $name;
$info['type'] = $type;

View File

@ -2,13 +2,10 @@
namespace Friendica\Module\Search;
use Friendica\App\Arguments;
use Friendica\App\BaseURL;
use Friendica\Content\Nav;
use Friendica\Content\Pager;
use Friendica\Content\Text\HTML;
use Friendica\Content\Widget;
use Friendica\Core\Cache;
use Friendica\Core\Cache\Cache as CacheClass;
use Friendica\Core\Config;
use Friendica\Core\L10n;
@ -53,15 +50,15 @@ class Index extends BaseSearchModule
$crawl_permit_period = 10;
$remote = $_SERVER['REMOTE_ADDR'];
$result = Cache::get('remote_search:' . $remote);
$result = DI::cache()->get('remote_search:' . $remote);
if (!is_null($result)) {
$resultdata = json_decode($result);
if (($resultdata->time > (time() - $crawl_permit_period)) && ($resultdata->accesses > $free_crawls)) {
throw new HTTPException\TooManyRequestsException(L10n::t('Only one search per minute is permitted for not logged in users.'));
}
Cache::set('remote_search:' . $remote, json_encode(['time' => time(), 'accesses' => $resultdata->accesses + 1]), CacheClass::HOUR);
DI::cache()->set('remote_search:' . $remote, json_encode(['time' => time(), 'accesses' => $resultdata->accesses + 1]), CacheClass::HOUR);
} else {
Cache::set('remote_search:' . $remote, json_encode(['time' => time(), 'accesses' => 1]), CacheClass::HOUR);
DI::cache()->set('remote_search:' . $remote, json_encode(['time' => time(), 'accesses' => 1]), CacheClass::HOUR);
}
}

View File

@ -11,7 +11,7 @@ namespace Friendica\Network;
use DOMDocument;
use DomXPath;
use Friendica\Core\Cache;
use Friendica\Core\Cache\Cache;
use Friendica\Core\Config;
use Friendica\Core\Logger;
use Friendica\Core\Protocol;
@ -332,7 +332,7 @@ class Probe
public static function uri($uri, $network = '', $uid = -1, $cache = true)
{
if ($cache) {
$result = Cache::get('Probe::uri:' . $network . ':' . $uri);
$result = DI::cache()->get('Probe::uri:' . $network . ':' . $uri);
if (!is_null($result)) {
return $result;
}
@ -409,7 +409,7 @@ class Probe
// Only store into the cache if the value seems to be valid
if (!in_array($data['network'], [Protocol::PHANTOM, Protocol::MAIL])) {
Cache::set('Probe::uri:' . $network . ':' . $uri, $data, Cache::DAY);
DI::cache()->set('Probe::uri:' . $network . ':' . $uri, $data, Cache::DAY);
}
return $data;

View File

@ -7,7 +7,7 @@ namespace Friendica\Protocol\ActivityPub;
use Friendica\Content\Feature;
use Friendica\Content\Text\BBCode;
use Friendica\Content\Text\Plaintext;
use Friendica\Core\Cache;
use Friendica\Core\Cache\Cache;
use Friendica\Core\Config;
use Friendica\Core\Logger;
use Friendica\Core\Protocol;
@ -819,7 +819,7 @@ class Transmitter
$cachekey = 'APDelivery:createActivity:' . $item_id;
if (!$force) {
$data = Cache::get($cachekey);
$data = DI::cache()->get($cachekey);
if (!is_null($data)) {
return $data;
}
@ -827,7 +827,7 @@ class Transmitter
$data = ActivityPub\Transmitter::createActivityFromItem($item_id);
Cache::set($cachekey, $data, Cache::QUARTER_HOUR);
DI::cache()->set($cachekey, $data, Cache::QUARTER_HOUR);
return $data;
}

View File

@ -13,7 +13,7 @@ namespace Friendica\Protocol;
use Friendica\Content\Feature;
use Friendica\Content\Text\BBCode;
use Friendica\Content\Text\Markdown;
use Friendica\Core\Cache;
use Friendica\Core\Cache\Cache;
use Friendica\Core\Config;
use Friendica\Core\L10n;
use Friendica\Core\Logger;
@ -3246,7 +3246,7 @@ class Diaspora
$cachekey = "diaspora:sendParticipation:".$item['guid'];
$result = Cache::get($cachekey);
$result = DI::cache()->get($cachekey);
if (!is_null($result)) {
return;
}
@ -3272,7 +3272,7 @@ class Diaspora
Logger::log("Send participation for ".$item["guid"]." by ".$author, Logger::DEBUG);
// It doesn't matter what we store, we only want to avoid sending repeated notifications for the same item
Cache::set($cachekey, $item["guid"], Cache::QUARTER_HOUR);
DI::cache()->set($cachekey, $item["guid"], Cache::QUARTER_HOUR);
return self::buildAndTransmit($owner, $contact, "participation", $message);
}
@ -3524,7 +3524,7 @@ class Diaspora
{
$cachekey = "diaspora:buildStatus:".$item['guid'];
$result = Cache::get($cachekey);
$result = DI::cache()->get($cachekey);
if (!is_null($result)) {
return $result;
}
@ -3628,7 +3628,7 @@ class Diaspora
$msg = ["type" => $type, "message" => $message];
Cache::set($cachekey, $msg, Cache::QUARTER_HOUR);
DI::cache()->set($cachekey, $msg, Cache::QUARTER_HOUR);
return $msg;
}
@ -3749,7 +3749,7 @@ class Diaspora
{
$cachekey = "diaspora:constructComment:".$item['guid'];
$result = Cache::get($cachekey);
$result = DI::cache()->get($cachekey);
if (!is_null($result)) {
return $result;
}
@ -3798,7 +3798,7 @@ class Diaspora
$comment['thread_parent_guid'] = $thread_parent_item['guid'];
}
Cache::set($cachekey, $comment, Cache::QUARTER_HOUR);
DI::cache()->set($cachekey, $comment, Cache::QUARTER_HOUR);
return($comment);
}

View File

@ -8,7 +8,7 @@ use DOMDocument;
use DOMXPath;
use Friendica\Content\Text\BBCode;
use Friendica\Content\Text\HTML;
use Friendica\Core\Cache;
use Friendica\Core\Cache\Cache;
use Friendica\Core\Config;
use Friendica\Core\L10n;
use Friendica\Core\Lock;
@ -2185,7 +2185,7 @@ class OStatus
// Don't cache when the last item was posted less then 15 minutes ago (Cache duration)
if ((time() - strtotime($owner['last-item'])) < 15*60) {
$result = Cache::get($cachekey);
$result = DI::cache()->get($cachekey);
if (!$nocache && !is_null($result)) {
Logger::log('Feed duration: ' . number_format(microtime(true) - $stamp, 3) . ' - ' . $owner_nick . ' - ' . $filter . ' - ' . $previous_created . ' (cached)', Logger::DEBUG);
$last_update = $result['last_update'];
@ -2246,7 +2246,7 @@ class OStatus
$feeddata = trim($doc->saveXML());
$msg = ['feed' => $feeddata, 'last_update' => $last_update];
Cache::set($cachekey, $msg, Cache::QUARTER_HOUR);
DI::cache()->set($cachekey, $msg, Cache::QUARTER_HOUR);
Logger::log('Feed duration: ' . number_format(microtime(true) - $stamp, 3) . ' - ' . $owner_nick . ' - ' . $filter . ' - ' . $previous_created, Logger::DEBUG);

View File

@ -2,7 +2,6 @@
namespace Friendica\Util;
use Friendica\Core\Cache;
use Friendica\Core\Logger;
use Friendica\Core\System;
use Friendica\DI;
@ -125,12 +124,12 @@ class Images
return $data;
}
$data = Cache::get($url);
$data = DI::cache()->get($url);
if (empty($data) || !is_array($data)) {
$data = self::getInfoFromURL($url);
Cache::set($url, $data);
DI::cache()->set($url, $data);
}
return $data;

View File

@ -4,9 +4,10 @@
*/
namespace Friendica\Util;
use Friendica\Core\Cache;
use Friendica\Core\Cache\Cache;
use Friendica\Core\Logger;
use Exception;
use Friendica\DI;
/**
* @brief This class contain methods to work with JsonLD data
@ -39,13 +40,13 @@ class JsonLD
exit();
}
$result = Cache::get('documentLoader:' . $url);
$result = DI::cache()->get('documentLoader:' . $url);
if (!is_null($result)) {
return $result;
}
$data = jsonld_default_document_loader($url);
Cache::set('documentLoader:' . $url, $data, Cache::DAY);
DI::cache()->set('documentLoader:' . $url, $data, Cache::DAY);
return $data;
}

View File

@ -84,6 +84,8 @@ class Cron
// check upstream version?
Worker::add(PRIORITY_LOW, 'CheckVersion');
self::checkdeletedContacts();
Config::set('system', 'last_expire_day', $d2);
}
@ -121,6 +123,19 @@ class Cron
return;
}
/**
* Checks for contacts that are about to be deleted and ensures that they are removed.
* This should be done automatically in the "remove" function. This here is a cleanup job.
*/
private static function checkdeletedContacts()
{
$contacts = DBA::select('contact', ['id'], ['deleted' => true]);
while ($contact = DBA::fetch($contacts)) {
Worker::add(PRIORITY_MEDIUM, 'RemoveContact', $contact['id']);
}
DBA::close($contacts);
}
/**
* @brief Update public contacts
* @throws \Friendica\Network\HTTPException\InternalServerErrorException

View File

@ -5,7 +5,6 @@
namespace Friendica\Worker;
use Friendica\App;
use Friendica\Core\Cache;
use Friendica\Core\Config;
use Friendica\Core\Logger;
use Friendica\Core\Protocol;
@ -154,7 +153,7 @@ class CronJobs
}
// clear old cache
Cache::clear();
DI::cache()->clear();
// clear old item cache files
clear_cache();
@ -324,8 +323,8 @@ class CronJobs
*/
private static function moveStorage()
{
$current = StorageManager::getBackend();
$moved = StorageManager::move($current);
$current = DI::storage();
$moved = DI::storageManager()->move($current);
if ($moved) {
Worker::add(PRIORITY_LOW, "CronJobs", "move_storage");

View File

@ -197,6 +197,11 @@ class Delivery
$contact['network'] = Protocol::DIASPORA;
}
// Ensure that local contacts are delivered locally
if (Model\Contact::isLocal($contact['url'])) {
$contact['network'] = Protocol::DFRN;
}
Logger::notice('Delivering', ['cmd' => $cmd, 'target' => $target_id, 'followup' => $followup, 'network' => $contact['network']]);
switch ($contact['network']) {
@ -287,11 +292,8 @@ class Delivery
Logger::debug('Notifier entry: ' . $contact["url"] . ' ' . (($target_item['guid'] ?? '') ?: $target_item['id']) . ' entry: ' . $atom);
$basepath = implode('/', array_slice(explode('/', $contact['url']), 0, 3));
// perform local delivery if we are on the same site
if (Strings::compareLink($basepath, DI::baseUrl())) {
if (Model\Contact::isLocal($contact['url'])) {
$condition = ['nurl' => Strings::normaliseLink($contact['url']), 'self' => true];
$target_self = DBA::selectFirst('contact', ['uid'], $condition);
if (!DBA::isResult($target_self)) {

View File

@ -444,6 +444,11 @@ class Notifier
if (DBA::isResult($r)) {
foreach ($r as $rr) {
// Ensure that local contacts are delivered via DFRN
if (Contact::isLocal($rr['url'])) {
$contact['network'] = Protocol::DFRN;
}
if (!empty($rr['addr']) && ($rr['network'] == Protocol::ACTIVITYPUB) && !DBA::exists('fcontact', ['addr' => $rr['addr']])) {
Logger::info('Contact is AP omly', ['target' => $target_id, 'contact' => $rr['url']]);
continue;
@ -489,6 +494,11 @@ class Notifier
// delivery loop
while ($contact = DBA::fetch($delivery_contacts_stmt)) {
// Ensure that local contacts are delivered via DFRN
if (Contact::isLocal($contact['url'])) {
$contact['network'] = Protocol::DFRN;
}
if (!empty($contact['addr']) && ($contact['network'] == Protocol::ACTIVITYPUB) && !DBA::exists('fcontact', ['addr' => $contact['addr']])) {
Logger::info('Contact is AP omly', ['target' => $target_id, 'contact' => $contact['url']]);
continue;

View File

@ -13,8 +13,7 @@ class RemoveContact {
public static function execute($id) {
// Only delete if the contact is to be deleted
$condition = ['network' => Protocol::PHANTOM, 'id' => $id];
$contact = DBA::selectFirst('contact', ['uid'], $condition);
$contact = DBA::selectFirst('contact', ['uid'], ['deleted' => true]);
if (!DBA::isResult($contact)) {
return;
}

View File

@ -4,12 +4,13 @@
*/
namespace Friendica\Worker;
use Friendica\Core\Cache;
use Friendica\Core\Cache\Cache;
use Friendica\Core\Config;
use Friendica\Core\Logger;
use Friendica\Core\Protocol;
use Friendica\Core\Search;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\GContact;
use Friendica\Model\GServer;
use Friendica\Network\Probe;
@ -26,7 +27,7 @@ class SearchDirectory
return;
}
$data = Cache::get('SearchDirectory:' . $search);
$data = DI::cache()->get('SearchDirectory:' . $search);
if (!is_null($data)) {
// Only search for the same item every 24 hours
if (time() < $data + (60 * 60 * 24)) {
@ -80,6 +81,6 @@ class SearchDirectory
}
}
}
Cache::set('SearchDirectory:' . $search, time(), Cache::DAY);
DI::cache()->set('SearchDirectory:' . $search, time(), Cache::DAY);
}
}

View File

@ -34,7 +34,7 @@
use Friendica\Database\DBA;
if (!defined('DB_UPDATE_VERSION')) {
define('DB_UPDATE_VERSION', 1329);
define('DB_UPDATE_VERSION', 1330);
}
return [

View File

@ -8,8 +8,10 @@ use Friendica\Core\L10n\L10n;
use Friendica\Core\Lock\ILock;
use Friendica\Core\Process;
use Friendica\Core\Session\ISession;
use Friendica\Core\StorageManager;
use Friendica\Database\Database;
use Friendica\Factory;
use Friendica\Model\Storage\IStorage;
use Friendica\Model\User\Cookie;
use Friendica\Util;
use Psr\Log\LoggerInterface;
@ -193,5 +195,11 @@ return [
'constructParams' => [
$_SERVER, $_COOKIE
],
]
],
IStorage::class => [
'instanceOf' => StorageManager::class,
'call' => [
['getBackend', [], Dice::CHAIN_CALL],
],
],
];

View File

@ -2,7 +2,7 @@
namespace Friendica\Test\Util;
use Friendica\Core\Cache;
use Friendica\Core\Cache\Cache;
use Friendica\Core\Lock\DatabaseLock;
trait DbaLockMockTrait

View File

@ -0,0 +1,106 @@
<?php
namespace Friendica\Test\Util;
use Friendica\App;
use Friendica\Core\Hook;
use Friendica\Model\Storage\IStorage;
use Friendica\Core\L10n\L10n;
use Mockery\MockInterface;
/**
* A backend storage example class
*/
class SampleStorageBackend implements IStorage
{
const NAME = 'Sample Storage';
/** @var L10n */
private $l10n;
/** @var array */
private $options = [
'filename' => [
'input', // will use a simple text input
'The file to return', // the label
'sample', // the current value
'Enter the path to a file', // the help text
// no extra data for 'input' type..
],
];
/** @var array Just save the data in memory */
private $data = [];
/**
* SampleStorageBackend constructor.
*
* @param L10n $l10n The configuration of Friendica
*
* You can add here every dynamic class as dependency you like and add them to a private field
* Friendica automatically creates these classes and passes them as argument to the constructor
*/
public function __construct(L10n $l10n)
{
$this->l10n = $l10n;
}
public function get(string $reference)
{
// we return always the same image data. Which file we load is defined by
// a config key
return $this->data[$reference] ?? null;
}
public function put(string $data, string $reference = '')
{
if ($reference === '') {
$reference = 'sample';
}
$this->data[$reference] = $data;
return $reference;
}
public function delete(string $reference)
{
if (isset($this->data[$reference])) {
unset($this->data[$reference]);
}
return true;
}
public function getOptions()
{
return $this->options;
}
public function saveOptions(array $data)
{
$this->options = $data;
// no errors, return empty array
return $this->options;
}
public function __toString()
{
return self::NAME;
}
public static function getName()
{
return self::NAME;
}
/**
* This one is a hack to register this class to the hook
*/
public static function registerHook()
{
Hook::register('storage_instance', __DIR__ . '/SampleStorageBackendInstance.php', 'create_instance');
}
}

View File

@ -0,0 +1,18 @@
<?php
// contains a test-hook call for creating a storage instance
use Friendica\App;
use Friendica\Core\L10n\L10n;
use Friendica\Test\Util\SampleStorageBackend;
use Mockery\MockInterface;
function create_instance(App $a, &$data)
{
/** @var L10n|MockInterface $l10n */
$l10n = \Mockery::mock(L10n::class);
if ($data['name'] == SampleStorageBackend::getName()) {
$data['storage'] = new SampleStorageBackend($l10n);
}
}

View File

@ -0,0 +1,40 @@
<?php
return [
'photo' => [
// move from data-attribute to storage backend
[
'id' => 1,
'backend-class' => null,
'backend-ref' => 'f0c0d0i2',
'data' => 'without class',
],
// move from storage-backend to maybe filesystem backend, skip at database backend
[
'id' => 2,
'backend-class' => 'Database',
'backend-ref' => 1,
'data' => '',
],
// move data if invalid storage
[
'id' => 3,
'backend-class' => 'invalid!',
'backend-ref' => 'unimported',
'data' => 'invalid data moved',
],
// skip everytime because of invalid storage and no data
[
'id' => 3,
'backend-class' => 'invalid!',
'backend-ref' => 'unimported',
'data' => '',
],
],
'storage' => [
[
'id' => 1,
'data' => 'inside database',
],
],
];

View File

@ -0,0 +1,264 @@
<?php
namespace Friendica\Test\src\Core;
use Dice\Dice;
use Friendica\Core\Config\IConfiguration;
use Friendica\Core\Config\PreloadConfiguration;
use Friendica\Core\Hook;
use Friendica\Core\L10n\L10n;
use Friendica\Core\Session\ISession;
use Friendica\Core\StorageManager;
use Friendica\Database\Database;
use Friendica\DI;
use Friendica\Factory\ConfigFactory;
use Friendica\Model\Config\Config;
use Friendica\Model\Storage;
use Friendica\Core\Session;
use Friendica\Test\DatabaseTest;
use Friendica\Test\Util\Database\StaticDatabase;
use Friendica\Test\Util\VFSTrait;
use Friendica\Util\ConfigFileLoader;
use Friendica\Util\Profiler;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Friendica\Test\Util\SampleStorageBackend;
class StorageManagerTest extends DatabaseTest
{
/** @var Database */
private $dba;
/** @var IConfiguration */
private $config;
/** @var LoggerInterface */
private $logger;
/** @var L10n */
private $l10n;
use VFSTrait;
public function setUp()
{
parent::setUp();
$this->setUpVfsDir();
$this->logger = new NullLogger();
$profiler = \Mockery::mock(Profiler::class);
$profiler->shouldReceive('saveTimestamp')->withAnyArgs()->andReturn(true);
// load real config to avoid mocking every config-entry which is related to the Database class
$configFactory = new ConfigFactory();
$loader = new ConfigFileLoader($this->root->url());
$configCache = $configFactory->createCache($loader);
$this->dba = new StaticDatabase($configCache, $profiler, $this->logger);
$configModel = new Config($this->dba);
$this->config = new PreloadConfiguration($configCache, $configModel);
$this->l10n = \Mockery::mock(L10n::class);
}
/**
* Test plain instancing first
*/
public function testInstance()
{
$storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n);
$this->assertInstanceOf(StorageManager::class, $storageManager);
}
public function dataStorages()
{
return [
'empty' => [
'name' => '',
'assert' => null,
'assertName' => '',
'userBackend' => false,
],
'database' => [
'name' => Storage\Database::NAME,
'assert' => Storage\Database::class,
'assertName' => Storage\Database::NAME,
'userBackend' => true,
],
'filesystem' => [
'name' => Storage\Filesystem::NAME,
'assert' => Storage\Filesystem::class,
'assertName' => Storage\Filesystem::NAME,
'userBackend' => true,
],
'systemresource' => [
'name' => Storage\SystemResource::NAME,
'assert' => Storage\SystemResource::class,
'assertName' => Storage\SystemResource::NAME,
// false here, because SystemResource isn't meant to be a user backend,
// it's for system resources only
'userBackend' => false,
],
'invalid' => [
'name' => 'invalid',
'assert' => null,
'assertName' => '',
'userBackend' => false,
],
];
}
/**
* Test the getByName() method
*
* @dataProvider dataStorages
*/
public function testGetByName($name, $assert, $assertName, $userBackend)
{
$storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n);
$storage = $storageManager->getByName($name, $userBackend);
if (!empty($assert)) {
$this->assertInstanceOf(Storage\IStorage::class, $storage);
$this->assertInstanceOf($assert, $storage);
$this->assertEquals($name, $storage::getName());
} else {
$this->assertNull($storage);
}
$this->assertEquals($assertName, $storage);
}
/**
* Test the isValidBackend() method
*
* @dataProvider dataStorages
*/
public function testIsValidBackend($name, $assert, $assertName, $userBackend)
{
$storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n);
$this->assertEquals($userBackend, $storageManager->isValidBackend($name));
}
/**
* Test the method listBackends() with default setting
*/
public function testListBackends()
{
$storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n);
$this->assertEquals(StorageManager::DEFAULT_BACKENDS, $storageManager->listBackends());
}
/**
* Test the method getBackend()
*
* @dataProvider dataStorages
*/
public function testGetBackend($name, $assert, $assertName, $userBackend)
{
$storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n);
$this->assertNull($storageManager->getBackend());
if ($userBackend) {
$storageManager->setBackend($name);
$this->assertInstanceOf($assert, $storageManager->getBackend());
}
}
/**
* Test the method getBackend() with a pre-configured backend
*
* @dataProvider dataStorages
*/
public function testPresetBackend($name, $assert, $assertName, $userBackend)
{
$this->config->set('storage', 'name', $name);
$storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n);
if ($userBackend) {
$this->assertInstanceOf($assert, $storageManager->getBackend());
} else {
$this->assertNull($storageManager->getBackend());
}
}
/**
* Tests the register and unregister methods for a new backend storage class
*
* Uses a sample storage for testing
*
* @see SampleStorageBackend
*/
public function testRegisterUnregisterBackends()
{
/// @todo Remove dice once "Hook" is dynamic and mockable
$dice = (new Dice())
->addRules(include __DIR__ . '/../../../static/dependencies.config.php')
->addRule(Database::class, ['instanceOf' => StaticDatabase::class, 'shared' => true])
->addRule(ISession::class, ['instanceOf' => Session\Memory::class, 'shared' => true, 'call' => null]);
DI::init($dice);
$storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n);
$this->assertTrue($storageManager->register(SampleStorageBackend::class));
$this->assertEquals(array_merge(StorageManager::DEFAULT_BACKENDS, [
SampleStorageBackend::getName() => SampleStorageBackend::class,
]), $storageManager->listBackends());
$this->assertEquals(array_merge(StorageManager::DEFAULT_BACKENDS, [
SampleStorageBackend::getName() => SampleStorageBackend::class,
]), $this->config->get('storage', 'backends'));
// inline call to register own class as hook (testing purpose only)
SampleStorageBackend::registerHook();
Hook::loadHooks();
$this->assertTrue($storageManager->setBackend(SampleStorageBackend::NAME));
$this->assertEquals(SampleStorageBackend::NAME, $this->config->get('storage', 'name'));
$this->assertInstanceOf(SampleStorageBackend::class, $storageManager->getBackend());
$this->assertTrue($storageManager->unregister(SampleStorageBackend::class));
$this->assertEquals(StorageManager::DEFAULT_BACKENDS, $this->config->get('storage', 'backends'));
$this->assertEquals(StorageManager::DEFAULT_BACKENDS, $storageManager->listBackends());
$this->assertNull($storageManager->getBackend());
$this->assertNull($this->config->get('storage', 'name'));
}
/**
* Test moving data to a new storage (currently testing db & filesystem)
*
* @dataProvider dataStorages
*/
public function testMoveStorage($name, $assert, $assertName, $userBackend)
{
if (!$userBackend) {
return;
}
$this->loadFixture(__DIR__ . '/../../datasets/storage/database.fixture.php', $this->dba);
$storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n);
$storage = $storageManager->getByName($name);
$storageManager->move($storage);
$photos = $this->dba->select('photo', ['backend-ref', 'backend-class', 'id', 'data']);
while ($photo = $this->dba->fetch($photos)) {
$this->assertEmpty($photo['data']);
$storage = $storageManager->getByName($photo['backend-class']);
$data = $storage->get($photo['backend-ref']);
$this->assertNotEmpty($data);
}
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace Friendica\Test\src\Model\Storage;
use Friendica\Core\L10n\L10n;
use Friendica\Factory\ConfigFactory;
use Friendica\Model\Storage\Database;
use Friendica\Model\Storage\IStorage;
use Friendica\Test\DatabaseTestTrait;
use Friendica\Test\Util\Database\StaticDatabase;
use Friendica\Test\Util\VFSTrait;
use Friendica\Util\ConfigFileLoader;
use Friendica\Util\Profiler;
use Mockery\MockInterface;
use Psr\Log\NullLogger;
class DatabaseStorageTest extends StorageTest
{
use DatabaseTestTrait;
use VFSTrait;
protected function setUp()
{
$this->setUpVfsDir();
parent::setUp();
}
protected function getInstance()
{
$logger = new NullLogger();
$profiler = \Mockery::mock(Profiler::class);
$profiler->shouldReceive('saveTimestamp')->withAnyArgs()->andReturn(true);
// load real config to avoid mocking every config-entry which is related to the Database class
$configFactory = new ConfigFactory();
$loader = new ConfigFileLoader($this->root->url());
$configCache = $configFactory->createCache($loader);
$dba = new StaticDatabase($configCache, $profiler, $logger);
/** @var MockInterface|L10n $l10n */
$l10n = \Mockery::mock(L10n::class)->makePartial();
return new Database($dba, $logger, $l10n);
}
protected function assertOption(IStorage $storage)
{
$this->assertEmpty($storage->getOptions());
}
}

View File

@ -0,0 +1,111 @@
<?php
namespace Friendica\Test\src\Model\Storage;
use Friendica\Core\Config\IConfiguration;
use Friendica\Core\L10n\L10n;
use Friendica\Model\Storage\Filesystem;
use Friendica\Model\Storage\IStorage;
use Friendica\Test\Util\VFSTrait;
use Friendica\Util\Profiler;
use Mockery\MockInterface;
use org\bovigo\vfs\vfsStream;
use Psr\Log\NullLogger;
use function GuzzleHttp\Psr7\uri_for;
class FilesystemStorageTest extends StorageTest
{
use VFSTrait;
/** @var MockInterface|IConfiguration */
protected $config;
protected function setUp()
{
$this->setUpVfsDir();
vfsStream::create(['storage' => []], $this->root);
parent::setUp();
}
protected function getInstance()
{
$logger = new NullLogger();
$profiler = \Mockery::mock(Profiler::class);
$profiler->shouldReceive('saveTimestamp')->withAnyArgs()->andReturn(true);
/** @var MockInterface|L10n $l10n */
$l10n = \Mockery::mock(L10n::class)->makePartial();
$this->config = \Mockery::mock(IConfiguration::class);
$this->config->shouldReceive('get')
->with('storage', 'filesystem_path', Filesystem::DEFAULT_BASE_FOLDER)
->andReturn($this->root->getChild('storage')->url());
return new Filesystem($this->config, $logger, $l10n);
}
protected function assertOption(IStorage $storage)
{
$this->assertEquals([
'storagepath' => [
'input', 'Storage base path',
$this->root->getChild('storage')->url(),
'Folder where uploaded files are saved. For maximum security, This should be a path outside web server folder tree'
]
], $storage->getOptions());
}
/**
* Test the exception in case of missing directorsy permissions
*
* @expectedException \Friendica\Model\Storage\StorageException
* @expectedExceptionMessageRegExp /Filesystem storage failed to create \".*\". Check you write permissions./
*/
public function testMissingDirPermissions()
{
$this->root->getChild('storage')->chmod(000);
$instance = $this->getInstance();
$instance->put('test');
}
/**
* Test the exception in case of missing file permissions
*
* @expectedException \Friendica\Model\Storage\StorageException
* @expectedExceptionMessageRegExp /Filesystem storage failed to save data to \".*\". Check your write permissions/
*/
public function testMissingFilePermissions()
{
$this->markTestIncomplete("Cannot catch file_put_content() error due vfsStream failure");
vfsStream::create(['storage' => ['f0' => ['c0' => ['k0i0' => '']]]], $this->root);
$this->root->getChild('storage/f0/c0/k0i0')->chmod(000);
$instance = $this->getInstance();
$instance->put('test', 'f0c0k0i0');
}
/**
* Test the backend storage of the Filesystem Storage class
*/
public function testDirectoryTree()
{
$instance = $this->getInstance();
$instance->put('test', 'f0c0d0i0');
$dir = $this->root->getChild('storage/f0/c0')->url();
$file = $this->root->getChild('storage/f0/c0/d0i0')->url();
$this->assertDirectoryExists($dir);
$this->assertFileExists($file);
$this->assertDirectoryIsWritable($dir);
$this->assertFileIsWritable($file);
$this->assertEquals('test', file_get_contents($file));
}
}

View File

@ -0,0 +1,96 @@
<?php
namespace Friendica\Test\src\Model\Storage;
use Friendica\Model\Storage\IStorage;
use Friendica\Test\MockedTest;
abstract class StorageTest extends MockedTest
{
/** @return IStorage */
abstract protected function getInstance();
abstract protected function assertOption(IStorage $storage);
/**
* Test if the instance is "really" implementing the interface
*/
public function testInstance()
{
$instance = $this->getInstance();
$this->assertInstanceOf(IStorage::class, $instance);
}
/**
* Test if the "getOption" is asserted
*/
public function testGetOptions()
{
$instance = $this->getInstance();
$this->assertOption($instance);
}
/**
* Test basic put, get and delete operations
*/
public function testPutGetDelete()
{
$instance = $this->getInstance();
$ref = $instance->put('data12345');
$this->assertNotEmpty($ref);
$this->assertEquals('data12345', $instance->get($ref));
$this->assertTrue($instance->delete($ref));
}
/**
* Test a delete with an invalid reference
*/
public function testInvalidDelete()
{
$instance = $this->getInstance();
// Even deleting not existing references should return "true"
$this->assertTrue($instance->delete(-1234456));
}
/**
* Test a get with an invalid reference
*/
public function testInvalidGet()
{
$instance = $this->getInstance();
// Invalid references return an empty string
$this->assertEmpty($instance->get(-123456));
}
/**
* Test an update with a given reference
*/
public function testUpdateReference()
{
$instance = $this->getInstance();
$ref = $instance->put('data12345');
$this->assertNotEmpty($ref);
$this->assertEquals('data12345', $instance->get($ref));
$this->assertEquals($ref, $instance->put('data5432', $ref));
$this->assertEquals('data5432', $instance->get($ref));
}
/**
* Test that an invalid update results in an insert
*/
public function testInvalidUpdate()
{
$instance = $this->getInstance();
$this->assertEquals(-123, $instance->put('data12345', -123));
}
}

View File

@ -12,6 +12,7 @@ use Friendica\Model\Contact;
use Friendica\Model\GContact;
use Friendica\Model\Item;
use Friendica\Model\User;
use Friendica\Model\Storage;
use Friendica\Util\DateTimeFormat;
use Friendica\Worker\Delivery;
@ -408,3 +409,26 @@ function update_1327()
return Update::SUCCESS;
}
function update_1330()
{
$currStorage = Config::get('storage', 'class', '');
// set the name of the storage instead of the classpath as config
if (!empty($currStorage)) {
/** @var Storage\IStorage $currStorage */
if (!Config::set('storage', 'name', $currStorage::getName())) {
return Update::FAILED;
}
// try to delete the class since it isn't needed. This won't work with config files
Config::delete('storage', 'class');
}
// Update attachments and photos
if (!DBA::p("UPDATE `photo` SET `photo`.`backend-class` = SUBSTR(`photo`.`backend-class`, 22) WHERE `photo`.`backend-class` LIKE 'Friendica\\Model\\Storage\\%'") ||
!DBA::p("UPDATE `attach` SET `attach`.`backend-class` = SUBSTR(`attach`.`backend-class`, 22) WHERE `attach`.`backend-class` LIKE 'Friendica\\Model\\Storage\\%'")) {
return Update::FAILED;
};
return Update::SUCCESS;
}