Merge pull request #10233 from annando/api-again

Some fixes for the Mastodon API
This commit is contained in:
Hypolite Petovan 2021-05-12 12:35:24 -04:00 committed by GitHub
commit 78a85df86b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 392 additions and 101 deletions

View file

@ -255,6 +255,14 @@ abstract class DI
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
*/

View file

@ -0,0 +1,80 @@
<?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);
}
}

View file

@ -35,4 +35,31 @@ class Error extends BaseFactory
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());
}
}

View file

@ -86,14 +86,28 @@ class Status extends BaseFactory
$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]]);

View file

@ -2927,7 +2927,7 @@ class Item
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'])) {

View file

@ -288,9 +288,13 @@ class Media
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)) {
@ -363,8 +367,12 @@ class Media
*/
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);

View file

@ -38,7 +38,7 @@ class Accounts extends BaseApi
public static function rawContent(array $parameters = [])
{
if (empty($parameters['id'])) {
DI::mstdnError()->RecordNotFound();
DI::mstdnError()->UnprocessableEntity();
}
$id = $parameters['id'];

View file

@ -41,7 +41,7 @@ class Followers extends BaseApi
$uid = self::getCurrentUserID();
if (empty($parameters['id'])) {
DI::mstdnError()->RecordNotFound();
DI::mstdnError()->UnprocessableEntity();
}
$id = $parameters['id'];

View file

@ -41,7 +41,7 @@ class Following extends BaseApi
$uid = self::getCurrentUserID();
if (empty($parameters['id'])) {
DI::mstdnError()->RecordNotFound();
DI::mstdnError()->UnprocessableEntity();
}
$id = $parameters['id'];

View file

@ -42,7 +42,7 @@ class Lists extends BaseApi
$uid = self::getCurrentUserID();
if (empty($parameters['id'])) {
DI::mstdnError()->RecordNotFound();
DI::mstdnError()->UnprocessableEntity();
}
$id = $parameters['id'];

View file

@ -43,7 +43,7 @@ class Statuses extends BaseApi
public static function rawContent(array $parameters = [])
{
if (empty($parameters['id'])) {
DI::mstdnError()->RecordNotFound();
DI::mstdnError()->UnprocessableEntity();
}
$id = $parameters['id'];
@ -52,7 +52,7 @@ class Statuses extends BaseApi
}
// Show only statuses with media attached? Defaults to false.
$only_media = (bool)!isset($_REQUEST['only_media']) ? false : ($_REQUEST['only_media'] == 'true'); // Currently not supported
$only_media = (bool)!isset($_REQUEST['only_media']) ? false : ($_REQUEST['only_media'] == 'true');
// Return results older than this id
$max_id = (int)!isset($_REQUEST['max_id']) ? 0 : $_REQUEST['max_id'];
// Return results newer than this id

View file

@ -43,12 +43,12 @@ class VerifyCredentials extends BaseApi
$self = User::getOwnerDataById($uid);
if (empty($self)) {
DI::mstdnError()->RecordNotFound();
DI::mstdnError()->InternalError();
}
$cdata = Contact::getPublicAndUserContacID($self['id'], $uid);
if (empty($cdata)) {
DI::mstdnError()->RecordNotFound();
DI::mstdnError()->InternalError();
}
// @todo Support the source property,

View file

@ -37,13 +37,13 @@ class Apps extends BaseApi
*/
public static function post(array $parameters = [])
{
$name = !isset($_REQUEST['client_name']) ? '' : $_REQUEST['client_name'];
$redirect = !isset($_REQUEST['redirect_uris']) ? '' : $_REQUEST['redirect_uris'];
$scopes = !isset($_REQUEST['scopes']) ? '' : $_REQUEST['scopes'];
$website = !isset($_REQUEST['website']) ? '' : $_REQUEST['website'];
$name = $_REQUEST['client_name'] ?? '';
$redirect = $_REQUEST['redirect_uris'] ?? '';
$scopes = $_REQUEST['scopes'] ?? '';
$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));
@ -60,7 +60,7 @@ class Apps extends BaseApi
}
if (!DBA::insert('application', $fields)) {
DI::mstdnError()->RecordNotFound();
DI::mstdnError()->InternalError();
}
System::jsonExit(DI::mstdnApplication()->createFromApplicationId(DBA::lastInsertId()));

View file

@ -41,7 +41,7 @@ class Blocks extends BaseApi
$uid = self::getCurrentUserID();
if (empty($parameters['id'])) {
DI::mstdnError()->RecordNotFound();
DI::mstdnError()->UnprocessableEntity();
}
$id = $parameters['id'];

View file

@ -44,7 +44,7 @@ class Directory extends BaseApi
{
$offset = (int)!isset($_REQUEST['offset']) ? 0 : $_REQUEST['offset'];
$limit = (int)!isset($_REQUEST['limit']) ? 40 : $_REQUEST['limit'];
$order = !isset($_REQUEST['order']) ? 'active' : $_REQUEST['order'];
$order = $_REQUEST['order'] ?? 'active';
$local = (bool)!isset($_REQUEST['local']) ? false : ($_REQUEST['local'] == 'true');
Logger::info('directory', ['offset' => $offset, 'limit' => $limit, 'order' => $order, 'local' => $local]);

View file

@ -53,7 +53,7 @@ class Accounts extends BaseApi
$uid = self::getCurrentUserID();
if (empty($parameters['id'])) {
DI::mstdnError()->RecordNotFound();
DI::mstdnError()->UnprocessableEntity();
}
$id = $parameters['id'];
@ -65,12 +65,18 @@ class Accounts extends BaseApi
$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)) {
@ -87,6 +93,8 @@ class Accounts extends BaseApi
$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);

View file

@ -46,7 +46,7 @@ class Media extends BaseApi
$uid = self::getCurrentUserID();
if (empty($parameters['id'])) {
DI::mstdnError()->RecordNotFound();
DI::mstdnError()->UnprocessableEntity();
}
$id = $parameters['id'];

View file

@ -41,7 +41,7 @@ class Mutes extends BaseApi
$uid = self::getCurrentUserID();
if (empty($parameters['id'])) {
DI::mstdnError()->RecordNotFound();
DI::mstdnError()->UnprocessableEntity();
}
$id = $parameters['id'];

View file

@ -63,7 +63,7 @@ class Notifications extends BaseApi
$limit = (int)!isset($_REQUEST['limit']) ? 20 : $_REQUEST['limit'];
// Array of types to exclude (follow, favourite, reblog, mention, poll, follow_request)
$exclude_types = !isset($_REQUEST['exclude_types']) ? [] : $_REQUEST['exclude_types'];
$exclude_types = $_REQUEST['exclude_types'] ?? [];
// Return only notifications received from this account
$account_id = (int)!isset($_REQUEST['account_id']) ? 0 : $_REQUEST['account_id'];

View file

@ -42,7 +42,7 @@ class Statuses extends BaseApi
public static function rawContent(array $parameters = [])
{
if (empty($parameters['id'])) {
DI::mstdnError()->RecordNotFound();
DI::mstdnError()->UnprocessableEntity();
}
System::jsonExit(DI::mstdnStatus()->createFromUriId($parameters['id'], self::getCurrentUserID()));

View file

@ -41,7 +41,7 @@ class Context extends BaseApi
$uid = self::getCurrentUserID();
if (empty($parameters['id'])) {
DI::mstdnError()->RecordNotFound();
DI::mstdnError()->UnprocessableEntity();
}
$id = $parameters['id'];
@ -54,7 +54,8 @@ class Context extends BaseApi
$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;
@ -67,12 +68,24 @@ class Context extends BaseApi
$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);

View file

@ -41,7 +41,7 @@ class FavouritedBy extends BaseApi
$uid = self::getCurrentUserID();
if (empty($parameters['id'])) {
DI::mstdnError()->RecordNotFound();
DI::mstdnError()->UnprocessableEntity();
}
$id = $parameters['id'];

View file

@ -41,7 +41,7 @@ class RebloggedBy extends BaseApi
$uid = self::getCurrentUserID();
if (empty($parameters['id'])) {
DI::mstdnError()->RecordNotFound();
DI::mstdnError()->UnprocessableEntity();
}
$id = $parameters['id'];

View file

@ -43,7 +43,7 @@ class ListTimeline extends BaseApi
$uid = self::getCurrentUserID();
if (empty($parameters['id'])) {
DI::mstdnError()->RecordNotFound();
DI::mstdnError()->UnprocessableEntity();
}
// Return results older than id

View file

@ -46,7 +46,7 @@ class PublicTimeline extends BaseApi
// Show only remote statuses? Defaults to false.
$remote = (bool)!isset($_REQUEST['remote']) ? false : ($_REQUEST['remote'] == 'true');
// Show only statuses with media attached? Defaults to false.
$only_media = (bool)!isset($_REQUEST['only_media']) ? false : ($_REQUEST['only_media'] == 'true'); // Currently not supported
$only_media = (bool)!isset($_REQUEST['only_media']) ? false : ($_REQUEST['only_media'] == 'true');
// Return results older than this id
$max_id = (int)!isset($_REQUEST['max_id']) ? 0 : $_REQUEST['max_id'];
// Return results newer than this id

View file

@ -44,13 +44,13 @@ class Tag extends BaseApi
$uid = self::getCurrentUserID();
if (empty($parameters['hashtag'])) {
DI::mstdnError()->RecordNotFound();
DI::mstdnError()->UnprocessableEntity();
}
// If true, return only local statuses. Defaults to false.
$local = (bool)!isset($_REQUEST['local']) ? false : ($_REQUEST['local'] == 'true');
// If true, return only statuses with media attachments. Defaults to false.
$only_media = (bool)!isset($_REQUEST['only_media']) ? false : ($_REQUEST['only_media'] == 'true'); // Currently not supported
$only_media = (bool)!isset($_REQUEST['only_media']) ? false : ($_REQUEST['only_media'] == 'true');
// Return results older than this ID.
$max_id = (int)!isset($_REQUEST['max_id']) ? 0 : $_REQUEST['max_id'];
// Return results newer than this ID.

View file

@ -110,12 +110,18 @@ class BaseApi extends BaseModule
}
}
/**
* Quit execution with the message that the endpoint isn't implemented
*
* @param string $method
* @return void
*/
public static function unsupported(string $method = 'all')
{
$path = DI::args()->getQueryString();
Logger::info('Unimplemented API call', ['method' => $method, 'path' => $path, 'agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'request' => $_REQUEST ?? []]);
$error = DI::l10n()->t('API endpoint %s %s is not implemented', strtoupper($method), $path);
$error_description = DI::l10n()->t('The API endpoint is currently not implemented but might be in the future.');;
$error_description = DI::l10n()->t('The API endpoint is currently not implemented but might be in the future.');
$errorobj = new \Friendica\Object\Api\Mastodon\Error($error, $error_description);
System::jsonError(501, $errorobj->toArray());
}
@ -138,15 +144,14 @@ class BaseApi extends BaseModule
*/
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)) {
// The execution stops here if no one is logged in
api_login(DI::app());
}
self::$current_user_id = api_user();
@ -160,15 +165,12 @@ class BaseApi extends BaseModule
*/
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)) {
// Fetch the user id if logged in - but don't fail if not
api_login(DI::app(), false);
self::$current_user_id = api_user();
@ -177,14 +179,20 @@ class BaseApi extends BaseModule
return (int)self::$current_user_id;
}
private static function checkBearer(string $authorization)
/**
* Get the user id via the Bearer token
*
* @return int User-ID
*/
private static function getUserByBearer()
{
return (substr($authorization, 0, 7) == 'Bearer ');
}
$authorization = $_SERVER['HTTP_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)) {
@ -195,17 +203,30 @@ class BaseApi extends BaseModule
return $token['uid'];
}
/**
* Get the application record via the proved request header fields
*
* @return array application record
*/
public static function getApplication()
{
$redirect_uri = !isset($_REQUEST['redirect_uri']) ? '' : $_REQUEST['redirect_uri'];
$client_id = !isset($_REQUEST['client_id']) ? '' : $_REQUEST['client_id'];
$redirect_uri = $_REQUEST['redirect_uri'] ?? '';
$client_id = $_REQUEST['client_id'] ?? '';
$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);
@ -214,7 +235,38 @@ class BaseApi extends BaseModule
return $application;
}
/**
* Check if an token for the application and user exists
*
* @param array $application
* @param integer $uid
* @return boolean
*/
public static function existsTokenForUser(array $application, int $uid)
{
return DBA::exists('application-token', ['application-id' => $application['id'], 'uid' => $uid]);
}
/**
* Fetch the token for the given application and user
*
* @param array $application
* @param integer $uid
* @return array application record
*/
public static function getTokenForUser(array $application, int $uid)
{
return DBA::selectFirst('application-token', [], ['application-id' => $application['id'], 'uid' => $uid]);
}
/**
* Create and fetch an token for the application and user
*
* @param array $application
* @param integer $uid
* @return array application record
*/
public static function createTokenForUser(array $application, int $uid)
{
$code = bin2hex(random_bytes(32));
$access_token = bin2hex(random_bytes(32));
@ -226,6 +278,7 @@ class BaseApi extends BaseModule
return DBA::selectFirst('application-token', [], ['application-id' => $application['id'], 'uid' => $uid]);
}
/**
* Get user info array.
*

View file

@ -0,0 +1,53 @@
<?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\Renderer;
use Friendica\DI;
use Friendica\Module\BaseApi;
/**
* Acknowledgement of OAuth requests
*/
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'] ?? '');
$o = Renderer::replaceMacros(Renderer::getMarkupTemplate('oauth_authorize.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;
}
}

View file

@ -26,7 +26,7 @@ use Friendica\DI;
use Friendica\Module\BaseApi;
/**
* Dummy class for all currently unimplemented endpoints
* @see https://docs.joinmastodon.org/spec/oauth/
*/
class Authorize extends BaseApi
{
@ -36,30 +36,39 @@ class Authorize extends BaseApi
*/
public static function rawContent(array $parameters = [])
{
//return;
$response_type = !isset($_REQUEST['response_type']) ? '' : $_REQUEST['response_type'];
$response_type = $_REQUEST['response_type'] ?? '';
if ($response_type != 'code') {
Logger::warning('Wrong or missing response type', ['response_type' => $response_type]);
DI::mstdnError()->RecordNotFound();
DI::mstdnError()->UnprocessableEntity();
}
$application = self::getApplication();
if (empty($application)) {
DI::mstdnError()->RecordNotFound();
DI::mstdnError()->UnprocessableEntity();
}
$request = $_REQUEST;
unset($request['pagename']);
$redirect = '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=' . urlencode($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?' . http_build_query(['return_path' => $redirect, 'application' => $application['name']]));
}
DI::session()->remove('oauth_acknowledge');
$token = self::createTokenForUser($application, $uid);
if (!$token) {
DI::mstdnError()->RecordNotFound();
DI::mstdnError()->UnprocessableEntity();
}
DI::app()->redirect($application['redirect_uri'] . '?code=' . $token['code']);

View file

@ -24,7 +24,7 @@ namespace Friendica\Module\OAuth;
use Friendica\Module\BaseApi;
/**
* Dummy class for all currently unimplemented endpoints
* @see https://docs.joinmastodon.org/spec/oauth/
*/
class Revoke extends BaseApi
{

View file

@ -28,37 +28,37 @@ use Friendica\DI;
use Friendica\Module\BaseApi;
/**
* Dummy class for all currently unimplemented endpoints
* @see https://docs.joinmastodon.org/spec/oauth/
*/
class Token extends BaseApi
{
public static function post(array $parameters = [])
{
$client_secret = !isset($_REQUEST['client_secret']) ? '' : $_REQUEST['client_secret'];
$code = !isset($_REQUEST['code']) ? '' : $_REQUEST['code'];
$grant_type = !isset($_REQUEST['grant_type']) ? '' : $_REQUEST['grant_type'];
$client_secret = $_REQUEST['client_secret'] ?? '';
$code = $_REQUEST['code'] ?? '';
$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

View file

@ -36,7 +36,7 @@ class Login extends BaseModule
{
public static function content(array $parameters = [])
{
$return_path = !isset($_REQUEST['return_path']) ? '' : $_REQUEST['return_path'];
$return_path = $_REQUEST['return_path'] ?? '' ;
if (local_user()) {
DI::baseUrl()->redirect($return_path);

View file

@ -39,9 +39,17 @@ class Card extends BaseDataTransferObject
/** @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;
@ -57,9 +65,13 @@ class Card extends BaseDataTransferObject
$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'] ?? '';
}
/**

View file

@ -134,7 +134,7 @@ class Status extends BaseDataTransferObject
$this->mentions = $mentions;
$this->tags = $tags;
$this->emojis = [];
//$this->card = $card;
$this->card = $card->toArray() ?: null;
$this->poll = null;
}

View file

@ -331,9 +331,14 @@ return [
'/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' => [

View file

@ -1,11 +1,11 @@
<h1>{{$title}}</h1>
<h2>{{$title}}</h2>
<div class='oauthapp'>
<img src='{{$app.icon}}'>
<h4>{{$app.name}}</h4>
<div class="oauthapp">
{{if $app.icon}}<img src="{{$app.icon}}" alt="">{{/if}}
<h3>{{$app.name}}</h3>
</div>
<h3>{{$authorize}}</h3>
<p>{{$authorize}}</p>
<form method="POST">
<div class="settings-submit-wrapper"><input class="settings-submit" type="submit" name="oauth_yes" value="{{$yes}}" /></div>
<div class="settings-submit-wrapper"><input class="settings-submit" type="submit" name="oauth_yes" value="{{$yes}}" /></div>
</form>

View file

@ -3410,6 +3410,7 @@ section .profile-match-wrapper {
.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 {