return self::$dice->create(Factory\Api\Mastodon\Attachment::class);
}
+ /**
+ * @return Factory\Api\Mastodon\Card
+ */
+ public static function mstdnCard()
+ {
+ return self::$dice->create(Factory\Api\Mastodon\Card::class);
+ }
+
/**
* @return Factory\Api\Mastodon\Emoji
*/
--- /dev/null
+<?php
+/**
+ * @copyright Copyright (C) 2010-2021, the Friendica project
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace Friendica\Factory\Api\Mastodon;
+
+use Friendica\BaseFactory;
+use Friendica\Content\Text\BBCode;
+use Friendica\Model\Post;
+use Friendica\Network\HTTPException;
+use Friendica\Util\Strings;
+
+class Card extends BaseFactory
+{
+ /**
+ * @param int $uriId Uri-ID of the item
+ * @return \Friendica\Object\Api\Mastodon\Card
+ * @throws HTTPException\InternalServerErrorException
+ * @throws \ImagickException
+ */
+ public function createFromUriId(int $uriId)
+ {
+ $item = Post::selectFirst(['nody'], ['uri-id' => $uriId]);
+ if (!empty($item['body'])) {
+ $data = BBCode::getAttachmentData($item['body']);
+ } else {
+ $data = [];
+ }
+
+ foreach (Post\Media::getByURIId($uriId, [Post\Media::HTML]) as $attached) {
+ if ((empty($data['url']) || Strings::compareLink($data['url'], $attached['url'])) &&
+ (!empty($attached['description']) || !empty($attached['image']) || !empty($attached['preview']))) {
+ $parts = parse_url($attached['url']);
+ if (!empty($parts['scheme']) && !empty($parts['host'])) {
+ if (empty($attached['publisher-name'])) {
+ $attached['publisher-name'] = $parts['host'];
+ }
+ if (empty($attached['publisher-url']) || empty(parse_url($attached['publisher-url'], PHP_URL_SCHEME))) {
+ $attached['publisher-url'] = $parts['scheme'] . '://' . $parts['host'];
+
+ if (!empty($parts['port'])) {
+ $attached['publisher-url'] .= ':' . $parts['port'];
+ }
+ }
+ }
+
+ $data['url'] = $attached['url'];
+ $data['title'] = $attached['name'];
+ $data['description'] = $attached['description'];
+ $data['type'] = 'link';
+ $data['author_name'] = $attached['author-name'];
+ $data['author_url'] = $attached['author-url'];
+ $data['provider_name'] = $attached['publisher-name'];
+ $data['provider_url'] = $attached['publisher-url'];
+ $data['image'] = $attached['preview'];
+ $data['width'] = $attached['preview-width'];
+ $data['height'] = $attached['preview-height'];
+ }
+ }
+
+ return new \Friendica\Object\Api\Mastodon\Card($data);
+ }
+}
System::jsonError(404, $errorobj->toArray());
}
+
+ public function UnprocessableEntity(string $error = '')
+ {
+ $error = $error ?: DI::l10n()->t('Unprocessable Entity');
+ $error_description = '';
+ $errorobj = New \Friendica\Object\Api\Mastodon\Error($error, $error_description);
+
+ System::jsonError(422, $errorobj->toArray());
+ }
+
+ public function Unauthorized(string $error = '')
+ {
+ $error = $error ?: DI::l10n()->t('Unauthorized');
+ $error_description = '';
+ $errorobj = New \Friendica\Object\Api\Mastodon\Error($error, $error_description);
+
+ System::jsonError(401, $errorobj->toArray());
+ }
+
+ public function InternalError(string $error = '')
+ {
+ $error = $error ?: DI::l10n()->t('Internal Server Error');
+ $error_description = '';
+ $errorobj = New \Friendica\Object\Api\Mastodon\Error($error, $error_description);
+
+ System::jsonError(500, $errorobj->toArray());
+ }
}
$sensitive = DBA::exists('tag-view', ['uri-id' => $uriId, 'name' => 'nsfw']);
$application = new \Friendica\Object\Api\Mastodon\Application($item['app'] ?: ContactSelector::networkToName($item['network'], $item['author-link']));
- $mentions = DI::mstdnMention()->createFromUriId($uriId);
- $tags = DI::mstdnTag()->createFromUriId($uriId);
-
- $data = BBCode::getAttachmentData($item['body']);
- $card = new \Friendica\Object\Api\Mastodon\Card($data);
+ $mentions = DI::mstdnMention()->createFromUriId($uriId);
+ $tags = DI::mstdnTag()->createFromUriId($uriId);
+ $card = DI::mstdnCard()->createFromUriId($uriId);
$attachments = DI::mstdnAttachment()->createFromUriId($uriId);
+ $shared = BBCode::fetchShareAttributes($item['body']);
+ if (!empty($shared['guid'])) {
+ $shared_item = Post::selectFirst(['uri-id', 'plink'], ['guid' => $shared['guid']]);
+
+ $shared_uri_id = $shared_item['uri-id'] ?? 0;
+
+ $mentions = array_merge($mentions, DI::mstdnMention()->createFromUriId($shared_uri_id));
+ $tags = array_merge($tags, DI::mstdnTag()->createFromUriId($shared_uri_id));
+ $attachments = array_merge($attachments, DI::mstdnAttachment()->createFromUriId($shared_uri_id));
+
+ if (empty($card->toArray())) {
+ $card = DI::mstdnCard()->createFromUriId($shared_uri_id);
+ }
+ }
+
+
if ($item['vid'] == Verb::getID(Activity::ANNOUNCE)) {
$reshare = $this->createFromUriId($item['thr-parent-id'], $uid)->toArray();
$reshared_item = Post::selectFirst(['title', 'body'], ['uri-id' => $item['thr-parent-id'], 'uid' => [0, $uid]]);
DI::profiler()->saveTimestamp($stamp1, 'rendering');
if (isset($data['url']) && !in_array($data['url'], $ignore_links)) {
- if (!empty($data['description']) || !empty($data['image'] || !empty($data['preview']))) {
+ if (!empty($data['description']) || !empty($data['image']) || !empty($data['preview'])) {
$parts = parse_url($data['url']);
if (!empty($parts['scheme']) && !empty($parts['host'])) {
if (empty($data['provider_name'])) {
public static function insertFromBody(int $uriid, string $body)
{
// Simplify image codes
- $body = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $body);
+ $unshared_body = $body = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $body);
- $unshared_body = preg_replace("/\s*\[share .*?\].*?\[\/share\]\s*/ism", '', $body);
+ // Only remove the shared data from "real" reshares
+ $shared = BBCode::fetchShareAttributes($body);
+ if (!empty($shared['guid'])) {
+ $unshared_body = preg_replace("/\s*\[share .*?\].*?\[\/share\]\s*/ism", '', $body);
+ }
$attachments = [];
if (preg_match_all("#\[url=([^\]]+?)\]\s*\[img=([^\[\]]*)\]([^\[\]]*)\[\/img\]\s*\[/url\]#ism", $body, $pictures, PREG_SET_ORDER)) {
*/
public static function insertFromRelevantUrl(int $uriid, string $body)
{
- // Don't look at the shared content
- $body = preg_replace("/\s*\[share .*?\].*?\[\/share\]\s*/ism", '', $body);
+ // Only remove the shared data from "real" reshares
+ $shared = BBCode::fetchShareAttributes($body);
+ if (!empty($shared['guid'])) {
+ // Don't look at the shared content
+ $body = preg_replace("/\s*\[share .*?\].*?\[\/share\]\s*/ism", '', $body);
+ }
// Remove all hashtags and mentions
$body = preg_replace("/([#@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", '', $body);
$website = !isset($_REQUEST['website']) ? '' : $_REQUEST['website'];
if (empty($name) || empty($redirect)) {
- DI::mstdnError()->RecordNotFound();
+ DI::mstdnError()->UnprocessableEntity(DI::l10n()->t('Missing parameters'));
}
$client_id = bin2hex(random_bytes(32));
}
if (!DBA::insert('application', $fields)) {
- DI::mstdnError()->RecordNotFound();
+ DI::mstdnError()->InternalError();
}
System::jsonExit(DI::mstdnApplication()->createFromApplicationId(DBA::lastInsertId()));
$max_id = (int)!isset($_REQUEST['max_id']) ? 0 : $_REQUEST['max_id'];
// Return results newer than this id
$since_id = (int)!isset($_REQUEST['since_id']) ? 0 : $_REQUEST['since_id'];
- // Maximum number of results to return. Defaults to 20.
+ // Maximum number of results. Defaults to 40. Max 40.
+ // Set to 0 in order to get all accounts without pagination.
$limit = (int)!isset($_REQUEST['limit']) ? 40 : $_REQUEST['limit'];
- $params = ['order' => ['contact-id' => true], 'limit' => $limit];
+ $params = ['order' => ['contact-id' => true]];
+ if ($limit != 0) {
+ $params['limit'] = $limit;
+
+ }
+
$condition = ['gid' => $id];
if (!empty($max_id)) {
$params['order'] = ['contact-id'];
}
+ $accounts = [];
+
$members = DBA::select('group_member', ['contact-id'], $condition, $params);
while ($member = DBA::fetch($members)) {
$accounts[] = DI::mstdnAccount()->createFromContactId($member['contact-id'], $uid);
$parents = [];
$children = [];
- $posts = Post::select(['uri-id', 'thr-parent-id'], ['parent-uri-id' => $parent['parent-uri-id']], [], false);
+ $posts = Post::select(['uri-id', 'thr-parent-id'],
+ ['parent-uri-id' => $parent['parent-uri-id'], 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT]], [], false);
while ($post = Post::fetch($posts)) {
if ($post['uri-id'] == $post['thr-parent-id']) {
continue;
$statuses = ['ancestors' => [], 'descendants' => []];
+ $ancestors = [];
foreach (self::getParents($id, $parents) as $ancestor) {
- $statuses['ancestors'][] = DI::mstdnStatus()->createFromUriId($ancestor, $uid);
+ $ancestors[$ancestor] = DI::mstdnStatus()->createFromUriId($ancestor, $uid);
}
+ ksort($ancestors);
+ foreach ($ancestors as $ancestor) {
+ $statuses['ancestors'][] = $ancestor;
+ }
+
+ $descendants = [];
foreach (self::getChildren($id, $children) as $descendant) {
- $statuses['descendants'][] = DI::mstdnStatus()->createFromUriId($descendant, $uid);
+ $descendants[] = DI::mstdnStatus()->createFromUriId($descendant, $uid);
+ }
+
+ ksort($descendants);
+ foreach ($descendants as $descendant) {
+ $statuses['descendants'][] = $descendant;
}
System::jsonExit($statuses);
*/
protected static function login()
{
- $authorization = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
- $authorization = $_SERVER['AUTHORIZATION'] ?? $authorization;
-
- if (self::checkBearer($authorization)) {
- self::$current_user_id = self::getUserByBearer($authorization);
- return (bool)self::$current_user_id;
+ if (empty(self::$current_user_id)) {
+ self::$current_user_id = self::getUserByBearer();
}
- api_login(DI::app());
+ if (empty(self::$current_user_id)) {
+ api_login(DI::app());
+ }
self::$current_user_id = api_user();
*/
protected static function getCurrentUserID()
{
- $authorization = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
- $authorization = $_SERVER['AUTHORIZATION'] ?? $authorization;
-
- if (self::checkBearer($authorization)) {
- self::$current_user_id = self::getUserByBearer($authorization);
- return (int)self::$current_user_id;
+ if (empty(self::$current_user_id)) {
+ self::$current_user_id = self::getUserByBearer();
}
- if (is_null(self::$current_user_id)) {
+ if (empty(self::$current_user_id)) {
api_login(DI::app(), false);
self::$current_user_id = api_user();
return (int)self::$current_user_id;
}
- private static function checkBearer(string $authorization)
+ private static function getUserByBearer()
{
- return (substr($authorization, 0, 7) == 'Bearer ');
- }
+ $authorization = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
+ $authorization = $_SERVER['AUTHORIZATION'] ?? $authorization;
- private static function getUserByBearer(string $authorization)
- {
- $bearer = trim(substr($authorization, 6));
+ if (substr($authorization, 0, 7) != 'Bearer ') {
+ return 0;
+ }
+
+ $bearer = trim(substr($authorization, 7));
$condition = ['access_token' => $bearer];
$token = DBA::selectFirst('application-token', ['uid'], $condition);
if (!DBA::isResult($token)) {
public static function getApplication()
{
- $redirect_uri = !isset($_REQUEST['redirect_uri']) ? '' : $_REQUEST['redirect_uri'];
- $client_id = !isset($_REQUEST['client_id']) ? '' : $_REQUEST['client_id'];
+ $redirect_uri = !isset($_REQUEST['redirect_uri']) ? '' : $_REQUEST['redirect_uri'];
+ $client_id = !isset($_REQUEST['client_id']) ? '' : $_REQUEST['client_id'];
+ $client_secret = !isset($_REQUEST['client_secret']) ? '' : $_REQUEST['client_secret'];
- if (empty($redirect_uri) || empty($client_id)) {
- Logger::warning('Incomplete request');
+ if ((empty($redirect_uri) && empty($client_secret)) || empty($client_id)) {
+ Logger::warning('Incomplete request', ['request' => $_REQUEST]);
return [];
}
- $condition = ['redirect_uri' => $redirect_uri, 'client_id' => $client_id];
+ $condition = ['client_id' => $client_id];
+ if (!empty($client_secret)) {
+ $condition['client_secret'] = $client_secret;
+ }
+ if (!empty($redirect_uri)) {
+ $condition['redirect_uri'] = $redirect_uri;
+ }
+
$application = DBA::selectFirst('application', [], $condition);
if (!DBA::isResult($application)) {
Logger::warning('Application not found', $condition);
return $application;
}
+ public static function existsTokenForUser(array $application, int $uid)
+ {
+ return DBA::exists('application-token', ['application-id' => $application['id'], 'uid' => $uid]);
+ }
+
public static function getTokenForUser(array $application, int $uid)
+ {
+ return DBA::selectFirst('application-token', [], ['application-id' => $application['id'], 'uid' => $uid]);
+ }
+
+ public static function createTokenForUser(array $application, int $uid)
{
$code = bin2hex(random_bytes(32));
$access_token = bin2hex(random_bytes(32));
return DBA::selectFirst('application-token', [], ['application-id' => $application['id'], 'uid' => $uid]);
}
+
/**
* Get user info array.
*
--- /dev/null
+<?php
+/**
+ * @copyright Copyright (C) 2010-2021, the Friendica project
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace Friendica\Module\OAuth;
+
+use Friendica\Core\Logger;
+use Friendica\Core\Renderer;
+use Friendica\DI;
+use Friendica\Module\BaseApi;
+
+/**
+ * Dummy class for all currently unimplemented endpoints
+ */
+class Acknowledge extends BaseApi
+{
+ public static function post(array $parameters = [])
+ {
+ DI::session()->set('oauth_acknowledge', true);
+ DI::app()->redirect(DI::session()->get('return_path'));
+ }
+
+ public static function content(array $parameters = [])
+ {
+ DI::session()->set('return_path', $_REQUEST['return_path'] ?? '');
+
+ $tpl = Renderer::getMarkupTemplate('oauth_authorize.tpl');
+ $o = Renderer::replaceMacros($tpl, [
+ '$title' => DI::l10n()->t('Authorize application connection'),
+ '$app' => ['name' => $_REQUEST['application'] ?? ''],
+ '$authorize' => DI::l10n()->t('Do you want to authorize this application to access your posts and contacts, and/or create new posts for you?'),
+ '$yes' => DI::l10n()->t('Yes'),
+ '$no' => DI::l10n()->t('No'),
+ ]);
+
+ return $o;
+ }
+}
*/
public static function rawContent(array $parameters = [])
{
- //return;
-
$response_type = !isset($_REQUEST['response_type']) ? '' : $_REQUEST['response_type'];
if ($response_type != 'code') {
Logger::warning('Wrong or missing response type', ['response_type' => $response_type]);
DI::mstdnError()->RecordNotFound();
}
+ $request = $_REQUEST;
+ unset($request['pagename']);
+ $redirect = urlencode('oauth/authorize?' . http_build_query($request));
+
$uid = local_user();
if (empty($uid)) {
Logger::info('Redirect to login');
- DI::app()->redirect('login?return_path=/oauth/authorize');
+ DI::app()->redirect('login?return_path=' . $redirect);
} else {
Logger::info('Already logged in user', ['uid' => $uid]);
}
- $token = self::getTokenForUser($application, $uid);
+ if (!self::existsTokenForUser($application, $uid) && !DI::session()->get('oauth_acknowledge')) {
+ Logger::info('Redirect to acknowledge');
+ DI::app()->redirect('oauth/acknowledge?return_path=' . $redirect);
+ }
+
+ DI::session()->remove('oauth_acknowledge');
+
+ $token = self::createTokenForUser($application, $uid);
if (!$token) {
DI::mstdnError()->RecordNotFound();
}
$grant_type = !isset($_REQUEST['grant_type']) ? '' : $_REQUEST['grant_type'];
if ($grant_type != 'authorization_code') {
- Logger::warning('Wrong or missing grant type', ['grant_type' => $grant_type]);
- DI::mstdnError()->RecordNotFound();
+ Logger::warning('Unsupported or missing grant type', ['request' => $_REQUEST]);
+ DI::mstdnError()->UnprocessableEntity(DI::l10n()->t('Unsupported or missing grant type'));
}
$application = self::getApplication();
if (empty($application)) {
- DI::mstdnError()->RecordNotFound();
+ DI::mstdnError()->UnprocessableEntity();
}
if ($application['client_secret'] != $client_secret) {
Logger::warning('Wrong client secret', $client_secret);
- DI::mstdnError()->RecordNotFound();
+ DI::mstdnError()->Unauthorized();
}
$condition = ['application-id' => $application['id'], 'code' => $code];
- $token = DBA::selectFirst('application-token', ['access_token'], $condition);
+ $token = DBA::selectFirst('application-token', ['access_token', 'created_at'], $condition);
if (!DBA::isResult($token)) {
Logger::warning('Token not found', $condition);
- DI::mstdnError()->RecordNotFound();
+ DI::mstdnError()->Unauthorized();
}
// @todo Use entity class
/** @var string */
protected $type;
/** @var string */
+ protected $author_name;
+ /** @var string */
+ protected $author_url;
+ /** @var string */
protected $provider_name;
/** @var string */
protected $provider_url;
+ /** @var int */
+ protected $width;
+ /** @var int */
+ protected $height;
/** @var string */
protected $image;
$this->title = $attachment['title'] ?? '';
$this->description = $attachment['description'] ?? '';
$this->type = $attachment['type'] ?? '';
- $this->image = $attachment['image'] ?? '';
+ $this->author_name = $attachment['author_name'] ?? '';
+ $this->author_url = $attachment['author_url'] ?? '';
$this->provider_name = $attachment['provider_name'] ?? '';
$this->provider_url = $attachment['provider_url'] ?? '';
+ $this->width = $attachment['width'] ?? 0;
+ $this->height = $attachment['height'] ?? 0;
+ $this->image = $attachment['image'] ?? '';
}
/**
$this->mentions = $mentions;
$this->tags = $tags;
$this->emojis = [];
- //$this->card = $card;
+ $this->card = $card->toArray() ?: null;
$this->poll = null;
}
'/mark/all' => [Module\Notifications\Notification::class, [R::GET]],
'/{id:\d+}' => [Module\Notifications\Notification::class, [R::GET, R::POST]],
],
- '/oauth/authorize' => [Module\OAuth\Authorize::class, [R::GET]],
- '/oauth/revoke' => [Module\OAuth\Revoke::class, [R::POST]],
- '/oauth/token' => [Module\OAuth\Token::class, [R::POST]],
+
+ '/oauth' => [
+ '/acknowledge' => [Module\OAuth\Acknowledge::class, [R::GET, R::POST]],
+ '/authorize' => [Module\OAuth\Authorize::class, [R::GET]],
+ '/revoke' => [Module\OAuth\Revoke::class, [R::POST]],
+ '/token' => [Module\OAuth\Token::class, [R::POST]],
+ ],
+
'/objects/{guid}[/{activity}]' => [Module\Objects::class, [R::GET]],
'/oembed' => [
.mod-home.is-not-singleuser nav.navbar,
.mod-login nav.navbar {
background-color: transparent;
+ position: inherit;
}
.mod-home.is-not-singleuser #topbar-second,
.mod-login #topbar-second {