diff --git a/database.sql b/database.sql index 6252f8057..8db6c1d6f 100644 --- a/database.sql +++ b/database.sql @@ -1,6 +1,6 @@ -- ------------------------------------------ -- Friendica 2020.03-dev (Dalmatian Bellflower) --- DB_UPDATE_VERSION 1329 +-- DB_UPDATE_VERSION 1330 -- ------------------------------------------ diff --git a/doc/AddonStorageBackend.md b/doc/AddonStorageBackend.md index d42c8bbbd..f69dfff45 100644 --- a/doc/AddonStorageBackend.md +++ b/doc/AddonStorageBackend.md @@ -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 */ -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()); + } +} +``` diff --git a/doc/Addons.md b/doc/Addons.md index d448b026b..9260ee013 100644 --- a/doc/Addons.md +++ b/doc/Addons.md @@ -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); diff --git a/doc/de/Addons.md b/doc/de/Addons.md index 32add2181..37bf114d2 100644 --- a/doc/de/Addons.md +++ b/doc/de/Addons.md @@ -424,6 +424,10 @@ Eine komplette Liste aller Hook-Callbacks mit den zugehörigen Dateien (am 01-Ap ### src/Core/Authentication.php Hook::callAll('logged_in', $a->user); + +### src/Core/StorageManager + + Hook::callAll('storage_instance', $data); ### src/Worker/Directory.php diff --git a/mod/events.php b/mod/events.php index d1d3d758b..9e75e1d2e 100644 --- a/mod/events.php +++ b/mod/events.php @@ -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'), diff --git a/mod/ping.php b/mod/ping.php index c76780f69..26d0efb37 100644 --- a/mod/ping.php +++ b/mod/ping.php @@ -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); } } diff --git a/mod/poco.php b/mod/poco.php index 2ed871285..595a4bd2e 100644 --- a/mod/poco.php +++ b/mod/poco.php @@ -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 diff --git a/src/Console/Storage.php b/src/Console/Storage.php index 30b556782..f4b4de562 100644 --- a/src/Console/Storage.php +++ b/src/Console/Storage.php @@ -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 = <<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)); } diff --git a/src/Content/OEmbed.php b/src/Content/OEmbed.php index 95fd6530b..4bcb485fc 100644 --- a/src/Content/OEmbed.php +++ b/src/Content/OEmbed.php @@ -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') { diff --git a/src/Content/Text/BBCode.php b/src/Content/Text/BBCode.php index 51fa08ad3..19ff88a9c 100644 --- a/src/Content/Text/BBCode.php +++ b/src/Content/Text/BBCode.php @@ -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; } diff --git a/src/Content/Widget/TrendingTags.php b/src/Content/Widget/TrendingTags.php index a7cfa8557..4ac8a1119 100644 --- a/src/Content/Widget/TrendingTags.php +++ b/src/Content/Widget/TrendingTags.php @@ -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; /** diff --git a/src/Core/Cache.php b/src/Core/Cache.php deleted file mode 100644 index d0a04b9ec..000000000 --- a/src/Core/Cache.php +++ /dev/null @@ -1,102 +0,0 @@ -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); - } -} diff --git a/src/Core/Lock/DatabaseLock.php b/src/Core/Lock/DatabaseLock.php index 0788f04b2..eeb488fba 100644 --- a/src/Core/Lock/DatabaseLock.php +++ b/src/Core/Lock/DatabaseLock.php @@ -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; diff --git a/src/Core/StorageManager.php b/src/Core/StorageManager.php index 832d9819c..6a8fac5b8 100644 --- a/src/Core/StorageManager.php +++ b/src/Core/StorageManager.php @@ -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); - - return true; + 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 int $limit Limit of the process batch size, defaults to 5000 + * @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( - $table, + $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)) { - $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); + while ($resource = $this->dba->fetch($resources)) { + $id = $resource['id']; + $data = $resource['data']; + $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; } } - diff --git a/src/Core/Update.php b/src/Core/Update.php index badbdc785..62ea573db 100644 --- a/src/Core/Update.php +++ b/src/Core/Update.php @@ -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 { diff --git a/src/DI.php b/src/DI.php index 7ed581254..b7be36212 100644 --- a/src/DI.php +++ b/src/DI.php @@ -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() @@ -47,38 +49,40 @@ use Psr\Log\LoggerInterface; abstract class DI { const CLASS_MAPPING = [ - 'app' => App::class, - 'auth' => App\Authentication::class, - 'args' => App\Arguments::class, - 'baseUrl' => App\BaseURL::class, - 'mode' => App\Mode::class, - 'module' => App\Module::class, - 'page' => App\Page::class, - 'router' => App\Router::class, - 'contentItem' => Content\Item::class, - 'bbCodeVideo' => Content\Text\BBCode\Video::class, - 'cache' => Core\Cache\ICache::class, - 'config' => Core\Config\IConfiguration::class, - 'pConfig' => Core\Config\IPConfiguration::class, - 'l10n' => Core\L10n\L10n::class, - 'lock' => Core\Lock\ILock::class, - 'process' => Core\Process::class, - 'session' => Core\Session\ISession::class, - 'dba' => Database\Database::class, - 'mstdnAccount' => Factory\Mastodon\Account::class, + 'app' => App::class, + 'auth' => App\Authentication::class, + 'args' => App\Arguments::class, + 'baseUrl' => App\BaseURL::class, + 'mode' => App\Mode::class, + 'module' => App\Module::class, + 'page' => App\Page::class, + 'router' => App\Router::class, + 'contentItem' => Content\Item::class, + 'bbCodeVideo' => Content\Text\BBCode\Video::class, + 'cache' => Core\Cache\ICache::class, + 'config' => Core\Config\IConfiguration::class, + 'pConfig' => Core\Config\IPConfiguration::class, + 'l10n' => Core\L10n\L10n::class, + '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, - 'intro' => Repository\Introduction::class, - 'activity' => Protocol\Activity::class, - 'aclFormatter' => Util\ACLFormatter::class, - 'dtFormat' => Util\DateTimeFormat::class, - 'fs' => Util\FileSystem::class, - 'workerLogger' => Util\Logger\WorkerLogger::class, - 'profiler' => Util\Profiler::class, - 'logger' => LoggerInterface::class, - 'devLogger' => '$devLogger', + '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, + 'dtFormat' => Util\DateTimeFormat::class, + 'fs' => Util\FileSystem::class, + 'workerLogger' => Util\Logger\WorkerLogger::class, + 'profiler' => Util\Profiler::class, + 'logger' => LoggerInterface::class, + 'devLogger' => '$devLogger', ]; /** @var Dice */ diff --git a/src/Model/Attach.php b/src/Model/Attach.php index c1d5c033b..fc0ebb8e2 100644 --- a/src/Model/Attach.php +++ b/src/Model/Attach.php @@ -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); - $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'] ?? ''); } } diff --git a/src/Model/Contact.php b/src/Model/Contact.php index 0540bf7be..e6c00b1e3 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -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); diff --git a/src/Model/GServer.php b/src/Model/GServer.php index fe5ef6d6f..9c75da347 100644 --- a/src/Model/GServer.php +++ b/src/Model/GServer.php @@ -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,11 +1071,20 @@ 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'], '"'); - $serverdata['network'] = Protocol::OSTATUS; + + 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 * diff --git a/src/Model/Mail.php b/src/Model/Mail.php index eeea130a6..292d797b8 100644 --- a/src/Model/Mail.php +++ b/src/Model/Mail.php @@ -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()]); } } } diff --git a/src/Model/Nodeinfo.php b/src/Model/Nodeinfo.php index 6d9c77154..7cde25953 100644 --- a/src/Model/Nodeinfo.php +++ b/src/Model/Nodeinfo.php @@ -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); } } diff --git a/src/Model/Photo.php b/src/Model/Photo.php index c4dbf2b30..e70ac2d97 100644 --- a/src/Model/Photo.php +++ b/src/Model/Photo.php @@ -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']); } /** @@ -222,11 +219,11 @@ class Photo $fields = self::getFields(); $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 = array_combine($fields, $values); + $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); } diff --git a/src/Model/Profile.php b/src/Model/Profile.php index d7cc906fd..bf6da81cb 100644 --- a/src/Model/Profile.php +++ b/src/Model/Profile.php @@ -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); diff --git a/src/Model/Storage/AbstractStorage.php b/src/Model/Storage/AbstractStorage.php new file mode 100644 index 000000000..270d67562 --- /dev/null +++ b/src/Model/Storage/AbstractStorage.php @@ -0,0 +1,32 @@ +l10n = $l10n; + $this->logger = $logger; + } + + public function __toString() + { + return static::getName(); + } +} diff --git a/src/Model/Storage/Database.php b/src/Model/Storage/Database.php index 60bd154e6..182add861 100644 --- a/src/Model/Storage/Database.php +++ b/src/Model/Storage/Database.php @@ -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)); - } - return $ref; - } 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')); + 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 DBA::lastInsertId(); + + return $reference; + } else { + $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 $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; + } } diff --git a/src/Model/Storage/Filesystem.php b/src/Model/Storage/Filesystem.php index ff7c59444..9c429cfb3 100644 --- a/src/Model/Storage/Filesystem.php +++ b/src/Model/Storage/Filesystem.php @@ -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; + } } diff --git a/src/Model/Storage/IStorage.php b/src/Model/Storage/IStorage.php index 1b0129e5e..c3ec3197c 100644 --- a/src/Model/Storage/IStorage.php +++ b/src/Model/Storage/IStorage.php @@ -13,26 +13,32 @@ 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 $data Data to save + * @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,19 +77,30 @@ interface IStorage * * See https://github.com/friendica/friendica/wiki/Quick-Template-Guide */ - public static function getOptions(); - + public function getOptions(); + /** * @brief Validate and save options * - * @param array $data Array [optionname => value] to be saved + * @param array $data Array [optionname => value] to be saved * * @return array Validation errors: [optionname => error message] * * 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(); } - - diff --git a/src/Model/Storage/SystemResource.php b/src/Model/Storage/SystemResource.php index 3afe8ee6f..43514c1dc 100644 --- a/src/Model/Storage/SystemResource.php +++ b/src/Model/Storage/SystemResource.php @@ -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; + } } - diff --git a/src/Model/Term.php b/src/Model/Term.php index 08a02ff07..2f8da0fac 100644 --- a/src/Model/Term.php +++ b/src/Model/Term.php @@ -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); } } diff --git a/src/Module/Admin/Federation.php b/src/Module/Admin/Federation.php index 35afb2144..cc0067808 100644 --- a/src/Module/Admin/Federation.php +++ b/src/Module/Admin/Federation.php @@ -14,168 +14,103 @@ 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']; - - // 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); - - // - // 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')]; - } - } - - // Reformat and compact version numbers - if ($platform == 'Pleroma') { - $compacted = []; - foreach ($versionCounts as $key => $value) { - $version = $versionCounts[$key]['version']; - $parts = explode(' ', trim($version)); - do { - $part = array_pop($parts); - } while (!empty($parts) && ((strlen($part) >= 40) || (strlen($part) <= 3))); - // only take the x.x.x part of the version, not the "release" after the dash - if (!empty($part) && strpos($part, '-')) { - $part = explode('-', $part)[0]; - } - if (!empty($part)) { - if (empty($compacted[$part])) { - $compacted[$part] = $versionCounts[$key]['total']; - } else { - $compacted[$part] += $versionCounts[$key]['total']; - } - } - } - - $versionCounts = []; - foreach ($compacted as $version => $pl_total) { - $versionCounts[] = ['version' => $version, 'total' => $pl_total]; - } - } - - // 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); - } - 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 - $v2 = []; - $versions = []; - foreach ($versionCounts as $vv) { - $version = trim(strip_tags($vv["version"])); - $v2[$version] = $vv; - $versions[] = $version; - } - - usort($versions, 'version_compare'); + $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']; $versionCounts = []; - foreach ($versions as $version) { - $versionCounts[] = $v2[$version]; + $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']); + + if (in_array($gserver['platform'], ['Red Matrix', 'redmatrix', 'red'])) { + $version['version'] = 'Red ' . $version['version']; + } + + $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'; } - // 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]]; + 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.'); @@ -194,4 +129,144 @@ class Federation extends BaseAdminModule '$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']; + $parts = explode(' ', trim($version)); + do { + $part = array_pop($parts); + } while (!empty($parts) && ((strlen($part) >= 40) || (strlen($part) <= 3))); + // only take the x.x.x part of the version, not the "release" after the dash + if (!empty($part) && strpos($part, '-')) { + $part = explode('-', $part)[0]; + } + if (!empty($part)) { + if (empty($compacted[$part])) { + $compacted[$part] = $versionCounts[$key]['total']; + } else { + $compacted[$part] += $versionCounts[$key]['total']; + } + } + } + + $versionCounts = []; + foreach ($compacted as $version => $pl_total) { + $versionCounts[] = ['version' => $version, 'total' => $pl_total]; + } + + return $versionCounts; + } + + /** + * 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')]; + } + } + + // Assure that the versions are sorted correctly + $v2 = []; + $versions = []; + foreach ($versionCounts as $vv) { + $version = trim(strip_tags($vv["version"])); + $v2[$version] = $vv; + $versions[] = $version; + } + + usort($versions, 'version_compare'); + + $versionCounts = []; + foreach ($versions as $version) { + $versionCounts[] = $v2[$version]; + } + + return $versionCounts; + } } diff --git a/src/Module/Admin/Site.php b/src/Module/Admin/Site.php index 3302e0dc1..433cddf6f 100644 --- a/src/Module/Admin/Site.php +++ b/src/Module/Admin/Site.php @@ -199,42 +199,37 @@ 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(); - $storage_form_prefix = preg_replace('|[^a-zA-Z0-9]|', '', $storagebackend); - $storage_opts_data = []; - foreach ($storage_opts as $name => $info) { - $fieldname = $storage_form_prefix . '_' . $name; - switch ($info[0]) { // type - case 'checkbox': - case 'yesno': - $value = !empty($_POST[$fieldname]); - break; - default: - $value = $_POST[$fieldname] ?? ''; - } - $storage_opts_data[$name] = $value; + 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) { + $fieldname = $storage_form_prefix . '_' . $name; + switch ($info[0]) { // type + case 'checkbox': + case 'yesno': + $value = !empty($_POST[$fieldname]); + break; + default: + $value = $_POST[$fieldname] ?? ''; } - unset($name); - unset($info); - - $storage_form_errors = $storagebackend::saveOptions($storage_opts_data); - if (count($storage_form_errors)) { - foreach ($storage_form_errors as $name => $err) { - notice('Storage backend, ' . $storage_opts[$name][1] . ': ' . $err); - } - DI::baseUrl()->redirect('admin/site' . $active_panel); - } - } else { - info(L10n::t('Invalid storage backend setting value.')); + $storage_opts_data[$name] = $value; } + unset($name); + unset($info); + + $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); + } + DI::baseUrl()->redirect('admin/site' . $active_panel); + } + } else { + info(L10n::t('Invalid storage backend setting value.')); } // Has the directory url changed? If yes, then resubmit the existing profiles there @@ -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; diff --git a/src/Module/Search/Index.php b/src/Module/Search/Index.php index 247ac5988..995fb85d1 100644 --- a/src/Module/Search/Index.php +++ b/src/Module/Search/Index.php @@ -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); } } diff --git a/src/Network/Probe.php b/src/Network/Probe.php index 122df70ce..497fd3c51 100644 --- a/src/Network/Probe.php +++ b/src/Network/Probe.php @@ -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; diff --git a/src/Protocol/ActivityPub/Transmitter.php b/src/Protocol/ActivityPub/Transmitter.php index 6a512323c..dbfd67cb0 100644 --- a/src/Protocol/ActivityPub/Transmitter.php +++ b/src/Protocol/ActivityPub/Transmitter.php @@ -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; } diff --git a/src/Protocol/Diaspora.php b/src/Protocol/Diaspora.php index a683a2351..b3e6956f5 100644 --- a/src/Protocol/Diaspora.php +++ b/src/Protocol/Diaspora.php @@ -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); } diff --git a/src/Protocol/OStatus.php b/src/Protocol/OStatus.php index 2e47f18d1..0eec477a6 100644 --- a/src/Protocol/OStatus.php +++ b/src/Protocol/OStatus.php @@ -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); diff --git a/src/Util/Images.php b/src/Util/Images.php index e19304bba..e5a4da5b1 100644 --- a/src/Util/Images.php +++ b/src/Util/Images.php @@ -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; diff --git a/src/Util/JsonLD.php b/src/Util/JsonLD.php index 926fa1437..a56738ffb 100644 --- a/src/Util/JsonLD.php +++ b/src/Util/JsonLD.php @@ -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; } diff --git a/src/Worker/Cron.php b/src/Worker/Cron.php index f0c85f668..2d46ce328 100644 --- a/src/Worker/Cron.php +++ b/src/Worker/Cron.php @@ -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 diff --git a/src/Worker/CronJobs.php b/src/Worker/CronJobs.php index 6267bf6f3..e423fa28b 100644 --- a/src/Worker/CronJobs.php +++ b/src/Worker/CronJobs.php @@ -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"); diff --git a/src/Worker/Delivery.php b/src/Worker/Delivery.php index a2088fde5..37a4ec445 100644 --- a/src/Worker/Delivery.php +++ b/src/Worker/Delivery.php @@ -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)) { diff --git a/src/Worker/Notifier.php b/src/Worker/Notifier.php index fea2da7e3..eed573e2a 100644 --- a/src/Worker/Notifier.php +++ b/src/Worker/Notifier.php @@ -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; diff --git a/src/Worker/RemoveContact.php b/src/Worker/RemoveContact.php index 00027dca4..2f005cd14 100644 --- a/src/Worker/RemoveContact.php +++ b/src/Worker/RemoveContact.php @@ -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; } diff --git a/src/Worker/SearchDirectory.php b/src/Worker/SearchDirectory.php index 4503f21ca..3975fe1f7 100644 --- a/src/Worker/SearchDirectory.php +++ b/src/Worker/SearchDirectory.php @@ -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); } } diff --git a/static/dbstructure.config.php b/static/dbstructure.config.php index 20bd93769..49934c63c 100755 --- a/static/dbstructure.config.php +++ b/static/dbstructure.config.php @@ -34,7 +34,7 @@ use Friendica\Database\DBA; if (!defined('DB_UPDATE_VERSION')) { - define('DB_UPDATE_VERSION', 1329); + define('DB_UPDATE_VERSION', 1330); } return [ diff --git a/static/dependencies.config.php b/static/dependencies.config.php index 6cd077b03..ec80123aa 100644 --- a/static/dependencies.config.php +++ b/static/dependencies.config.php @@ -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], + ], + ], ]; diff --git a/tests/Util/DbaLockMockTrait.php b/tests/Util/DbaLockMockTrait.php index 09bfd130c..b7b9fb91d 100644 --- a/tests/Util/DbaLockMockTrait.php +++ b/tests/Util/DbaLockMockTrait.php @@ -2,7 +2,7 @@ namespace Friendica\Test\Util; -use Friendica\Core\Cache; +use Friendica\Core\Cache\Cache; use Friendica\Core\Lock\DatabaseLock; trait DbaLockMockTrait diff --git a/tests/Util/SampleStorageBackend.php b/tests/Util/SampleStorageBackend.php new file mode 100644 index 000000000..a788c6d3d --- /dev/null +++ b/tests/Util/SampleStorageBackend.php @@ -0,0 +1,106 @@ + [ + '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'); + } +} + diff --git a/tests/Util/SampleStorageBackendInstance.php b/tests/Util/SampleStorageBackendInstance.php new file mode 100644 index 000000000..d55ff04de --- /dev/null +++ b/tests/Util/SampleStorageBackendInstance.php @@ -0,0 +1,18 @@ + [ + // 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', + ], + ], +]; diff --git a/tests/src/Core/StorageManagerTest.php b/tests/src/Core/StorageManagerTest.php new file mode 100644 index 000000000..443f8e786 --- /dev/null +++ b/tests/src/Core/StorageManagerTest.php @@ -0,0 +1,264 @@ +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); + } + } +} diff --git a/tests/src/Model/Storage/DatabaseStorageTest.php b/tests/src/Model/Storage/DatabaseStorageTest.php new file mode 100644 index 000000000..d6dff9932 --- /dev/null +++ b/tests/src/Model/Storage/DatabaseStorageTest.php @@ -0,0 +1,52 @@ +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()); + } +} diff --git a/tests/src/Model/Storage/FilesystemStorageTest.php b/tests/src/Model/Storage/FilesystemStorageTest.php new file mode 100644 index 000000000..500fd93bd --- /dev/null +++ b/tests/src/Model/Storage/FilesystemStorageTest.php @@ -0,0 +1,111 @@ +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)); + } +} diff --git a/tests/src/Model/Storage/StorageTest.php b/tests/src/Model/Storage/StorageTest.php new file mode 100644 index 000000000..ae3f8f01f --- /dev/null +++ b/tests/src/Model/Storage/StorageTest.php @@ -0,0 +1,96 @@ +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)); + } +} diff --git a/update.php b/update.php index 40f39ebeb..924fac545 100644 --- a/update.php +++ b/update.php @@ -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; +}