Merge branch 'develop' into show_image_upload_limit

# Conflicts:
#	src/Util/Strings.php
#	view/lang/C/messages.po
This commit is contained in:
Marek Bachmann 2022-11-27 23:52:58 +01:00
commit a01872a117
72 changed files with 1605 additions and 1038 deletions

View file

@ -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",

26
composer.lock generated
View file

@ -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",

View file

@ -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/)

View file

@ -1,160 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2022, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* @package Friendica\modules
* @subpackage FileBrowser
* @author Fabio Comuni <fabrixxm@kirgroup.com>
*/
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);
}
}

View file

@ -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/

View file

@ -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
{

View file

@ -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

View file

@ -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);
}
/**

View file

@ -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 [];
}

View file

@ -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 {

View file

@ -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));
}

View file

@ -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
*/

View file

@ -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) {

View file

@ -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);
}

View file

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

View file

@ -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);
}
/**

View file

@ -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'])) {

View file

@ -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;

View file

@ -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
*

View file

@ -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'];

View file

@ -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';

View file

@ -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');
}

View file

@ -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());
}
}

View file

@ -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, '#');

View file

@ -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';
}
}
}

View file

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

View file

@ -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);

View file

@ -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();
}

View file

@ -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,

View file

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

View file

@ -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);

View file

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

View file

@ -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");
}
/**

View file

@ -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.

View file

@ -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;

View file

@ -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'
);
}
}

View file

@ -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 = [];

View file

@ -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);
}
}

View file

@ -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]);
}
}

View file

@ -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);
}
}
}

View file

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

View file

@ -200,6 +200,7 @@ class Email implements IEmail
/**
* @inheritDoc
*/
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->toArray();

View file

@ -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;
}

View file

@ -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);
}
}

View file

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

View file

@ -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.
*

View file

@ -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 [];
}
}
/**

View file

@ -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, '-_', '+/'));
}
/**

View file

@ -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]],

View file

@ -0,0 +1 @@
RSA.tvsoBZbLUvqWs-0d8C5hVQLjLCjjxyZb17Rm8_9FDqBYUigBSFDcJCzG27FM-zuddwpgJB0vDuPKQnt59kKRsw.AQAB

View file

@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALb7KAWWy1L6lrPtHfAuYVUC4ywo48cm
W9e0ZvP/RQ6gWFIoAUhQ3CQsxtuxTPs7nXcKYCQdLw7jykJ7efZCkbMCAwEAAQ==
-----END PUBLIC KEY-----

View file

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

View file

@ -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);
}
}
/**

View file

@ -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; }

View file

@ -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.<type>.[<eventname>]
*
* <type> 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

View file

@ -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() {

View file

@ -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.<type>.[<eventname>]
*
* <type> 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

View file

@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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 ""

View file

@ -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);
});

View file

@ -1,11 +1,11 @@
<!--
This is the template used by mod/fbrowser.php
-->
<script type="text/javascript" src="{{$baseurl}}/view/js/ajaxupload.js?v={{$smarty.const.FRIENDICA_VERSION}}"></script>
<script type="text/javascript" src="{{$baseurl}}/view/js/filebrowser.js?v={{$smarty.const.FRIENDICA_VERSION}}"></script>
<script type="text/javascript" src="view/js/ajaxupload.js?v={{$smarty.const.FRIENDICA_VERSION}}"></script>
<script type="text/javascript" src="view/js/module/media/browser.js?v={{$smarty.const.FRIENDICA_VERSION}}"></script>
<script>
$(function() {
FileBrowser.init("{{$nickname}}", "{{$type}}");
Browser.init("{{$nickname}}", "{{$type}}");
});
</script>
<div class="fbrowser {{$type}}">
@ -33,7 +33,7 @@
{{foreach $files as $f}}
<div class="photo-album-image-wrapper">
<a href="#" class="photo-album-photo-link" data-link="{{$f.0}}" data-filename="{{$f.1}}" data-img="{{$f.2}}" data-alt="{{$f.3}}">
<img src="{{$f.2}}">
<img alt="{{$f.3}}" src="{{$f.1}}">
<p>{{$f.1}}</p>
</a>
</div>

View file

@ -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 {

View file

@ -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.<type>.[<eventname>]
*
* <type> 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

View file

@ -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]);
});
};

View file

@ -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.<type>.[<eventname>]
*
* <type> 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

View file

@ -6,7 +6,7 @@
<div class="contact-entry-photo mframe" id="contact-entry-photo-{{$contact.id}}">
<div class="contact-photo-image-wrapper hidden-xs">
<a href="{{if $contact.photo_menu.edit}}{{$contact.photo_menu.edit.1}}{{else}}{{$contact.url}}{{/if}}">
<a href="{{if !empty($contact.photo_menu.edit)}}{{$contact.photo_menu.edit.1}}{{else}}{{$contact.url}}{{/if}}">
<img class="contact-photo media-object xl" src="{{$contact.thumb}}" {{$contact.sparkle}} alt="{{$contact.name}}" />
</a>
</div>

View file

@ -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);

View file

@ -16,9 +16,9 @@
{{/foreach}}
{{* Switch between image and file mode *}}
<div class="fbswitcher btn-group btn-group-xs pull-right" aria-label="Switch between image and file mode">
<button type="button" class="btn btn-default" data-mode="image" aria-label="Image Mode"><i class="fa fa-picture-o" aria-hidden="true"></i></button>
<button type="button" class="btn btn-default" data-mode="file" aria-label="File Mode"><i class="fa fa-file-o" aria-hidden="true"></i></button>
<div class="fbswitcher btn-group btn-group-xs pull-right" aria-label="Switch between photo and attachment mode">
<button type="button" class="btn btn-default" data-mode="photo" aria-label="Photo Mode"><i class="fa fa-picture-o" aria-hidden="true"></i></button>
<button type="button" class="btn btn-default" data-mode="attachment" aria-label="Attachment Mode"><i class="fa fa-file-o" aria-hidden="true"></i></button>
</div>
</ol>

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;}

View file

@ -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; }