diff --git a/composer.json b/composer.json index 34c0f62582..d0fc5619b9 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,7 @@ "paragonie/hidden-string": "^1.0", "patrickschur/language-detection": "^5.0.0", "pear/console_table": "^1.3", - "phpseclib/phpseclib": "^2.0", + "phpseclib/phpseclib": "^3.0", "pragmarx/google2fa": "^5.0", "pragmarx/recovery": "^0.2", "psr/container": "^1.0", diff --git a/composer.lock b/composer.lock index 786880fab8..1805c9d21a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e8626dc6957dff9cc783daad10cfc26f", + "content-hash": "2e082bac083ca61cc0c22f7055d690bf", "packages": [ { "name": "asika/simple-console", @@ -2952,32 +2952,32 @@ }, { "name": "phpseclib/phpseclib", - "version": "2.0.38", + "version": "3.0.17", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "b03536539f43a4f9aa33c4f0b2f3a1c752088fcd" + "reference": "dbc2307d5c69aeb22db136c52e91130d7f2ca761" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/b03536539f43a4f9aa33c4f0b2f3a1c752088fcd", - "reference": "b03536539f43a4f9aa33c4f0b2f3a1c752088fcd", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/dbc2307d5c69aeb22db136c52e91130d7f2ca761", + "reference": "dbc2307d5c69aeb22db136c52e91130d7f2ca761", "shasum": "" }, "require": { - "php": ">=5.3.3" + "paragonie/constant_time_encoding": "^1|^2", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": ">=5.6.1" }, "require-dev": { - "phing/phing": "~2.7", - "phpunit/phpunit": "^4.8.35|^5.7|^6.0|^9.4", - "squizlabs/php_codesniffer": "~2.0" + "phpunit/phpunit": "*" }, "suggest": { + "ext-dom": "Install the DOM extension to load XML formatted public keys.", "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", - "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations.", - "ext-xml": "Install the XML extension to load XML formatted public keys." + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." }, "type": "library", "autoload": { @@ -2985,7 +2985,7 @@ "phpseclib/bootstrap.php" ], "psr-4": { - "phpseclib\\": "phpseclib/" + "phpseclib3\\": "phpseclib/" } }, "notification-url": "https://packagist.org/downloads/", @@ -3054,7 +3054,7 @@ "type": "tidelift" } ], - "time": "2022-09-02T17:04:26+00:00" + "time": "2022-10-24T10:51:50+00:00" }, { "name": "pragmarx/google2fa", diff --git a/doc/API-Mastodon.md b/doc/API-Mastodon.md index b09ecca8af..63c487caf3 100644 --- a/doc/API-Mastodon.md +++ b/doc/API-Mastodon.md @@ -47,6 +47,7 @@ These endpoints use the [Mastodon API entities](https://docs.joinmastodon.org/en - [`POST /api/v1/accounts/:id/unmute`](https://docs.joinmastodon.org/methods/accounts/) - [`GET /api/v1/accounts/relationships`](https://docs.joinmastodon.org/methods/accounts/) - [`GET /api/v1/accounts/search`](https://docs.joinmastodon.org/methods/accounts) +- [`PATCH /api/v1/accounts/update_credentials`](https://docs.joinmastodon.org/methods/accounts/#update_credentials) - [`GET /api/v1/accounts/verify_credentials`](https://docs.joinmastodon.org/methods/accounts) - [`POST /api/v1/apps`](https://docs.joinmastodon.org/methods/apps/) - [`GET /api/v1/apps/verify_credentials`](https://docs.joinmastodon.org/methods/apps/) @@ -104,24 +105,26 @@ These endpoints use the [Mastodon API entities](https://docs.joinmastodon.org/en - [`DELETE /api/v1/scheduled_statuses/:id`](https://docs.joinmastodon.org/methods/statuses/scheduled_statuses/) - [`GET /api/v1/scheduled_statuses/:id`](https://docs.joinmastodon.org/methods/statuses/scheduled_statuses/) - [`GET /api/v1/search`](https://docs.joinmastodon.org/methods/search/) -- [`POST /api/v1/statuses`](https://docs.joinmastodon.org/methods/statuses/) +- [`POST /api/v1/statuses`](https://docs.joinmastodon.org/methods/statuses/#create) - Additionally to the static values `public`, `unlisted` and `private`, the `visibility` parameter can contain a numeric value with a group id. -- [`GET /api/v1/statuses/:id`](https://docs.joinmastodon.org/methods/statuses/) -- [`DELETE /api/v1/statuses/:id`](https://docs.joinmastodon.org/methods/statuses/) -- [`GET /api/v1/statuses/:id/card`](https://docs.joinmastodon.org/methods/statuses/) -- [`GET /api/v1/statuses/:id/context`](https://docs.joinmastodon.org/methods/statuses/) -- [`GET /api/v1/statuses/:id/reblogged_by`](https://docs.joinmastodon.org/methods/statuses/) -- [`GET /api/v1/statuses/:id/favourited_by`](https://docs.joinmastodon.org/methods/statuses/) -- [`POST /api/v1/statuses/:id/favourite`](https://docs.joinmastodon.org/methods/statuses/) -- [`POST /api/v1/statuses/:id/unfavourite`](https://docs.joinmastodon.org/methods/statuses/) -- [`POST /api/v1/statuses/:id/reblog`](https://docs.joinmastodon.org/methods/statuses/) -- [`POST /api/v1/statuses/:id/unreblog`](https://docs.joinmastodon.org/methods/statuses/) -- [`POST /api/v1/statuses/:id/bookmark`](https://docs.joinmastodon.org/methods/statuses/) -- [`POST /api/v1/statuses/:id/unbookmark`](https://docs.joinmastodon.org/methods/statuses/) -- [`POST /api/v1/statuses/:id/mute`](https://docs.joinmastodon.org/methods/statuses/) -- [`POST /api/v1/statuses/:id/unmute`](https://docs.joinmastodon.org/methods/statuses/) -- [`POST /api/v1/statuses/:id/pin`](https://docs.joinmastodon.org/methods/statuses/) -- [`POST /api/v1/statuses/:id/unpin`](https://docs.joinmastodon.org/methods/statuses/) +- [`GET /api/v1/statuses/:id`](https://docs.joinmastodon.org/methods/statuses/#get) +- [`DELETE /api/v1/statuses/:id`](https://docs.joinmastodon.org/methods/statuses/#delete) +- [`GET /api/v1/statuses/:id/context`](https://docs.joinmastodon.org/methods/statuses/#context) +- [`GET /api/v1/statuses/:id/reblogged_by`](https://docs.joinmastodon.org/methods/statuses/#reblogged_by) +- [`GET /api/v1/statuses/:id/favourited_by`](https://docs.joinmastodon.org/methods/statuses/#favourited_by) +- [`POST /api/v1/statuses/:id/favourite`](https://docs.joinmastodon.org/methods/statuses/#favourite) +- [`POST /api/v1/statuses/:id/unfavourite`](https://docs.joinmastodon.org/methods/statuses/#unfavourite) +- [`POST /api/v1/statuses/:id/reblog`](https://docs.joinmastodon.org/methods/statuses/#boost) +- [`POST /api/v1/statuses/:id/unreblog`](https://docs.joinmastodon.org/methods/statuses/#unreblog) +- [`POST /api/v1/statuses/:id/bookmark`](https://docs.joinmastodon.org/methods/statuses/#bookmark) +- [`POST /api/v1/statuses/:id/unbookmark`](https://docs.joinmastodon.org/methods/statuses/#unbookmark) +- [`POST /api/v1/statuses/:id/mute`](https://docs.joinmastodon.org/methods/statuses/#mute) +- [`POST /api/v1/statuses/:id/unmute`](https://docs.joinmastodon.org/methods/statuses/#unmute) +- [`POST /api/v1/statuses/:id/pin`](https://docs.joinmastodon.org/methods/statuses/#pin) +- [`POST /api/v1/statuses/:id/unpin`](https://docs.joinmastodon.org/methods/statuses/#unpin) +- [`POST /api/v1/statuses/:id`](https://docs.joinmastodon.org/methods/statuses/#edit) +- [`GET /api/v1/statuses/:id/source`](https://docs.joinmastodon.org/methods/statuses/#source) +- [`GET /api/v1/statuses/:id/card`](https://docs.joinmastodon.org/methods/statuses/#card) - [`GET /api/v1/suggestions`](https://docs.joinmastodon.org/methods/accounts/suggestions/) - [`GET /api/v1/timelines/direct`](https://docs.joinmastodon.org/methods/timelines/) - [`GET /api/v1/timelines/home`](https://docs.joinmastodon.org/methods/timelines/) @@ -136,7 +139,6 @@ These endpoints use the [Mastodon API entities](https://docs.joinmastodon.org/en These emdpoints are planned to be implemented somewhere in the future. -- [`PATCH /api/v1/accounts/update_credentials`](https://docs.joinmastodon.org/methods/accounts/) - [`POST /api/v1/accounts/:id/remove_from_followers`](https://github.com/mastodon/mastodon/pull/16864) - [`GET /api/v1/accounts/familiar_followers`](https://github.com/mastodon/mastodon/pull/17700) - [`GET /api/v1/accounts/lookup`](https://github.com/mastodon/mastodon/pull/15740) @@ -144,7 +146,6 @@ These emdpoints are planned to be implemented somewhere in the future. - [`GET /api/v1/trends/statuses`](https://github.com/mastodon/mastodon/pull/17431) - [`GET /api/v1/trends/tags`](https://github.com/mastodon/mastodon/pull/16917) - [`POST /api/v1/polls/:id/votes`](https://docs.joinmastodon.org/methods/statuses/polls/) -- [`GET /api/v1/statuses/{id:\d+}/source`](https://github.com/mastodon/mastodon/pull/16697) - [`GET /api/v1/featured_tags`](https://docs.joinmastodon.org/methods/accounts/featured_tags/) - [`POST /api/v1/featured_tags`](https://docs.joinmastodon.org/methods/accounts/featured_tags/) - [`DELETE /api/v1/featured_tags/:id`](https://docs.joinmastodon.org/methods/accounts/featured_tags/) diff --git a/mod/fbrowser.php b/mod/fbrowser.php deleted file mode 100644 index 952ab1452c..0000000000 --- a/mod/fbrowser.php +++ /dev/null @@ -1,160 +0,0 @@ -. - * - * @package Friendica\modules - * @subpackage FileBrowser - * @author Fabio Comuni - */ - -use Friendica\App; -use Friendica\Core\Renderer; -use Friendica\Core\System; -use Friendica\Database\DBA; -use Friendica\DI; -use Friendica\Model\Photo; -use Friendica\Util\Images; -use Friendica\Util\Strings; - -/** - * @param App $a - * @return string - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ -function fbrowser_content(App $a) -{ - if (!DI::userSession()->getLocalUserId()) { - System::exit(); - } - - if (DI::args()->getArgc() == 1) { - System::exit(); - } - - // Needed to match the correct template in a module that uses a different theme than the user/site/default - $theme = Strings::sanitizeFilePathItem($_GET['theme'] ?? ''); - if ($theme && is_file("view/theme/$theme/config.php")) { - $a->setCurrentTheme($theme); - } - - $template_file = "filebrowser.tpl"; - - $o = ''; - - switch (DI::args()->getArgv()[1]) { - case "image": - $path = ['' => DI::l10n()->t('Photos')]; - $albums = false; - $sql_extra = ""; - $sql_extra2 = " ORDER BY created DESC LIMIT 0, 10"; - - if (DI::args()->getArgc() == 2) { - $photos = DBA::toArray(DBA::p("SELECT distinct(`album`) AS `album` FROM `photo` WHERE `uid` = ? AND NOT `photo-type` IN (?, ?)", - DI::userSession()->getLocalUserId(), - Photo::CONTACT_AVATAR, - Photo::CONTACT_BANNER - )); - - $albums = array_column($photos, 'album'); - } - - if (DI::args()->getArgc() == 3) { - $album = DI::args()->getArgv()[2]; - $sql_extra = sprintf("AND `album` = '%s' ", DBA::escape($album)); - $sql_extra2 = ""; - $path[$album] = $album; - } - - $r = DBA::toArray(DBA::p("SELECT `resource-id`, ANY_VALUE(`id`) AS `id`, ANY_VALUE(`filename`) AS `filename`, ANY_VALUE(`type`) AS `type`, - min(`scale`) AS `hiq`, max(`scale`) AS `loq`, ANY_VALUE(`desc`) AS `desc`, ANY_VALUE(`created`) AS `created` - FROM `photo` WHERE `uid` = ? $sql_extra AND NOT `photo-type` IN (?, ?) - GROUP BY `resource-id` $sql_extra2", - DI::userSession()->getLocalUserId(), - Photo::CONTACT_AVATAR, - Photo::CONTACT_BANNER - )); - - function _map_files1($rr) - { - $a = DI::app(); - $types = Images::supportedTypes(); - $ext = $types[$rr['type']]; - $filename_e = $rr['filename']; - - // Take the largest picture that is smaller or equal 640 pixels - $photo = Photo::selectFirst(['scale'], ["`resource-id` = ? AND `height` <= ? AND `width` <= ?", $rr['resource-id'], 640, 640], ['order' => ['scale']]); - $scale = $photo['scale'] ?? $rr['loq']; - - return [ - DI::baseUrl() . '/photos/' . $a->getLoggedInUserNickname() . '/image/' . $rr['resource-id'], - $filename_e, - DI::baseUrl() . '/photo/' . $rr['resource-id'] . '-' . $scale . '.'. $ext, - $rr['desc'] - ]; - } - $files = array_map("_map_files1", $r); - - $tpl = Renderer::getMarkupTemplate($template_file); - - $o = Renderer::replaceMacros($tpl, [ - '$type' => 'image', - '$path' => $path, - '$folders' => $albums, - '$files' => $files, - '$cancel' => DI::l10n()->t('Cancel'), - '$nickname' => $a->getLoggedInUserNickname(), - '$upload' => DI::l10n()->t('Upload') - ]); - - break; - case "file": - if (DI::args()->getArgc()==2) { - $files = DBA::selectToArray('attach', ['id', 'filename', 'filetype'], ['uid' => DI::userSession()->getLocalUserId()]); - - function _map_files2($rr) - { - list($m1, $m2) = explode("/", $rr['filetype']); - $filetype = ( (file_exists("images/icons/$m1.png"))?$m1:"zip"); - $filename_e = $rr['filename']; - - return [DI::baseUrl() . '/attach/' . $rr['id'], $filename_e, DI::baseUrl() . '/images/icons/16/' . $filetype . '.png']; - } - $files = array_map("_map_files2", $files); - - - $tpl = Renderer::getMarkupTemplate($template_file); - $o = Renderer::replaceMacros($tpl, [ - '$type' => 'file', - '$path' => ['' => DI::l10n()->t('Files')], - '$folders' => false, - '$files' => $files, - '$cancel' => DI::l10n()->t('Cancel'), - '$nickname' => $a->getLoggedInUserNickname(), - '$upload' => DI::l10n()->t('Upload') - ]); - } - - break; - } - - if (!empty($_GET['mode'])) { - return $o; - } else { - System::httpExit($o); - } -} diff --git a/mods/release-list-include.txt b/mods/release-list-include.txt index ebe1cccc76..a18d3d0c16 100644 --- a/mods/release-list-include.txt +++ b/mods/release-list-include.txt @@ -7,7 +7,6 @@ bin/daemon.php bin/testargs.php bin/wait-for-connection bin/worker.php -config/addon-sample.config.php config/local-sample.config.php doc/ images/ diff --git a/src/App/Arguments.php b/src/App/Arguments.php index 6dfdcb560f..3268530b20 100644 --- a/src/App/Arguments.php +++ b/src/App/Arguments.php @@ -85,6 +85,8 @@ class Arguments /** * @return string The module name based on the arguments + * @deprecated 2022.12 - With the new (sub-)routes, it's not trustworthy anymore, use the ModuleClass instead + * @see Router::getModuleClass() */ public function getModuleName(): string { diff --git a/src/App/Router.php b/src/App/Router.php index 4e5f29521a..35ea9ada8b 100644 --- a/src/App/Router.php +++ b/src/App/Router.php @@ -40,6 +40,7 @@ use Friendica\Module\HTTPException\MethodNotAllowed; use Friendica\Module\HTTPException\PageNotFound; use Friendica\Module\Special\Options; use Friendica\Network\HTTPException; +use Friendica\Network\HTTPException\InternalServerErrorException; use Friendica\Network\HTTPException\MethodNotAllowedException; use Friendica\Network\HTTPException\NotFoundException; use Friendica\Util\Router\FriendicaGroupCountBased; @@ -114,6 +115,9 @@ class Router /** @var array */ private $server; + /** @var string|null */ + protected $moduleClass = null; + /** * @param array $server The $_SERVER variable * @param string $baseRoutesFilepath The path to a base routes file to leverage cache, can be empty @@ -216,7 +220,7 @@ class Router * * @return bool */ - private function isGroup(array $config) + private function isGroup(array $config): bool { return is_array($config) && @@ -252,21 +256,39 @@ class Router * * @return RouteCollector|null */ - public function getRouteCollector() + public function getRouteCollector(): ?RouteCollector { return $this->routeCollector; } + /** + * Returns the Friendica\BaseModule-extending class name if a route rule matched + * + * @return string + * + * @throws InternalServerErrorException + * @throws MethodNotAllowedException + * @throws NotFoundException + */ + public function getModuleClass(): string + { + if (empty($this->moduleClass)) { + $this->determineModuleClass(); + } + + return $this->moduleClass; + } + /** * Returns the relevant module class name for the given page URI or NULL if no route rule matched. * - * @return string A Friendica\BaseModule-extending class name if a route rule matched + * @return void * * @throws HTTPException\InternalServerErrorException * @throws HTTPException\MethodNotAllowedException If a rule matched but the method didn't * @throws HTTPException\NotFoundException If no rule matched */ - private function getModuleClass(): string + private function determineModuleClass(): void { $cmd = $this->args->getCommand(); $cmd = '/' . ltrim($cmd, '/'); @@ -277,21 +299,19 @@ class Router // Check if the HTTP method is OPTIONS and return the special Options Module with the possible HTTP methods if ($this->args->getMethod() === static::OPTIONS) { - $moduleClass = Options::class; - $this->parameters = ['allowedMethods' => $dispatcher->getOptions($cmd)]; + $this->moduleClass = Options::class; + $this->parameters = ['allowedMethods' => $dispatcher->getOptions($cmd)]; } else { $routeInfo = $dispatcher->dispatch($this->args->getMethod(), $cmd); if ($routeInfo[0] === Dispatcher::FOUND) { - $moduleClass = $routeInfo[1]; - $this->parameters = $routeInfo[2]; + $this->moduleClass = $routeInfo[1]; + $this->parameters = $routeInfo[2]; } elseif ($routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED) { throw new HTTPException\MethodNotAllowedException($this->l10n->t('Method not allowed for this module. Allowed method(s): %s', implode(', ', $routeInfo[1]))); } else { throw new HTTPException\NotFoundException($this->l10n->t('Page not found.')); } } - - return $moduleClass; } public function getModule(?string $module_class = null): ICanHandleRequests diff --git a/src/BaseCollection.php b/src/BaseCollection.php index f6fa9bbd4d..ad68dc81cc 100644 --- a/src/BaseCollection.php +++ b/src/BaseCollection.php @@ -48,25 +48,27 @@ class BaseCollection extends \ArrayIterator /** * @inheritDoc */ - public function offsetSet($offset, $value) + #[\ReturnTypeWillChange] + public function offsetSet($key, $value): void { - if (is_null($offset)) { + if (is_null($key)) { $this->totalCount++; } - parent::offsetSet($offset, $value); + parent::offsetSet($key, $value); } /** * @inheritDoc */ - public function offsetUnset($offset) + #[\ReturnTypeWillChange] + public function offsetUnset($key): void { - if ($this->offsetExists($offset)) { + if ($this->offsetExists($key)) { $this->totalCount--; } - parent::offsetUnset($offset); + parent::offsetUnset($key); } /** diff --git a/src/Content/Conversation.php b/src/Content/Conversation.php index be7448a336..6d19eb1635 100644 --- a/src/Content/Conversation.php +++ b/src/Content/Conversation.php @@ -794,7 +794,7 @@ class Conversation return []; } - $str_blocked = str_replace(["\n", "\r"], ",", $this->pConfig->get($this->session->getLocalUserId(), 'system', 'blocked')); + $str_blocked = str_replace(["\n", "\r"], ",", $this->pConfig->get($this->session->getLocalUserId(), 'system', 'blocked') ?? ''); if (empty($str_blocked)) { return []; } diff --git a/src/Core/Session/Handler/Database.php b/src/Core/Session/Handler/Database.php index 41ccb6b33f..337397be60 100644 --- a/src/Core/Session/Handler/Database.php +++ b/src/Core/Session/Handler/Database.php @@ -57,6 +57,7 @@ class Database extends AbstractSessionHandler return true; } + #[\ReturnTypeWillChange] public function read($id) { if (empty($id)) { @@ -136,6 +137,7 @@ class Database extends AbstractSessionHandler } } + #[\ReturnTypeWillChange] public function gc($max_lifetime): bool { try { diff --git a/src/Core/Worker.php b/src/Core/Worker.php index 96f3e7ae04..47da04b35c 100644 --- a/src/Core/Worker.php +++ b/src/Core/Worker.php @@ -568,7 +568,15 @@ class Worker // Set the workerLogger as new default logger if ($method_call) { - call_user_func_array(sprintf('Friendica\Worker\%s::execute', $funcname), $argv); + try { + call_user_func_array(sprintf('Friendica\Worker\%s::execute', $funcname), $argv); + } catch (\TypeError $e) { + // No need to defer a worker queue entry if the arguments are invalid + Logger::notice('Wrong worker arguments', ['class' => $funcname, 'argv' => $argv, 'queue' => $queue, 'message' => $e->getMessage()]); + } catch (\Throwable $e) { + Logger::error('Uncaught exception in worker execution', ['class' => get_class($e), 'message' => $e->getMessage(), 'code' => $e->getCode(), 'file' => $e->getFile() . ':' . $e->getLine(), 'trace' => $e->getTraceAsString()]); + Worker::defer(); + } } else { $funcname($argv, count($argv)); } diff --git a/src/DI.php b/src/DI.php index 0b5c9c0558..3a8a9b6d24 100644 --- a/src/DI.php +++ b/src/DI.php @@ -379,6 +379,14 @@ abstract class DI return self::$dice->create(Factory\Api\Mastodon\Status::class); } + /** + * @return Factory\Api\Mastodon\StatusSource + */ + public static function mstdnStatusSource() + { + return self::$dice->create(Factory\Api\Mastodon\StatusSource::class); + } + /** * @return Factory\Api\Mastodon\ScheduledStatus */ diff --git a/src/Database/Database.php b/src/Database/Database.php index a34b17adbe..036e6ec2cb 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -110,7 +110,7 @@ class Database $this->connected = false; $port = 0; - $serveraddr = trim($this->configCache->get('database', 'hostname')); + $serveraddr = trim($this->configCache->get('database', 'hostname') ?? ''); $serverdata = explode(':', $serveraddr); $host = trim($serverdata[0]); if (count($serverdata) > 1) { diff --git a/src/Factory/Api/Mastodon/Status.php b/src/Factory/Api/Mastodon/Status.php index 54726815f2..9d78e5892a 100644 --- a/src/Factory/Api/Mastodon/Status.php +++ b/src/Factory/Api/Mastodon/Status.php @@ -76,16 +76,17 @@ class Status extends BaseFactory } /** - * @param int $uriId Uri-ID of the item - * @param int $uid Item user + * @param int $uriId Uri-ID of the item + * @param int $uid Item user + * @param bool $reblog Check for reblogged post * * @return \Friendica\Object\Api\Mastodon\Status * @throws HTTPException\InternalServerErrorException * @throws ImagickException|HTTPException\NotFoundException */ - public function createFromUriId(int $uriId, int $uid = 0): \Friendica\Object\Api\Mastodon\Status + public function createFromUriId(int $uriId, int $uid = 0, bool $reblog = true): \Friendica\Object\Api\Mastodon\Status { - $fields = ['uri-id', 'uid', 'author-id', 'author-uri-id', 'author-link', 'starred', 'app', 'title', 'body', 'raw-body', 'content-warning', 'question-id', + $fields = ['uri-id', 'uid', 'author-id', 'author-uri-id', 'author-link', 'causer-uri-id', 'post-reason', 'starred', 'app', 'title', 'body', 'raw-body', 'content-warning', 'question-id', 'created', 'network', 'thr-parent-id', 'parent-author-id', 'language', 'uri', 'plink', 'private', 'vid', 'gravity', 'featured', 'has-media', 'quote-uri-id']; $item = Post::selectFirst($fields, ['uri-id' => $uriId, 'uid' => [0, $uid]], ['order' => ['uid' => true]]); if (!$item) { @@ -95,7 +96,10 @@ class Status extends BaseFactory } throw new HTTPException\NotFoundException('Item with URI ID ' . $uriId . ' not found' . ($uid ? ' for user ' . $uid : '.')); } - $account = $this->mstdnAccountFactory->createFromUriId($item['author-uri-id'], $uid); + + $is_reshare = $reblog && !is_null($item['causer-uri-id']) && ($item['post-reason'] == Item::PR_ANNOUNCEMENT); + + $account = $this->mstdnAccountFactory->createFromUriId($is_reshare ? $item['causer-uri-id']:$item['author-uri-id'], $uid); $count_announce = Post::countPosts([ 'thr-parent-id' => $uriId, @@ -183,6 +187,10 @@ class Status extends BaseFactory $reshare = []; } + if ($is_reshare) { + $reshare = $this->createFromUriId($uriId, $uid, false)->toArray(); + } + return new \Friendica\Object\Api\Mastodon\Status($item, $account, $counts, $userAttributes, $sensitive, $application, $mentions, $tags, $card, $attachments, $reshare, $poll); } diff --git a/src/Factory/Api/Mastodon/StatusSource.php b/src/Factory/Api/Mastodon/StatusSource.php new file mode 100644 index 0000000000..7f00543fbb --- /dev/null +++ b/src/Factory/Api/Mastodon/StatusSource.php @@ -0,0 +1,48 @@ +. + * + */ + +namespace Friendica\Factory\Api\Mastodon; + +use Friendica\BaseFactory; +use Friendica\Content\Text\BBCode; +use Friendica\Core\Protocol; +use Friendica\Model\Post; +use Friendica\Network\HTTPException; + +class StatusSource extends BaseFactory +{ + /** + * @param int $uriId Uri-ID of the item + * + * @return \Friendica\Object\Api\Mastodon\StatusSource + * @throws HTTPException\InternalServerErrorException + * @throws \ImagickException*@throws \Exception + */ + public function createFromUriId(int $uriId, int $uid): \Friendica\Object\Api\Mastodon\StatusSource + { + $post = Post::selectFirst(['uri-id', 'raw-body', 'body', 'title'], ['uri-id' => $uriId, 'uid' => [0, $uid]]); + + $spoiler_text = $post['title'] ?: BBCode::toPlaintext(BBCode::getAbstract($post['body'], Protocol::ACTIVITYPUB)); + $body = BBCode::toMarkdown($post['body']); + + return new \Friendica\Object\Api\Mastodon\StatusSource($post['uri-id'], $body, $spoiler_text); + } +} diff --git a/src/Model/Contact.php b/src/Model/Contact.php index e0c3c1682a..eabe378a46 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -3420,8 +3420,7 @@ class Contact ["(NOT `unsearchable` OR `nurl` IN (SELECT `nurl` FROM `owner-view` WHERE `publish` OR `net-publish`)) AND (`addr` LIKE ? OR `name` LIKE ? OR `nick` LIKE ?)", $search, $search, $search]); - $contacts = self::selectToArray([], $condition, $params); - return $contacts; + return self::selectToArray([], $condition, $params); } /** diff --git a/src/Model/GServer.php b/src/Model/GServer.php index 5bf95583fe..03e42c62b4 100644 --- a/src/Model/GServer.php +++ b/src/Model/GServer.php @@ -1213,7 +1213,7 @@ class GServer if (!empty($data['url'])) { $serverdata['platform'] = strtolower($data['platform']); - $serverdata['version'] = $data['version']; + $serverdata['version'] = $data['version'] ?? 'N/A'; } if (!empty($data['plugins'])) { @@ -2172,7 +2172,7 @@ class GServer foreach ($servers['instances'] as $server) { $url = (is_null($server['https_score']) ? 'http' : 'https') . '://' . $server['name']; self::add($url); - } + } } } } diff --git a/src/Model/Item.php b/src/Model/Item.php index 0390730fd8..cb998c0fdc 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -385,6 +385,9 @@ class Item Post\ThreadUser::update($item['uri-id'], $item['uid'], ['hidden' => true]); } + DI::notify()->deleteForItem($item['uri-id']); + DI::notification()->deleteForItem($item['uri-id']); + Logger::info('Item has been marked for deletion.', ['id' => $item_id]); return true; diff --git a/src/Model/Photo.php b/src/Model/Photo.php index 7c620b0ad4..990fb7e616 100644 --- a/src/Model/Photo.php +++ b/src/Model/Photo.php @@ -173,6 +173,64 @@ class Photo return $photo; } + /** + * Returns all browsable albums for a given user + * + * @param int $uid The given user + * + * @return array An array of albums + * @throws \Exception + */ + public static function getBrowsableAlbumsForUser(int $uid): array + { + $photos = DBA::toArray( + DBA::p( + "SELECT DISTINCT(`album`) AS `album` FROM `photo` WHERE `uid` = ? AND NOT `photo-type` IN (?, ?)", + $uid, + static::CONTACT_AVATAR, + static::CONTACT_BANNER + ) + ); + + return array_column($photos, 'album'); + } + + /** + * Returns browsable photos for a given user (optional and a given album) + * + * @param int $uid The given user id + * @param string|null $album (optional) The given album + * + * @return array All photos of the user/album + * @throws \Exception + */ + public static function getBrowsablePhotosForUser(int $uid, string $album = null): array + { + $values = [ + $uid, + Photo::CONTACT_AVATAR, + Photo::CONTACT_BANNER + ]; + + if (!empty($album)) { + $sqlExtra = "AND `album` = ? "; + $values[] = $album; + $sqlExtra2 = ""; + } else { + $sqlExtra = ''; + $sqlExtra2 = ' ORDER BY created DESC LIMIT 0, 10'; + } + + return DBA::toArray( + DBA::p( + "SELECT `resource-id`, ANY_VALUE(`id`) AS `id`, ANY_VALUE(`filename`) AS `filename`, ANY_VALUE(`type`) AS `type`, + min(`scale`) AS `hiq`, max(`scale`) AS `loq`, ANY_VALUE(`desc`) AS `desc`, ANY_VALUE(`created`) AS `created` + FROM `photo` WHERE `uid` = ? AND NOT `photo-type` IN (?, ?) $sqlExtra + GROUP BY `resource-id` $sqlExtra2", + $values + )); + } + /** * Check if photo with given conditions exists * diff --git a/src/Model/Post/Delayed.php b/src/Model/Post/Delayed.php index ac5a9c20be..6e92465fac 100644 --- a/src/Model/Post/Delayed.php +++ b/src/Model/Post/Delayed.php @@ -195,7 +195,7 @@ class Delayed $id = Item::insert($item, $notify, $preparation_mode == self::PREPARED); - Logger::notice('Post stored', ['id' => $id, 'uid' => $item['uid'], 'cid' => $item['contact-id']]); + Logger::notice('Post stored', ['id' => $id, 'uid' => $item['uid'], 'cid' => $item['contact-id'] ?? 'N/A']); if (empty($uri) && !empty($item['uri'])) { $uri = $item['uri']; diff --git a/src/Model/Post/Media.php b/src/Model/Post/Media.php index 04c0db0f80..854e5d8f91 100644 --- a/src/Model/Post/Media.php +++ b/src/Model/Post/Media.php @@ -687,7 +687,7 @@ class Media $previews[] = $medium['preview']; } - $type = explode('/', explode(';', $medium['mimetype'])[0]); + $type = explode('/', explode(';', $medium['mimetype'] ?? '')[0]); if (count($type) < 2) { Logger::info('Unknown MimeType', ['type' => $type, 'media' => $medium]); $filetype = 'unkn'; diff --git a/src/Module/Admin/Site.php b/src/Module/Admin/Site.php index 7943984333..0c627894b8 100644 --- a/src/Module/Admin/Site.php +++ b/src/Module/Admin/Site.php @@ -325,8 +325,8 @@ class Site extends BaseAdmin /* Installed langs */ $lang_choices = DI::l10n()->getAvailableLanguages(); - if (strlen(DI::config()->get('system', 'directory_submit_url')) && - !strlen(DI::config()->get('system', 'directory'))) { + if (DI::config()->get('system', 'directory_submit_url') && + !DI::config()->get('system', 'directory')) { DI::config()->set('system', 'directory', dirname(DI::config()->get('system', 'directory_submit_url'))); DI::config()->delete('system', 'directory_submit_url'); } diff --git a/src/Module/Api/Mastodon/Accounts/UpdateCredentials.php b/src/Module/Api/Mastodon/Accounts/UpdateCredentials.php index a2e7f395a4..6427a904a8 100644 --- a/src/Module/Api/Mastodon/Accounts/UpdateCredentials.php +++ b/src/Module/Api/Mastodon/Accounts/UpdateCredentials.php @@ -21,8 +21,12 @@ namespace Friendica\Module\Api\Mastodon\Accounts; -use Friendica\App\Router; use Friendica\Core\Logger; +use Friendica\DI; +use Friendica\Model\Contact; +use Friendica\Model\Photo; +use Friendica\Model\Profile; +use Friendica\Model\User; use Friendica\Module\BaseApi; /** @@ -35,8 +39,72 @@ class UpdateCredentials extends BaseApi self::checkAllowedScope(self::SCOPE_WRITE); $uid = self::getCurrentUserID(); - Logger::info('Patch data', ['data' => $request]); + $owner = User::getOwnerDataById($uid); - $this->response->unsupported(Router::PATCH, $request); + $request = $this->getRequest([ + 'bot' => ($owner['contact-type'] == Contact::TYPE_NEWS), + 'discoverable' => $owner['net-publish'], + 'display_name' => $owner['name'], + 'fields_attributes' => [], + 'locked' => $owner['manually-approve'], + 'note' => $owner['about'], + 'avatar' => [], + 'header' => [], + ], $request); + + $user = []; + $profile = []; + + if ($request['bot']) { + $user['account-type'] = Contact::TYPE_NEWS; + $user['page-flags'] = User::PAGE_FLAGS_SOAPBOX; + } elseif ($owner['contact-type'] == Contact::TYPE_NEWS) { + $user['account-type'] = Contact::TYPE_PERSON; + } else { + $user['account-type'] = $owner['contact-type']; + } + + $profile['net-publish'] = $request['discoverable']; + + if (!empty($request['display_name'])) { + $user['username'] = $request['display_name']; + } + + if ($user['account-type'] == Contact::TYPE_COMMUNITY) { + $user['page-flags'] = $request['locked'] ? User::PAGE_FLAGS_PRVGROUP : User::PAGE_FLAGS_COMMUNITY; + } elseif ($user['account-type'] == Contact::TYPE_PERSON) { + if ($request['locked']) { + $user['page-flags'] = User::PAGE_FLAGS_NORMAL; + } elseif ($owner['page-flags'] == User::PAGE_FLAGS_NORMAL) { + $user['page-flags'] = User::PAGE_FLAGS_SOAPBOX; + } + } + + if (!empty($request['note'])) { + $profile['about'] = $request['note']; + } + + Logger::debug('Patch data', ['data' => $request, 'files' => $_FILES]); + + Logger::info('Update profile and user', ['uid' => $uid, 'user' => $user, 'profile' => $profile]); + + if (!empty($request['avatar'])) { + Photo::uploadAvatar(1, $request['avatar']); + } + + if (!empty($request['header'])) { + Photo::uploadBanner(1, $request['header']); + } + + User::update($user, $uid); + Profile::update($profile, $uid); + + $cdata = Contact::getPublicAndUserContactID($owner['id'], $uid); + if (empty($cdata)) { + DI::mstdnError()->InternalError(); + } + + $account = DI::mstdnAccount()->createFromContactId($cdata['user'], $uid); + $this->response->exitWithJson($account->toArray()); } } diff --git a/src/Module/Api/Mastodon/Search.php b/src/Module/Api/Mastodon/Search.php index fafdab0fd1..fc94673082 100644 --- a/src/Module/Api/Mastodon/Search.php +++ b/src/Module/Api/Mastodon/Search.php @@ -26,6 +26,7 @@ use Friendica\Core\System; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\Contact; +use Friendica\Model\Item; use Friendica\Model\Post; use Friendica\Model\Tag; use Friendica\Module\BaseApi; @@ -67,10 +68,24 @@ class Search extends BaseApi if (empty($request['type']) || ($request['type'] == 'accounts')) { $result['accounts'] = self::searchAccounts($uid, $request['q'], $request['resolve'], $limit, $request['offset'], $request['following']); + + if (!is_array($result['accounts'])) { + // Curbing the search if we got an exact result + $request['type'] = 'accounts'; + $result['accounts'] = [$result['accounts']]; + } } + if ((empty($request['type']) || ($request['type'] == 'statuses')) && (strpos($request['q'], '@') == false)) { $result['statuses'] = self::searchStatuses($uid, $request['q'], $request['account_id'], $request['max_id'], $request['min_id'], $limit, $request['offset']); + + if (!is_array($result['statuses'])) { + // Curbing the search if we got an exact result + $request['type'] = 'statuses'; + $result['statuses'] = [$result['statuses']]; + } } + if ((empty($request['type']) || ($request['type'] == 'hashtags')) && (strpos($request['q'], '@') == false)) { $result['hashtags'] = self::searchHashtags($request['q'], $request['exclude_unreviewed'], $limit, $request['offset'], $this->parameters['version']); } @@ -78,31 +93,59 @@ class Search extends BaseApi System::jsonExit($result); } + /** + * @param int $uid + * @param string $q + * @param bool $resolve + * @param int $limit + * @param int $offset + * @param bool $following + * @return array|\Friendica\Object\Api\Mastodon\Account Object if result is absolute (exact account match), list if not + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws \Friendica\Network\HTTPException\NotFoundException + * @throws \ImagickException + */ private static function searchAccounts(int $uid, string $q, bool $resolve, int $limit, int $offset, bool $following) { - $accounts = []; - - if ((strrpos($q, '@') > 0) || Network::isValidHttpUrl($q)) { - $id = Contact::getIdForURL($q, 0, $resolve ? null : false); - - if (!empty($id)) { - $accounts[] = DI::mstdnAccount()->createFromContactId($id, $uid); - } + if ( + (strrpos($q, '@') > 0 || Network::isValidHttpUrl($q)) + && $id = Contact::getIdForURL($q, 0, $resolve ? null : false) + ) { + return DI::mstdnAccount()->createFromContactId($id, $uid); } - if (empty($accounts)) { - $contacts = Contact::searchByName($q, '', $following ? $uid : 0, $limit, $offset); - foreach ($contacts as $contact) { - $accounts[] = DI::mstdnAccount()->createFromContactId($contact['id'], $uid); - } - DBA::close($contacts); + $accounts = []; + foreach (Contact::searchByName($q, '', $following ? $uid : 0, $limit, $offset) as $contact) { + $accounts[] = DI::mstdnAccount()->createFromContactId($contact['id'], $uid); } return $accounts; } + /** + * @param int $uid + * @param string $q + * @param string $account_id + * @param int $max_id + * @param int $min_id + * @param int $limit + * @param int $offset + * @return array|\Friendica\Object\Api\Mastodon\Status Object is result is absolute (exact post match), list if not + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws \Friendica\Network\HTTPException\NotFoundException + * @throws \ImagickException + */ private static function searchStatuses(int $uid, string $q, string $account_id, int $max_id, int $min_id, int $limit, int $offset) { + if (Network::isValidHttpUrl($q)) { + $q = Network::convertToIdn($q); + // If the user-specific search failed, we search and probe a public post + $item_id = Item::fetchByLink($q, $uid) ?: Item::fetchByLink($q); + if ($item_id && $item = Post::selectFirst(['uri-id'], ['id' => $item_id])) { + return DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid); + } + } + $params = ['order' => ['uri-id' => true], 'limit' => [$offset, $limit]]; if (substr($q, 0, 1) == '#') { @@ -148,7 +191,7 @@ class Search extends BaseApi return $statuses; } - private static function searchHashtags(string $q, bool $exclude_unreviewed, int $limit, int $offset, int $version) + private static function searchHashtags(string $q, bool $exclude_unreviewed, int $limit, int $offset, int $version): array { $q = ltrim($q, '#'); diff --git a/src/Module/Api/Mastodon/Statuses.php b/src/Module/Api/Mastodon/Statuses.php index 7fe369d00e..b2479da08e 100644 --- a/src/Module/Api/Mastodon/Statuses.php +++ b/src/Module/Api/Mastodon/Statuses.php @@ -21,7 +21,6 @@ namespace Friendica\Module\Api\Mastodon; -use Friendica\App\Router; use Friendica\Content\Text\Markdown; use Friendica\Core\Protocol; use Friendica\Core\System; @@ -35,6 +34,7 @@ use Friendica\Model\Photo; use Friendica\Model\Post; use Friendica\Model\User; use Friendica\Module\BaseApi; +use Friendica\Network\HTTPException; use Friendica\Protocol\Activity; use Friendica\Util\Images; @@ -48,7 +48,47 @@ class Statuses extends BaseApi self::checkAllowedScope(self::SCOPE_WRITE); $uid = self::getCurrentUserID(); - $this->response->unsupported(Router::PUT, $request); + $request = $this->getRequest([ + 'status' => '', // Text content of the status. If media_ids is provided, this becomes optional. Attaching a poll is optional while status is provided. + 'in_reply_to_id' => 0, // ID of the status being replied to, if status is a reply + 'spoiler_text' => '', // Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field. + 'language' => '', // ISO 639 language code for this status. + ], $request); + + $owner = User::getOwnerDataById($uid); + + $condition = [ + 'uid' => $uid, + 'uri-id' => $this->parameters['id'], + 'contact-id' => $owner['id'], + 'author-id' => Contact::getPublicIdByUserId($uid), + 'origin' => true, + ]; + + $post = Post::selectFirst(['uri-id', 'id'], $condition); + if (empty($post['id'])) { + throw new HTTPException\NotFoundException('Item with URI ID ' . $this->parameters['id'] . ' not found for user ' . $uid . '.'); + } + + // The imput is defined as text. So we can use Markdown for some enhancements + $item = ['body' => Markdown::toBBCode($request['status']), 'app' => $this->getApp()]; + + if (!empty($request['language'])) { + $item['language'] = json_encode([$request['language'] => 1]); + } + + if (!empty($request['spoiler_text'])) { + if ($request['in_reply_to_id'] != $post['uri-id']) { + $item['body'] = '[abstract=' . Protocol::ACTIVITYPUB . ']' . $request['spoiler_text'] . "[/abstract]\n" . $item['body']; + } else { + $item['title'] = $request['spoiler_text']; + } + } + + Item::update($item, ['id' => $post['id']]); + Item::updateDisplayCache($post['uri-id']); + + System::jsonExit(DI::mstdnStatus()->createFromUriId($post['uri-id'], $uid)); } protected function post(array $request = []) @@ -80,14 +120,7 @@ class Statuses extends BaseApi $item['contact-id'] = $owner['id']; $item['author-id'] = $item['owner-id'] = Contact::getPublicIdByUserId($uid); $item['body'] = $body; - - if (!empty(self::getCurrentApplication()['name'])) { - $item['app'] = self::getCurrentApplication()['name']; - } - - if (empty($item['app'])) { - $item['app'] = 'API'; - } + $item['app'] = $this->getApp(); switch ($request['visibility']) { case 'public': @@ -257,4 +290,13 @@ class Statuses extends BaseApi System::jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid)); } + + private function getApp(): string + { + if (!empty(self::getCurrentApplication()['name'])) { + return self::getCurrentApplication()['name']; + } else { + return 'API'; + } + } } diff --git a/src/Module/Api/Mastodon/Statuses/Source.php b/src/Module/Api/Mastodon/Statuses/Source.php new file mode 100644 index 0000000000..22fdd2faeb --- /dev/null +++ b/src/Module/Api/Mastodon/Statuses/Source.php @@ -0,0 +1,57 @@ +. + * + */ + +namespace Friendica\Module\Api\Mastodon\Statuses; + +use Friendica\Core\System; +use Friendica\DI; +use Friendica\Model\Post; +use Friendica\Module\BaseApi; +use Friendica\Network\HTTPException; + +/** + * @see https://docs.joinmastodon.org/methods/statuses/#source + */ +class Source extends BaseApi +{ + /** + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + protected function rawContent(array $request = []) + { + self::checkAllowedScope(self::SCOPE_READ); + $uid = self::getCurrentUserID(); + + if (empty($this->parameters['id'])) { + DI::mstdnError()->UnprocessableEntity(); + } + + $id = $this->parameters['id']; + + if (!Post::exists(['uri-id' => $id, 'uid' => [0, $uid]])) { + throw new HTTPException\NotFoundException('Item with URI ID ' . $id . ' not found' . ($uid ? ' for user ' . $uid : '.')); + } + + $source = DI::mstdnStatusSource()->createFromUriId($id, $uid); + + System::jsonExit($source->toArray()); + } +} diff --git a/src/Module/Api/Mastodon/Suggestions.php b/src/Module/Api/Mastodon/Suggestions.php index f623a79a98..6eb2509269 100644 --- a/src/Module/Api/Mastodon/Suggestions.php +++ b/src/Module/Api/Mastodon/Suggestions.php @@ -48,7 +48,10 @@ class Suggestions extends BaseApi $accounts = []; foreach ($suggestions as $suggestion) { - $accounts[] = DI::mstdnAccount()->createFromContactId($suggestion['id'], $uid); + $accounts[] = [ + 'source' => 'past_interactions', + 'account' => DI::mstdnAccount()->createFromContactId($suggestion['id'], $uid) + ]; } System::jsonExit($accounts); diff --git a/src/Module/Attach.php b/src/Module/Attach.php index a73beb2b8d..17b2d6e908 100644 --- a/src/Module/Attach.php +++ b/src/Module/Attach.php @@ -37,7 +37,6 @@ class Attach extends BaseModule */ protected function rawContent(array $request = []) { - $a = DI::app(); if (empty($this->parameters['item'])) { throw new \Friendica\Network\HTTPException\BadRequestException(); } diff --git a/src/Module/Contact/Profile.php b/src/Module/Contact/Profile.php index b3d0c03733..eadc67818d 100644 --- a/src/Module/Contact/Profile.php +++ b/src/Module/Contact/Profile.php @@ -327,7 +327,7 @@ class Profile extends BaseModule '$submit' => $this->t('Submit'), '$lbl_info1' => $lbl_info1, '$lbl_info2' => $this->t('Their personal note'), - '$reason' => trim($contact['reason']), + '$reason' => trim($contact['reason'] ?? ''), '$infedit' => $this->t('Edit contact notes'), '$common_link' => 'contact/' . $contact['id'] . '/contacts/common', '$relation_text' => $relation_text, diff --git a/src/Module/Media/Attachment/Browser.php b/src/Module/Media/Attachment/Browser.php new file mode 100644 index 0000000000..86ea00faf8 --- /dev/null +++ b/src/Module/Media/Attachment/Browser.php @@ -0,0 +1,100 @@ +. + * + */ + +namespace Friendica\Module\Media\Attachment; + +use Friendica\App; +use Friendica\BaseModule; +use Friendica\Core\L10n; +use Friendica\Core\Renderer; +use Friendica\Core\Session\Capability\IHandleUserSessions; +use Friendica\Core\System; +use Friendica\Model\Attach; +use Friendica\Module\Response; +use Friendica\Network\HTTPException\UnauthorizedException; +use Friendica\Util\Profiler; +use Friendica\Util\Strings; +use Psr\Log\LoggerInterface; + +/** + * Browser for Attachments + */ +class Browser extends BaseModule +{ + /** @var IHandleUserSessions */ + protected $session; + /** @var App */ + protected $app; + + public function __construct(L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, IHandleUserSessions $session, App $app, array $server, array $parameters = []) + { + parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); + + $this->session = $session; + $this->app = $app; + } + + protected function content(array $request = []): string + { + if (!$this->session->getLocalUserId()) { + throw new UnauthorizedException($this->t('You need to be logged in to access this page.')); + } + + // Needed to match the correct template in a module that uses a different theme than the user/site/default + $theme = Strings::sanitizeFilePathItem($request['theme'] ?? ''); + if ($theme && is_file("view/theme/$theme/config.php")) { + $this->app->setCurrentTheme($theme); + } + + $files = Attach::selectToArray(['id', 'filename', 'filetype'], ['uid' => $this->session->getLocalUserId()]); + + $fileArray = array_map([$this, 'map_files'], $files); + + $tpl = Renderer::getMarkupTemplate('media/browser.tpl'); + $output = Renderer::replaceMacros($tpl, [ + '$type' => 'attachment', + '$path' => ['' => $this->t('Files')], + '$folders' => false, + '$files' => $fileArray, + '$cancel' => $this->t('Cancel'), + '$nickname' => $this->app->getLoggedInUserNickname(), + '$upload' => $this->t('Upload'), + ]); + + if (empty($request['mode'])) { + System::httpExit($output); + } + + return $output; + } + + protected function map_files(array $record): array + { + list($m1, $m2) = explode('/', $record['filetype']); + $filetype = file_exists(sprintf('images/icons/%s.png', $m1) ? $m1 : 'text'); + + return [ + sprintf('%s/attach/%s', $this->baseUrl, $record['id']), + $record['filename'], + sprintf('%s/images/icon/16/%s.png', $this->baseUrl, $filetype), + ]; + } +} diff --git a/src/Module/Profile/Attachment/Upload.php b/src/Module/Media/Attachment/Upload.php similarity index 80% rename from src/Module/Profile/Attachment/Upload.php rename to src/Module/Media/Attachment/Upload.php index 0624ea5a3f..0bc95fe792 100644 --- a/src/Module/Profile/Attachment/Upload.php +++ b/src/Module/Media/Attachment/Upload.php @@ -19,7 +19,7 @@ * */ -namespace Friendica\Module\Profile\Attachment; +namespace Friendica\Module\Media\Attachment; use Friendica\App; use Friendica\Core\Config\Capability\IManageConfigValues; @@ -73,30 +73,12 @@ class Upload extends \Friendica\BaseModule $this->response->setType(Response::TYPE_JSON, 'application/json'); } - $nick = $this->parameters['nickname']; - $owner = User::getOwnerDataByNick($nick); + $owner = User::getOwnerDataById($this->userSession->getLocalUserId()); if (!$owner) { - $this->logger->warning('owner is not a valid record:', ['owner' => $owner, 'nick' => $nick]); + $this->logger->warning('Owner not found.', ['uid' => $this->userSession->getLocalUserId()]); return $this->return(401, $this->t('Invalid request.')); } - $can_post = false; - $contact_id = 0; - $page_owner_uid = $owner['uid']; - $community_page = $owner['page-flags'] == User::PAGE_FLAGS_COMMUNITY; - - if ($this->userSession->getLocalUserId() && $this->userSession->getLocalUserId() == $page_owner_uid) { - $can_post = true; - } elseif ($community_page && !empty($this->userSession->getRemoteContactID($page_owner_uid))) { - $contact_id = $this->userSession->getRemoteContactID($page_owner_uid); - $can_post = $this->database->exists('contact', ['blocked' => false, 'pending' => false, 'id' => $contact_id, 'uid' => $page_owner_uid]); - } - - if (!$can_post) { - $this->logger->warning('User does not have required permissions', ['contact_id' => $contact_id, 'page_owner_uid' => $page_owner_uid]); - return $this->return(403, $this->t('Permission denied.'), true); - } - if (empty($_FILES['userfile'])) { $this->logger->warning('No file uploaded (empty userfile)'); return $this->return(401, $this->t('Invalid request.'), true); @@ -126,7 +108,7 @@ class Upload extends \Friendica\BaseModule return $this->return(401, $msg); } - $newid = Attach::storeFile($tempFileName, $page_owner_uid, $fileName, '<' . $owner['id'] . '>'); + $newid = Attach::storeFile($tempFileName, $owner['uid'], $fileName, '<' . $owner['id'] . '>'); @unlink($tempFileName); diff --git a/src/Module/Media/Photo/Browser.php b/src/Module/Media/Photo/Browser.php new file mode 100644 index 0000000000..0df19d0098 --- /dev/null +++ b/src/Module/Media/Photo/Browser.php @@ -0,0 +1,125 @@ +. + * + */ + +namespace Friendica\Module\Media\Photo; + +use Friendica\App; +use Friendica\BaseModule; +use Friendica\Core\L10n; +use Friendica\Core\Renderer; +use Friendica\Core\Session\Capability\IHandleUserSessions; +use Friendica\Core\System; +use Friendica\Model\Photo; +use Friendica\Module\Response; +use Friendica\Network\HTTPException\UnauthorizedException; +use Friendica\Util\Images; +use Friendica\Util\Profiler; +use Friendica\Util\Strings; +use Psr\Log\LoggerInterface; + +/** + * Browser for Photos + */ +class Browser extends BaseModule +{ + /** @var IHandleUserSessions */ + protected $session; + /** @var App */ + protected $app; + + public function __construct(L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, IHandleUserSessions $session, App $app, array $server, array $parameters = []) + { + parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); + + $this->session = $session; + $this->app = $app; + } + + protected function content(array $request = []): string + { + if (!$this->session->getLocalUserId()) { + throw new UnauthorizedException($this->t('You need to be logged in to access this page.')); + } + + // Needed to match the correct template in a module that uses a different theme than the user/site/default + $theme = Strings::sanitizeFilePathItem($request['theme'] ?? ''); + if ($theme && is_file("view/theme/$theme/config.php")) { + $this->app->setCurrentTheme($theme); + } + + $album = $this->parameters['album'] ?? null; + + $photos = Photo::getBrowsablePhotosForUser($this->session->getLocalUserId(), $album); + $albums = $album ? false : Photo::getBrowsableAlbumsForUser($this->session->getLocalUserId()); + + $path = [ + '' => $this->t('Photos'), + ]; + if (!empty($album)) { + $path[$album] = $album; + } + + $photosArray = array_map([$this, 'map_files'], $photos); + + $tpl = Renderer::getMarkupTemplate('media/browser.tpl'); + $output = Renderer::replaceMacros($tpl, [ + '$type' => 'photo', + '$path' => $path, + '$folders' => $albums, + '$files' => $photosArray, + '$cancel' => $this->t('Cancel'), + '$nickname' => $this->app->getLoggedInUserNickname(), + '$upload' => $this->t('Upload'), + ]); + + if (empty($request['mode'])) { + System::httpExit($output); + } + + return $output; + } + + protected function map_files(array $record): array + { + $types = Images::supportedTypes(); + $ext = $types[$record['type']]; + $filename_e = $record['filename']; + + // Take the largest picture that is smaller or equal 640 pixels + $photo = Photo::selectFirst( + ['scale'], + [ + "`resource-id` = ? AND `height` <= ? AND `width` <= ?", + $record['resource-id'], + 640, + 640 + ], + ['order' => ['scale']]); + $scale = $photo['scale'] ?? $record['loq']; + + return [ + sprintf('%s/photos/%s/image/%s', $this->baseUrl, $this->app->getLoggedInUserNickname(), $record['resource-id']), + $filename_e, + sprintf('%s/photo/%s-%s.%s', $this->baseUrl, $record['resource-id'], $scale, $ext), + $record['desc'], + ]; + } +} diff --git a/src/Module/Profile/Photos/Upload.php b/src/Module/Media/Photo/Upload.php similarity index 78% rename from src/Module/Profile/Photos/Upload.php rename to src/Module/Media/Photo/Upload.php index e99d7e1083..c07249c902 100644 --- a/src/Module/Profile/Photos/Upload.php +++ b/src/Module/Media/Photo/Upload.php @@ -19,7 +19,7 @@ * */ -namespace Friendica\Module\Profile\Photos; +namespace Friendica\Module\Media\Photo; use Friendica\App; use Friendica\Core\Config\Capability\IManageConfigValues; @@ -76,36 +76,11 @@ class Upload extends \Friendica\BaseModule $album = trim($request['album'] ?? ''); - if (empty($_FILES['media'])) { - $user = $this->database->selectFirst('owner-view', ['id', 'uid', 'nickname', 'page-flags'], ['nickname' => $this->parameters['nickname'], 'blocked' => false]); - } else { - $user = $this->database->selectFirst('owner-view', ['id', 'uid', 'nickname', 'page-flags'], ['uid' => BaseApi::getCurrentUserID() ?: null, 'blocked' => false]); - } + $owner = User::getOwnerDataById($this->userSession->getLocalUserId()); - if (!$this->database->isResult($user)) { - $this->logger->warning('User is not valid', ['nickname' => $this->parameters['nickname'], 'user' => $user]); - return $this->return(404, $this->t('User not found.')); - } - - /* - * Setup permissions structures - */ - $can_post = false; - $visitor = 0; - $contact_id = 0; - $page_owner_uid = $user['uid']; - - if ($this->userSession->getLocalUserId() && $this->userSession->getLocalUserId() == $page_owner_uid) { - $can_post = true; - } elseif ($user['page-flags'] == User::PAGE_FLAGS_COMMUNITY && !$this->userSession->getRemoteContactID($page_owner_uid)) { - $contact_id = $this->userSession->getRemoteContactID($page_owner_uid); - $can_post = $this->database->exists('contact', ['blocked' => false, 'pending' => false, 'id' => $contact_id, 'uid' => $page_owner_uid]); - $visitor = $contact_id; - } - - if (!$can_post) { - $this->logger->warning('No permission to upload files', ['contact_id' => $contact_id, 'page_owner_uid' => $page_owner_uid]); - return $this->return(403, $this->t('Permission denied.'), true); + if (!$owner) { + $this->logger->warning('Owner not found.', ['uid' => $this->userSession->getLocalUserId()]); + return $this->return(401, $this->t('Invalid request.')); } if (empty($_FILES['userfile']) && empty($_FILES['media'])) { @@ -223,9 +198,9 @@ class Upload extends \Friendica\BaseModule $album = $this->t('Wall Photos'); } - $allow_cid = '<' . $user['id'] . '>'; + $allow_cid = '<' . $owner['id'] . '>'; - $result = Photo::store($image, $page_owner_uid, $visitor, $resource_id, $filename, $album, 0, Photo::DEFAULT, $allow_cid); + $result = Photo::store($image, $owner['uid'], 0, $resource_id, $filename, $album, 0, Photo::DEFAULT, $allow_cid); if (!$result) { $this->logger->warning('Photo::store() failed', ['result' => $result]); return $this->return(401, $this->t('Image upload failed.')); @@ -233,7 +208,7 @@ class Upload extends \Friendica\BaseModule if ($width > 640 || $height > 640) { $image->scaleDown(640); - $result = Photo::store($image, $page_owner_uid, $visitor, $resource_id, $filename, $album, 1, Photo::DEFAULT, $allow_cid); + $result = Photo::store($image, $owner['uid'], 0, $resource_id, $filename, $album, 1, Photo::DEFAULT, $allow_cid); if ($result) { $smallest = 1; } @@ -241,14 +216,14 @@ class Upload extends \Friendica\BaseModule if ($width > 320 || $height > 320) { $image->scaleDown(320); - $result = Photo::store($image, $page_owner_uid, $visitor, $resource_id, $filename, $album, 2, Photo::DEFAULT, $allow_cid); + $result = Photo::store($image, $owner['uid'], 0, $resource_id, $filename, $album, 2, Photo::DEFAULT, $allow_cid); if ($result && ($smallest == 0)) { $smallest = 2; } } $this->logger->info('upload done'); - return $this->return(200, "\n\n" . '[url=' . $this->baseUrl . '/photos/' . $user['nickname'] . '/image/' . $resource_id . '][img]' . $this->baseUrl . "/photo/$resource_id-$smallest." . $image->getExt() . "[/img][/url]\n\n"); + return $this->return(200, "\n\n" . '[url=' . $this->baseUrl . '/photos/' . $owner['nickname'] . '/image/' . $resource_id . '][img]' . $this->baseUrl . "/photo/$resource_id-$smallest." . $image->getExt() . "[/img][/url]\n\n"); } /** diff --git a/src/Module/OStatus/Salmon.php b/src/Module/OStatus/Salmon.php index f674f9300b..0d393afcce 100644 --- a/src/Module/OStatus/Salmon.php +++ b/src/Module/OStatus/Salmon.php @@ -142,14 +142,9 @@ class Salmon extends \Friendica\BaseModule throw new HTTPException\BadRequestException(); } - $key_info = explode('.', $key); + $this->logger->info('Key details', ['info' => $key]); - $m = Strings::base64UrlDecode($key_info[1]); - $e = Strings::base64UrlDecode($key_info[2]); - - $this->logger->info('Key details', ['info' => $key_info]); - - $pubkey = Crypto::meToPem($m, $e); + $pubkey = SalmonProtocol::magicKeyToPem($key); // We should have everything we need now. Let's see if it verifies. diff --git a/src/Module/Profile/Photos/Index.php b/src/Module/Profile/Photos.php similarity index 98% rename from src/Module/Profile/Photos/Index.php rename to src/Module/Profile/Photos.php index 82803ac570..50af6f0e96 100644 --- a/src/Module/Profile/Photos/Index.php +++ b/src/Module/Profile/Photos.php @@ -19,7 +19,7 @@ * */ -namespace Friendica\Module\Profile\Photos; +namespace Friendica\Module\Profile; use Friendica\App; use Friendica\Content\Pager; @@ -40,7 +40,7 @@ use Friendica\Util\Images; use Friendica\Util\Profiler; use Psr\Log\LoggerInterface; -class Index extends \Friendica\Module\BaseProfile +class Photos extends \Friendica\Module\BaseProfile { /** @var IHandleUserSessions */ private $session; diff --git a/src/Module/PublicRSAKey.php b/src/Module/PublicRSAKey.php index 523ab174fb..fb1eeeb278 100644 --- a/src/Module/PublicRSAKey.php +++ b/src/Module/PublicRSAKey.php @@ -23,11 +23,9 @@ namespace Friendica\Module; use Friendica\BaseModule; use Friendica\Core\System; -use Friendica\DI; use Friendica\Model\User; use Friendica\Network\HTTPException\BadRequestException; -use Friendica\Util\Crypto; -use Friendica\Util\Strings; +use Friendica\Protocol\Salmon; /** * prints the public RSA key of a user @@ -47,9 +45,10 @@ class PublicRSAKey extends BaseModule throw new BadRequestException(); } - Crypto::pemToMe($user['spubkey'], $modulus, $exponent); - - $content = 'RSA' . '.' . Strings::base64UrlEncode($modulus, true) . '.' . Strings::base64UrlEncode($exponent, true); - System::httpExit($content, Response::TYPE_BLANK, 'application/magic-public-key'); + System::httpExit( + Salmon::salmonKey($user['spubkey']), + Response::TYPE_BLANK, + 'application/magic-public-key' + ); } } diff --git a/src/Module/User/PortableContacts.php b/src/Module/User/PortableContacts.php index 6629994810..6a3c1201d9 100644 --- a/src/Module/User/PortableContacts.php +++ b/src/Module/User/PortableContacts.php @@ -233,7 +233,7 @@ class PortableContacts extends BaseModule } if ($selectedFields['tags']) { - $tags = str_replace(',', ' ', $contact['keywords']); + $tags = str_replace(',', ' ', $contact['keywords'] ?? ''); $tags = explode(' ', $tags); $cleaned = []; diff --git a/src/Navigation/Notifications/Repository/Notification.php b/src/Navigation/Notifications/Repository/Notification.php index a9630464aa..6a513c5dc2 100644 --- a/src/Navigation/Notifications/Repository/Notification.php +++ b/src/Navigation/Notifications/Repository/Notification.php @@ -33,6 +33,7 @@ use Friendica\Navigation\Notifications\Collection; use Friendica\Navigation\Notifications\Entity; use Friendica\Navigation\Notifications\Factory; use Friendica\Network\HTTPException\NotFoundException; +use Friendica\Protocol\Activity; use Friendica\Util\DateTimeFormat; use Psr\Log\LoggerInterface; @@ -268,4 +269,23 @@ class Notification extends BaseRepository return $this->db->delete(self::$table_name, $condition); } + + public function deleteForItem(int $itemUriId): bool + { + $conditionTarget = [ + 'vid' => Verb::getID(Activity::POST), + 'target-uri-id' => $itemUriId, + ]; + + $conditionParent = [ + 'vid' => Verb::getID(Activity::POST), + 'parent-uri-id' => $itemUriId, + ]; + + $this->logger->notice('deleteForItem', ['conditionTarget' => $conditionTarget, 'conditionParent' => $conditionParent]); + + return + $this->db->delete(self::$table_name, $conditionTarget) + && $this->db->delete(self::$table_name, $conditionParent); + } } diff --git a/src/Navigation/Notifications/Repository/Notify.php b/src/Navigation/Notifications/Repository/Notify.php index 3891a6e256..75aff6b870 100644 --- a/src/Navigation/Notifications/Repository/Notify.php +++ b/src/Navigation/Notifications/Repository/Notify.php @@ -807,4 +807,10 @@ class Notify extends BaseRepository return $this->storeAndSend($params, $sitelink, $tsitelink, $hsitelink, $title, $subject, $preamble, $epreamble, $item['body'], $itemlink, true); } + + public function deleteForItem(int $itemUriId): void + { + $this->db->delete('notify', ['otype' => 'item', 'uri-id' => $itemUriId]); + $this->db->delete('notify', ['otype' => 'item', 'parent-uri-id' => $itemUriId]); + } } diff --git a/src/Network/Probe.php b/src/Network/Probe.php index fc7113dbda..5c188f2522 100644 --- a/src/Network/Probe.php +++ b/src/Network/Probe.php @@ -40,6 +40,7 @@ use Friendica\Protocol\ActivityNamespace; use Friendica\Protocol\ActivityPub; use Friendica\Protocol\Email; use Friendica\Protocol\Feed; +use Friendica\Protocol\Salmon; use Friendica\Util\Crypto; use Friendica\Util\DateTimeFormat; use Friendica\Util\Network; @@ -1512,12 +1513,10 @@ class Probe $pubkey = $curlResult->getBody(); } - $key = explode('.', $pubkey); + try { + $data['pubkey'] = Salmon::magicKeyToPem($pubkey); + } catch (\Throwable $e) { - if (sizeof($key) >= 3) { - $m = Strings::base64UrlDecode($key[1]); - $e = Strings::base64UrlDecode($key[2]); - $data['pubkey'] = Crypto::meToPem($m, $e); } } } diff --git a/src/Object/Api/Mastodon/StatusSource.php b/src/Object/Api/Mastodon/StatusSource.php new file mode 100644 index 0000000000..dfe73b7600 --- /dev/null +++ b/src/Object/Api/Mastodon/StatusSource.php @@ -0,0 +1,54 @@ +. + * + */ + +namespace Friendica\Object\Api\Mastodon; + +use Friendica\BaseDataTransferObject; + +/** + * Class StatusSource + * + * @see https://docs.joinmastodon.org/entities/StatusSource/ + */ +class StatusSource extends BaseDataTransferObject +{ + /** @var string */ + protected $id; + /** @var string */ + protected $text; + /** @var string */ + protected $spoiler_text = ""; + + /** + * Creates a source record from an post array. + * + * @param integer $id + * @param string $text + * @param string $spoiler_text + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + public function __construct(int $id, string $text, string $spoiler_text) + { + $this->id = (string)$id; + $this->text = $text; + $this->spoiler_text = $spoiler_text; + } +} diff --git a/src/Object/Email.php b/src/Object/Email.php index 4d5a11715c..fdde4e96dd 100644 --- a/src/Object/Email.php +++ b/src/Object/Email.php @@ -200,6 +200,7 @@ class Email implements IEmail /** * @inheritDoc */ + #[\ReturnTypeWillChange] public function jsonSerialize() { return $this->toArray(); diff --git a/src/Protocol/Diaspora.php b/src/Protocol/Diaspora.php index 4a42a5e0b2..54f09e9d97 100644 --- a/src/Protocol/Diaspora.php +++ b/src/Protocol/Diaspora.php @@ -224,14 +224,34 @@ class Diaspora // Is it a private post? Then decrypt the outer Salmon if (is_object($data)) { - $encrypted_aes_key_bundle = base64_decode($data->aes_key); - $ciphertext = base64_decode($data->encrypted_magic_envelope); + try { + if (!isset($data->aes_key) || !isset($data->encrypted_magic_envelope)) { + Logger::info('Missing keys "aes_key" and/or "encrypted_magic_envelope"', ['data' => $data]); + throw new \RuntimeException('Missing keys "aes_key" and/or "encrypted_magic_envelope"'); + } - $outer_key_bundle = ''; - @openssl_private_decrypt($encrypted_aes_key_bundle, $outer_key_bundle, $privKey); - $j_outer_key_bundle = json_decode($outer_key_bundle); + $encrypted_aes_key_bundle = base64_decode($data->aes_key); + $ciphertext = base64_decode($data->encrypted_magic_envelope); - if (!is_object($j_outer_key_bundle)) { + $outer_key_bundle = ''; + @openssl_private_decrypt($encrypted_aes_key_bundle, $outer_key_bundle, $privKey); + $j_outer_key_bundle = json_decode($outer_key_bundle); + + if (!is_object($j_outer_key_bundle)) { + Logger::info('Unable to decode outer key bundle', ['outer_key_bundle' => $outer_key_bundle]); + throw new \RuntimeException('Unable to decode outer key bundle'); + } + + if (!isset($j_outer_key_bundle->iv) || !isset($j_outer_key_bundle->key)) { + Logger::info('Missing keys "iv" and/or "key" from outer Salmon', ['j_outer_key_bundle' => $j_outer_key_bundle]); + throw new \RuntimeException('Missing keys "iv" and/or "key" from outer Salmon'); + } + + $outer_iv = base64_decode($j_outer_key_bundle->iv); + $outer_key = base64_decode($j_outer_key_bundle->key); + + $xml = self::aesDecrypt($outer_key, $outer_iv, $ciphertext); + } catch (\Throwable $e) { Logger::notice('Outer Salmon did not verify. Discarding.'); if ($no_exit) { return false; @@ -239,11 +259,6 @@ class Diaspora throw new \Friendica\Network\HTTPException\BadRequestException(); } } - - $outer_iv = base64_decode($j_outer_key_bundle->iv); - $outer_key = base64_decode($j_outer_key_bundle->key); - - $xml = self::aesDecrypt($outer_key, $outer_iv, $ciphertext); } else { $xml = $raw; } diff --git a/src/Protocol/Salmon.php b/src/Protocol/Salmon.php index b1bdb67e1d..7e91b0e3e0 100644 --- a/src/Protocol/Salmon.php +++ b/src/Protocol/Salmon.php @@ -25,9 +25,11 @@ use Friendica\Core\Logger; use Friendica\DI; use Friendica\Network\HTTPClient\Client\HttpClientAccept; use Friendica\Network\Probe; +use Friendica\Protocol\Salmon\Format\Magic; use Friendica\Util\Crypto; use Friendica\Util\Strings; use Friendica\Util\XML; +use phpseclib3\Crypt\PublicKeyLoader; /** * Salmon Protocol class @@ -243,7 +245,19 @@ class Salmon */ public static function salmonKey(string $pubkey): string { - Crypto::pemToMe($pubkey, $modulus, $exponent); - return 'RSA' . '.' . Strings::base64UrlEncode($modulus, true) . '.' . Strings::base64UrlEncode($exponent, true); + \phpseclib3\Crypt\RSA::addFileFormat(Magic::class); + + return PublicKeyLoader::load($pubkey)->toString('Magic'); + } + + /** + * @param string $magic Magic key format starting with "RSA." + * @return string + */ + public static function magicKeyToPem(string $magic): string + { + \phpseclib3\Crypt\RSA::addFileFormat(Magic::class); + + return (string) PublicKeyLoader::load($magic); } } diff --git a/src/Protocol/Salmon/Format/Magic.php b/src/Protocol/Salmon/Format/Magic.php new file mode 100644 index 0000000000..a94ae6bd61 --- /dev/null +++ b/src/Protocol/Salmon/Format/Magic.php @@ -0,0 +1,77 @@ +. + * + */ + +namespace Friendica\Protocol\Salmon\Format; + +use Friendica\Util\Strings; +use phpseclib3\Math\BigInteger; + +/** + * This custom public RSA key format class is meant to be used with the \phpseclib3\Crypto\RSA::addFileFormat method. + * + * It handles Salmon's specific magic key string starting with "RSA." and which MIME type is application/magic-key or + * application/magic-public-key + * + * @see https://web.archive.org/web/20160506073138/http://salmon-protocol.googlecode.com:80/svn/trunk/draft-panzer-magicsig-01.html#anchor13 + */ +class Magic +{ + public static function load($key, $password = ''): array + { + if (!is_string($key)) { + throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key)); + } + + $key_info = explode('.', $key); + + if (count($key_info) !== 3) { + throw new \UnexpectedValueException('Key should have three components separated by periods'); + } + + if ($key_info[0] !== 'RSA') { + throw new \UnexpectedValueException('Key first component should be "RSA"'); + } + + if (preg_match('#[+/]#', $key_info[1]) + || preg_match('#[+/]#', $key_info[1]) + ) { + throw new \UnexpectedValueException('Wrong encoding, expecting Base64URLencoding'); + } + + $m = Strings::base64UrlDecode($key_info[1]); + $e = Strings::base64UrlDecode($key_info[2]); + + if (!$m || !$e) { + throw new \UnexpectedValueException('Base64 decoding produced an error'); + } + + return [ + 'modulus' => new BigInteger($m, 256), + 'publicExponent' => new BigInteger($e, 256), + 'isPublicKey' => true, + ]; + } + + public static function savePublicKey(BigInteger $n, BigInteger $e, array $options = []): string + { + return 'RSA.' . Strings::base64UrlEncode($n->toBytes(), true) . '.' . Strings::base64UrlEncode($e->toBytes(), true); + } +} diff --git a/src/Util/Crypto.php b/src/Util/Crypto.php index d983202c0c..a5ca5e517b 100644 --- a/src/Util/Crypto.php +++ b/src/Util/Crypto.php @@ -21,14 +21,11 @@ namespace Friendica\Util; -use Exception; use Friendica\Core\Hook; use Friendica\Core\Logger; use Friendica\Core\System; use Friendica\DI; -use ParagonIE\ConstantTime\Base64UrlSafe; -use phpseclib\Crypt\RSA; -use phpseclib\Math\BigInteger; +use phpseclib3\Crypt\PublicKeyLoader; /** * Crypto class @@ -66,22 +63,6 @@ class Crypto return openssl_verify($data, $sig, $key, (($alg == 'sha1') ? OPENSSL_ALGO_SHA1 : $alg)); } - /** - /** - * @param string $m modulo - * @param string $e exponent - * @return string - */ - public static function meToPem($m, $e) - { - $rsa = new RSA(); - $rsa->loadKey([ - 'e' => new BigInteger($e, 256), - 'n' => new BigInteger($m, 256) - ]); - return $rsa->getPublicKey(); - } - /** * Transform RSA public keys to standard PEM output * @@ -91,29 +72,7 @@ class Crypto */ public static function rsaToPem(string $key) { - $rsa = new RSA(); - $rsa->setPublicKey($key); - - return $rsa->getPublicKey(RSA::PUBLIC_FORMAT_PKCS8); - } - - /** - * Extracts the modulo and exponent reference from a public PEM key - * - * @param string $key public PEM key - * @param string $modulus (ref) modulo reference - * @param string $exponent (ref) exponent reference - * - * @return void - */ - public static function pemToMe(string $key, &$modulus, &$exponent) - { - $rsa = new RSA(); - $rsa->loadKey($key); - $rsa->setPublicKey(); - - $modulus = $rsa->modulus->toBytes(); - $exponent = $rsa->exponent->toBytes(); + return (string)PublicKeyLoader::load($key); } /** @@ -152,50 +111,6 @@ class Crypto return $response; } - /** - * Create a new elliptic curve key pair - * - * @return array with the elements "prvkey", "pubkey", "vapid-public" and "vapid-private" - */ - public static function newECKeypair() - { - $openssl_options = [ - 'curve_name' => 'prime256v1', - 'private_key_type' => OPENSSL_KEYTYPE_EC - ]; - - $conf = DI::config()->get('system', 'openssl_conf_file'); - if ($conf) { - $openssl_options['config'] = $conf; - } - $result = openssl_pkey_new($openssl_options); - - if (empty($result)) { - throw new Exception('Key creation failed'); - } - - $response = ['prvkey' => '', 'pubkey' => '']; - - // Get private key - openssl_pkey_export($result, $response['prvkey']); - - // Get public key - $pkey = openssl_pkey_get_details($result); - $response['pubkey'] = $pkey['key']; - - // Create VAPID keys - // @see https://github.com/web-push-libs/web-push-php/blob/256a18b2a2411469c94943725fb6eccb9681bd75/src/Utils.php#L60-L62 - $hexString = '04'; - $hexString .= str_pad(bin2hex($pkey['ec']['x']), 64, '0', STR_PAD_LEFT); - $hexString .= str_pad(bin2hex($pkey['ec']['y']), 64, '0', STR_PAD_LEFT); - $response['vapid-public'] = Base64UrlSafe::encode(hex2bin($hexString)); - - // @see https://github.com/web-push-libs/web-push-php/blob/256a18b2a2411469c94943725fb6eccb9681bd75/src/VAPID.php - $response['vapid-private'] = Base64UrlSafe::encode(hex2bin(str_pad(bin2hex($pkey['ec']['d']), 64, '0', STR_PAD_LEFT))); - - return $response; - } - /** * Encrypt a string with 'aes-256-cbc' cipher method. * diff --git a/src/Util/Proxy.php b/src/Util/Proxy.php index fc7d369ad5..aad09d118b 100644 --- a/src/Util/Proxy.php +++ b/src/Util/Proxy.php @@ -24,6 +24,7 @@ namespace Friendica\Util; use Friendica\Core\Logger; use Friendica\Core\System; use Friendica\DI; +use GuzzleHttp\Psr7\Uri; /** * Proxy utilities class @@ -173,12 +174,15 @@ class Proxy */ private static function parseQuery(string $url): array { - $query = parse_url($url, PHP_URL_QUERY); - $query = html_entity_decode($query); + try { + $uri = new Uri($url); - parse_str($query, $arr); + parse_str($uri->getQuery(), $arr); - return $arr; + return $arr; + } catch (\Throwable $e) { + return []; + } } /** diff --git a/src/Util/Strings.php b/src/Util/Strings.php index 0972e44486..5988cc89bc 100644 --- a/src/Util/Strings.php +++ b/src/Util/Strings.php @@ -23,6 +23,7 @@ namespace Friendica\Util; use Friendica\Content\ContactSelector; use Friendica\Core\Logger; +use ParagonIE\ConstantTime\Base64; /** * This class handles string functions @@ -245,16 +246,17 @@ class Strings * @param string $s URL to encode * @param boolean $strip_padding Optional. Default false * @return string Encoded URL + * @see https://web.archive.org/web/20160506073138/http://salmon-protocol.googlecode.com:80/svn/trunk/draft-panzer-magicsig-01.html#params */ public static function base64UrlEncode(string $s, bool $strip_padding = false): string { - $s = strtr(base64_encode($s), '+/', '-_'); - if ($strip_padding) { - $s = str_replace('=', '', $s); + $s = Base64::encodeUnpadded($s); + } else { + $s = Base64::encode($s); } - return $s; + return strtr($s, '+/', '-_'); } /** @@ -263,26 +265,11 @@ class Strings * @param string $s URL to decode * @return string Decoded URL * @throws \Exception + * @see https://web.archive.org/web/20160506073138/http://salmon-protocol.googlecode.com:80/svn/trunk/draft-panzer-magicsig-01.html#params */ public static function base64UrlDecode(string $s): string { - /* - * // Placeholder for new rev of salmon which strips base64 padding. - * // PHP base64_decode handles the un-padded input without requiring this step - * // Uncomment if you find you need it. - * - * $l = strlen($s); - * if (!strpos($s,'=')) { - * $m = $l % 4; - * if ($m == 2) - * $s .= '=='; - * if ($m == 3) - * $s .= '='; - * } - * - */ - - return base64_decode(strtr($s, '-_', '+/')); + return Base64::decode(strtr($s, '-_', '+/')); } /** diff --git a/static/routes.config.php b/static/routes.config.php index 13c8737187..d9e61c63fc 100644 --- a/static/routes.config.php +++ b/static/routes.config.php @@ -31,18 +31,16 @@ use Friendica\App\Router as R; use Friendica\Module; $profileRoutes = [ - '' => [Module\Profile\Index::class, [R::GET]], - '/attachment/upload' => [Module\Profile\Attachment\Upload::class, [ R::POST]], - '/contacts/common' => [Module\Profile\Common::class, [R::GET]], - '/contacts[/{type}]' => [Module\Profile\Contacts::class, [R::GET]], - '/media' => [Module\Profile\Media::class, [R::GET]], - '/photos' => [Module\Profile\Photos\Index::class, [R::GET ]], - '/photos/upload' => [Module\Profile\Photos\Upload::class, [ R::POST]], - '/profile' => [Module\Profile\Profile::class, [R::GET]], - '/remote_follow' => [Module\Profile\RemoteFollow::class, [R::GET, R::POST]], - '/schedule' => [Module\Profile\Schedule::class, [R::GET, R::POST]], - '/status[/{category}[/{date1}[/{date2}]]]' => [Module\Profile\Status::class, [R::GET]], - '/unkmail' => [Module\Profile\UnkMail::class, [R::GET, R::POST]], + '' => [Module\Profile\Index::class, [R::GET]], + '/contacts/common' => [Module\Profile\Common::class, [R::GET]], + '/contacts[/{type}]' => [Module\Profile\Contacts::class, [R::GET]], + '/media' => [Module\Profile\Media::class, [R::GET]], + '/photos' => [Module\Profile\Photos::class, [R::GET ]], + '/profile' => [Module\Profile\Profile::class, [R::GET]], + '/remote_follow' => [Module\Profile\RemoteFollow::class, [R::GET, R::POST]], + '/schedule' => [Module\Profile\Schedule::class, [R::GET, R::POST]], + '/status[/{category}[/{date1}[/{date2}]]]' => [Module\Profile\Status::class, [R::GET]], + '/unkmail' => [Module\Profile\UnkMail::class, [R::GET, R::POST]], ]; $apiRoutes = [ @@ -277,7 +275,7 @@ return [ '/statuses/{id:\d+}/pin' => [Module\Api\Mastodon\Statuses\Pin::class, [ R::POST]], '/statuses/{id:\d+}/unpin' => [Module\Api\Mastodon\Statuses\Unpin::class, [ R::POST]], '/statuses/{id:\d+}/history' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // not implemented - '/statuses/{id:\d+}/source' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // not implemented + '/statuses/{id:\d+}/source' => [Module\Api\Mastodon\Statuses\Source::class, [R::GET ]], '/streaming/direct' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // not implemented '/streaming/hashtag' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // not implemented '/streaming/hashtag/local' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // not implemented @@ -469,6 +467,14 @@ return [ '/magic' => [Module\Magic::class, [R::GET]], '/manifest' => [Module\Manifest::class, [R::GET]], '/friendica.webmanifest' => [Module\Manifest::class, [R::GET]], + + '/media' => [ + '/attachment/browser' => [Module\Media\Attachment\Browser::class, [R::GET]], + '/attachment/upload' => [Module\Media\Attachment\Upload::class, [ R::POST]], + '/photo/browser[/{album}]' => [Module\Media\Photo\Browser::class, [R::GET]], + '/photo/upload' => [Module\Media\Photo\Upload::class, [ R::POST]], + ], + '/moderation' => [ '[/]' => [Module\Moderation\Summary::class, [R::GET]], @@ -556,7 +562,7 @@ return [ // Kept for backwards-compatibility // @TODO remove by version 2023.12 - '/photos/{nickname}' => [Module\Profile\Photos\Index::class, [R::GET]], + '/photos/{nickname}' => [Module\Profile\Photos::class, [R::GET]], '/ping' => [Module\Notifications\Ping::class, [R::GET]], diff --git a/tests/datasets/crypto/rsa/salmon-public-magic b/tests/datasets/crypto/rsa/salmon-public-magic new file mode 100644 index 0000000000..2e38237049 --- /dev/null +++ b/tests/datasets/crypto/rsa/salmon-public-magic @@ -0,0 +1 @@ +RSA.tvsoBZbLUvqWs-0d8C5hVQLjLCjjxyZb17Rm8_9FDqBYUigBSFDcJCzG27FM-zuddwpgJB0vDuPKQnt59kKRsw.AQAB \ No newline at end of file diff --git a/tests/datasets/crypto/rsa/salmon-public-pem b/tests/datasets/crypto/rsa/salmon-public-pem new file mode 100644 index 0000000000..1163959859 --- /dev/null +++ b/tests/datasets/crypto/rsa/salmon-public-pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALb7KAWWy1L6lrPtHfAuYVUC4ywo48cm +W9e0ZvP/RQ6gWFIoAUhQ3CQsxtuxTPs7nXcKYCQdLw7jykJ7efZCkbMCAwEAAQ== +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/tests/src/Protocol/SalmonTest.php b/tests/src/Protocol/SalmonTest.php new file mode 100644 index 0000000000..f0c16ca8c3 --- /dev/null +++ b/tests/src/Protocol/SalmonTest.php @@ -0,0 +1,105 @@ +. + * + */ + +namespace Friendica\Test\src\Protocol; + +use Friendica\Protocol\Salmon; + +class SalmonTest extends \PHPUnit\Framework\TestCase +{ + public function dataMagic(): array + { + return [ + 'salmon' => [ + 'magic' => file_get_contents(__DIR__ . '/../../datasets/crypto/rsa/salmon-public-magic'), + 'pem' => file_get_contents(__DIR__ . '/../../datasets/crypto/rsa/salmon-public-pem'), + ], + ]; + } + + /** + * @dataProvider dataMagic + * + * @param $magic + * @param $pem + * @return void + * @throws \Exception + */ + public function testSalmonKey($magic, $pem) + { + $this->assertEquals($magic, Salmon::salmonKey($pem)); + } + + /** + * @dataProvider dataMagic + * + * @param $magic + * @param $pem + * @return void + */ + public function testMagicKeyToPem($magic, $pem) + { + $this->assertEquals($pem, Salmon::magicKeyToPem($magic)); + } + + public function dataMagicFailure(): array + { + return [ + 'empty string' => [ + 'magic' => '', + ], + 'Missing algo' => [ + 'magic' => 'tvsoBZbLUvqWs-0d8C5hVQLjLCjjxyZb17Rm8_9FDqBYUigBSFDcJCzG27FM-zuddwpgJB0vDuPKQnt59kKRsw.AQAB', + ], + 'Missing modulus' => [ + 'magic' => 'RSA.AQAB', + ], + 'Missing exponent' => [ + 'magic' => 'RSA.tvsoBZbLUvqWs-0d8C5hVQLjLCjjxyZb17Rm8_9FDqBYUigBSFDcJCzG27FM-zuddwpgJB0vDuPKQnt59kKRsw', + ], + 'Missing key parts' => [ + 'magic' => 'RSA.', + ], + 'Too many parts' => [ + 'magic' => 'RSA.tvsoBZbLUvqWs-0d8C5hVQLjLCjjxyZb17Rm8_9FDqBYUigBSFDcJCzG27FM-zuddwpgJB0vDuPKQnt59kKRsw.AQAB.AQAB', + ], + 'Wrong encoding' => [ + 'magic' => 'RSA.tvsoBZbLUvqWs-0d8C5hVQLjLCjjxyZb17Rm8/9FDqBYUigBSFDcJCzG27FM+zuddwpgJB0vDuPKQnt59kKRsw.AQAB', + ], + 'Wrong algo' => [ + 'magic' => 'ECDSA.tvsoBZbLUvqWs-0d8C5hVQLjLCjjxyZb17Rm8_9FDqBYUigBSFDcJCzG27FM-zuddwpgJB0vDuPKQnt59kKRsw.AQAB', + ], + ]; + } + + /** + * @dataProvider dataMagicFailure + * + * @param $magic + * @return void + */ + public function testMagicKeyToPemFailure($magic) + { + $this->expectException(\Throwable::class); + + Salmon::magicKeyToPem($magic); + } +} diff --git a/tests/src/Util/CryptoTest.php b/tests/src/Util/CryptoTest.php index 8be5bbcc87..142a1af6e3 100644 --- a/tests/src/Util/CryptoTest.php +++ b/tests/src/Util/CryptoTest.php @@ -65,7 +65,7 @@ class CryptoTest extends TestCase self::assertEquals(11111111, $test); } - public function dataRsa() + public function dataRsa(): array { return [ 'diaspora' => [ @@ -92,34 +92,6 @@ class CryptoTest extends TestCase ], ]; } - - /** - * @dataProvider dataPEM - */ - public function testPemToMe(string $key) - { - Crypto::pemToMe($key, $m, $e); - - $expectedRSA = new RSA(); - $expectedRSA->loadKey([ - 'e' => new BigInteger($e, 256), - 'n' => new BigInteger($m, 256) - ]); - - self::assertEquals($expectedRSA->getPublicKey(), $key); - } - - /** - * @dataProvider dataPEM - */ - public function testMeToPem(string $key) - { - Crypto::pemToMe($key, $m, $e); - - $checkKey = Crypto::meToPem($m, $e); - - self::assertEquals($key, $checkKey); - } } /** diff --git a/view/global.css b/view/global.css index 800a3ea34d..7c0798c21f 100644 --- a/view/global.css +++ b/view/global.css @@ -345,12 +345,12 @@ img.acpopup-img { .fbrowser .path a:before, .fbrowser .path .btn-link:before { content: "/"; padding-right: 5px;} .fbrowser .folders ul { list-style-type: none; padding-left: 10px;} .fbrowser .list { height: auto; overflow-y: hidden; margin: 10px 0px; } -.fbrowser.image .photo-album-image-wrapper { float: left; } -.fbrowser.image a img, .fbrowser.image .btn-link img { height: 48px; } -.fbrowser.image a p, .fbrowser.image .btn-link p { display: none;} -.fbrowser.file .photo-album-image-wrapper { float:none; white-space: nowrap; } -.fbrowser.file img { display: inline; } -.fbrowser.file p { display: inline; white-space: nowrap; } +.fbrowser.photo .photo-album-image-wrapper { float: left; } +.fbrowser.photo a img, .fbrowser.photo .btn-link img { height: 48px; } +.fbrowser.photo a p, .fbrowser.photo .btn-link p { display: none;} +.fbrowser.attachment .photo-album-image-wrapper { float:none; white-space: nowrap; } +.fbrowser.attachment img { display: inline; } +.fbrowser.attachment p { display: inline; white-space: nowrap; } .fbrowser .upload { clear: both; padding-top: 1em;} .fbrowser .error { background: #ffeeee; border: 1px solid #994444; color: #994444; padding: 0.5em;} .fbrowser .error .close { float: right; font-weight: bold; } diff --git a/view/js/filebrowser.js b/view/js/filebrowser.js deleted file mode 100644 index e5bd2730c4..0000000000 --- a/view/js/filebrowser.js +++ /dev/null @@ -1,144 +0,0 @@ -// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPLv3-or-later -/** - * Filebrowser - Friendica Communications Server - * - * Copyright (c) 2010-2021, the Friendica project - * - * 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 code handle user interaction for image/file upload/browser dialog. - * Is loaded from filebrowser_plain.tpl - * - * To load filebrowser in colorbox, call - * - * Dialog.doImageBrowser(eventname, id); - * - * or - * - * Dialog.doFileBrowser(eventname, id); - * - * where: - * - * eventname: event name to catch return value - * id: id returned to event handler - * - * When user select an item, an event in fired in parent page, on body element - * The event is named - * - * fbrowser..[] - * - * will be one of "image" or "file", and the event handler will - * get the following params: - * - * filemane: filename of item choosed by user - * embed: bbcode to embed element into posts - * id: id from caller code - * - * example: - * - * // open dialog for select an image for a textarea with id "myeditor" - * var id="myeditor"; - * Dialog.doImageBrowser("example", id); - * - * // setup event handler to get user selection - * $("body").on("fbrowser.image.example", function(event, filename, bbcode, id) { - * // close colorbox - * $.colorbox.close(); - * // replace textxarea text with bbcode - * $(id).value = bbcode; - * }); - **/ - -var FileBrowser = { - nickname : "", - type : "", - event: "", - id : null, - - init: function(nickname, type) { - FileBrowser.nickname = nickname; - FileBrowser.type = type; - FileBrowser.event = "fbrowser."+type; - if (location['hash']!=="") { - var h = location['hash'].replace("#",""); - FileBrowser.event = FileBrowser.event + "." + h.split("-")[0]; - FileBrowser.id = h.split("-")[1]; - } - - console.log("FileBrowser:", nickname, type,FileBrowser.event, FileBrowser.id ); - - $(".error a.close").on("click", function(e) { - e.preventDefault(); - $(".error").addClass("hidden"); - }); - - $(".folders a, .path a").on("click", function(e){ - e.preventDefault(); - location.href = baseurl + "/fbrowser/" + FileBrowser.type + "/" + encodeURIComponent(this.dataset.folder) + "?mode=minimal" + location['hash']; - }); - - $(".photo-album-photo-link").on('click', function(e){ - e.preventDefault(); - - var embed = ""; - if (FileBrowser.type == "image") { - embed = "[url="+this.dataset.link+"][img="+this.dataset.img+"]"+this.dataset.alt+"[/img][/url]"; - } - if (FileBrowser.type=="file") { - // attachment links are "baseurl/attach/id"; we need id - embed = "[attachment]"+this.dataset.link.split("/").pop()+"[/attachment]"; - } - console.log(FileBrowser.event, this.dataset.filename, embed, FileBrowser.id); - parent.$("body").trigger(FileBrowser.event, [ - this.dataset.filename, - embed, - FileBrowser.id - ]); - - }); - - if ($("#upload-image").length) - var image_uploader = new window.AjaxUpload( - 'upload-image', - { action: 'profile/' + FileBrowser.nickname + '/photos/upload?response=json', - name: 'userfile', - responseType: 'json', - onSubmit: function(file,ext) { $('#profile-rotator').show(); $(".error").addClass('hidden'); }, - onComplete: function(file,response) { - if (response['error']!= undefined) { - $(".error span").html(response['error']); - $(".error").removeClass('hidden'); - $('#profile-rotator').hide(); - return; - } - location = baseurl + "/fbrowser/image/?mode=minimal"+location['hash']; - location.reload(true); - } - } - ); - - if ($("#upload-file").length) - var file_uploader = new window.AjaxUpload( - 'upload-file', - { action: 'profile/' + FileBrowser.nickname + '/attachment/upload?response=json', - name: 'userfile', - responseType: 'json', - onSubmit: function(file,ext) { $('#profile-rotator').show(); $(".error").addClass('hidden'); }, - onComplete: function(file,response) { - if (response['error']!= undefined) { - $(".error span").html(response['error']); - $(".error").removeClass('hidden'); - $('#profile-rotator').hide(); - return; - } - location = baseurl + "/fbrowser/file/?mode=minimal"+location['hash']; - location.reload(true); - } - } - ); - } -}; -// @license-end diff --git a/view/js/main.js b/view/js/main.js index 93340dc37f..a1446bca5b 100644 --- a/view/js/main.js +++ b/view/js/main.js @@ -166,7 +166,7 @@ $(function() { /* event from comment textarea button popups */ /* insert returned bbcode at cursor position or replace selected text */ - $("body").on("fbrowser.image.comment", function(e, filename, bbcode, id) { + $('body').on('fbrowser.photo.comment', function(e, filename, bbcode, id) { $.colorbox.close(); var textarea = document.getElementById("comment-edit-text-" +id); var start = textarea.selectionStart; @@ -1069,7 +1069,7 @@ var Dialog = { * to the event handler */ doImageBrowser : function (name, id) { - var url = Dialog._get_url("image",name,id); + var url = Dialog._get_url('photo', name, id); return Dialog.show(url); }, @@ -1086,7 +1086,7 @@ var Dialog = { * to the event handler */ doFileBrowser : function (name, id) { - var url = Dialog._get_url("file",name,id); + var url = Dialog._get_url('attachment', name, id); return Dialog.show(url); }, @@ -1095,7 +1095,7 @@ var Dialog = { if (id !== undefined) { hash = hash + "-" + id; } - return baseurl + "/fbrowser/"+type+"/?mode=minimal#"+hash; + return 'media/' + type + '/browser?mode=minimal#' + hash; }, _get_size: function() { diff --git a/view/js/module/media/browser.js b/view/js/module/media/browser.js new file mode 100644 index 0000000000..c78a7f13b6 --- /dev/null +++ b/view/js/module/media/browser.js @@ -0,0 +1,156 @@ +// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPLv3-or-later +/** + * Filebrowser - Friendica Communications Server + * + * Copyright (c) 2010-2021, the Friendica project + * + * 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 code handle user interaction for image/file upload/browser dialog. + * Is loaded from filebrowser_plain.tpl + * + * To load filebrowser in colorbox, call + * + * Dialog.doImageBrowser(eventname, id); + * + * or + * + * Dialog.doFileBrowser(eventname, id); + * + * where: + * + * eventname: event name to catch return value + * id: id returned to event handler + * + * When user select an item, an event in fired in parent page, on body element + * The event is named + * + * fbrowser..[] + * + * will be one of "image" or "file", and the event handler will + * get the following params: + * + * filename: filename of item chosen by user + * embed: bbcode to embed element into posts + * id: id from caller code + * + * example: + * + * // open dialog for select an image for a textarea with id "myeditor" + * var id="myeditor"; + * Dialog.doImageBrowser("example", id); + * + * // setup event handler to get user selection + * $("body").on("fbrowser.image.example", function(event, filename, bbcode, id) { + * // close colorbox + * $.colorbox.close(); + * // replace textarea text with bbcode + * $(id).value = bbcode; + * }); + **/ +const Browser = { + nickname: '', + type: '', + event: '', + id: null, + + init: function (nickname, type) { + Browser.nickname = nickname; + Browser.type = type; + Browser.event = 'fbrowser.' + type; + if (location['hash'] !== '') { + const h = location['hash'].replace('#', ''); + Browser.event = Browser.event + '.' + h.split('-')[0]; + Browser.id = h.split('-')[1]; + } + + $('.error a.close').on('click', function (e) { + e.preventDefault(); + $('.error').addClass('hidden'); + }); + + $('.folders a, .path a').on('click', function (e) { + e.preventDefault(); + location.href = Browser._getUrl("minimal", location['hash'], this.dataset.folder); + location.reload(); + }); + + $(".photo-album-photo-link").on('click', function (e) { + e.preventDefault(); + + let embed = ''; + if (Browser.type === "photo") { + embed = '[url=' + this.dataset.link + '][img=' + this.dataset.img + ']' + this.dataset.alt + '[/img][/url]'; + } + if (Browser.type === "attachment") { + embed = '[attachment]' + this.dataset.link + '[/attachment]'; + } + parent.$('body').trigger(Browser.event, [ + this.dataset.filename, + embed, + Browser.id + ]); + + }); + + if ($('#upload-photo').length) { + new window.AjaxUpload( + 'upload-photo', + { + action: 'media/photo/upload?response=json', + name: 'userfile', + responseType: 'json', + onSubmit: function (file, ext) { + $('#profile-rotator').show(); + $('.error').addClass('hidden'); + }, + onComplete: function (file, response) { + if (response['error'] !== undefined) { + $('.error span').html(response['error']); + $('.error').removeClass('hidden'); + $('#profile-rotator').hide(); + return; + } + location.href = Browser._getUrl("minimal", location['hash']); + location.reload(); + } + } + ); + } + + if ($('#upload-attachment').length) { + new window.AjaxUpload( + 'upload-attachment', + { + action: 'media/attachment/upload?response=json', + name: 'userfile', + responseType: 'json', + onSubmit: function (file, ext) { + $('#profile-rotator').show(); + $('.error').addClass('hidden'); + }, + onComplete: function (file, response) { + if (response['error'] !== undefined) { + $('.error span').html(response['error']); + $('.error').removeClass('hidden'); + $('#profile-rotator').hide(); + return; + } + location.href = Browser._getUrl("minimal", location['hash']); + location.reload(); + } + } + ); + } + }, + + _getUrl: function (mode, hash, folder) { + let folderValue = folder !== undefined ? folder : Browser.folder; + let folderUrl = folderValue !== undefined ? '/' + encodeURIComponent(folderValue) : ''; + return 'media/' + Browser.type + '/browser' + folderUrl + '?mode=' + mode + hash; + } +}; +// @license-end diff --git a/view/lang/C/messages.po b/view/lang/C/messages.po index f60cb384d6..fc9256dd25 100644 --- a/view/lang/C/messages.po +++ b/view/lang/C/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: 2022.12-dev\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-11-26 23:37+0100\n" +"POT-Creation-Date: 2022-11-27 00:36+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,36 +18,13 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" -#: mod/fbrowser.php:61 src/Content/Nav.php:195 src/Module/BaseProfile.php:64 -#: view/theme/frio/theme.php:242 -msgid "Photos" -msgstr "" - -#: mod/fbrowser.php:119 mod/fbrowser.php:146 mod/photos.php:1007 -#: mod/photos.php:1108 src/Content/Conversation.php:389 -#: src/Module/Contact/Follow.php:173 src/Module/Contact/Revoke.php:109 -#: src/Module/Contact/Unfollow.php:126 src/Module/Post/Edit.php:164 -#: src/Module/Post/Tag/Remove.php:109 src/Module/Profile/RemoteFollow.php:134 -#: src/Module/Security/TwoFactor/SignOut.php:125 -msgid "Cancel" -msgstr "" - -#: mod/fbrowser.php:121 mod/fbrowser.php:148 -#: src/Module/Settings/Profile/Photo/Index.php:128 -msgid "Upload" -msgstr "" - -#: mod/fbrowser.php:143 -msgid "Files" -msgstr "" - #: mod/item.php:129 mod/item.php:133 msgid "Unable to locate original post." msgstr "" #: mod/item.php:179 mod/item.php:184 mod/item.php:855 mod/message.php:69 #: mod/message.php:114 mod/notes.php:44 mod/photos.php:159 mod/photos.php:884 -#: src/Module/Attach.php:56 src/Module/BaseApi.php:94 +#: src/Module/Attach.php:55 src/Module/BaseApi.php:94 #: src/Module/BaseNotifications.php:98 src/Module/BaseSettings.php:52 #: src/Module/Calendar/Event/API.php:88 src/Module/Calendar/Event/Form.php:84 #: src/Module/Calendar/Event/Show.php:54 src/Module/Calendar/Export.php:62 @@ -62,9 +39,8 @@ msgstr "" #: src/Module/Notifications/Notification.php:76 #: src/Module/Notifications/Notification.php:107 #: src/Module/OStatus/Repair.php:60 src/Module/OStatus/Subscribe.php:66 -#: src/Module/Post/Edit.php:76 src/Module/Profile/Attachment/Upload.php:97 -#: src/Module/Profile/Common.php:55 src/Module/Profile/Contacts.php:55 -#: src/Module/Profile/Photos/Upload.php:108 src/Module/Profile/Schedule.php:39 +#: src/Module/Post/Edit.php:76 src/Module/Profile/Common.php:55 +#: src/Module/Profile/Contacts.php:55 src/Module/Profile/Schedule.php:39 #: src/Module/Profile/Schedule.php:56 src/Module/Profile/UnkMail.php:69 #: src/Module/Profile/UnkMail.php:121 src/Module/Profile/UnkMail.php:132 #: src/Module/Register.php:77 src/Module/Register.php:90 @@ -414,31 +390,28 @@ msgstr "" #: src/Module/HCard.php:51 src/Module/Profile/Common.php:40 #: src/Module/Profile/Common.php:51 src/Module/Profile/Contacts.php:39 #: src/Module/Profile/Contacts.php:49 src/Module/Profile/Media.php:38 -#: src/Module/Profile/Photos/Index.php:77 -#: src/Module/Profile/Photos/Upload.php:87 -#: src/Module/Profile/RemoteFollow.php:71 src/Module/Profile/Status.php:58 -#: src/Module/Register.php:267 +#: src/Module/Profile/Photos.php:77 src/Module/Profile/RemoteFollow.php:71 +#: src/Module/Profile/Status.php:58 src/Module/Register.php:267 msgid "User not found." msgstr "" #: mod/photos.php:107 src/Module/BaseProfile.php:67 -#: src/Module/Profile/Photos/Index.php:169 +#: src/Module/Profile/Photos.php:169 msgid "Photo Albums" msgstr "" -#: mod/photos.php:108 src/Module/Profile/Photos/Index.php:170 -#: src/Module/Profile/Photos/Index.php:187 +#: mod/photos.php:108 src/Module/Profile/Photos.php:170 +#: src/Module/Profile/Photos.php:187 msgid "Recent Photos" msgstr "" -#: mod/photos.php:110 mod/photos.php:1076 -#: src/Module/Profile/Photos/Index.php:172 -#: src/Module/Profile/Photos/Index.php:189 +#: mod/photos.php:110 mod/photos.php:1066 src/Module/Profile/Photos.php:172 +#: src/Module/Profile/Photos.php:189 msgid "Upload New Photos" msgstr "" #: mod/photos.php:128 src/Module/BaseSettings.php:74 -#: src/Module/Profile/Photos/Index.php:153 +#: src/Module/Profile/Photos.php:153 msgid "everybody" msgstr "" @@ -472,7 +445,7 @@ msgid "%1$s was tagged in %2$s by %3$s" msgstr "" #: mod/photos.php:630 mod/photos.php:633 mod/photos.php:660 -#: src/Module/Profile/Photos/Upload.php:213 +#: src/Module/Media/Photo/Upload.php:188 #: src/Module/Settings/Profile/Photo/Index.php:59 #, php-format msgid "Image exceeds size limit of %s" @@ -496,19 +469,19 @@ msgstr "" msgid "Image file is empty." msgstr "" -#: mod/photos.php:683 src/Module/Profile/Photos/Upload.php:179 -#: src/Module/Profile/Photos/Upload.php:180 +#: mod/photos.php:683 src/Module/Media/Photo/Upload.php:154 +#: src/Module/Media/Photo/Upload.php:155 #: src/Module/Settings/Profile/Photo/Index.php:68 msgid "Unable to process image." msgstr "" -#: mod/photos.php:709 src/Module/Profile/Photos/Upload.php:231 +#: mod/photos.php:709 src/Module/Media/Photo/Upload.php:206 #: src/Module/Settings/Profile/Photo/Index.php:95 msgid "Image upload failed." msgstr "" #: mod/photos.php:795 src/Module/Conversation/Community.php:187 -#: src/Module/Directory.php:48 src/Module/Profile/Photos/Index.php:72 +#: src/Module/Directory.php:48 src/Module/Profile/Photos.php:72 #: src/Module/Search/Index.php:64 msgid "Public access denied." msgstr "" @@ -517,7 +490,7 @@ msgstr "" msgid "No photos selected" msgstr "" -#: mod/photos.php:869 src/Module/Profile/Photos/Index.php:92 +#: mod/photos.php:869 src/Module/Profile/Photos.php:92 msgid "Access to this item is restricted." msgstr "" @@ -554,7 +527,17 @@ msgstr "" msgid "Delete Album" msgstr "" -#: mod/photos.php:1033 +#: mod/photos.php:997 mod/photos.php:1098 src/Content/Conversation.php:389 +#: src/Module/Contact/Follow.php:173 src/Module/Contact/Revoke.php:109 +#: src/Module/Contact/Unfollow.php:126 +#: src/Module/Media/Attachment/Browser.php:78 +#: src/Module/Media/Photo/Browser.php:88 src/Module/Post/Edit.php:164 +#: src/Module/Post/Tag/Remove.php:109 src/Module/Profile/RemoteFollow.php:134 +#: src/Module/Security/TwoFactor/SignOut.php:125 +msgid "Cancel" +msgstr "" + +#: mod/photos.php:1023 msgid "Edit Album" msgstr "" @@ -570,7 +553,7 @@ msgstr "" msgid "Show Oldest First" msgstr "" -#: mod/photos.php:1061 src/Module/Profile/Photos/Index.php:140 +#: mod/photos.php:1051 src/Module/Profile/Photos.php:140 msgid "View Photo" msgstr "" @@ -1690,6 +1673,11 @@ msgstr "" msgid "Your profile page" msgstr "" +#: src/Content/Nav.php:195 src/Module/BaseProfile.php:64 +#: src/Module/Media/Photo/Browser.php:74 view/theme/frio/theme.php:242 +msgid "Photos" +msgstr "" + #: src/Content/Nav.php:195 view/theme/frio/theme.php:242 msgid "Your photos" msgstr "" @@ -3184,7 +3172,7 @@ msgstr "" msgid "[no subject]" msgstr "" -#: src/Model/Photo.php:1086 src/Module/Profile/Photos/Upload.php:223 +#: src/Model/Photo.php:1139 src/Module/Media/Photo/Upload.php:198 msgid "Wall Photos" msgstr "" @@ -5279,7 +5267,7 @@ msgstr "" msgid "Applications" msgstr "" -#: src/Module/Attach.php:50 src/Module/Attach.php:62 +#: src/Module/Attach.php:49 src/Module/Attach.php:61 msgid "Item was not found." msgstr "" @@ -5908,10 +5896,10 @@ msgid "The contact could not be added." msgstr "" #: src/Module/Contact/MatchInterests.php:94 -#: src/Module/Profile/Attachment/Upload.php:80 -#: src/Module/Profile/Attachment/Upload.php:102 -#: src/Module/Profile/Photos/Upload.php:113 -#: src/Module/Profile/Photos/Upload.php:162 +#: src/Module/Media/Attachment/Upload.php:80 +#: src/Module/Media/Attachment/Upload.php:85 +#: src/Module/Media/Photo/Upload.php:83 src/Module/Media/Photo/Upload.php:88 +#: src/Module/Media/Photo/Upload.php:137 msgid "Invalid request." msgstr "" @@ -7143,6 +7131,38 @@ msgstr "" msgid "A Decentralized Social Network" msgstr "" +#: src/Module/Media/Attachment/Browser.php:58 +#: src/Module/Media/Photo/Browser.php:59 +msgid "You need to be logged in to access this page." +msgstr "" + +#: src/Module/Media/Attachment/Browser.php:75 +msgid "Files" +msgstr "" + +#: src/Module/Media/Attachment/Browser.php:80 +#: src/Module/Media/Photo/Browser.php:90 +#: src/Module/Settings/Profile/Photo/Index.php:128 +msgid "Upload" +msgstr "" + +#: src/Module/Media/Attachment/Upload.php:100 +msgid "Sorry, maybe your upload is bigger than the PHP configuration allows" +msgstr "" + +#: src/Module/Media/Attachment/Upload.php:100 +msgid "Or - did you try to upload an empty file?" +msgstr "" + +#: src/Module/Media/Attachment/Upload.php:107 +#, php-format +msgid "File exceeds size limit of %s" +msgstr "" + +#: src/Module/Media/Attachment/Upload.php:117 +msgid "File upload failed." +msgstr "" + #: src/Module/Moderation/BaseUsers.php:72 msgid "List of all users" msgstr "" @@ -8148,28 +8168,11 @@ msgstr "" msgid "Remove" msgstr "" -#: src/Module/Profile/Attachment/Upload.php:117 -msgid "Sorry, maybe your upload is bigger than the PHP configuration allows" -msgstr "" - -#: src/Module/Profile/Attachment/Upload.php:117 -msgid "Or - did you try to upload an empty file?" -msgstr "" - -#: src/Module/Profile/Attachment/Upload.php:124 -#, php-format -msgid "File exceeds size limit of %s" -msgstr "" - -#: src/Module/Profile/Attachment/Upload.php:134 -msgid "File upload failed." -msgstr "" - #: src/Module/Profile/Contacts.php:119 msgid "No contacts." msgstr "" -#: src/Module/Profile/Photos/Index.php:146 +#: src/Module/Profile/Photos.php:146 msgid "View Album" msgstr "" diff --git a/view/templates/jot-header.tpl b/view/templates/jot-header.tpl index a67f9c3f0c..808d511262 100644 --- a/view/templates/jot-header.tpl +++ b/view/templates/jot-header.tpl @@ -61,11 +61,11 @@ function enableOnUser(){ **/ /* callback */ - $('body').on('fbrowser.image.main', function(e, filename, embedcode, id) { + $('body').on('fbrowser.photo.main', function(e, filename, embedcode, id) { $.colorbox.close(); addeditortext(embedcode); }); - $('body').on('fbrowser.file.main', function(e, filename, embedcode, id) { + $('body').on('fbrowser.attachment.main', function(e, filename, embedcode, id) { $.colorbox.close(); addeditortext(embedcode); }); diff --git a/view/templates/filebrowser.tpl b/view/templates/media/browser.tpl similarity index 76% rename from view/templates/filebrowser.tpl rename to view/templates/media/browser.tpl index 09bf563ee7..749c2bea7e 100644 --- a/view/templates/filebrowser.tpl +++ b/view/templates/media/browser.tpl @@ -1,11 +1,11 @@ - - + +
@@ -33,7 +33,7 @@ {{foreach $files as $f}} diff --git a/view/theme/frio/css/style.css b/view/theme/frio/css/style.css index b3250d8055..eecb0b242d 100644 --- a/view/theme/frio/css/style.css +++ b/view/theme/frio/css/style.css @@ -1563,10 +1563,10 @@ textarea.comment-edit-text:focus + .comment-edit-form .preview { max-height: calc(100vh - 220px); } } -.fbrowser.image .photo-album-image-wrapper { +.fbrowser.photo .photo-album-image-wrapper { box-shadow: 2px 2px 5px 0px rgba(0, 0, 0, 0.2); } -.fbrowser.image .photo-album-image-wrapper .caption { +.fbrowser.photo .photo-album-image-wrapper .caption { pointer-events: none; } .fbrowser .profile-rotator-wrapper { diff --git a/view/theme/frio/js/filebrowser.js b/view/theme/frio/js/filebrowser.js deleted file mode 100644 index 67d3b7923e..0000000000 --- a/view/theme/frio/js/filebrowser.js +++ /dev/null @@ -1,264 +0,0 @@ -// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPLv3-or-later -/** - * Filebrowser - Friendica Communications Server - * - * Copyright (c) 2010-2021, the Friendica project - * - * 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 code handle user interaction for image/file upload/browser dialog. - * Is loaded from filebrowser_plain.tpl - * - * To load filebrowser in colorbox, call - * - * Dialog.doImageBrowser(eventname, id); - * - * or - * - * Dialog.doFileBrowser(eventname, id); - * - * where: - * - * eventname: event name to catch return value - * id: id returned to event handler - * - * When user select an item, an event in fired in parent page, on body element - * The event is named - * - * fbrowser..[] - * - * will be one of "image" or "file", and the event handler will - * get the following params: - * - * filemane: filename of item choosed by user - * embed: bbcode to embed element into posts - * id: id from caller code - * - * example: - * - * // open dialog for select an image for a textarea with id "myeditor" - * var id="myeditor"; - * Dialog.doImageBrowser("example", id); - * - * // setup event handler to get user selection - * $("body").on("fbrowser.image.example", function(event, filename, bbcode, id) { - * // close colorbox - * $.colorbox.close(); - * // replace textxarea text with bbcode - * $(id).value = bbcode; - * }); - **/ - -/* - * IMPORTANT - * - * This is a modified version to work with - * the frio theme.and bootstrap modals - * - * The origninal file is under: - * js/filebrowser.js - * - */ - -var FileBrowser = { - nickname: "", - type: "", - event: "", - folder: "", - id: null, - - init: function (nickname, type, hash) { - FileBrowser.nickname = nickname; - FileBrowser.type = type; - FileBrowser.event = "fbrowser." + type; - - if (hash !== "") { - var h = hash.replace("#", ""); - var destination = h.split("-")[0]; - FileBrowser.id = h.split("-")[1]; - FileBrowser.event = FileBrowser.event + "." + destination; - if (destination === "comment") { - // Get the comment textimput field - var commentElm = document.getElementById("comment-edit-text-" + FileBrowser.id); - } - } - - console.log("FileBrowser: " + nickname, type, FileBrowser.event, FileBrowser.id); - - FileBrowser.postLoad(); - - $(".error .close").on("click", function (e) { - e.preventDefault(); - $(".error").addClass("hidden"); - }); - - // Click on album link - $(".fbrowser").on("click", ".folders button, .path button", function (e) { - e.preventDefault(); - var url = - baseurl + - "/fbrowser/" + - FileBrowser.type + - "/" + - encodeURIComponent(this.dataset.folder) + - "?mode=none&theme=frio"; - FileBrowser.folder = this.dataset.folder; - - FileBrowser.loadContent(url); - }); - - //Embed on click - $(".fbrowser").on("click", ".photo-album-photo-link", function (e) { - e.preventDefault(); - - var embed = ""; - if (FileBrowser.type === "image") { - embed = "[url=" + this.dataset.link + "][img=" + this.dataset.img + "]" + this.dataset.alt + "[/img][/url]"; - } - if (FileBrowser.type === "file") { - // attachment links are "baseurl/attach/id"; we need id - embed = "[attachment]" + this.dataset.link.split("/").pop() + "[/attachment]"; - } - - // Delete prefilled Text of the comment input - // Note: not the best solution but function commentOpenUI don't - // work as expected (we need a way to wait until commentOpenUI would be finished). - // As for now we insert pieces of this function here - if (commentElm !== null && typeof commentElm !== "undefined") { - if (commentElm.value === "") { - $("#comment-edit-text-" + FileBrowser.id) - .addClass("comment-edit-text-full") - .removeClass("comment-edit-text-empty"); - $("#comment-edit-submit-wrapper-" + FileBrowser.id).show(); - $("#comment-edit-text-" + FileBrowser.id).attr("tabindex", "9"); - $("#comment-edit-submit-" + FileBrowser.id).attr("tabindex", "10"); - } - } - - console.log(FileBrowser.event, this.dataset.filename, embed, FileBrowser.id); - - $("body").trigger(FileBrowser.event, [this.dataset.filename, embed, FileBrowser.id, this.dataset.img]); - - // Close model - $("#modal").modal("hide"); - // Update autosize for this textarea - autosize.update($(".text-autosize")); - }); - - // EventListener for switching between image and file mode - $(".fbrowser").on("click", ".fbswitcher .btn", function (e) { - e.preventDefault(); - FileBrowser.type = this.getAttribute("data-mode"); - $(".fbrowser") - .removeClass() - .addClass("fbrowser " + FileBrowser.type); - url = baseurl + "/fbrowser/" + FileBrowser.type + "?mode=none&theme=frio"; - - FileBrowser.loadContent(url); - }); - }, - - // Initialize the AjaxUpload for the upload buttons - uploadButtons: function () { - if ($("#upload-image").length) { - //AjaxUpload for images - var image_uploader = new window.AjaxUpload("upload-image", { - action: - "profile/" + - FileBrowser.nickname + - "/photos/upload?response=json&album=" + - encodeURIComponent(FileBrowser.folder), - name: "userfile", - responseType: "json", - onSubmit: function (file, ext) { - $(".fbrowser-content").hide(); - $(".fbrowser .profile-rotator-wrapper").show(); - $(".error").addClass("hidden"); - }, - onComplete: function (file, response) { - if (response["error"] != undefined) { - $(".error span").html(response["error"]); - $(".error").removeClass("hidden"); - $(".fbrowser .profile-rotator-wrapper").hide(); - $(".fbrowser-content").show(); - return; - } - - // load new content to fbrowser window - FileBrowser.loadContent( - baseurl + - "/fbrowser/" + - FileBrowser.type + - "/" + - encodeURIComponent(FileBrowser.folder) + - "?mode=none&theme=frio", - ); - }, - }); - } - - if ($("#upload-file").length) { - //AjaxUpload for files - var file_uploader = new window.AjaxUpload("upload-file", { - action: "profile/" + FileBrowser.nickname + "/attachment/upload?response=json", - name: "userfile", - responseType: "json", - onSubmit: function (file, ext) { - $(".fbrowser-content").hide(); - $(".fbrowser .profile-rotator-wrapper").show(); - $(".error").addClass("hidden"); - }, - onComplete: function (file, response) { - if (response["error"] != undefined) { - $(".error span").html(response["error"]); - $(".error").removeClass("hidden"); - $(".fbrowser .profile-rotator-wrapper").hide(); - $(".fbrowser-content").show(); - return; - } - - var url = baseurl + "/fbrowser/" + FileBrowser.type + "?mode=none&theme=frio"; - // Load new content to fbrowser window - FileBrowser.loadContent(url); - }, - }); - } - }, - - // Stuff which should be executed if ne content was loaded - postLoad: function () { - FileBrowser.initGallery(); - $(".fbrowser .fbswitcher .btn").removeClass("active"); - $(".fbrowser .fbswitcher [data-mode=" + FileBrowser.type + "]").addClass("active"); - // We need to add the AjaxUpload to the button - FileBrowser.uploadButtons(); - }, - - // Load new content (e.g. change photo album) - loadContent: function (url) { - $(".fbrowser-content").hide(); - $(".fbrowser .profile-rotator-wrapper").show(); - - // load new content to fbrowser window - $(".fbrowser").load(url, function (responseText, textStatus) { - $(".profile-rotator-wrapper").hide(); - if (textStatus === "success") { - $(".fbrowser_content").show(); - FileBrowser.postLoad(); - } - }); - }, - - // Initialize justified Gallery - initGallery: function () { - $(".fbrowser.image .fbrowser-content-container").justifiedGallery({ - rowHeight: 80, - margins: 4, - border: 0, - }); - }, -}; -// @license-end diff --git a/view/theme/frio/js/modal.js b/view/theme/frio/js/modal.js index eaf41eeb87..c3c4009b05 100644 --- a/view/theme/frio/js/modal.js +++ b/view/theme/frio/js/modal.js @@ -82,7 +82,7 @@ $(document).ready(function () { }); // Insert filebrowser images into the input field (field_fileinput.tpl). - $("body").on("fbrowser.image.input", function (e, filename, embedcode, id, img) { + $("body").on("fbrowser.photo.input", function (e, filename, embedcode, id, img) { // Select the clicked button by it's attribute. var elm = $("[image-input='select']"); // Select the input field which belongs to this button. @@ -132,12 +132,12 @@ Dialog.show = function (url, title) { Dialog._get_url = function (type, name, id) { var hash = name; if (id !== undefined) hash = hash + "-" + id; - return "fbrowser/" + type + "/?mode=none&theme=frio#" + hash; + return 'media/' + type + '/browser?mode=none&theme=frio#' + hash; }; // Does load the filebrowser into the jot modal. Dialog.showJot = function () { - var type = "image"; + var type = "photo"; var name = "main"; var url = Dialog._get_url(type, name); @@ -159,15 +159,15 @@ Dialog._load = function (url) { let filebrowser = document.getElementById("filebrowser"); // Try to fetch the hash form the url. - let match = url.match(/fbrowser\/[a-z]+\/.*(#.*)/); + let match = url.match(/media\/[a-z]+\/.*(#.*)/); if (!filebrowser || match === null) { return; //not fbrowser } // Initialize the filebrowser. loadScript("view/js/ajaxupload.js"); - loadScript("view/theme/frio/js/filebrowser.js", function () { - FileBrowser.init(filebrowser.dataset.nickname, filebrowser.dataset.type, match[1]); + loadScript("view/theme/frio/js/module/media/browser.js", function () { + Browser.init(filebrowser.dataset.nickname, filebrowser.dataset.type, match[1]); }); }; diff --git a/view/theme/frio/js/module/media/browser.js b/view/theme/frio/js/module/media/browser.js new file mode 100644 index 0000000000..ccd2e1b5be --- /dev/null +++ b/view/theme/frio/js/module/media/browser.js @@ -0,0 +1,250 @@ +// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPLv3-or-later +/** + * Filebrowser - Friendica Communications Server + * + * Copyright (c) 2010-2021, the Friendica project + * + * 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 code handle user interaction for photo/file upload/browser dialog. + * Is loaded from filebrowser_plain.tpl + * + * To load filebrowser in colorbox, call + * + * Dialog.doImageBrowser(eventname, id); + * + * or + * + * Dialog.doFileBrowser(eventname, id); + * + * where: + * + * eventname: event name to catch return value + * id: id returned to event handler + * + * When user select an item, an event in fired in parent page, on body element + * The event is named + * + * fbrowser..[] + * + * will be one of "image" or "file", and the event handler will + * get the following params: + * + * filename: filename of item chosen by user + * embed: bbcode to embed element into posts + * id: id from caller code + * + * example: + * + * // open dialog for select an image for a textarea with id "myeditor" + * var id="myeditor"; + * Dialog.doImageBrowser("example", id); + * + * // setup event handler to get user selection + * $("body").on("fbrowser.image.example", function(event, filename, bbcode, id) { + * // close colorbox + * $.colorbox.close(); + * // replace textarea text with bbcode + * $(id).value = bbcode; + * }); + **/ + +/* + * IMPORTANT + * + * This is a modified version to work with + * the frio theme and Bootstrap modals + * + * The original file is under: + * js/module/media/browser.js + * + */ + +var Browser = { + nickname: '', + type: '', + event: '', + folder: '', + id: null, + + init: function (nickname, type, hash) { + Browser.nickname = nickname; + Browser.type = type; + Browser.event = 'fbrowser.' + type; + + if (hash !== '') { + const h = hash.replace('#', ''); + const destination = h.split('-')[0]; + Browser.id = h.split('-')[1]; + Browser.event = Browser.event + '.' + destination; + if (destination === 'comment') { + // Get the comment textinput field + var commentElm = document.getElementById('comment-edit-text-' + Browser.id); + } + } + + Browser.postLoad(); + + $('.error .close').on('click', function (e) { + e.preventDefault(); + $('.error').addClass('hidden'); + }); + + // Click on album link + $('.fbrowser').on('click', '.folders button, .path button', function (e) { + e.preventDefault(); + let url = Browser._getUrl("none", this.dataset.folder); + Browser.folder = this.dataset.folder; + + Browser.loadContent(url); + }); + + //Embed on click + $('.fbrowser').on('click', '.photo-album-photo-link', function (e) { + e.preventDefault(); + + let embed = ''; + if (Browser.type === 'photo') { + embed = '[url=' + this.dataset.link + '][img=' + this.dataset.img + ']' + this.dataset.alt + '[/img][/url]'; + } + if (Browser.type === 'attachment') { + embed = '[attachment]' + this.dataset.link + '[/attachment]'; + } + + // Delete prefilled Text of the comment input + // Note: not the best solution but function commentOpenUI don't + // work as expected (we need a way to wait until commentOpenUI would be finished). + // As for now we insert pieces of this function here + if (commentElm !== null && typeof commentElm !== 'undefined') { + if (commentElm.value === '') { + $('#comment-edit-text-' + Browser.id) + .addClass('comment-edit-text-full') + .removeClass('comment-edit-text-empty'); + $('#comment-edit-submit-wrapper-' + Browser.id).show(); + $('#comment-edit-text-' + Browser.id).attr('tabindex', '9'); + $('#comment-edit-submit-' + Browser.id).attr('tabindex', '10'); + } + } + + console.log(Browser.event, this.dataset.filename, embed, Browser.id); + + $('body').trigger(Browser.event, [this.dataset.filename, embed, Browser.id, this.dataset.img]); + + // Close model + $('#modal').modal('hide'); + // Update autosize for this textarea + autosize.update($('.text-autosize')); + }); + + // EventListener for switching between photo and file mode + $('.fbrowser').on('click', '.fbswitcher .btn', function (e) { + e.preventDefault(); + Browser.type = this.getAttribute('data-mode'); + $('.fbrowser') + .removeClass() + .addClass('fbrowser ' + Browser.type); + + Browser.loadContent(Browser._getUrl("none")); + }); + }, + + // Initialize the AjaxUpload for the upload buttons + uploadButtons: function () { + if ($('#upload-photo').length) { + //AjaxUpload for photos + new window.AjaxUpload( + 'upload-photo', + { + action: 'media/photo/upload?response=json&album=' + encodeURIComponent(Browser.folder), + name: 'userfile', + responseType: 'json', + onSubmit: function (file, ext) { + $('.fbrowser-content').hide(); + $('.fbrowser .profile-rotator-wrapper').show(); + $('.error').addClass('hidden'); + }, + onComplete: function (file, response) { + if (response['error'] !== undefined) { + $('.error span').html(response['error']); + $('.error').removeClass('hidden'); + $('.fbrowser .profile-rotator-wrapper').hide(); + $('.fbrowser-content').show(); + return; + } + // load new content to fbrowser window + Browser.loadContent(Browser._getUrl("none")); + }, + }); + } + + if ($('#upload-attachment').length) { + //AjaxUpload for files + new window.AjaxUpload( + 'upload-attachment', + { + action: 'media/attachment/upload?response=json', + name: 'userfile', + responseType: 'json', + onSubmit: function (file, ext) { + $('.fbrowser-content').hide(); + $('.fbrowser .profile-rotator-wrapper').show(); + $('.error').addClass('hidden'); + }, + onComplete: function (file, response) { + if (response["error"] !== undefined) { + $('.error span').html(response['error']); + $('.error').removeClass('hidden'); + $('.fbrowser .profile-rotator-wrapper').hide(); + $('.fbrowser-content').show(); + return; + } + // Load new content to fbrowser window + Browser.loadContent(Browser._getUrl("none")); + }, + }); + } + }, + + // Stuff which should be executed if no content was loaded + postLoad: function () { + Browser.initGallery(); + $('.fbrowser .fbswitcher .btn').removeClass('active'); + $('.fbrowser .fbswitcher [data-mode=' + Browser.type + ']').addClass('active'); + // We need to add the AjaxUpload to the button + Browser.uploadButtons(); + }, + + // Load new content (e.g. change photo album) + loadContent: function (url) { + $('.fbrowser-content').hide(); + $('.fbrowser .profile-rotator-wrapper').show(); + + // load new content to fbrowser window + $('.fbrowser').load(url, function (responseText, textStatus) { + $('.profile-rotator-wrapper').hide(); + if (textStatus === 'success') { + $(".fbrowser_content").show(); + Browser.postLoad(); + } + }); + }, + + // Initialize justified Gallery + initGallery: function () { + $('.fbrowser.photo .fbrowser-content-container').justifiedGallery({ + rowHeight: 80, + margins: 4, + border: 0, + }); + }, + + _getUrl: function (mode, folder) { + let folderValue = folder !== undefined ? folder : Browser.folder; + let folderUrl = folderValue !== undefined ? '/' + encodeURIComponent(folderValue) : ''; + return 'media/' + Browser.type + '/browser' + folderUrl + '?mode=' + mode + "&theme=frio"; + } +}; +// @license-end diff --git a/view/theme/frio/templates/contact/entry.tpl b/view/theme/frio/templates/contact/entry.tpl index 5d29238e73..17436b5704 100644 --- a/view/theme/frio/templates/contact/entry.tpl +++ b/view/theme/frio/templates/contact/entry.tpl @@ -6,7 +6,7 @@
diff --git a/view/theme/frio/templates/jot-header.tpl b/view/theme/frio/templates/jot-header.tpl index 25a02b9b60..9ac56d27d5 100644 --- a/view/theme/frio/templates/jot-header.tpl +++ b/view/theme/frio/templates/jot-header.tpl @@ -61,13 +61,13 @@ **/ /* callback */ - $('body').on('fbrowser.image.main', function(e, filename, embedcode, id) { + $('body').on('fbrowser.photo.main', function(e, filename, embedcode, id) { ///@todo this part isn't ideal and need to be done in a better way jotTextOpenUI(document.getElementById("profile-jot-text")); jotActive(); addeditortext(embedcode); }) - .on('fbrowser.file.main', function(e, filename, embedcode, id) { + .on('fbrowser.attachment.main', function(e, filename, embedcode, id) { jotTextOpenUI(document.getElementById("profile-jot-text")); jotActive(); addeditortext(embedcode); diff --git a/view/theme/frio/templates/filebrowser.tpl b/view/theme/frio/templates/media/browser.tpl similarity index 86% rename from view/theme/frio/templates/filebrowser.tpl rename to view/theme/frio/templates/media/browser.tpl index 13c4790487..71974eeea8 100644 --- a/view/theme/frio/templates/filebrowser.tpl +++ b/view/theme/frio/templates/media/browser.tpl @@ -16,9 +16,9 @@ {{/foreach}} {{* Switch between image and file mode *}} -
- - +
+ +
diff --git a/view/theme/quattro/dark/style.css b/view/theme/quattro/dark/style.css index e4f7fb3b95..2b29887b29 100644 --- a/view/theme/quattro/dark/style.css +++ b/view/theme/quattro/dark/style.css @@ -2517,29 +2517,29 @@ footer { .fbrowser .list { padding: 10px; } -.fbrowser.image .photo-album-image-wrapper { +.fbrowser.photo .photo-album-image-wrapper { width: 48px; height: 48px; } -.fbrowser.image a img { +.fbrowser.photo a img { width: auto; height: 48px; } -.fbrowser.image a p { +.fbrowser.photo a p { display: none; } -.fbrowser.file .photo-album-image-wrapper { +.fbrowser.attachment .photo-album-image-wrapper { float: none; white-space: nowrap; width: 100%; height: auto; } -.fbrowser.file img { +.fbrowser.attachment img { display: inline; width: 16px; height: 16px; } -.fbrowser.file p { +.fbrowser.attachment p { display: inline; white-space: nowrap; } diff --git a/view/theme/quattro/green/style.css b/view/theme/quattro/green/style.css index 3ab55fbaff..5e9770543b 100644 --- a/view/theme/quattro/green/style.css +++ b/view/theme/quattro/green/style.css @@ -2516,29 +2516,29 @@ footer { .fbrowser .list { padding: 10px; } -.fbrowser.image .photo-album-image-wrapper { +.fbrowser.photo .photo-album-image-wrapper { width: 48px; height: 48px; } -.fbrowser.image a img { +.fbrowser.photo a img { width: auto; height: 48px; } -.fbrowser.image a p { +.fbrowser.photo a p { display: none; } -.fbrowser.file .photo-album-image-wrapper { +.fbrowser.attachment .photo-album-image-wrapper { float: none; white-space: nowrap; width: 100%; height: auto; } -.fbrowser.file img { +.fbrowser.attachment img { display: inline; width: 16px; height: 16px; } -.fbrowser.file p { +.fbrowser.attachment p { display: inline; white-space: nowrap; } diff --git a/view/theme/quattro/lilac/style.css b/view/theme/quattro/lilac/style.css index 7adf87ad93..b8b66ff327 100644 --- a/view/theme/quattro/lilac/style.css +++ b/view/theme/quattro/lilac/style.css @@ -2516,29 +2516,29 @@ footer { .fbrowser .list { padding: 10px; } -.fbrowser.image .photo-album-image-wrapper { +.fbrowser.photo .photo-album-image-wrapper { width: 48px; height: 48px; } -.fbrowser.image a img { +.fbrowser.photo a img { width: auto; height: 48px; } -.fbrowser.image a p { +.fbrowser.photo a p { display: none; } -.fbrowser.file .photo-album-image-wrapper { +.fbrowser.attachment .photo-album-image-wrapper { float: none; white-space: nowrap; width: 100%; height: auto; } -.fbrowser.file img { +.fbrowser.attachment img { display: inline; width: 16px; height: 16px; } -.fbrowser.file p { +.fbrowser.attachment p { display: inline; white-space: nowrap; } diff --git a/view/theme/quattro/quattro.less b/view/theme/quattro/quattro.less index 9a990557e4..8163e14ba8 100644 --- a/view/theme/quattro/quattro.less +++ b/view/theme/quattro/quattro.less @@ -1673,11 +1673,11 @@ footer { height: 100px; display: table-row; } } .fbrowser .folders ul { list-style: url("icons/folder.png"); padding-left: 22px;} .fbrowser .list { padding: 10px; } -.fbrowser.image .photo-album-image-wrapper { width: 48px; height: 48px; } -.fbrowser.image a img { width: auto; height: 48px; } -.fbrowser.image a p { display: none;} -.fbrowser.file .photo-album-image-wrapper { float:none; white-space: nowrap; width: 100%; height: auto; } -.fbrowser.file img { display: inline; width: 16px; height: 16px} -.fbrowser.file p { display: inline; white-space: nowrap; } +.fbrowser.photo .photo-album-image-wrapper { width: 48px; height: 48px; } +.fbrowser.photo a img { width: auto; height: 48px; } +.fbrowser.photo a p { display: none;} +.fbrowser.attachment .photo-album-image-wrapper { float:none; white-space: nowrap; width: 100%; height: auto; } +.fbrowser.attachment img { display: inline; width: 16px; height: 16px} +.fbrowser.attachment p { display: inline; white-space: nowrap; } .fbrowser .upload { clear: both; padding-top: 1em;} diff --git a/view/theme/vier/style.css b/view/theme/vier/style.css index 9054f62f7d..b574a9f3e7 100644 --- a/view/theme/vier/style.css +++ b/view/theme/vier/style.css @@ -3261,7 +3261,7 @@ img.photo-album-photo { } /* upload/select popup */ -fbrowser.image .photo-album-image-wrapper { margin-left: 10px; } +fbrowser.photo .photo-album-image-wrapper { margin-left: 10px; } #message-preview { margin-top: 15px; } #message-preview span { width: 100%; } #message-preview .mail-count, #message-preview .mail-delete { display:none; }