Merge remote-tracking branch 'upstream/develop' into improved-payload

This commit is contained in:
Michael 2021-08-17 23:00:00 +00:00
commit b521e45903
37 changed files with 2564 additions and 685 deletions

View File

@ -126,7 +126,8 @@
}, },
"require-dev": { "require-dev": {
"mockery/mockery": "^1.3", "mockery/mockery": "^1.3",
"mikey179/vfsstream": "^1.6" "mikey179/vfsstream": "^1.6",
"phpunit/phpunit": "^8.5"
}, },
"scripts": { "scripts": {
"test": "phpunit", "test": "phpunit",

1660
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -10,12 +10,12 @@ A storage backend is implemented as a class, and the plugin register the class t
The class must live in `Friendica\Addon\youraddonname` namespace, where `youraddonname` the folder name of your addon. The class must live in `Friendica\Addon\youraddonname` namespace, where `youraddonname` the folder name of your addon.
The class must implement `Friendica\Model\Storage\IStorage` interface. All method in the interface must be implemented: The class must implement `Friendica\Model\Storage\IWritableStorage` interface. All method in the interface must be implemented:
namespace Friendica\Model\Storage; namespace Friendica\Model\IWritableStorage;
```php ```php
interface IStorage interface IWritableStorage
{ {
public function get(string $reference); public function get(string $reference);
public function put(string $data, string $reference = ''); public function put(string $data, string $reference = '');
@ -79,7 +79,7 @@ Each label should be translatable
]; ];
See doxygen documentation of `IStorage` interface for details about each method. See doxygen documentation of `IWritableStorage` interface for details about each method.
## Register a storage backend class ## Register a storage backend class
@ -105,8 +105,9 @@ Each new Storage class should be added to the test-environment at [Storage Tests
Add a new test class which's naming convention is `StorageClassTest`, which extend the `StorageTest` in the same directory. Add a new test class which's naming convention is `StorageClassTest`, which extend the `StorageTest` in the same directory.
Override the two necessary instances: Override the two necessary instances:
```php ```php
use Friendica\Model\Storage\IStorage; use Friendica\Model\Storage\IWritableStorage;
abstract class StorageTest abstract class StorageTest
{ {
@ -114,13 +115,48 @@ abstract class StorageTest
abstract protected function getInstance(); abstract protected function getInstance();
// Assertion for the option array you return for your new StorageClass // Assertion for the option array you return for your new StorageClass
abstract protected function assertOption(IStorage $storage); abstract protected function assertOption(IWritableStorage $storage);
}
```
## Exception handling
There are two intended types of exceptions for storages
### `ReferenceStorageExecption`
This storage exception should be used in case the caller tries to use an invalid references.
This could happen in case the caller tries to delete or update an unknown reference.
The implementation of the storage backend must not ignore invalid references.
Avoid throwing the common `StorageExecption` instead of the `ReferenceStorageException` at this particular situation!
### `StorageException`
This is the common exception in case unexpected errors happen using the storage backend.
If there's a predecessor to this exception (e.g. you caught an exception and are throwing this execption), you should add the predecessor for transparency reasons.
Example:
```php
use Friendica\Model\Storage\IWritableStorage;
class ExampleStorage implements IWritableStorage
{
public function get(string $reference) : string
{
try {
throw new Exception('a real bad exception');
} catch (Exception $exception) {
throw new \Friendica\Model\Storage\StorageException(sprintf('The Example Storage throws an exception for reference %s', $reference), 500, $exception);
}
}
} }
``` ```
## Example ## Example
Here an hypotetical addon which register an unusefull storage backend. Here an hypotetical addon which register a useless storage backend.
Let's call it `samplestorage`. Let's call it `samplestorage`.
This backend will discard all data we try to save and will return always the same image when we ask for some data. This backend will discard all data we try to save and will return always the same image when we ask for some data.
@ -133,12 +169,12 @@ The file will be `addon/samplestorage/SampleStorageBackend.php`:
<?php <?php
namespace Friendica\Addon\samplestorage; namespace Friendica\Addon\samplestorage;
use Friendica\Model\Storage\IStorage; use Friendica\Model\Storage\IWritableStorage;
use Friendica\Core\Config\IConfig; use Friendica\Core\Config\IConfig;
use Friendica\Core\L10n; use Friendica\Core\L10n;
class SampleStorageBackend implements IStorage class SampleStorageBackend implements IWritableStorage
{ {
const NAME = 'Sample Storage'; const NAME = 'Sample Storage';
@ -270,7 +306,7 @@ function samplestorage_storage_instance(\Friendica\App $a, array $data)
**Theoretically - until tests for Addons are enabled too - create a test class with the name `addon/tests/SampleStorageTest.php`: **Theoretically - until tests for Addons are enabled too - create a test class with the name `addon/tests/SampleStorageTest.php`:
```php ```php
use Friendica\Model\Storage\IStorage; use Friendica\Model\Storage\IWritableStorage;
use Friendica\Test\src\Model\Storage\StorageTest; use Friendica\Test\src\Model\Storage\StorageTest;
class SampleStorageTest extends StorageTest class SampleStorageTest extends StorageTest
@ -284,7 +320,7 @@ class SampleStorageTest extends StorageTest
} }
// Assertion for the option array you return for your new StorageClass // Assertion for the option array you return for your new StorageClass
protected function assertOption(IStorage $storage) protected function assertOption(IWritableStorage $storage)
{ {
$this->assertEquals([ $this->assertEquals([
'filename' => [ 'filename' => [

View File

@ -65,6 +65,9 @@ function photos_init(App $a) {
if (DI::args()->getArgc() > 1) { if (DI::args()->getArgc() > 1) {
$owner = User::getOwnerDataByNick(DI::args()->getArgv()[1]); $owner = User::getOwnerDataByNick(DI::args()->getArgv()[1]);
if (!$owner) {
throw new HTTPException\NotFoundException(DI::l10n()->t('User not found.'));
}
$is_owner = (local_user() && (local_user() == $owner['uid'])); $is_owner = (local_user() && (local_user() == $owner['uid']));

View File

@ -23,6 +23,7 @@ namespace Friendica\Console;
use Asika\SimpleConsole\CommandArgsException; use Asika\SimpleConsole\CommandArgsException;
use Friendica\Core\StorageManager; use Friendica\Core\StorageManager;
use Friendica\Model\Storage\ReferenceStorageException;
use Friendica\Model\Storage\StorageException; use Friendica\Model\Storage\StorageException;
/** /**
@ -105,7 +106,7 @@ HELP;
$this->out(sprintf($rowfmt, 'Sel', 'Name')); $this->out(sprintf($rowfmt, 'Sel', 'Name'));
$this->out('-----------------------'); $this->out('-----------------------');
$isregisterd = false; $isregisterd = false;
foreach ($this->storageManager->listBackends() as $name => $class) { foreach ($this->storageManager->listBackends() as $name) {
$issel = ' '; $issel = ' ';
if ($current && $current::getName() == $name) { if ($current && $current::getName() == $name) {
$issel = '*'; $issel = '*';
@ -127,23 +128,23 @@ HELP;
protected function doSet() protected function doSet()
{ {
if (count($this->args) !== 2) { if (count($this->args) !== 2 || empty($this->args[1])) {
throw new CommandArgsException('Invalid arguments'); throw new CommandArgsException('Invalid arguments');
} }
$name = $this->args[1]; $name = $this->args[1];
$class = $this->storageManager->getByName($name); try {
$class = $this->storageManager->getWritableStorageByName($name);
if ($class === '') { if (!$this->storageManager->setBackend($class)) {
$this->out($class . ' is not a valid backend storage class.');
return -1;
}
} catch (ReferenceStorageException $exception) {
$this->out($name . ' is not a registered backend.'); $this->out($name . ' is not a registered backend.');
return -1; return -1;
} }
if (!$this->storageManager->setBackend($class)) {
$this->out($class . ' is not a valid backend storage class.');
return -1;
}
return 0; return 0;
} }

View File

@ -129,6 +129,10 @@ class Installer
$returnVal = false; $returnVal = false;
} }
if (!$this->checkTLS()) {
$returnVal = false;
}
if (!$this->checkKeys()) { if (!$this->checkKeys()) {
$returnVal = false; $returnVal = false;
} }
@ -580,6 +584,38 @@ class Installer
return $status; return $status;
} }
/**
* TLS Check
*
* Tries to determine whether the connection to the server is secured
* by TLS or not. If not the user will be warned that it is higly
* encuraged to use TLS.
*
* @return bool (true) as TLS is not mandatory
*/
public function checkTLS()
{
$tls = false;
if (isset($_SERVER['HTTPS'])) {
if (($_SERVER['HTTPS'] == 1) || ($_SERVER['HTTPS'] == 'on')) {
$tls = true;
}
}
if (!$tls) {
$help = DI::l10n()->t('The detection of TLS to secure the communication between the browser and the new Friendica server failed.');
$help .= ' ' . DI::l10n()->t('It is highly encouraged to use Friendica only over a secure connection as sensitive information like passwords will be transmitted.');
$help .= ' ' . DI::l10n()->t('Please ensure that the connection to the server is secure.');
$this->addCheck(DI::l10n()->t('No TLS detected'), $tls, false, $help);
} else {
$this->addCheck(DI::l10n()->t('TLS detected'), $tls, false, '');
}
// TLS is not required
return true;
}
/** /**
* Imagick Check * Imagick Check
* *

View File

@ -25,10 +25,9 @@ use Exception;
use Friendica\Core\Config\IConfig; use Friendica\Core\Config\IConfig;
use Friendica\Database\Database; use Friendica\Database\Database;
use Friendica\Model\Storage; use Friendica\Model\Storage;
use Friendica\Network\IHTTPRequest; use Friendica\Network\HTTPException\InternalServerErrorException;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
/** /**
* Manage storage backends * Manage storage backends
* *
@ -41,12 +40,14 @@ class StorageManager
const TABLES = ['photo', 'attach']; const TABLES = ['photo', 'attach'];
// Default storage backends // Default storage backends
/** @var string[] */
const DEFAULT_BACKENDS = [ const DEFAULT_BACKENDS = [
Storage\Filesystem::NAME => Storage\Filesystem::class, Storage\Filesystem::NAME,
Storage\Database::NAME => Storage\Database::class, Storage\Database::NAME,
]; ];
private $backends = []; /** @var string[] List of valid backend classes */
private $validBackends;
/** /**
* @var Storage\IStorage[] A local cache for storage instances * @var Storage\IStorage[] A local cache for storage instances
@ -61,10 +62,8 @@ class StorageManager
private $logger; private $logger;
/** @var L10n */ /** @var L10n */
private $l10n; private $l10n;
/** @var IHTTPRequest */
private $httpRequest;
/** @var Storage\IStorage */ /** @var Storage\IWritableStorage */
private $currentBackend; private $currentBackend;
/** /**
@ -72,82 +71,106 @@ class StorageManager
* @param IConfig $config * @param IConfig $config
* @param LoggerInterface $logger * @param LoggerInterface $logger
* @param L10n $l10n * @param L10n $l10n
*
* @throws Storage\InvalidClassStorageException in case the active backend class is invalid
* @throws Storage\StorageException in case of unexpected errors during the active backend class loading
*/ */
public function __construct(Database $dba, IConfig $config, LoggerInterface $logger, L10n $l10n, IHTTPRequest $httpRequest) public function __construct(Database $dba, IConfig $config, LoggerInterface $logger, L10n $l10n)
{ {
$this->dba = $dba; $this->dba = $dba;
$this->config = $config; $this->config = $config;
$this->logger = $logger; $this->logger = $logger;
$this->l10n = $l10n; $this->l10n = $l10n;
$this->httpRequest = $httpRequest; $this->validBackends = $config->get('storage', 'backends', self::DEFAULT_BACKENDS);
$this->backends = $config->get('storage', 'backends', self::DEFAULT_BACKENDS);
$currentName = $this->config->get('storage', 'name', ''); $currentName = $this->config->get('storage', 'name');
// you can only use user backends as a "default" backend, so the second parameter is true // you can only use user backends as a "default" backend, so the second parameter is true
$this->currentBackend = $this->getByName($currentName, true); $this->currentBackend = $this->getWritableStorageByName($currentName);
} }
/** /**
* Return current storage backend class * Return current storage backend class
* *
* @return Storage\IStorage|null * @return Storage\IWritableStorage
*/ */
public function getBackend() public function getBackend()
{ {
return $this->currentBackend; return $this->currentBackend;
} }
/**
* Returns a writable storage backend class by registered name
*
* @param string $name Backend name
*
* @return Storage\IWritableStorage
*
* @throws Storage\InvalidClassStorageException in case there's no backend class for the name
* @throws Storage\StorageException in case of an unexpected failure during the hook call
*/
public function getWritableStorageByName(string $name): Storage\IWritableStorage
{
$storage = $this->getByName($name, $this->validBackends);
if (!$storage instanceof Storage\IWritableStorage) {
throw new Storage\InvalidClassStorageException(sprintf('Backend %s is not writable', $name));
}
return $storage;
}
/** /**
* Return storage backend class by registered name * Return storage backend class by registered name
* *
* @param string|null $name Backend name * @param string $name Backend name
* @param boolean $onlyUserBackend True, if just user specific instances should be returrned (e.g. not SystemResource) * @param string[]|null $validBackends possible, manual override of the valid backends
* *
* @return Storage\IStorage|null null if no backend registered at $name * @return Storage\IStorage
* *
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws Storage\InvalidClassStorageException in case there's no backend class for the name
* @throws Storage\StorageException in case of an unexpected failure during the hook call
*/ */
public function getByName(string $name = null, $onlyUserBackend = false) public function getByName(string $name, array $validBackends = null): Storage\IStorage
{ {
// @todo 2020.09 Remove this call after 2 releases
$name = $this->checkLegacyBackend($name);
// If there's no cached instance create a new instance // If there's no cached instance create a new instance
if (!isset($this->backendInstances[$name])) { if (!isset($this->backendInstances[$name])) {
// If the current name isn't a valid backend (or the SystemResource instance) create it // If the current name isn't a valid backend (or the SystemResource instance) create it
if ($this->isValidBackend($name, $onlyUserBackend)) { if (!$this->isValidBackend($name, $validBackends)) {
switch ($name) { throw new Storage\InvalidClassStorageException(sprintf('Backend %s is not valid', $name));
// Try the filesystem backend }
case Storage\Filesystem::getName():
$this->backendInstances[$name] = new Storage\Filesystem($this->config, $this->logger, $this->l10n); switch ($name) {
break; // Try the filesystem backend
// try the database backend case Storage\Filesystem::getName():
case Storage\Database::getName(): $this->backendInstances[$name] = new Storage\Filesystem($this->config, $this->l10n);
$this->backendInstances[$name] = new Storage\Database($this->dba, $this->logger, $this->l10n); break;
break; // try the database backend
// at least, try if there's an addon for the backend case Storage\Database::getName():
case Storage\SystemResource::getName(): $this->backendInstances[$name] = new Storage\Database($this->dba);
$this->backendInstances[$name] = new Storage\SystemResource(); break;
break; // at least, try if there's an addon for the backend
case Storage\ExternalResource::getName(): case Storage\SystemResource::getName():
$this->backendInstances[$name] = new Storage\ExternalResource($this->httpRequest); $this->backendInstances[$name] = new Storage\SystemResource();
break; break;
default: case Storage\ExternalResource::getName():
$data = [ $this->backendInstances[$name] = new Storage\ExternalResource();
'name' => $name, break;
'storage' => null, default:
]; $data = [
'name' => $name,
'storage' => null,
];
try {
Hook::callAll('storage_instance', $data); Hook::callAll('storage_instance', $data);
if (($data['storage'] ?? null) instanceof Storage\IStorage) { if (!($data['storage'] ?? null) instanceof Storage\IStorage) {
$this->backendInstances[$data['name'] ?? $name] = $data['storage']; throw new Storage\InvalidClassStorageException(sprintf('Backend %s was not found', $name));
} else {
return null;
} }
break;
} $this->backendInstances[$data['name'] ?? $name] = $data['storage'];
} else { } catch (InternalServerErrorException $exception) {
return null; throw new Storage\StorageException(sprintf('Failed calling hook::storage_instance for backend %s', $name), $exception);
}
break;
} }
} }
@ -157,51 +180,32 @@ class StorageManager
/** /**
* Checks, if the storage is a valid backend * Checks, if the storage is a valid backend
* *
* @param string|null $name The name or class of the backend * @param string|null $name The name or class of the backend
* @param boolean $onlyUserBackend True, if just user backend should get returned (e.g. not SystemResource) * @param string[]|null $validBackends Possible, valid backends to check
* *
* @return boolean True, if the backend is a valid backend * @return boolean True, if the backend is a valid backend
*/ */
public function isValidBackend(string $name = null, bool $onlyUserBackend = false) public function isValidBackend(string $name = null, array $validBackends = null): bool
{ {
return array_key_exists($name, $this->backends) || $validBackends = $validBackends ?? array_merge($this->validBackends,
(!$onlyUserBackend && in_array($name, [Storage\SystemResource::getName(), Storage\ExternalResource::getName()])); [
} Storage\SystemResource::getName(),
Storage\ExternalResource::getName(),
/** ]);
* Check for legacy backend storage class names (= full model class name) return in_array($name, $validBackends);
*
* @todo 2020.09 Remove this function after 2 releases, because there shouldn't be any legacy backend classes left
*
* @param string|null $name a potential, legacy storage name ("Friendica\Model\Storage\...")
*
* @return string|null The current storage name
*/
private function checkLegacyBackend(string $name = null)
{
if (stristr($name, 'Friendica\Model\Storage\\')) {
$this->logger->notice('Using deprecated storage class value', ['name' => $name]);
return substr($name, 24);
}
return $name;
} }
/** /**
* Set current storage backend class * Set current storage backend class
* *
* @param string $name Backend class name * @param Storage\IWritableStorage $storage The storage class
* *
* @return boolean True, if the set was successful * @return boolean True, if the set was successful
*/ */
public function setBackend(string $name = null) public function setBackend(Storage\IWritableStorage $storage): bool
{ {
if (!$this->isValidBackend($name, false)) { if ($this->config->set('storage', 'name', $storage::getName())) {
return false; $this->currentBackend = $storage;
}
if ($this->config->set('storage', 'name', $name)) {
$this->currentBackend = $this->getByName($name, false);
return true; return true;
} else { } else {
return false; return false;
@ -211,11 +215,11 @@ class StorageManager
/** /**
* Get registered backends * Get registered backends
* *
* @return array * @return string[]
*/ */
public function listBackends() public function listBackends(): array
{ {
return $this->backends; return $this->validBackends;
} }
/** /**
@ -227,16 +231,20 @@ class StorageManager
* *
* @return boolean True, if the registration was successful * @return boolean True, if the registration was successful
*/ */
public function register(string $class) public function register(string $class): bool
{ {
if (is_subclass_of($class, Storage\IStorage::class)) { if (is_subclass_of($class, Storage\IStorage::class)) {
/** @var Storage\IStorage $class */ /** @var Storage\IStorage $class */
$backends = $this->backends; if ($this->isValidBackend($class::getName(), $this->validBackends)) {
$backends[$class::getName()] = $class; return true;
}
$backends = $this->validBackends;
$backends[] = $class::getName();
if ($this->config->set('storage', 'backends', $backends)) { if ($this->config->set('storage', 'backends', $backends)) {
$this->backends = $backends; $this->validBackends = $backends;
return true; return true;
} else { } else {
return false; return false;
@ -252,20 +260,33 @@ class StorageManager
* @param string $class Backend class name * @param string $class Backend class name
* *
* @return boolean True, if unregistering was successful * @return boolean True, if unregistering was successful
*
* @throws Storage\StorageException
*/ */
public function unregister(string $class) public function unregister(string $class): bool
{ {
if (is_subclass_of($class, Storage\IStorage::class)) { if (is_subclass_of($class, Storage\IStorage::class)) {
/** @var Storage\IStorage $class */ /** @var Storage\IStorage $class */
unset($this->backends[$class::getName()]); if ($this->currentBackend::getName() == $class::getName()) {
throw new Storage\StorageException(sprintf('Cannot unregister %s, because it\'s currently active.', $class::getName()));
if ($this->currentBackend instanceof $class) {
$this->config->set('storage', 'name', null);
$this->currentBackend = null;
} }
return $this->config->set('storage', 'backends', $this->backends); $key = array_search($class::getName(), $this->validBackends);
if ($key !== false) {
$backends = $this->validBackends;
unset($backends[$key]);
$backends = array_values($backends);
if ($this->config->set('storage', 'backends', $backends)) {
$this->validBackends = $backends;
return true;
} else {
return false;
}
} else {
return true;
}
} else { } else {
return false; return false;
} }
@ -277,17 +298,17 @@ class StorageManager
* Copy existing data to destination storage and delete from source. * Copy existing data to destination storage and delete from source.
* This method cannot move to legacy in-table `data` field. * This method cannot move to legacy in-table `data` field.
* *
* @param Storage\IStorage $destination Destination storage class name * @param Storage\IWritableStorage $destination Destination storage class name
* @param array $tables Tables to look in for resources. Optional, defaults to ['photo', 'attach'] * @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 * @param int $limit Limit of the process batch size, defaults to 5000
* *
* @return int Number of moved resources * @return int Number of moved resources
* @throws Storage\StorageException * @throws Storage\StorageException
* @throws Exception * @throws Exception
*/ */
public function move(Storage\IStorage $destination, array $tables = self::TABLES, int $limit = 5000) public function move(Storage\IWritableStorage $destination, array $tables = self::TABLES, int $limit = 5000): int
{ {
if (!$this->isValidBackend($destination, true)) { if (!$this->isValidBackend($destination, $this->validBackends)) {
throw new Storage\StorageException(sprintf("Can't move to storage backend '%s'", $destination::getName())); throw new Storage\StorageException(sprintf("Can't move to storage backend '%s'", $destination::getName()));
} }
@ -303,13 +324,19 @@ class StorageManager
while ($resource = $this->dba->fetch($resources)) { while ($resource = $this->dba->fetch($resources)) {
$id = $resource['id']; $id = $resource['id'];
$data = $resource['data'];
$source = $this->getByName($resource['backend-class']);
$sourceRef = $resource['backend-ref']; $sourceRef = $resource['backend-ref'];
$source = null;
if (!empty($source)) { try {
$source = $this->getWritableStorageByName($resource['backend-class'] ?? '');
$this->logger->info('Get data from old backend.', ['oldBackend' => $source, 'oldReference' => $sourceRef]); $this->logger->info('Get data from old backend.', ['oldBackend' => $source, 'oldReference' => $sourceRef]);
$data = $source->get($sourceRef); $data = $source->get($sourceRef);
} catch (Storage\InvalidClassStorageException $exception) {
$this->logger->info('Get data from DB resource field.', ['oldReference' => $sourceRef]);
$data = $resource['data'];
} catch (Storage\ReferenceStorageException $exception) {
$this->logger->info('Invalid source reference.', ['oldBackend' => $source, 'oldReference' => $sourceRef]);
continue;
} }
$this->logger->info('Save data to new backend.', ['newBackend' => $destination::getName()]); $this->logger->info('Save data to new backend.', ['newBackend' => $destination::getName()]);

View File

@ -387,11 +387,11 @@ abstract class DI
} }
/** /**
* @return Model\Storage\IStorage * @return Model\Storage\IWritableStorage
*/ */
public static function storage() public static function storage()
{ {
return self::$dice->create(Model\Storage\IStorage::class); return self::$dice->create(Model\Storage\IWritableStorage::class);
} }
// //

View File

@ -25,6 +25,8 @@ use Friendica\Core\System;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\Database\DBStructure; use Friendica\Database\DBStructure;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Storage\InvalidClassStorageException;
use Friendica\Model\Storage\ReferenceStorageException;
use Friendica\Object\Image; use Friendica\Object\Image;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
use Friendica\Util\Mimetype; use Friendica\Util\Mimetype;
@ -163,17 +165,20 @@ class Attach
return $item['data']; return $item['data'];
} }
$backendClass = DI::storageManager()->getByName($item['backend-class'] ?? ''); try {
if (empty($backendClass)) { $backendClass = DI::storageManager()->getByName($item['backend-class'] ?? '');
$backendRef = $item['backend-ref'];
return $backendClass->get($backendRef);
} catch (InvalidClassStorageException $storageException) {
// legacy data storage in 'data' column // legacy data storage in 'data' column
$i = self::selectFirst(['data'], ['id' => $item['id']]); $i = self::selectFirst(['data'], ['id' => $item['id']]);
if ($i === false) { if ($i === false) {
return null; return null;
} }
return $i['data']; return $i['data'];
} else { } catch (ReferenceStorageException $referenceStorageException) {
$backendRef = $item['backend-ref']; DI::logger()->debug('No data found for item', ['item' => $item, 'exception' => $referenceStorageException]);
return $backendClass->get($backendRef); return '';
} }
} }
@ -278,11 +283,13 @@ class Attach
$items = self::selectToArray(['backend-class','backend-ref'], $conditions); $items = self::selectToArray(['backend-class','backend-ref'], $conditions);
foreach($items as $item) { foreach($items as $item) {
$backend_class = DI::storageManager()->getByName($item['backend-class'] ?? ''); try {
if (!empty($backend_class)) { $backend_class = DI::storageManager()->getWritableStorageByName($item['backend-class'] ?? '');
$fields['backend-ref'] = $backend_class->put($img->asString(), $item['backend-ref'] ?? ''); $fields['backend-ref'] = $backend_class->put($img->asString(), $item['backend-ref'] ?? '');
} else { } catch (InvalidClassStorageException $storageException) {
$fields['data'] = $img->asString(); DI::logger()->debug('Storage class not found.', ['conditions' => $conditions, 'exception' => $storageException]);
} catch (ReferenceStorageException $referenceStorageException) {
DI::logger()->debug('Item doesn\'t exist.', ['conditions' => $conditions, 'exception' => $referenceStorageException]);
} }
} }
} }
@ -310,9 +317,13 @@ class Attach
$items = self::selectToArray(['backend-class','backend-ref'], $conditions); $items = self::selectToArray(['backend-class','backend-ref'], $conditions);
foreach($items as $item) { foreach($items as $item) {
$backend_class = DI::storageManager()->getByName($item['backend-class'] ?? ''); try {
if (!empty($backend_class)) { $backend_class = DI::storageManager()->getWritableStorageByName($item['backend-class'] ?? '');
$backend_class->delete($item['backend-ref'] ?? ''); $backend_class->delete($item['backend-ref'] ?? '');
} catch (InvalidClassStorageException $storageException) {
DI::logger()->debug('Storage class not found.', ['conditions' => $conditions, 'exception' => $storageException]);
} catch (ReferenceStorageException $referenceStorageException) {
DI::logger()->debug('Item doesn\'t exist.', ['conditions' => $conditions, 'exception' => $referenceStorageException]);
} }
} }

View File

@ -2757,6 +2757,8 @@ class Item
$filter_reasons[] = DI::l10n()->t('Content warning: %s', $item['content-warning']); $filter_reasons[] = DI::l10n()->t('Content warning: %s', $item['content-warning']);
} }
$item['attachments'] = $attachments;
$hook_data = [ $hook_data = [
'item' => $item, 'item' => $item,
'filter_reasons' => $filter_reasons 'filter_reasons' => $filter_reasons

View File

@ -28,6 +28,9 @@ use Friendica\Database\DBA;
use Friendica\Database\DBStructure; use Friendica\Database\DBStructure;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Storage\ExternalResource; use Friendica\Model\Storage\ExternalResource;
use Friendica\Model\Storage\InvalidClassStorageException;
use Friendica\Model\Storage\ReferenceStorageException;
use Friendica\Model\Storage\StorageException;
use Friendica\Model\Storage\SystemResource; use Friendica\Model\Storage\SystemResource;
use Friendica\Object\Image; use Friendica\Object\Image;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
@ -184,8 +187,6 @@ class Photo
* @param array $photo Photo data. Needs at least 'id', 'type', 'backend-class', 'backend-ref' * @param array $photo Photo data. Needs at least 'id', 'type', 'backend-class', 'backend-ref'
* *
* @return \Friendica\Object\Image * @return \Friendica\Object\Image
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/ */
public static function getImageDataForPhoto(array $photo) public static function getImageDataForPhoto(array $photo)
{ {
@ -193,19 +194,31 @@ class Photo
return $photo['data']; return $photo['data'];
} }
$backendClass = DI::storageManager()->getByName($photo['backend-class'] ?? ''); try {
if (empty($backendClass)) { $backendClass = DI::storageManager()->getByName($photo['backend-class'] ?? '');
// legacy data storage in "data" column /// @todo refactoring this returning, because the storage returns a "string" which is casted in different ways - a check "instanceof Image" will fail!
$i = self::selectFirst(['data'], ['id' => $photo['id']]); return $backendClass->get($photo['backend-ref'] ?? '');
if ($i === false) { } catch (InvalidClassStorageException $storageException) {
return null; try {
// legacy data storage in "data" column
$i = self::selectFirst(['data'], ['id' => $photo['id']]);
if ($i !== false) {
return $i['data'];
} else {
DI::logger()->info('Stored legacy data is empty', ['photo' => $photo]);
}
} catch (\Exception $exception) {
DI::logger()->info('Unexpected database exception', ['photo' => $photo, 'exception' => $exception]);
} }
$data = $i['data']; } catch (ReferenceStorageException $referenceStorageException) {
} else { DI::logger()->debug('Invalid reference for photo', ['photo' => $photo, 'exception' => $referenceStorageException]);
$backendRef = $photo['backend-ref'] ?? ''; } catch (StorageException $storageException) {
$data = $backendClass->get($backendRef); DI::logger()->info('Unexpected storage exception', ['photo' => $photo, 'exception' => $storageException]);
} catch (\ImagickException $imagickException) {
DI::logger()->info('Unexpected imagick exception', ['photo' => $photo, 'exception' => $imagickException]);
} }
return $data;
return null;
} }
/** /**
@ -217,14 +230,9 @@ class Photo
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException * @throws \ImagickException
*/ */
public static function getImageForPhoto(array $photo) public static function getImageForPhoto(array $photo): Image
{ {
$data = self::getImageDataForPhoto($photo); return new Image(self::getImageDataForPhoto($photo), $photo['type']);
if (empty($data)) {
return null;
}
return new Image($data, $photo['type']);
} }
/** /**
@ -334,20 +342,20 @@ class Photo
// Get defined storage backend. // Get defined storage backend.
// if no storage backend, we use old "data" column in photo table. // if no storage backend, we use old "data" column in photo table.
// if is an existing photo, reuse same backend // if is an existing photo, reuse same backend
$data = ""; $data = "";
$backend_ref = ""; $backend_ref = "";
$storage = "";
if (DBA::isResult($existing_photo)) { try {
$backend_ref = (string)$existing_photo["backend-ref"]; if (DBA::isResult($existing_photo)) {
$storage = DI::storageManager()->getByName($existing_photo["backend-class"] ?? ''); $backend_ref = (string)$existing_photo["backend-ref"];
} else { $storage = DI::storageManager()->getWritableStorageByName($existing_photo["backend-class"] ?? '');
$storage = DI::storage(); } else {
} $storage = DI::storage();
}
if (empty($storage)) {
$data = $Image->asString();
} else {
$backend_ref = $storage->put($Image->asString(), $backend_ref); $backend_ref = $storage->put($Image->asString(), $backend_ref);
} catch (InvalidClassStorageException $storageException) {
$data = $Image->asString();
} }
$fields = [ $fields = [
@ -403,12 +411,15 @@ class Photo
$photos = DBA::select('photo', ['id', 'backend-class', 'backend-ref'], $conditions); $photos = DBA::select('photo', ['id', 'backend-class', 'backend-ref'], $conditions);
while ($photo = DBA::fetch($photos)) { while ($photo = DBA::fetch($photos)) {
$backend_class = DI::storageManager()->getByName($photo['backend-class'] ?? ''); try {
if (!empty($backend_class)) { $backend_class = DI::storageManager()->getWritableStorageByName($photo['backend-class'] ?? '');
if ($backend_class->delete($photo["backend-ref"] ?? '')) { $backend_class->delete($item['backend-ref'] ?? '');
// Delete the photos after they had been deleted successfully // Delete the photos after they had been deleted successfully
DBA::delete("photo", ['id' => $photo['id']]); DBA::delete("photo", ['id' => $photo['id']]);
} } catch (InvalidClassStorageException $storageException) {
DI::logger()->debug('Storage class not found.', ['conditions' => $conditions, 'exception' => $storageException]);
} catch (ReferenceStorageException $referenceStorageException) {
DI::logger()->debug('Photo doesn\'t exist.', ['conditions' => $conditions, 'exception' => $referenceStorageException]);
} }
} }
@ -437,10 +448,10 @@ class Photo
$photos = self::selectToArray(['backend-class', 'backend-ref'], $conditions); $photos = self::selectToArray(['backend-class', 'backend-ref'], $conditions);
foreach($photos as $photo) { foreach($photos as $photo) {
$backend_class = DI::storageManager()->getByName($photo['backend-class'] ?? ''); try {
if (!empty($backend_class)) { $backend_class = DI::storageManager()->getWritableStorageByName($photo['backend-class'] ?? '');
$fields["backend-ref"] = $backend_class->put($img->asString(), $photo['backend-ref']); $fields["backend-ref"] = $backend_class->put($img->asString(), $photo['backend-ref']);
} else { } catch (InvalidClassStorageException $storageException) {
$fields["data"] = $img->asString(); $fields["data"] = $img->asString();
} }
} }

View File

@ -534,7 +534,7 @@ class Media
* *
* @param int $uri_id * @param int $uri_id
* @param string $guid * @param string $guid
* @param array $links ist of links that shouldn't be added * @param array $links list of links that shouldn't be added
* @return array attachments * @return array attachments
*/ */
public static function splitAttachments(int $uri_id, string $guid = '', array $links = []) public static function splitAttachments(int $uri_id, string $guid = '', array $links = [])

View File

@ -21,8 +21,7 @@
namespace Friendica\Model\Storage; namespace Friendica\Model\Storage;
use Friendica\Core\L10n; use Exception;
use Psr\Log\LoggerInterface;
use Friendica\Database\Database as DBA; use Friendica\Database\Database as DBA;
/** /**
@ -30,7 +29,7 @@ use Friendica\Database\Database as DBA;
* *
* This class manage data stored in database table. * This class manage data stored in database table.
*/ */
class Database extends AbstractStorage class Database implements IWritableStorage
{ {
const NAME = 'Database'; const NAME = 'Database';
@ -39,47 +38,57 @@ class Database extends AbstractStorage
/** /**
* @param DBA $dba * @param DBA $dba
* @param LoggerInterface $logger
* @param L10n $l10n
*/ */
public function __construct(DBA $dba, LoggerInterface $logger, L10n $l10n) public function __construct(DBA $dba)
{ {
parent::__construct($l10n, $logger);
$this->dba = $dba; $this->dba = $dba;
} }
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function get(string $reference) public function get(string $reference): string
{ {
$result = $this->dba->selectFirst('storage', ['data'], ['id' => $reference]); try {
if (!$this->dba->isResult($result)) { $result = $this->dba->selectFirst('storage', ['data'], ['id' => $reference]);
return ''; if (!$this->dba->isResult($result)) {
} throw new ReferenceStorageException(sprintf('Database storage cannot find data for reference %s', $reference));
}
return $result['data']; return $result['data'];
} catch (Exception $exception) {
if ($exception instanceof ReferenceStorageException) {
throw $exception;
} else {
throw new StorageException(sprintf('Database storage failed to get %s', $reference), $exception->getCode(), $exception);
}
}
} }
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function put(string $data, string $reference = '') public function put(string $data, string $reference = ''): string
{ {
if ($reference !== '') { if ($reference !== '') {
$result = $this->dba->update('storage', ['data' => $data], ['id' => $reference]); try {
$result = $this->dba->update('storage', ['data' => $data], ['id' => $reference]);
} catch (Exception $exception) {
throw new StorageException(sprintf('Database storage failed to update %s', $reference), $exception->getCode(), $exception);
}
if ($result === false) { if ($result === false) {
$this->logger->warning('Failed to update data.', ['id' => $reference, 'errorCode' => $this->dba->errorNo(), 'errorMessage' => $this->dba->errorMessage()]); throw new StorageException(sprintf('Database storage failed to update %s', $reference), 500, new Exception($this->dba->errorMessage(), $this->dba->errorNo()));
throw new StorageException($this->l10n->t('Database storage failed to update %s', $reference));
} }
return $reference; return $reference;
} else { } else {
$result = $this->dba->insert('storage', ['data' => $data]); try {
$result = $this->dba->insert('storage', ['data' => $data]);
} catch (Exception $exception) {
throw new StorageException(sprintf('Database storage failed to insert %s', $reference), $exception->getCode(), $exception);
}
if ($result === false) { if ($result === false) {
$this->logger->warning('Failed to insert data.', ['errorCode' => $this->dba->errorNo(), 'errorMessage' => $this->dba->errorMessage()]); throw new StorageException(sprintf('Database storage failed to update %s', $reference), 500, new Exception($this->dba->errorMessage(), $this->dba->errorNo()));
throw new StorageException($this->l10n->t('Database storage failed to insert data'));
} }
return $this->dba->lastInsertId(); return $this->dba->lastInsertId();
@ -91,13 +100,23 @@ class Database extends AbstractStorage
*/ */
public function delete(string $reference) public function delete(string $reference)
{ {
return $this->dba->delete('storage', ['id' => $reference]); try {
if (!$this->dba->delete('storage', ['id' => $reference]) || $this->dba->affectedRows() === 0) {
throw new ReferenceStorageException(sprintf('Database storage failed to delete %s', $reference));
}
} catch (Exception $exception) {
if ($exception instanceof ReferenceStorageException) {
throw $exception;
} else {
throw new StorageException(sprintf('Database storage failed to delete %s', $reference), $exception->getCode(), $exception);
}
}
} }
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function getOptions() public function getOptions(): array
{ {
return []; return [];
} }
@ -105,7 +124,7 @@ class Database extends AbstractStorage
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function saveOptions(array $data) public function saveOptions(array $data): array
{ {
return []; return [];
} }
@ -113,8 +132,13 @@ class Database extends AbstractStorage
/** /**
* @inheritDoc * @inheritDoc
*/ */
public static function getName() public static function getName(): string
{ {
return self::NAME; return self::NAME;
} }
public function __toString()
{
return self::getName();
}
} }

View File

@ -21,9 +21,8 @@
namespace Friendica\Model\Storage; namespace Friendica\Model\Storage;
use BadMethodCallException; use Exception;
use Friendica\Util\HTTPSignature; use Friendica\Util\HTTPSignature;
use Friendica\Network\IHTTPRequest;
/** /**
* External resource storage class * External resource storage class
@ -35,66 +34,33 @@ class ExternalResource implements IStorage
{ {
const NAME = 'ExternalResource'; const NAME = 'ExternalResource';
/** @var IHTTPRequest */
private $httpRequest;
public function __construct(IHTTPRequest $httpRequest)
{
$this->httpRequest = $httpRequest;
}
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function get(string $reference) public function get(string $reference): string
{ {
$data = json_decode($reference); $data = json_decode($reference);
if (empty($data->url)) { if (empty($data->url)) {
return ""; throw new ReferenceStorageException(sprintf('Invalid reference %s, cannot retrieve URL', $reference));
} }
$parts = parse_url($data->url); $parts = parse_url($data->url);
if (empty($parts['scheme']) || empty($parts['host'])) { if (empty($parts['scheme']) || empty($parts['host'])) {
return ""; throw new ReferenceStorageException(sprintf('Invalid reference %s, cannot extract scheme and host', $reference));
} }
$fetchResult = HTTPSignature::fetchRaw($data->url, $data->uid, ['accept_content' => '']); try {
$fetchResult = HTTPSignature::fetchRaw($data->url, $data->uid, ['accept_content' => '']);
} catch (Exception $exception) {
throw new ReferenceStorageException(sprintf('External resource failed to get %s', $reference), $exception->getCode(), $exception);
}
if ($fetchResult->isSuccess()) { if ($fetchResult->isSuccess()) {
return $fetchResult->getBody(); return $fetchResult->getBody();
} else { } else {
return ""; throw new ReferenceStorageException(sprintf('External resource failed to get %s', $reference), $fetchResult->getReturnCode(), new Exception($fetchResult->getBody()));
} }
} }
/**
* @inheritDoc
*/
public function put(string $data, string $reference = '')
{
throw new BadMethodCallException();
}
public function delete(string $reference)
{
throw new BadMethodCallException();
}
/**
* @inheritDoc
*/
public function getOptions()
{
return [];
}
/**
* @inheritDoc
*/
public function saveOptions(array $data)
{
return [];
}
/** /**
* @inheritDoc * @inheritDoc
*/ */
@ -106,7 +72,7 @@ class ExternalResource implements IStorage
/** /**
* @inheritDoc * @inheritDoc
*/ */
public static function getName() public static function getName(): string
{ {
return self::NAME; return self::NAME;
} }

View File

@ -21,10 +21,10 @@
namespace Friendica\Model\Storage; namespace Friendica\Model\Storage;
use Exception;
use Friendica\Core\Config\IConfig; use Friendica\Core\Config\IConfig;
use Friendica\Core\L10n; use Friendica\Core\L10n;
use Friendica\Util\Strings; use Friendica\Util\Strings;
use Psr\Log\LoggerInterface;
/** /**
* Filesystem based storage backend * Filesystem based storage backend
@ -36,7 +36,7 @@ use Psr\Log\LoggerInterface;
* Each new resource gets a value as reference and is saved in a * Each new resource gets a value as reference and is saved in a
* folder tree stucture created from that value. * folder tree stucture created from that value.
*/ */
class Filesystem extends AbstractStorage class Filesystem implements IWritableStorage
{ {
const NAME = 'Filesystem'; const NAME = 'Filesystem';
@ -49,18 +49,19 @@ class Filesystem extends AbstractStorage
/** @var string */ /** @var string */
private $basePath; private $basePath;
/** @var L10n */
private $l10n;
/** /**
* Filesystem constructor. * Filesystem constructor.
* *
* @param IConfig $config * @param IConfig $config
* @param LoggerInterface $logger
* @param L10n $l10n * @param L10n $l10n
*/ */
public function __construct(IConfig $config, LoggerInterface $logger, L10n $l10n) public function __construct(IConfig $config, L10n $l10n)
{ {
parent::__construct($l10n, $logger);
$this->config = $config; $this->config = $config;
$this->l10n = $l10n;
$path = $this->config->get('storage', 'filesystem_path', self::DEFAULT_BASE_FOLDER); $path = $this->config->get('storage', 'filesystem_path', self::DEFAULT_BASE_FOLDER);
$this->basePath = rtrim($path, '/'); $this->basePath = rtrim($path, '/');
@ -73,7 +74,7 @@ class Filesystem extends AbstractStorage
* *
* @return string * @return string
*/ */
private function pathForRef(string $reference) private function pathForRef(string $reference): string
{ {
$fold1 = substr($reference, 0, 2); $fold1 = substr($reference, 0, 2);
$fold2 = substr($reference, 2, 2); $fold2 = substr($reference, 2, 2);
@ -84,7 +85,7 @@ class Filesystem extends AbstractStorage
/** /**
* Create dirctory tree to store file, with .htaccess and index.html files * Create directory tree to store file, with .htaccess and index.html files
* *
* @param string $file Path and filename * @param string $file Path and filename
* *
@ -96,8 +97,7 @@ class Filesystem extends AbstractStorage
if (!is_dir($path)) { if (!is_dir($path)) {
if (!mkdir($path, 0770, true)) { if (!mkdir($path, 0770, true)) {
$this->logger->warning('Failed to create dir.', ['path' => $path]); throw new StorageException(sprintf('Filesystem storage failed to create "%s". Check you write permissions.', $path));
throw new StorageException($this->l10n->t('Filesystem storage failed to create "%s". Check you write permissions.', $path));
} }
} }
@ -118,23 +118,33 @@ class Filesystem extends AbstractStorage
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function get(string $reference) public function get(string $reference): string
{ {
$file = $this->pathForRef($reference); $file = $this->pathForRef($reference);
if (!is_file($file)) { if (!is_file($file)) {
return ''; throw new ReferenceStorageException(sprintf('Filesystem storage failed to get the file %s, The file is invalid', $reference));
} }
return file_get_contents($file); $result = file_get_contents($file);
if ($result === false) {
throw new StorageException(sprintf('Filesystem storage failed to get data to "%s". Check your write permissions', $file));
}
return $result;
} }
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function put(string $data, string $reference = '') public function put(string $data, string $reference = ''): string
{ {
if ($reference === '') { if ($reference === '') {
$reference = Strings::getRandomHex(); try {
$reference = Strings::getRandomHex();
} catch (Exception $exception) {
throw new StorageException('Filesystem storage failed to generate a random hex', $exception->getCode(), $exception);
}
} }
$file = $this->pathForRef($reference); $file = $this->pathForRef($reference);
@ -144,8 +154,7 @@ class Filesystem extends AbstractStorage
// just in case the result is REALLY false, not zero or empty or anything else, throw the exception // just in case the result is REALLY false, not zero or empty or anything else, throw the exception
if ($result === false) { if ($result === false) {
$this->logger->warning('Failed to write data.', ['file' => $file]); throw new StorageException(sprintf('Filesystem storage failed to save data to "%s". Check your write permissions', $file));
throw new StorageException($this->l10n->t('Filesystem storage failed to save data to "%s". Check your write permissions', $file));
} }
chmod($file, 0660); chmod($file, 0660);
@ -158,17 +167,19 @@ class Filesystem extends AbstractStorage
public function delete(string $reference) public function delete(string $reference)
{ {
$file = $this->pathForRef($reference); $file = $this->pathForRef($reference);
// return true if file doesn't exists. we want to delete it: success with zero work!
if (!is_file($file)) { if (!is_file($file)) {
return true; throw new ReferenceStorageException(sprintf('File with reference "%s" doesn\'t exist', $reference));
}
if (!unlink($file)) {
throw new StorageException(sprintf('Cannot delete with file with reference "%s"', $reference));
} }
return unlink($file);
} }
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function getOptions() public function getOptions(): array
{ {
return [ return [
'storagepath' => [ 'storagepath' => [
@ -183,7 +194,7 @@ class Filesystem extends AbstractStorage
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function saveOptions(array $data) public function saveOptions(array $data): array
{ {
$storagePath = $data['storagepath'] ?? ''; $storagePath = $data['storagepath'] ?? '';
if ($storagePath === '' || !is_dir($storagePath)) { if ($storagePath === '' || !is_dir($storagePath)) {
@ -199,8 +210,13 @@ class Filesystem extends AbstractStorage
/** /**
* @inheritDoc * @inheritDoc
*/ */
public static function getName() public static function getName(): string
{ {
return self::NAME; return self::NAME;
} }
public function __toString()
{
return self::getName();
}
} }

View File

@ -22,9 +22,7 @@
namespace Friendica\Model\Storage; namespace Friendica\Model\Storage;
/** /**
* Interface for storage backends * Interface for basic storage backends
*
* @todo Split this interface into "IStorage" for get() operations (including Resource fetching) and "IUserStorage" for real user backends including put/delete/options
*/ */
interface IStorage interface IStorage
{ {
@ -34,77 +32,11 @@ interface IStorage
* @param string $reference Data reference * @param string $reference Data reference
* *
* @return string * @return string
*
* @throws StorageException in case there's an unexpected error
* @throws ReferenceStorageException in case the reference doesn't exist
*/ */
public function get(string $reference); public function get(string $reference): string;
/**
* Put data in backend as $ref. If $ref is not defined a new reference is created.
*
* @param string $data Data to save
* @param string $reference Data reference. Optional.
*
* @return string Saved data reference
*/
public function put(string $data, string $reference = "");
/**
* Remove data from backend
*
* @param string $reference Data reference
*
* @return boolean True on success
*/
public function delete(string $reference);
/**
* Get info about storage options
*
* @return array
*
* This method return an array with informations about storage options
* from which the form presented to the user is build.
*
* The returned array is:
*
* [
* 'option1name' => [ ..info.. ],
* 'option2name' => [ ..info.. ],
* ...
* ]
*
* An empty array can be returned if backend doesn't have any options
*
* The info array for each option MUST be as follows:
*
* [
* 'type', // define the field used in form, and the type of data.
* // one of 'checkbox', 'combobox', 'custom', 'datetime',
* // 'input', 'intcheckbox', 'password', 'radio', 'richtext'
* // 'select', 'select_raw', 'textarea'
*
* 'label', // Translatable label of the field
* 'value', // Current value
* 'help text', // Translatable description for the field
* extra data // Optional. Depends on 'type':
* // select: array [ value => label ] of choices
* // intcheckbox: value of input element
* // select_raw: prebuild html string of < option > tags
* ]
*
* See https://github.com/friendica/friendica/wiki/Quick-Template-Guide
*/
public function getOptions();
/**
* Validate and save options
*
* @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 function saveOptions(array $data);
/** /**
* The name of the backend * The name of the backend
@ -118,5 +50,5 @@ interface IStorage
* *
* @return string * @return string
*/ */
public static function getName(); public static function getName(): string;
} }

View File

@ -0,0 +1,103 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Model\Storage;
/**
* Interface for writable storage backends
*
* Used for storages with CRUD functionality, mainly used for user data (e.g. photos, attachements).
* There's only one active writable storage possible. This type of storage is selectable by the current administrator.
*/
interface IWritableStorage extends IStorage
{
/**
* Put data in backend as $ref. If $ref is not defined a new reference is created.
*
* @param string $data Data to save
* @param string $reference Data reference. Optional.
*
* @return string Saved data reference
*
* @throws StorageException in case there's an unexpected error
*/
public function put(string $data, string $reference = ""): string;
/**
* Remove data from backend
*
* @param string $reference Data reference
*
* @throws StorageException in case there's an unexpected error
* @throws ReferenceStorageException in case the reference doesn't exist
*/
public function delete(string $reference);
/**
* Get info about storage options
*
* @return array
*
* This method return an array with informations about storage options
* from which the form presented to the user is build.
*
* The returned array is:
*
* [
* 'option1name' => [ ..info.. ],
* 'option2name' => [ ..info.. ],
* ...
* ]
*
* An empty array can be returned if backend doesn't have any options
*
* The info array for each option MUST be as follows:
*
* [
* 'type', // define the field used in form, and the type of data.
* // one of 'checkbox', 'combobox', 'custom', 'datetime',
* // 'input', 'intcheckbox', 'password', 'radio', 'richtext'
* // 'select', 'select_raw', 'textarea'
*
* 'label', // Translatable label of the field
* 'value', // Current value
* 'help text', // Translatable description for the field
* extra data // Optional. Depends on 'type':
* // select: array [ value => label ] of choices
* // intcheckbox: value of input element
* // select_raw: prebuild html string of < option > tags
* ]
*
* See https://github.com/friendica/friendica/wiki/Quick-Template-Guide
*/
public function getOptions(): array;
/**
* Validate and save options
*
* @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 function saveOptions(array $data): array;
}

View File

@ -21,31 +21,9 @@
namespace Friendica\Model\Storage; namespace Friendica\Model\Storage;
use Friendica\Core\L10n;
use Psr\Log\LoggerInterface;
/** /**
* A general storage class which loads common dependencies and implements common methods * Storage Exception in case of invalid storage class
*/ */
abstract class AbstractStorage implements IStorage class InvalidClassStorageException extends StorageException
{ {
/** @var L10n */
protected $l10n;
/** @var LoggerInterface */
protected $logger;
/**
* @param L10n $l10n
* @param LoggerInterface $logger
*/
public function __construct(L10n $l10n, LoggerInterface $logger)
{
$this->l10n = $l10n;
$this->logger = $logger;
}
public function __toString()
{
return static::getName();
}
} }

View File

@ -0,0 +1,29 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Model\Storage;
/**
* Storage Exception in case of invalid references
*/
class ReferenceStorageException extends StorageException
{
}

View File

@ -21,9 +21,11 @@
namespace Friendica\Model\Storage; namespace Friendica\Model\Storage;
use Exception;
/** /**
* Storage Exception * Storage Exception for unexpected failures
*/ */
class StorageException extends \Exception class StorageException extends Exception
{ {
} }

View File

@ -21,8 +21,6 @@
namespace Friendica\Model\Storage; namespace Friendica\Model\Storage;
use \BadMethodCallException;
/** /**
* System resource storage class * System resource storage class
* *
@ -39,45 +37,22 @@ class SystemResource implements IStorage
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function get(string $filename) public function get(string $reference): string
{ {
$folder = dirname($filename); $folder = dirname($reference);
if (!in_array($folder, self::VALID_FOLDERS)) { if (!in_array($folder, self::VALID_FOLDERS)) {
return ""; throw new ReferenceStorageException(sprintf('System Resource is invalid for reference %s, no valid folder found', $reference));
} }
if (!file_exists($filename)) { if (!file_exists($reference)) {
return ""; throw new StorageException(sprintf('System Resource is invalid for reference %s, the file doesn\'t exist', $reference));
} }
return file_get_contents($filename); $content = file_get_contents($reference);
}
/** if ($content === false) {
* @inheritDoc throw new StorageException(sprintf('Cannot get content for reference %s', $reference));
*/ }
public function put(string $data, string $filename = '')
{
throw new BadMethodCallException();
}
public function delete(string $filename) return $content;
{
throw new BadMethodCallException();
}
/**
* @inheritDoc
*/
public function getOptions()
{
return [];
}
/**
* @inheritDoc
*/
public function saveOptions(array $data)
{
return [];
} }
/** /**
@ -91,7 +66,7 @@ class SystemResource implements IStorage
/** /**
* @inheritDoc * @inheritDoc
*/ */
public static function getName() public static function getName(): string
{ {
return self::NAME; return self::NAME;
} }

View File

@ -23,7 +23,8 @@ namespace Friendica\Module\Admin;
use Friendica\Core\Renderer; use Friendica\Core\Renderer;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Storage\IStorage; use Friendica\Model\Storage\InvalidClassStorageException;
use Friendica\Model\Storage\IWritableStorage;
use Friendica\Module\BaseAdmin; use Friendica\Module\BaseAdmin;
use Friendica\Util\Strings; use Friendica\Util\Strings;
@ -37,8 +38,13 @@ class Storage extends BaseAdmin
$storagebackend = Strings::escapeTags(trim($parameters['name'] ?? '')); $storagebackend = Strings::escapeTags(trim($parameters['name'] ?? ''));
/** @var IStorage $newstorage */ try {
$newstorage = DI::storageManager()->getByName($storagebackend); /** @var IWritableStorage $newstorage */
$newstorage = DI::storageManager()->getWritableStorageByName($storagebackend);
} catch (InvalidClassStorageException $storageException) {
notice(DI::l10n()->t('Storage backend, %s is invalid.', $storagebackend));
DI::baseUrl()->redirect('admin/storage');
}
// save storage backend form // save storage backend form
$storage_opts = $newstorage->getOptions(); $storage_opts = $newstorage->getOptions();
@ -62,13 +68,20 @@ class Storage extends BaseAdmin
$storage_form_errors = $newstorage->saveOptions($storage_opts_data); $storage_form_errors = $newstorage->saveOptions($storage_opts_data);
if (count($storage_form_errors)) { if (count($storage_form_errors)) {
foreach ($storage_form_errors as $name => $err) { foreach ($storage_form_errors as $name => $err) {
notice('Storage backend, ' . $storage_opts[$name][1] . ': ' . $err); notice(DI::l10n()->t('Storage backend %s error: %s', $storage_opts[$name][1], $err));
} }
DI::baseUrl()->redirect('admin/storage'); DI::baseUrl()->redirect('admin/storage');
} }
if (!empty($_POST['submit_save_set'])) { if (!empty($_POST['submit_save_set'])) {
if (empty($storagebackend) || !DI::storageManager()->setBackend($storagebackend)) { try {
/** @var IWritableStorage $newstorage */
$newstorage = DI::storageManager()->getWritableStorageByName($storagebackend);
if (!DI::storageManager()->setBackend($newstorage)) {
notice(DI::l10n()->t('Invalid storage backend setting value.'));
}
} catch (InvalidClassStorageException $storageException) {
notice(DI::l10n()->t('Invalid storage backend setting value.')); notice(DI::l10n()->t('Invalid storage backend setting value.'));
} }
} }
@ -83,13 +96,13 @@ class Storage extends BaseAdmin
$current_storage_backend = DI::storage(); $current_storage_backend = DI::storage();
$available_storage_forms = []; $available_storage_forms = [];
foreach (DI::storageManager()->listBackends() as $name => $class) { foreach (DI::storageManager()->listBackends() as $name) {
// build storage config form, // build storage config form,
$storage_form_prefix = preg_replace('|[^a-zA-Z0-9]|', '', $name); $storage_form_prefix = preg_replace('|[^a-zA-Z0-9]|', '', $name);
$storage_form = []; $storage_form = [];
foreach (DI::storageManager()->getByName($name)->getOptions() as $option => $info) { foreach (DI::storageManager()->getWritableStorageByName($name)->getOptions() as $option => $info) {
$type = $info[0]; $type = $info[0];
// Backward compatibilty with yesno field description // Backward compatibilty with yesno field description
if ($type == 'yesno') { if ($type == 'yesno') {
@ -108,7 +121,7 @@ class Storage extends BaseAdmin
'name' => $name, 'name' => $name,
'prefix' => $storage_form_prefix, 'prefix' => $storage_form_prefix,
'form' => $storage_form, 'form' => $storage_form,
'active' => $current_storage_backend instanceof IStorage && $name === $current_storage_backend::getName(), 'active' => $current_storage_backend instanceof IWritableStorage && $name === $current_storage_backend::getName(),
]; ];
} }
@ -124,7 +137,7 @@ class Storage extends BaseAdmin
'$noconfig' => DI::l10n()->t('This backend doesn\'t have custom settings'), '$noconfig' => DI::l10n()->t('This backend doesn\'t have custom settings'),
'$baseurl' => DI::baseUrl()->get(true), '$baseurl' => DI::baseUrl()->get(true),
'$form_security_token' => self::getFormSecurityToken("admin_storage"), '$form_security_token' => self::getFormSecurityToken("admin_storage"),
'$storagebackend' => $current_storage_backend instanceof IStorage ? $current_storage_backend::getName() : DI::l10n()->t('Database (legacy)'), '$storagebackend' => $current_storage_backend instanceof IWritableStorage ? $current_storage_backend::getName() : DI::l10n()->t('Database (legacy)'),
'$availablestorageforms' => $available_storage_forms, '$availablestorageforms' => $available_storage_forms,
]); ]);
} }

View File

@ -30,7 +30,11 @@ use Friendica\Model\Photo as MPhoto;
use Friendica\Model\Post; use Friendica\Model\Post;
use Friendica\Model\Profile; use Friendica\Model\Profile;
use Friendica\Model\Storage\ExternalResource; use Friendica\Model\Storage\ExternalResource;
use Friendica\Model\Storage\ReferenceStorageException;
use Friendica\Model\Storage\StorageException;
use Friendica\Model\Storage\SystemResource; use Friendica\Model\Storage\SystemResource;
use Friendica\Network\HTTPException\InternalServerErrorException;
use Friendica\Network\HTTPException\NotFoundException;
use Friendica\Util\Proxy; use Friendica\Util\Proxy;
use Friendica\Object\Image; use Friendica\Object\Image;
use Friendica\Util\Images; use Friendica\Util\Images;
@ -105,7 +109,11 @@ class Photo extends BaseModule
$cacheable = ($photo["allow_cid"] . $photo["allow_gid"] . $photo["deny_cid"] . $photo["deny_gid"] === "") && (isset($photo["cacheable"]) ? $photo["cacheable"] : true); $cacheable = ($photo["allow_cid"] . $photo["allow_gid"] . $photo["deny_cid"] . $photo["deny_gid"] === "") && (isset($photo["cacheable"]) ? $photo["cacheable"] : true);
$stamp = microtime(true); $stamp = microtime(true);
$imgdata = MPhoto::getImageDataForPhoto($photo); $imgdata = MPhoto::getImageDataForPhoto($photo);
if (empty($imgdata)) {
throw new NotFoundException();
}
// The mimetype for an external or system resource can only be known reliably after it had been fetched // The mimetype for an external or system resource can only be known reliably after it had been fetched
if (in_array($photo['backend-class'], [ExternalResource::NAME, SystemResource::NAME])) { if (in_array($photo['backend-class'], [ExternalResource::NAME, SystemResource::NAME])) {

View File

@ -32,6 +32,7 @@ use Friendica\Core\System;
use Friendica\Model\Contact; use Friendica\Model\Contact;
use Friendica\Model\Profile; use Friendica\Model\Profile;
use Friendica\Model\User; use Friendica\Model\User;
use Friendica\Network\HTTPException;
use Friendica\Network\Probe; use Friendica\Network\Probe;
/** /**
@ -44,6 +45,9 @@ class RemoteFollow extends BaseModule
public static function init(array $parameters = []) public static function init(array $parameters = [])
{ {
self::$owner = User::getOwnerDataByNick($parameters['profile']); self::$owner = User::getOwnerDataByNick($parameters['profile']);
if (!self::$owner) {
throw new HTTPException\NotFoundException(DI::l10n()->t('User not found.'));
}
DI::page()['aside'] = Widget\VCard::getHTML(self::$owner); DI::page()['aside'] = Widget\VCard::getHTML(self::$owner);
} }

View File

@ -61,6 +61,10 @@ class Crop extends BaseSettings
$base_image = Photo::selectFirst([], ['resource-id' => $resource_id, 'uid' => local_user(), 'scale' => $scale]); $base_image = Photo::selectFirst([], ['resource-id' => $resource_id, 'uid' => local_user(), 'scale' => $scale]);
if (DBA::isResult($base_image)) { if (DBA::isResult($base_image)) {
$Image = Photo::getImageForPhoto($base_image); $Image = Photo::getImageForPhoto($base_image);
if (empty($Image)) {
throw new HTTPException\InternalServerErrorException();
}
if ($Image->isValid()) { if ($Image->isValid()) {
// If setting for the default profile, unset the profile photo flag from any other photos I own // If setting for the default profile, unset the profile photo flag from any other photos I own
DBA::update('photo', ['profile' => 0], ['uid' => local_user()]); DBA::update('photo', ['profile' => 0], ['uid' => local_user()]);
@ -188,6 +192,9 @@ class Crop extends BaseSettings
} }
$Image = Photo::getImageForPhoto($photos[0]); $Image = Photo::getImageForPhoto($photos[0]);
if (empty($Image)) {
throw new HTTPException\InternalServerErrorException();
}
$imagecrop = [ $imagecrop = [
'resource-id' => $resource_id, 'resource-id' => $resource_id,

View File

@ -2167,7 +2167,7 @@ class Probe
{ {
// Search for the newest entry in the feed // Search for the newest entry in the feed
$curlResult = DI::httpRequest()->get($data['poll']); $curlResult = DI::httpRequest()->get($data['poll']);
if (!$curlResult->isSuccess()) { if (!$curlResult->isSuccess() || !$curlResult->getBody()) {
return ''; return '';
} }

View File

@ -28,6 +28,7 @@ use Friendica\DI;
use Friendica\Model\APContact; use Friendica\Model\APContact;
use Friendica\Model\Contact; use Friendica\Model\Contact;
use Friendica\Model\User; use Friendica\Model\User;
use Friendica\Network\CurlResult;
/** /**
* Implements HTTP Signatures per draft-cavage-http-signatures-07. * Implements HTTP Signatures per draft-cavage-http-signatures-07.
@ -408,7 +409,7 @@ class HTTPSignature
* 'nobody' => only return the header * 'nobody' => only return the header
* 'cookiejar' => path to cookie jar file * 'cookiejar' => path to cookie jar file
* *
* @return object CurlResult * @return CurlResult CurlResult
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/ */
public static function fetchRaw($request, $uid = 0, $opts = ['accept_content' => 'application/activity+json, application/ld+json']) public static function fetchRaw($request, $uid = 0, $opts = ['accept_content' => 'application/activity+json, application/ld+json'])

View File

@ -34,7 +34,7 @@ class MoveStorage
public static function execute() public static function execute()
{ {
$current = DI::storage(); $current = DI::storage();
$moved = DI::storageManager()->move($current); $moved = DI::storageManager()->move($current);
if ($moved) { if ($moved) {
Worker::add(PRIORITY_LOW, 'MoveStorage'); Worker::add(PRIORITY_LOW, 'MoveStorage');

View File

@ -44,7 +44,7 @@ use Friendica\Core\Session\ISession;
use Friendica\Core\StorageManager; use Friendica\Core\StorageManager;
use Friendica\Database\Database; use Friendica\Database\Database;
use Friendica\Factory; use Friendica\Factory;
use Friendica\Model\Storage\IStorage; use Friendica\Model\Storage\IWritableStorage;
use Friendica\Model\User\Cookie; use Friendica\Model\User\Cookie;
use Friendica\Network; use Friendica\Network;
use Friendica\Util; use Friendica\Util;
@ -213,7 +213,7 @@ return [
$_SERVER, $_COOKIE $_SERVER, $_COOKIE
], ],
], ],
IStorage::class => [ IWritableStorage::class => [
'instanceOf' => StorageManager::class, 'instanceOf' => StorageManager::class,
'call' => [ 'call' => [
['getBackend', [], Dice::CHAIN_CALL], ['getBackend', [], Dice::CHAIN_CALL],

View File

@ -203,4 +203,11 @@ return [
// Used in the admin settings to lock certain features // Used in the admin settings to lock certain features
'featurelock' => [ 'featurelock' => [
], ],
// Storage backend configuration
'storage' => [
// name (String)
// The name of the current used backend (default is Database)
'name' => 'Database',
],
]; ];

View File

@ -22,14 +22,14 @@
namespace Friendica\Test\Util; namespace Friendica\Test\Util;
use Friendica\Core\Hook; use Friendica\Core\Hook;
use Friendica\Model\Storage\IStorage; use Friendica\Model\Storage\IWritableStorage;
use Friendica\Core\L10n; use Friendica\Core\L10n;
/** /**
* A backend storage example class * A backend storage example class
*/ */
class SampleStorageBackend implements IStorage class SampleStorageBackend implements IWritableStorage
{ {
const NAME = 'Sample Storage'; const NAME = 'Sample Storage';
@ -62,14 +62,14 @@ class SampleStorageBackend implements IStorage
$this->l10n = $l10n; $this->l10n = $l10n;
} }
public function get(string $reference) public function get(string $reference): string
{ {
// we return always the same image data. Which file we load is defined by // we return always the same image data. Which file we load is defined by
// a config key // a config key
return $this->data[$reference] ?? null; return $this->data[$reference] ?? '';
} }
public function put(string $data, string $reference = '') public function put(string $data, string $reference = ''): string
{ {
if ($reference === '') { if ($reference === '') {
$reference = 'sample'; $reference = 'sample';
@ -89,12 +89,12 @@ class SampleStorageBackend implements IStorage
return true; return true;
} }
public function getOptions() public function getOptions(): array
{ {
return $this->options; return $this->options;
} }
public function saveOptions(array $data) public function saveOptions(array $data): array
{ {
$this->options = $data; $this->options = $data;
@ -107,7 +107,7 @@ class SampleStorageBackend implements IStorage
return self::NAME; return self::NAME;
} }
public static function getName() public static function getName(): string
{ {
return self::NAME; return self::NAME;
} }
@ -120,4 +120,3 @@ class SampleStorageBackend implements IStorage
Hook::register('storage_instance', __DIR__ . '/SampleStorageBackendInstance.php', 'create_instance'); Hook::register('storage_instance', __DIR__ . '/SampleStorageBackendInstance.php', 'create_instance');
} }
} }

View File

@ -34,7 +34,6 @@ use Friendica\Factory\ConfigFactory;
use Friendica\Model\Config\Config; use Friendica\Model\Config\Config;
use Friendica\Model\Storage; use Friendica\Model\Storage;
use Friendica\Core\Session; use Friendica\Core\Session;
use Friendica\Model\Storage\StorageException;
use Friendica\Network\HTTPRequest; use Friendica\Network\HTTPRequest;
use Friendica\Test\DatabaseTest; use Friendica\Test\DatabaseTest;
use Friendica\Test\Util\Database\StaticDatabase; use Friendica\Test\Util\Database\StaticDatabase;
@ -47,6 +46,7 @@ use Friendica\Test\Util\SampleStorageBackend;
class StorageManagerTest extends DatabaseTest class StorageManagerTest extends DatabaseTest
{ {
use VFSTrait;
/** @var Database */ /** @var Database */
private $dba; private $dba;
/** @var IConfig */ /** @var IConfig */
@ -58,8 +58,6 @@ class StorageManagerTest extends DatabaseTest
/** @var HTTPRequest */ /** @var HTTPRequest */
private $httpRequest; private $httpRequest;
use VFSTrait;
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
@ -82,6 +80,7 @@ class StorageManagerTest extends DatabaseTest
$configModel = new Config($this->dba); $configModel = new Config($this->dba);
$this->config = new PreloadConfig($configCache, $configModel); $this->config = new PreloadConfig($configCache, $configModel);
$this->config->set('storage', 'name', 'Database');
$this->l10n = \Mockery::mock(L10n::class); $this->l10n = \Mockery::mock(L10n::class);
@ -101,34 +100,38 @@ class StorageManagerTest extends DatabaseTest
public function dataStorages() public function dataStorages()
{ {
return [ return [
'empty' => [ 'empty' => [
'name' => '', 'name' => '',
'assert' => null, 'valid' => false,
'assertName' => '', 'interface' => Storage\IStorage::class,
'userBackend' => false, 'assert' => null,
'assertName' => '',
], ],
'database' => [ 'database' => [
'name' => Storage\Database::NAME, 'name' => Storage\Database::NAME,
'assert' => Storage\Database::class, 'valid' => true,
'assertName' => Storage\Database::NAME, 'interface' => Storage\IWritableStorage::class,
'userBackend' => true, 'assert' => Storage\Database::class,
'assertName' => Storage\Database::NAME,
], ],
'filesystem' => [ 'filesystem' => [
'name' => Storage\Filesystem::NAME, 'name' => Storage\Filesystem::NAME,
'assert' => Storage\Filesystem::class, 'valid' => true,
'assertName' => Storage\Filesystem::NAME, 'interface' => Storage\IWritableStorage::class,
'userBackend' => true, 'assert' => Storage\Filesystem::class,
'assertName' => Storage\Filesystem::NAME,
], ],
'systemresource' => [ 'systemresource' => [
'name' => Storage\SystemResource::NAME, 'name' => Storage\SystemResource::NAME,
'assert' => Storage\SystemResource::class, 'valid' => true,
'assertName' => Storage\SystemResource::NAME, 'interface' => Storage\IStorage::class,
// false here, because SystemResource isn't meant to be a user backend, 'assert' => Storage\SystemResource::class,
// it's for system resources only 'assertName' => Storage\SystemResource::NAME,
'userBackend' => false,
], ],
'invalid' => [ 'invalid' => [
'name' => 'invalid', 'name' => 'invalid',
'valid' => false,
'interface' => null,
'assert' => null, 'assert' => null,
'assertName' => '', 'assertName' => '',
'userBackend' => false, 'userBackend' => false,
@ -136,55 +139,31 @@ class StorageManagerTest extends DatabaseTest
]; ];
} }
/**
* Data array for legacy backends
*
* @todo 2020.09 After 2 releases, remove the legacy functionality and these data array with it
*
* @return array
*/
public function dataLegacyBackends()
{
return [
'legacyDatabase' => [
'name' => 'Friendica\Model\Storage\Database',
'assert' => Storage\Database::class,
'assertName' => Storage\Database::NAME,
'userBackend' => true,
],
'legacyFilesystem' => [
'name' => 'Friendica\Model\Storage\Filesystem',
'assert' => Storage\Filesystem::class,
'assertName' => Storage\Filesystem::NAME,
'userBackend' => true,
],
'legacySystemResource' => [
'name' => 'Friendica\Model\Storage\SystemResource',
'assert' => Storage\SystemResource::class,
'assertName' => Storage\SystemResource::NAME,
'userBackend' => false,
],
];
}
/** /**
* Test the getByName() method * Test the getByName() method
* *
* @dataProvider dataStorages * @dataProvider dataStorages
* @dataProvider dataLegacyBackends
*/ */
public function testGetByName($name, $assert, $assertName, $userBackend) public function testGetByName($name, $valid, $interface, $assert, $assertName)
{ {
$storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n, $this->httpRequest); if (!$valid) {
$this->expectException(Storage\InvalidClassStorageException::class);
$storage = $storageManager->getByName($name, $userBackend);
if (!empty($assert)) {
self::assertInstanceOf(Storage\IStorage::class, $storage);
self::assertInstanceOf($assert, $storage);
} else {
self::assertNull($storage);
} }
if ($interface === Storage\IWritableStorage::class) {
$this->config->set('storage', 'name', $name);
}
$storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n);
if ($interface === Storage\IWritableStorage::class) {
$storage = $storageManager->getWritableStorageByName($name);
} else {
$storage = $storageManager->getByName($name);
}
self::assertInstanceOf($interface, $storage);
self::assertInstanceOf($assert, $storage);
self::assertEquals($assertName, $storage); self::assertEquals($assertName, $storage);
} }
@ -193,15 +172,15 @@ class StorageManagerTest extends DatabaseTest
* *
* @dataProvider dataStorages * @dataProvider dataStorages
*/ */
public function testIsValidBackend($name, $assert, $assertName, $userBackend) public function testIsValidBackend($name, $valid, $interface, $assert, $assertName)
{ {
$storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n, $this->httpRequest); $storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n);
// true in every of the backends // true in every of the backends
self::assertEquals(!empty($assertName), $storageManager->isValidBackend($name)); self::assertEquals(!empty($assertName), $storageManager->isValidBackend($name));
// if userBackend is set to true, filter out e.g. SystemRessource // if it's a IWritableStorage, the valid backend should return true, otherwise false
self::assertEquals($userBackend, $storageManager->isValidBackend($name, true)); self::assertEquals($interface === Storage\IWritableStorage::class, $storageManager->isValidBackend($name, StorageManager::DEFAULT_BACKENDS));
} }
/** /**
@ -209,7 +188,7 @@ class StorageManagerTest extends DatabaseTest
*/ */
public function testListBackends() public function testListBackends()
{ {
$storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n, $this->httpRequest); $storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n);
self::assertEquals(StorageManager::DEFAULT_BACKENDS, $storageManager->listBackends()); self::assertEquals(StorageManager::DEFAULT_BACKENDS, $storageManager->listBackends());
} }
@ -219,36 +198,35 @@ class StorageManagerTest extends DatabaseTest
* *
* @dataProvider dataStorages * @dataProvider dataStorages
*/ */
public function testGetBackend($name, $assert, $assertName, $userBackend) public function testGetBackend($name, $valid, $interface, $assert, $assertName)
{ {
$storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n, $this->httpRequest); if ($interface !== Storage\IWritableStorage::class) {
static::markTestSkipped('only works for IWritableStorage');
self::assertNull($storageManager->getBackend());
if ($userBackend) {
$storageManager->setBackend($name);
self::assertInstanceOf($assert, $storageManager->getBackend());
} }
$storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n);
$selBackend = $storageManager->getWritableStorageByName($name);
$storageManager->setBackend($selBackend);
self::assertInstanceOf($assert, $storageManager->getBackend());
} }
/** /**
* Test the method getBackend() with a pre-configured backend * Test the method getBackend() with a pre-configured backend
* *
* @dataProvider dataStorages * @dataProvider dataStorages
* @dataProvider dataLegacyBackends
*/ */
public function testPresetBackend($name, $assert, $assertName, $userBackend) public function testPresetBackend($name, $valid, $interface, $assert, $assertName)
{ {
$this->config->set('storage', 'name', $name); $this->config->set('storage', 'name', $name);
if ($interface !== Storage\IWritableStorage::class) {
$storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n, $this->httpRequest); $this->expectException(Storage\InvalidClassStorageException::class);
if ($userBackend) {
self::assertInstanceOf($assert, $storageManager->getBackend());
} else {
self::assertNull($storageManager->getBackend());
} }
$storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n);
self::assertInstanceOf($assert, $storageManager->getBackend());
} }
/** /**
@ -261,38 +239,64 @@ class StorageManagerTest extends DatabaseTest
public function testRegisterUnregisterBackends() public function testRegisterUnregisterBackends()
{ {
/// @todo Remove dice once "Hook" is dynamic and mockable /// @todo Remove dice once "Hook" is dynamic and mockable
$dice = (new Dice()) $dice = (new Dice())
->addRules(include __DIR__ . '/../../../static/dependencies.config.php') ->addRules(include __DIR__ . '/../../../static/dependencies.config.php')
->addRule(Database::class, ['instanceOf' => StaticDatabase::class, 'shared' => true]) ->addRule(Database::class, ['instanceOf' => StaticDatabase::class, 'shared' => true])
->addRule(ISession::class, ['instanceOf' => Session\Memory::class, 'shared' => true, 'call' => null]); ->addRule(ISession::class, ['instanceOf' => Session\Memory::class, 'shared' => true, 'call' => null]);
DI::init($dice); DI::init($dice);
$storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n, $this->httpRequest); $storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n);
self::assertTrue($storageManager->register(SampleStorageBackend::class)); self::assertTrue($storageManager->register(SampleStorageBackend::class));
self::assertEquals(array_merge(StorageManager::DEFAULT_BACKENDS, [ self::assertEquals(array_merge(StorageManager::DEFAULT_BACKENDS, [
SampleStorageBackend::getName() => SampleStorageBackend::class, SampleStorageBackend::getName(),
]), $storageManager->listBackends()); ]), $storageManager->listBackends());
self::assertEquals(array_merge(StorageManager::DEFAULT_BACKENDS, [ self::assertEquals(array_merge(StorageManager::DEFAULT_BACKENDS, [
SampleStorageBackend::getName() => SampleStorageBackend::class, SampleStorageBackend::getName()
]), $this->config->get('storage', 'backends'));
self::assertTrue($storageManager->unregister(SampleStorageBackend::class));
self::assertEquals(StorageManager::DEFAULT_BACKENDS, $this->config->get('storage', 'backends'));
self::assertEquals(StorageManager::DEFAULT_BACKENDS, $storageManager->listBackends());
}
/**
* tests that an active backend cannot get unregistered
*/
public function testUnregisterActiveBackend()
{
/// @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);
self::assertTrue($storageManager->register(SampleStorageBackend::class));
self::assertEquals(array_merge(StorageManager::DEFAULT_BACKENDS, [
SampleStorageBackend::getName(),
]), $storageManager->listBackends());
self::assertEquals(array_merge(StorageManager::DEFAULT_BACKENDS, [
SampleStorageBackend::getName()
]), $this->config->get('storage', 'backends')); ]), $this->config->get('storage', 'backends'));
// inline call to register own class as hook (testing purpose only) // inline call to register own class as hook (testing purpose only)
SampleStorageBackend::registerHook(); SampleStorageBackend::registerHook();
Hook::loadHooks(); Hook::loadHooks();
self::assertTrue($storageManager->setBackend(SampleStorageBackend::NAME)); self::assertTrue($storageManager->setBackend($storageManager->getWritableStorageByName(SampleStorageBackend::NAME)));
self::assertEquals(SampleStorageBackend::NAME, $this->config->get('storage', 'name')); self::assertEquals(SampleStorageBackend::NAME, $this->config->get('storage', 'name'));
self::assertInstanceOf(SampleStorageBackend::class, $storageManager->getBackend()); self::assertInstanceOf(SampleStorageBackend::class, $storageManager->getBackend());
self::assertTrue($storageManager->unregister(SampleStorageBackend::class)); self::expectException(Storage\StorageException::class);
self::assertEquals(StorageManager::DEFAULT_BACKENDS, $this->config->get('storage', 'backends')); self::expectExceptionMessage('Cannot unregister Sample Storage, because it\'s currently active.');
self::assertEquals(StorageManager::DEFAULT_BACKENDS, $storageManager->listBackends());
self::assertNull($storageManager->getBackend()); $storageManager->unregister(SampleStorageBackend::class);
self::assertNull($this->config->get('storage', 'name'));
} }
/** /**
@ -300,26 +304,25 @@ class StorageManagerTest extends DatabaseTest
* *
* @dataProvider dataStorages * @dataProvider dataStorages
*/ */
public function testMoveStorage($name, $assert, $assertName, $userBackend) public function testMoveStorage($name, $valid, $interface, $assert, $assertName)
{ {
if (!$userBackend) { if ($interface !== Storage\IWritableStorage::class) {
self::markTestSkipped("No user backend"); self::markTestSkipped("No user backend");
} }
$this->loadFixture(__DIR__ . '/../../datasets/storage/database.fixture.php', $this->dba); $this->loadFixture(__DIR__ . '/../../datasets/storage/database.fixture.php', $this->dba);
$storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n, $this->httpRequest); $storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n);
$storage = $storageManager->getByName($name); $storage = $storageManager->getWritableStorageByName($name);
$storageManager->move($storage); $storageManager->move($storage);
$photos = $this->dba->select('photo', ['backend-ref', 'backend-class', 'id', 'data']); $photos = $this->dba->select('photo', ['backend-ref', 'backend-class', 'id', 'data']);
while ($photo = $this->dba->fetch($photos)) { while ($photo = $this->dba->fetch($photos)) {
self::assertEmpty($photo['data']); self::assertEmpty($photo['data']);
$storage = $storageManager->getByName($photo['backend-class']); $storage = $storageManager->getByName($photo['backend-class']);
$data = $storage->get($photo['backend-ref']); $data = $storage->get($photo['backend-ref']);
self::assertNotEmpty($data); self::assertNotEmpty($data);
} }
@ -328,13 +331,13 @@ class StorageManagerTest extends DatabaseTest
/** /**
* Test moving data to a WRONG storage * Test moving data to a WRONG storage
*/ */
public function testMoveStorageWrong() public function testWrongWritableStorage()
{ {
$this->expectExceptionMessage("Can't move to storage backend 'SystemResource'"); $this->expectException(Storage\InvalidClassStorageException::class);
$this->expectException(StorageException::class); $this->expectExceptionMessage('Backend SystemResource is not valid');
$storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n, $this->httpRequest); $storageManager = new StorageManager($this->dba, $this->config, $this->logger, $this->l10n);
$storage = $storageManager->getByName(Storage\SystemResource::getName()); $storage = $storageManager->getWritableStorageByName(Storage\SystemResource::getName());
$storageManager->move($storage); $storageManager->move($storage);
} }
} }

View File

@ -21,16 +21,14 @@
namespace Friendica\Test\src\Model\Storage; namespace Friendica\Test\src\Model\Storage;
use Friendica\Core\L10n;
use Friendica\Factory\ConfigFactory; use Friendica\Factory\ConfigFactory;
use Friendica\Model\Storage\Database; use Friendica\Model\Storage\Database;
use Friendica\Model\Storage\IStorage; use Friendica\Model\Storage\IWritableStorage;
use Friendica\Test\DatabaseTestTrait; use Friendica\Test\DatabaseTestTrait;
use Friendica\Test\Util\Database\StaticDatabase; use Friendica\Test\Util\Database\StaticDatabase;
use Friendica\Test\Util\VFSTrait; use Friendica\Test\Util\VFSTrait;
use Friendica\Util\ConfigFileLoader; use Friendica\Util\ConfigFileLoader;
use Friendica\Util\Profiler; use Friendica\Util\Profiler;
use Mockery\MockInterface;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;
class DatabaseStorageTest extends StorageTest class DatabaseStorageTest extends StorageTest
@ -62,13 +60,10 @@ class DatabaseStorageTest extends StorageTest
$dba = new StaticDatabase($configCache, $profiler, $logger); $dba = new StaticDatabase($configCache, $profiler, $logger);
/** @var MockInterface|L10n $l10n */ return new Database($dba);
$l10n = \Mockery::mock(L10n::class)->makePartial();
return new Database($dba, $logger, $l10n);
} }
protected function assertOption(IStorage $storage) protected function assertOption(IWritableStorage $storage)
{ {
self::assertEmpty($storage->getOptions()); self::assertEmpty($storage->getOptions());
} }

View File

@ -24,13 +24,12 @@ namespace Friendica\Test\src\Model\Storage;
use Friendica\Core\Config\IConfig; use Friendica\Core\Config\IConfig;
use Friendica\Core\L10n; use Friendica\Core\L10n;
use Friendica\Model\Storage\Filesystem; use Friendica\Model\Storage\Filesystem;
use Friendica\Model\Storage\IStorage; use Friendica\Model\Storage\IWritableStorage;
use Friendica\Model\Storage\StorageException; use Friendica\Model\Storage\StorageException;
use Friendica\Test\Util\VFSTrait; use Friendica\Test\Util\VFSTrait;
use Friendica\Util\Profiler; use Friendica\Util\Profiler;
use Mockery\MockInterface; use Mockery\MockInterface;
use org\bovigo\vfs\vfsStream; use org\bovigo\vfs\vfsStream;
use Psr\Log\NullLogger;
class FilesystemStorageTest extends StorageTest class FilesystemStorageTest extends StorageTest
{ {
@ -50,7 +49,6 @@ class FilesystemStorageTest extends StorageTest
protected function getInstance() protected function getInstance()
{ {
$logger = new NullLogger();
$profiler = \Mockery::mock(Profiler::class); $profiler = \Mockery::mock(Profiler::class);
$profiler->shouldReceive('startRecording'); $profiler->shouldReceive('startRecording');
$profiler->shouldReceive('stopRecording'); $profiler->shouldReceive('stopRecording');
@ -63,10 +61,10 @@ class FilesystemStorageTest extends StorageTest
->with('storage', 'filesystem_path', Filesystem::DEFAULT_BASE_FOLDER) ->with('storage', 'filesystem_path', Filesystem::DEFAULT_BASE_FOLDER)
->andReturn($this->root->getChild('storage')->url()); ->andReturn($this->root->getChild('storage')->url());
return new Filesystem($this->config, $logger, $l10n); return new Filesystem($this->config, $l10n);
} }
protected function assertOption(IStorage $storage) protected function assertOption(IWritableStorage $storage)
{ {
self::assertEquals([ self::assertEquals([
'storagepath' => [ 'storagepath' => [

View File

@ -21,15 +21,17 @@
namespace Friendica\Test\src\Model\Storage; namespace Friendica\Test\src\Model\Storage;
use Friendica\Model\Storage\IWritableStorage;
use Friendica\Model\Storage\IStorage; use Friendica\Model\Storage\IStorage;
use Friendica\Model\Storage\ReferenceStorageException;
use Friendica\Test\MockedTest; use Friendica\Test\MockedTest;
abstract class StorageTest extends MockedTest abstract class StorageTest extends MockedTest
{ {
/** @return IStorage */ /** @return IWritableStorage */
abstract protected function getInstance(); abstract protected function getInstance();
abstract protected function assertOption(IStorage $storage); abstract protected function assertOption(IWritableStorage $storage);
/** /**
* Test if the instance is "really" implementing the interface * Test if the instance is "really" implementing the interface
@ -62,7 +64,7 @@ abstract class StorageTest extends MockedTest
self::assertEquals('data12345', $instance->get($ref)); self::assertEquals('data12345', $instance->get($ref));
self::assertTrue($instance->delete($ref)); $instance->delete($ref);
} }
/** /**
@ -70,10 +72,11 @@ abstract class StorageTest extends MockedTest
*/ */
public function testInvalidDelete() public function testInvalidDelete()
{ {
self::expectException(ReferenceStorageException::class);
$instance = $this->getInstance(); $instance = $this->getInstance();
// Even deleting not existing references should return "true" $instance->delete(-1234456);
self::assertTrue($instance->delete(-1234456));
} }
/** /**
@ -81,10 +84,11 @@ abstract class StorageTest extends MockedTest
*/ */
public function testInvalidGet() public function testInvalidGet()
{ {
self::expectException(ReferenceStorageException::class);
$instance = $this->getInstance(); $instance = $this->getInstance();
// Invalid references return an empty string $instance->get(-123456);
self::assertEmpty($instance->get(-123456));
} }
/** /**

View File

@ -981,3 +981,20 @@ function update_1429()
return Update::SUCCESS; return Update::SUCCESS;
} }
function update_1434()
{
$name = DI::config()->get('storage', 'name');
// in case of an empty config, set "Database" as default storage backend
if (empty($name)) {
DI::config()->set('storage', 'name', Storage\Database::getName());
}
// In case of a Using deprecated storage class value, set the right name for it
if (stristr($name, 'Friendica\Model\Storage\\')) {
DI::config()->set('storage', 'name', substr($name, 24));
}
return Update::SUCCESS;
}

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: 2021.09-dev\n" "Project-Id-Version: 2021.09-dev\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-08-17 22:57+0000\n" "POT-Creation-Date: 2021-08-17 08:39+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -40,10 +40,10 @@ msgstr ""
#: include/api.php:4437 mod/photos.php:86 mod/photos.php:195 mod/photos.php:623 #: include/api.php:4437 mod/photos.php:86 mod/photos.php:195 mod/photos.php:623
#: mod/photos.php:1031 mod/photos.php:1048 mod/photos.php:1594 #: mod/photos.php:1031 mod/photos.php:1048 mod/photos.php:1594
#: src/Model/User.php:1112 src/Model/User.php:1120 src/Model/User.php:1128 #: src/Model/User.php:1112 src/Model/User.php:1120 src/Model/User.php:1128
#: src/Module/Settings/Profile/Photo/Crop.php:97 #: src/Module/Settings/Profile/Photo/Crop.php:101
#: src/Module/Settings/Profile/Photo/Crop.php:113 #: src/Module/Settings/Profile/Photo/Crop.php:117
#: src/Module/Settings/Profile/Photo/Crop.php:129 #: src/Module/Settings/Profile/Photo/Crop.php:133
#: src/Module/Settings/Profile/Photo/Crop.php:175 #: src/Module/Settings/Profile/Photo/Crop.php:179
#: src/Module/Settings/Profile/Photo/Index.php:95 #: src/Module/Settings/Profile/Photo/Index.php:95
#: src/Module/Settings/Profile/Photo/Index.php:101 #: src/Module/Settings/Profile/Photo/Index.php:101
msgid "Profile Photos" msgid "Profile Photos"
@ -851,7 +851,7 @@ msgstr ""
#: src/Module/Search/Directory.php:38 src/Module/Settings/Delegation.php:42 #: src/Module/Search/Directory.php:38 src/Module/Settings/Delegation.php:42
#: src/Module/Settings/Delegation.php:70 src/Module/Settings/Display.php:43 #: src/Module/Settings/Delegation.php:70 src/Module/Settings/Display.php:43
#: src/Module/Settings/Display.php:121 #: src/Module/Settings/Display.php:121
#: src/Module/Settings/Profile/Photo/Crop.php:154 #: src/Module/Settings/Profile/Photo/Crop.php:158
#: src/Module/Settings/Profile/Photo/Index.php:112 #: src/Module/Settings/Profile/Photo/Index.php:112
#: src/Module/Settings/UserExport.php:58 src/Module/Settings/UserExport.php:93 #: src/Module/Settings/UserExport.php:58 src/Module/Settings/UserExport.php:93
#: src/Module/Settings/UserExport.php:199 #: src/Module/Settings/UserExport.php:199
@ -969,7 +969,7 @@ msgid "Edit post"
msgstr "" msgstr ""
#: mod/editpost.php:91 mod/notes.php:56 src/Content/Text/HTML.php:885 #: mod/editpost.php:91 mod/notes.php:56 src/Content/Text/HTML.php:885
#: src/Module/Admin/Storage.php:120 src/Module/Filer/SaveTag.php:69 #: src/Module/Admin/Storage.php:133 src/Module/Filer/SaveTag.php:69
msgid "Save" msgid "Save"
msgstr "" msgstr ""
@ -2716,7 +2716,7 @@ msgstr ""
msgid "File upload failed." msgid "File upload failed."
msgstr "" msgstr ""
#: mod/wall_upload.php:233 src/Model/Photo.php:1002 #: mod/wall_upload.php:233 src/Model/Photo.php:1013
msgid "Wall Photos" msgid "Wall Photos"
msgstr "" msgstr ""
@ -3679,29 +3679,29 @@ msgstr ""
msgid "Connectors" msgid "Connectors"
msgstr "" msgstr ""
#: src/Core/Installer.php:179 #: src/Core/Installer.php:183
msgid "" msgid ""
"The database configuration file \"config/local.config.php\" could not be " "The database configuration file \"config/local.config.php\" could not be "
"written. Please use the enclosed text to create a configuration file in your " "written. Please use the enclosed text to create a configuration file in your "
"web server root." "web server root."
msgstr "" msgstr ""
#: src/Core/Installer.php:198 #: src/Core/Installer.php:202
msgid "" msgid ""
"You may need to import the file \"database.sql\" manually using phpmyadmin " "You may need to import the file \"database.sql\" manually using phpmyadmin "
"or mysql." "or mysql."
msgstr "" msgstr ""
#: src/Core/Installer.php:199 src/Module/Install.php:206 #: src/Core/Installer.php:203 src/Module/Install.php:206
#: src/Module/Install.php:365 #: src/Module/Install.php:365
msgid "Please see the file \"doc/INSTALL.md\"." msgid "Please see the file \"doc/INSTALL.md\"."
msgstr "" msgstr ""
#: src/Core/Installer.php:260 #: src/Core/Installer.php:264
msgid "Could not find a command line version of PHP in the web server PATH." msgid "Could not find a command line version of PHP in the web server PATH."
msgstr "" msgstr ""
#: src/Core/Installer.php:261 #: src/Core/Installer.php:265
msgid "" msgid ""
"If you don't have a command line version of PHP installed on your server, " "If you don't have a command line version of PHP installed on your server, "
"you will not be able to run the background processing. See <a href='https://" "you will not be able to run the background processing. See <a href='https://"
@ -3709,259 +3709,283 @@ msgid ""
"worker'>'Setup the worker'</a>" "worker'>'Setup the worker'</a>"
msgstr "" msgstr ""
#: src/Core/Installer.php:266 #: src/Core/Installer.php:270
msgid "PHP executable path" msgid "PHP executable path"
msgstr "" msgstr ""
#: src/Core/Installer.php:266 #: src/Core/Installer.php:270
msgid "" msgid ""
"Enter full path to php executable. You can leave this blank to continue the " "Enter full path to php executable. You can leave this blank to continue the "
"installation." "installation."
msgstr "" msgstr ""
#: src/Core/Installer.php:271 #: src/Core/Installer.php:275
msgid "Command line PHP" msgid "Command line PHP"
msgstr "" msgstr ""
#: src/Core/Installer.php:280 #: src/Core/Installer.php:284
msgid "PHP executable is not the php cli binary (could be cgi-fgci version)" msgid "PHP executable is not the php cli binary (could be cgi-fgci version)"
msgstr "" msgstr ""
#: src/Core/Installer.php:281 #: src/Core/Installer.php:285
msgid "Found PHP version: " msgid "Found PHP version: "
msgstr "" msgstr ""
#: src/Core/Installer.php:283 #: src/Core/Installer.php:287
msgid "PHP cli binary" msgid "PHP cli binary"
msgstr "" msgstr ""
#: src/Core/Installer.php:296 #: src/Core/Installer.php:300
msgid "" msgid ""
"The command line version of PHP on your system does not have " "The command line version of PHP on your system does not have "
"\"register_argc_argv\" enabled." "\"register_argc_argv\" enabled."
msgstr "" msgstr ""
#: src/Core/Installer.php:297 #: src/Core/Installer.php:301
msgid "This is required for message delivery to work." msgid "This is required for message delivery to work."
msgstr "" msgstr ""
#: src/Core/Installer.php:302 #: src/Core/Installer.php:306
msgid "PHP register_argc_argv" msgid "PHP register_argc_argv"
msgstr "" msgstr ""
#: src/Core/Installer.php:334 #: src/Core/Installer.php:338
msgid "" msgid ""
"Error: the \"openssl_pkey_new\" function on this system is not able to " "Error: the \"openssl_pkey_new\" function on this system is not able to "
"generate encryption keys" "generate encryption keys"
msgstr "" msgstr ""
#: src/Core/Installer.php:335 #: src/Core/Installer.php:339
msgid "" msgid ""
"If running under Windows, please see \"http://www.php.net/manual/en/openssl." "If running under Windows, please see \"http://www.php.net/manual/en/openssl."
"installation.php\"." "installation.php\"."
msgstr "" msgstr ""
#: src/Core/Installer.php:338 #: src/Core/Installer.php:342
msgid "Generate encryption keys" msgid "Generate encryption keys"
msgstr "" msgstr ""
#: src/Core/Installer.php:390 #: src/Core/Installer.php:394
msgid "" msgid ""
"Error: Apache webserver mod-rewrite module is required but not installed." "Error: Apache webserver mod-rewrite module is required but not installed."
msgstr "" msgstr ""
#: src/Core/Installer.php:395 #: src/Core/Installer.php:399
msgid "Apache mod_rewrite module" msgid "Apache mod_rewrite module"
msgstr "" msgstr ""
#: src/Core/Installer.php:401 #: src/Core/Installer.php:405
msgid "Error: PDO or MySQLi PHP module required but not installed." msgid "Error: PDO or MySQLi PHP module required but not installed."
msgstr "" msgstr ""
#: src/Core/Installer.php:406 #: src/Core/Installer.php:410
msgid "Error: The MySQL driver for PDO is not installed." msgid "Error: The MySQL driver for PDO is not installed."
msgstr "" msgstr ""
#: src/Core/Installer.php:410 #: src/Core/Installer.php:414
msgid "PDO or MySQLi PHP module" msgid "PDO or MySQLi PHP module"
msgstr "" msgstr ""
#: src/Core/Installer.php:418 #: src/Core/Installer.php:422
msgid "Error, XML PHP module required but not installed." msgid "Error, XML PHP module required but not installed."
msgstr "" msgstr ""
#: src/Core/Installer.php:422 #: src/Core/Installer.php:426
msgid "XML PHP module" msgid "XML PHP module"
msgstr "" msgstr ""
#: src/Core/Installer.php:425 #: src/Core/Installer.php:429
msgid "libCurl PHP module" msgid "libCurl PHP module"
msgstr "" msgstr ""
#: src/Core/Installer.php:426 #: src/Core/Installer.php:430
msgid "Error: libCURL PHP module required but not installed." msgid "Error: libCURL PHP module required but not installed."
msgstr "" msgstr ""
#: src/Core/Installer.php:432 #: src/Core/Installer.php:436
msgid "GD graphics PHP module" msgid "GD graphics PHP module"
msgstr "" msgstr ""
#: src/Core/Installer.php:433 #: src/Core/Installer.php:437
msgid "" msgid ""
"Error: GD graphics PHP module with JPEG support required but not installed." "Error: GD graphics PHP module with JPEG support required but not installed."
msgstr "" msgstr ""
#: src/Core/Installer.php:439 #: src/Core/Installer.php:443
msgid "OpenSSL PHP module" msgid "OpenSSL PHP module"
msgstr "" msgstr ""
#: src/Core/Installer.php:440 #: src/Core/Installer.php:444
msgid "Error: openssl PHP module required but not installed." msgid "Error: openssl PHP module required but not installed."
msgstr "" msgstr ""
#: src/Core/Installer.php:446 #: src/Core/Installer.php:450
msgid "mb_string PHP module" msgid "mb_string PHP module"
msgstr "" msgstr ""
#: src/Core/Installer.php:447 #: src/Core/Installer.php:451
msgid "Error: mb_string PHP module required but not installed." msgid "Error: mb_string PHP module required but not installed."
msgstr "" msgstr ""
#: src/Core/Installer.php:453 #: src/Core/Installer.php:457
msgid "iconv PHP module" msgid "iconv PHP module"
msgstr "" msgstr ""
#: src/Core/Installer.php:454 #: src/Core/Installer.php:458
msgid "Error: iconv PHP module required but not installed." msgid "Error: iconv PHP module required but not installed."
msgstr "" msgstr ""
#: src/Core/Installer.php:460 #: src/Core/Installer.php:464
msgid "POSIX PHP module" msgid "POSIX PHP module"
msgstr "" msgstr ""
#: src/Core/Installer.php:461 #: src/Core/Installer.php:465
msgid "Error: POSIX PHP module required but not installed." msgid "Error: POSIX PHP module required but not installed."
msgstr "" msgstr ""
#: src/Core/Installer.php:467 #: src/Core/Installer.php:471
msgid "Program execution functions" msgid "Program execution functions"
msgstr "" msgstr ""
#: src/Core/Installer.php:468 #: src/Core/Installer.php:472
msgid "" msgid ""
"Error: Program execution functions (proc_open) required but not enabled." "Error: Program execution functions (proc_open) required but not enabled."
msgstr "" msgstr ""
#: src/Core/Installer.php:474 #: src/Core/Installer.php:478
msgid "JSON PHP module" msgid "JSON PHP module"
msgstr "" msgstr ""
#: src/Core/Installer.php:475 #: src/Core/Installer.php:479
msgid "Error: JSON PHP module required but not installed." msgid "Error: JSON PHP module required but not installed."
msgstr "" msgstr ""
#: src/Core/Installer.php:481 #: src/Core/Installer.php:485
msgid "File Information PHP module" msgid "File Information PHP module"
msgstr "" msgstr ""
#: src/Core/Installer.php:482 #: src/Core/Installer.php:486
msgid "Error: File Information PHP module required but not installed." msgid "Error: File Information PHP module required but not installed."
msgstr "" msgstr ""
#: src/Core/Installer.php:505 #: src/Core/Installer.php:509
msgid "" msgid ""
"The web installer needs to be able to create a file called \"local.config.php" "The web installer needs to be able to create a file called \"local.config.php"
"\" in the \"config\" folder of your web server and it is unable to do so." "\" in the \"config\" folder of your web server and it is unable to do so."
msgstr "" msgstr ""
#: src/Core/Installer.php:506 #: src/Core/Installer.php:510
msgid "" msgid ""
"This is most often a permission setting, as the web server may not be able " "This is most often a permission setting, as the web server may not be able "
"to write files in your folder - even if you can." "to write files in your folder - even if you can."
msgstr "" msgstr ""
#: src/Core/Installer.php:507 #: src/Core/Installer.php:511
msgid "" msgid ""
"At the end of this procedure, we will give you a text to save in a file " "At the end of this procedure, we will give you a text to save in a file "
"named local.config.php in your Friendica \"config\" folder." "named local.config.php in your Friendica \"config\" folder."
msgstr "" msgstr ""
#: src/Core/Installer.php:508 #: src/Core/Installer.php:512
msgid "" msgid ""
"You can alternatively skip this procedure and perform a manual installation. " "You can alternatively skip this procedure and perform a manual installation. "
"Please see the file \"doc/INSTALL.md\" for instructions." "Please see the file \"doc/INSTALL.md\" for instructions."
msgstr "" msgstr ""
#: src/Core/Installer.php:511 #: src/Core/Installer.php:515
msgid "config/local.config.php is writable" msgid "config/local.config.php is writable"
msgstr "" msgstr ""
#: src/Core/Installer.php:531 #: src/Core/Installer.php:535
msgid "" msgid ""
"Friendica uses the Smarty3 template engine to render its web views. Smarty3 " "Friendica uses the Smarty3 template engine to render its web views. Smarty3 "
"compiles templates to PHP to speed up rendering." "compiles templates to PHP to speed up rendering."
msgstr "" msgstr ""
#: src/Core/Installer.php:532 #: src/Core/Installer.php:536
msgid "" msgid ""
"In order to store these compiled templates, the web server needs to have " "In order to store these compiled templates, the web server needs to have "
"write access to the directory view/smarty3/ under the Friendica top level " "write access to the directory view/smarty3/ under the Friendica top level "
"folder." "folder."
msgstr "" msgstr ""
#: src/Core/Installer.php:533 #: src/Core/Installer.php:537
msgid "" msgid ""
"Please ensure that the user that your web server runs as (e.g. www-data) has " "Please ensure that the user that your web server runs as (e.g. www-data) has "
"write access to this folder." "write access to this folder."
msgstr "" msgstr ""
#: src/Core/Installer.php:534 #: src/Core/Installer.php:538
msgid "" msgid ""
"Note: as a security measure, you should give the web server write access to " "Note: as a security measure, you should give the web server write access to "
"view/smarty3/ only--not the template files (.tpl) that it contains." "view/smarty3/ only--not the template files (.tpl) that it contains."
msgstr "" msgstr ""
#: src/Core/Installer.php:537 #: src/Core/Installer.php:541
msgid "view/smarty3 is writable" msgid "view/smarty3 is writable"
msgstr "" msgstr ""
#: src/Core/Installer.php:565 #: src/Core/Installer.php:569
msgid "" msgid ""
"Url rewrite in .htaccess seems not working. Make sure you copied .htaccess-" "Url rewrite in .htaccess seems not working. Make sure you copied .htaccess-"
"dist to .htaccess." "dist to .htaccess."
msgstr "" msgstr ""
#: src/Core/Installer.php:566 #: src/Core/Installer.php:570
msgid "" msgid ""
"In some circumstances (like running inside containers), you can skip this " "In some circumstances (like running inside containers), you can skip this "
"error." "error."
msgstr "" msgstr ""
#: src/Core/Installer.php:568 #: src/Core/Installer.php:572
msgid "Error message from Curl when fetching" msgid "Error message from Curl when fetching"
msgstr "" msgstr ""
#: src/Core/Installer.php:574 #: src/Core/Installer.php:578
msgid "Url rewrite is working" msgid "Url rewrite is working"
msgstr "" msgstr ""
#: src/Core/Installer.php:603 #: src/Core/Installer.php:607
msgid ""
"The detection of TLS to secure the communication between the browser and the "
"new Friendica server failed."
msgstr ""
#: src/Core/Installer.php:608
msgid ""
"It is highly encouraged to use Friendica only over a secure connection as "
"sensitive information like passwords will be transmitted."
msgstr ""
#: src/Core/Installer.php:609
msgid "Please ensure that the connection to the server is secure."
msgstr ""
#: src/Core/Installer.php:610
msgid "No TLS detected"
msgstr ""
#: src/Core/Installer.php:612
msgid "TLS detected"
msgstr ""
#: src/Core/Installer.php:639
msgid "ImageMagick PHP extension is not installed" msgid "ImageMagick PHP extension is not installed"
msgstr "" msgstr ""
#: src/Core/Installer.php:605 #: src/Core/Installer.php:641
msgid "ImageMagick PHP extension is installed" msgid "ImageMagick PHP extension is installed"
msgstr "" msgstr ""
#: src/Core/Installer.php:607 #: src/Core/Installer.php:643
msgid "ImageMagick supports GIF" msgid "ImageMagick supports GIF"
msgstr "" msgstr ""
#: src/Core/Installer.php:629 #: src/Core/Installer.php:665
msgid "Database already in use." msgid "Database already in use."
msgstr "" msgstr ""
#: src/Core/Installer.php:634 #: src/Core/Installer.php:670
msgid "Could not connect to database." msgid "Could not connect to database."
msgstr "" msgstr ""
@ -4689,39 +4713,17 @@ msgstr ""
msgid "OpenWebAuth: %1$s welcomes %2$s" msgid "OpenWebAuth: %1$s welcomes %2$s"
msgstr "" msgstr ""
#: src/Model/Storage/Database.php:74 #: src/Model/Storage/Filesystem.php:187
#, php-format
msgid "Database storage failed to update %s"
msgstr ""
#: src/Model/Storage/Database.php:82
msgid "Database storage failed to insert data"
msgstr ""
#: src/Model/Storage/Filesystem.php:100
#, php-format
msgid ""
"Filesystem storage failed to create \"%s\". Check you write permissions."
msgstr ""
#: src/Model/Storage/Filesystem.php:148
#, php-format
msgid ""
"Filesystem storage failed to save data to \"%s\". Check your write "
"permissions"
msgstr ""
#: src/Model/Storage/Filesystem.php:176
msgid "Storage base path" msgid "Storage base path"
msgstr "" msgstr ""
#: src/Model/Storage/Filesystem.php:178 #: src/Model/Storage/Filesystem.php:189
msgid "" msgid ""
"Folder where uploaded files are saved. For maximum security, This should be " "Folder where uploaded files are saved. For maximum security, This should be "
"a path outside web server folder tree" "a path outside web server folder tree"
msgstr "" msgstr ""
#: src/Model/Storage/Filesystem.php:191 #: src/Model/Storage/Filesystem.php:202
msgid "Enter a valid existing folder" msgid "Enter a valid existing folder"
msgstr "" msgstr ""
@ -5004,7 +5006,7 @@ msgstr ""
#: src/Module/Admin/Blocklist/Server.php:88 src/Module/Admin/Federation.php:159 #: src/Module/Admin/Blocklist/Server.php:88 src/Module/Admin/Federation.php:159
#: src/Module/Admin/Item/Delete.php:65 src/Module/Admin/Logs/Settings.php:80 #: src/Module/Admin/Item/Delete.php:65 src/Module/Admin/Logs/Settings.php:80
#: src/Module/Admin/Logs/View.php:64 src/Module/Admin/Queue.php:72 #: src/Module/Admin/Logs/View.php:64 src/Module/Admin/Queue.php:72
#: src/Module/Admin/Site.php:498 src/Module/Admin/Storage.php:118 #: src/Module/Admin/Site.php:498 src/Module/Admin/Storage.php:131
#: src/Module/Admin/Summary.php:232 src/Module/Admin/Themes/Details.php:90 #: src/Module/Admin/Summary.php:232 src/Module/Admin/Themes/Details.php:90
#: src/Module/Admin/Themes/Index.php:111 src/Module/Admin/Tos.php:58 #: src/Module/Admin/Themes/Index.php:111 src/Module/Admin/Tos.php:58
#: src/Module/Admin/Users/Active.php:136 src/Module/Admin/Users/Blocked.php:137 #: src/Module/Admin/Users/Active.php:136 src/Module/Admin/Users/Blocked.php:137
@ -6463,31 +6465,41 @@ msgstr ""
msgid "Start Relocation" msgid "Start Relocation"
msgstr "" msgstr ""
#: src/Module/Admin/Storage.php:72 #: src/Module/Admin/Storage.php:45
#, php-format
msgid "Storage backend, %s is invalid."
msgstr ""
#: src/Module/Admin/Storage.php:71
#, php-format
msgid "Storage backend %s error: %s"
msgstr ""
#: src/Module/Admin/Storage.php:82 src/Module/Admin/Storage.php:85
msgid "Invalid storage backend setting value." msgid "Invalid storage backend setting value."
msgstr "" msgstr ""
#: src/Module/Admin/Storage.php:119 src/Module/BaseAdmin.php:91 #: src/Module/Admin/Storage.php:132 src/Module/BaseAdmin.php:91
msgid "Storage" msgid "Storage"
msgstr "" msgstr ""
#: src/Module/Admin/Storage.php:121 #: src/Module/Admin/Storage.php:134
msgid "Save & Activate" msgid "Save & Activate"
msgstr "" msgstr ""
#: src/Module/Admin/Storage.php:122 #: src/Module/Admin/Storage.php:135
msgid "Activate" msgid "Activate"
msgstr "" msgstr ""
#: src/Module/Admin/Storage.php:123 #: src/Module/Admin/Storage.php:136
msgid "Save & Reload" msgid "Save & Reload"
msgstr "" msgstr ""
#: src/Module/Admin/Storage.php:124 #: src/Module/Admin/Storage.php:137
msgid "This backend doesn't have custom settings" msgid "This backend doesn't have custom settings"
msgstr "" msgstr ""
#: src/Module/Admin/Storage.php:127 #: src/Module/Admin/Storage.php:140
msgid "Database (legacy)" msgid "Database (legacy)"
msgstr "" msgstr ""
@ -8700,17 +8712,17 @@ msgstr ""
msgid "Visible to:" msgid "Visible to:"
msgstr "" msgstr ""
#: src/Module/Photo.php:94 #: src/Module/Photo.php:98
#, php-format #, php-format
msgid "The Photo with id %s is not available." msgid "The Photo with id %s is not available."
msgstr "" msgstr ""
#: src/Module/Photo.php:124 #: src/Module/Photo.php:132
#, php-format #, php-format
msgid "Invalid external resource with url %s." msgid "Invalid external resource with url %s."
msgstr "" msgstr ""
#: src/Module/Photo.php:126 #: src/Module/Photo.php:134
#, php-format #, php-format
msgid "Invalid photo with id %s." msgid "Invalid photo with id %s."
msgstr "" msgstr ""
@ -9465,42 +9477,42 @@ msgid ""
"contacts or the Friendica contacts in the selected groups.</p>" "contacts or the Friendica contacts in the selected groups.</p>"
msgstr "" msgstr ""
#: src/Module/Settings/Profile/Photo/Crop.php:102 #: src/Module/Settings/Profile/Photo/Crop.php:106
#: src/Module/Settings/Profile/Photo/Crop.php:118 #: src/Module/Settings/Profile/Photo/Crop.php:122
#: src/Module/Settings/Profile/Photo/Crop.php:134 #: src/Module/Settings/Profile/Photo/Crop.php:138
#: src/Module/Settings/Profile/Photo/Index.php:102 #: src/Module/Settings/Profile/Photo/Index.php:102
#, php-format #, php-format
msgid "Image size reduction [%s] failed." msgid "Image size reduction [%s] failed."
msgstr "" msgstr ""
#: src/Module/Settings/Profile/Photo/Crop.php:139 #: src/Module/Settings/Profile/Photo/Crop.php:143
msgid "" msgid ""
"Shift-reload the page or clear browser cache if the new photo does not " "Shift-reload the page or clear browser cache if the new photo does not "
"display immediately." "display immediately."
msgstr "" msgstr ""
#: src/Module/Settings/Profile/Photo/Crop.php:144 #: src/Module/Settings/Profile/Photo/Crop.php:148
msgid "Unable to process image" msgid "Unable to process image"
msgstr "" msgstr ""
#: src/Module/Settings/Profile/Photo/Crop.php:163 #: src/Module/Settings/Profile/Photo/Crop.php:167
msgid "Photo not found." msgid "Photo not found."
msgstr "" msgstr ""
#: src/Module/Settings/Profile/Photo/Crop.php:185 #: src/Module/Settings/Profile/Photo/Crop.php:189
msgid "Profile picture successfully updated." msgid "Profile picture successfully updated."
msgstr "" msgstr ""
#: src/Module/Settings/Profile/Photo/Crop.php:208 #: src/Module/Settings/Profile/Photo/Crop.php:215
#: src/Module/Settings/Profile/Photo/Crop.php:212 #: src/Module/Settings/Profile/Photo/Crop.php:219
msgid "Crop Image" msgid "Crop Image"
msgstr "" msgstr ""
#: src/Module/Settings/Profile/Photo/Crop.php:209 #: src/Module/Settings/Profile/Photo/Crop.php:216
msgid "Please adjust the image cropping for optimum viewing." msgid "Please adjust the image cropping for optimum viewing."
msgstr "" msgstr ""
#: src/Module/Settings/Profile/Photo/Crop.php:211 #: src/Module/Settings/Profile/Photo/Crop.php:218
msgid "Use Image As Is" msgid "Use Image As Is"
msgstr "" msgstr ""