From 97356ed617dbfe9abf668deec10ae7fecd4078f2 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 28 May 2021 06:10:32 +0000 Subject: [PATCH 1/4] API: Unified request parameter handling --- src/Module/Api/Mastodon/Lists.php | 9 +++-- src/Module/Api/Mastodon/Media.php | 9 ++++- src/Module/Api/Mastodon/Statuses.php | 49 ++++++++++++----------- src/Module/BaseApi.php | 60 ++++++---------------------- 4 files changed, 50 insertions(+), 77 deletions(-) diff --git a/src/Module/Api/Mastodon/Lists.php b/src/Module/Api/Mastodon/Lists.php index 5681ebf8a9..38a9c16640 100644 --- a/src/Module/Api/Mastodon/Lists.php +++ b/src/Module/Api/Mastodon/Lists.php @@ -78,13 +78,16 @@ class Lists extends BaseApi public static function put(array $parameters = []) { - $data = self::getPutData(); + $request = self::getRequest([ + 'title' => '', // The title of the list to be updated. + 'replies_policy' => '', // Enumerable oneOf followed list none. + ]); - if (empty($data['title']) || empty($parameters['id'])) { + if (empty($request['title']) || empty($parameters['id'])) { DI::mstdnError()->UnprocessableEntity(); } - Group::update($parameters['id'], $data['title']); + Group::update($parameters['id'], $request['title']); } /** diff --git a/src/Module/Api/Mastodon/Media.php b/src/Module/Api/Mastodon/Media.php index daabf53f31..f232cd3d5e 100644 --- a/src/Module/Api/Mastodon/Media.php +++ b/src/Module/Api/Mastodon/Media.php @@ -58,7 +58,12 @@ class Media extends BaseApi self::login(self::SCOPE_WRITE); $uid = self::getCurrentUserID(); - $data = self::getPutData(); + $request = self::getRequest([ + 'file' => [], // The file to be attached, using multipart form data. + 'thumbnail' => [], // The custom thumbnail of the media to be attached, using multipart form data. + 'description' => '', // A plain-text description of the media, for accessibility purposes. + 'focus' => '', // Two floating points (x,y), comma-delimited ranging from -1.0 to 1.0 + ]); if (empty($parameters['id'])) { DI::mstdnError()->UnprocessableEntity(); @@ -69,7 +74,7 @@ class Media extends BaseApi DI::mstdnError()->RecordNotFound(); } - Photo::update(['desc' => $data['description'] ?? ''], ['resource-id' => $photo['resource-id']]); + Photo::update(['desc' => $request['description']], ['resource-id' => $photo['resource-id']]); System::jsonExit(DI::mstdnAttachment()->createFromPhoto($parameters['id'])); } diff --git a/src/Module/Api/Mastodon/Statuses.php b/src/Module/Api/Mastodon/Statuses.php index 9bbc3bf636..0699c41087 100644 --- a/src/Module/Api/Mastodon/Statuses.php +++ b/src/Module/Api/Mastodon/Statuses.php @@ -46,21 +46,22 @@ class Statuses extends BaseApi self::login(self::SCOPE_WRITE); $uid = self::getCurrentUserID(); - $data = self::getJsonPostData(); - - $status = $data['status'] ?? ''; - $media_ids = $data['media_ids'] ?? []; - $in_reply_to_id = $data['in_reply_to_id'] ?? 0; - $sensitive = $data['sensitive'] ?? false; // @todo Possibly trigger "nsfw" flag? - $spoiler_text = $data['spoiler_text'] ?? ''; - $visibility = $data['visibility'] ?? ''; - $scheduled_at = $data['scheduled_at'] ?? ''; // Currently unsupported, but maybe in the future - $language = $data['language'] ?? ''; + $request = self::getRequest([ + 'status' => '', // Text content of the status. If media_ids is provided, this becomes optional. Attaching a poll is optional while status is provided. + 'media_ids' => [], // Array of Attachment ids to be attached as media. If provided, status becomes optional, and poll cannot be used. + 'poll' => [], // Poll data. If provided, media_ids cannot be used, and poll[expires_in] must be provided. + 'in_reply_to_id' => 0, // ID of the status being replied to, if status is a reply + 'sensitive' => false, // Mark status and attached media as sensitive? + 'spoiler_text' => '', // Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field. + 'visibility' => '', // Visibility of the posted status. Enumerable oneOf public, unlisted, private, direct. + 'scheduled_at' => '', // ISO 8601 Datetime at which to schedule a status. Providing this paramter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future. + 'language' => '', // ISO 639 language code for this status. + ]); $owner = User::getOwnerDataById($uid); // The imput is defined as text. So we can use Markdown for some enhancements - $body = Markdown::toBBCode($status); + $body = Markdown::toBBCode($request['status']); $body = BBCode::expandTags($body); @@ -69,7 +70,7 @@ class Statuses extends BaseApi $item['verb'] = Activity::POST; $item['contact-id'] = $owner['id']; $item['author-id'] = $item['owner-id'] = Contact::getPublicIdByUserId($uid); - $item['title'] = $spoiler_text; + $item['title'] = $request['spoiler_text']; $item['body'] = $body; if (!empty(self::getCurrentApplication()['name'])) { @@ -80,7 +81,7 @@ class Statuses extends BaseApi $item['app'] = 'API'; } - switch ($visibility) { + switch ($request['visibility']) { case 'public': $item['allow_cid'] = ''; $item['allow_gid'] = ''; @@ -112,7 +113,7 @@ class Statuses extends BaseApi case 'direct': // Direct messages are currently unsupported DI::mstdnError()->InternalError('Direct messages are currently unsupported'); - break; + break; default: $item['allow_cid'] = $owner['allow_cid']; $item['allow_gid'] = $owner['allow_gid']; @@ -129,12 +130,12 @@ class Statuses extends BaseApi break; } - if (!empty($language)) { - $item['language'] = json_encode([$language => 1]); + if (!empty($request['language'])) { + $item['language'] = json_encode([$request['language'] => 1]); } - if ($in_reply_to_id) { - $parent = Post::selectFirst(['uri'], ['uri-id' => $in_reply_to_id, 'uid' => [0, $uid]]); + if ($request['in_reply_to_id']) { + $parent = Post::selectFirst(['uri'], ['uri-id' => $request['in_reply_to_id'], 'uid' => [0, $uid]]); $item['thr-parent'] = $parent['uri']; $item['gravity'] = GRAVITY_COMMENT; $item['object-type'] = Activity\ObjectType::COMMENT; @@ -143,16 +144,16 @@ class Statuses extends BaseApi $item['object-type'] = Activity\ObjectType::NOTE; } - if (!empty($media_ids)) { + if (!empty($request['media_ids'])) { $item['object-type'] = Activity\ObjectType::IMAGE; $item['post-type'] = Item::PT_IMAGE; $item['attachments'] = []; - foreach ($media_ids as $id) { + foreach ($request['media_ids'] as $id) { $media = DBA::toArray(DBA::p("SELECT `resource-id`, `scale`, `type`, `desc`, `filename`, `datasize`, `width`, `height` FROM `photo` WHERE `resource-id` IN (SELECT `resource-id` FROM `photo` WHERE `id` = ?) AND `photo`.`uid` = ? ORDER BY `photo`.`width` DESC LIMIT 2", $id, $uid)); - + if (empty($media)) { continue; } @@ -162,7 +163,7 @@ class Statuses extends BaseApi $ressources[] = $media[0]['resource-id']; $phototypes = Images::supportedTypes(); $ext = $phototypes[$media[0]['type']]; - + $attachment = ['type' => Post\Media::IMAGE, 'mimetype' => $media[0]['type'], 'url' => DI::baseUrl() . '/photo/' . $media[0]['resource-id'] . '-' . $media[0]['scale'] . '.' . $ext, 'size' => $media[0]['datasize'], @@ -170,7 +171,7 @@ class Statuses extends BaseApi 'description' => $media[0]['desc'] ?? '', 'width' => $media[0]['width'], 'height' => $media[0]['height']]; - + if (count($media) > 1) { $attachment['preview'] = DI::baseUrl() . '/photo/' . $media[1]['resource-id'] . '-' . $media[1]['scale'] . '.' . $ext; $attachment['preview-width'] = $media[1]['width']; @@ -184,7 +185,7 @@ class Statuses extends BaseApi if (!empty($id)) { $item = Post::selectFirst(['uri-id'], ['id' => $id]); if (!empty($item['uri-id'])) { - System::jsonExit(DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid)); + System::jsonExit(DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid)); } } diff --git a/src/Module/BaseApi.php b/src/Module/BaseApi.php index e0b1da01b4..aca6070ae1 100644 --- a/src/Module/BaseApi.php +++ b/src/Module/BaseApi.php @@ -29,7 +29,7 @@ use Friendica\Database\DBA; use Friendica\DI; use Friendica\Network\HTTPException; use Friendica\Util\DateTimeFormat; -use Friendica\Util\Network; +use Friendica\Util\HTTPInputData; require_once __DIR__ . '/../../include/api.php'; @@ -129,7 +129,7 @@ class BaseApi extends BaseModule 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 ?? []]); + Logger::info('Unimplemented API call', ['method' => $method, 'path' => $path, 'agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'request' => HTTPInputData::process()]); $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.'); $errorobj = new \Friendica\Object\Api\Mastodon\Error($error, $error_description); @@ -142,25 +142,28 @@ class BaseApi extends BaseModule * @return array request data */ public static function getRequest(array $defaults) { + $httpinput = HTTPInputData::process(); + $input = array_merge($httpinput['variables'], $httpinput['files'], $_REQUEST); + $request = []; foreach ($defaults as $parameter => $defaultvalue) { if (is_string($defaultvalue)) { - $request[$parameter] = $_REQUEST[$parameter] ?? $defaultvalue; + $request[$parameter] = $input[$parameter] ?? $defaultvalue; } elseif (is_int($defaultvalue)) { - $request[$parameter] = (int)($_REQUEST[$parameter] ?? $defaultvalue); + $request[$parameter] = (int)($input[$parameter] ?? $defaultvalue); } elseif (is_float($defaultvalue)) { - $request[$parameter] = (float)($_REQUEST[$parameter] ?? $defaultvalue); + $request[$parameter] = (float)($input[$parameter] ?? $defaultvalue); } elseif (is_array($defaultvalue)) { - $request[$parameter] = $_REQUEST[$parameter] ?? []; + $request[$parameter] = $input[$parameter] ?? []; } elseif (is_bool($defaultvalue)) { - $request[$parameter] = in_array(strtolower($_REQUEST[$parameter] ?? ''), ['true', '1']); + $request[$parameter] = in_array(strtolower($input[$parameter] ?? ''), ['true', '1']); } else { Logger::notice('Unhandled default value type', ['parameter' => $parameter, 'type' => gettype($defaultvalue)]); } } - foreach ($_REQUEST ?? [] as $parameter => $value) { + foreach ($input ?? [] as $parameter => $value) { if ($parameter == 'pagename') { continue; } @@ -173,45 +176,6 @@ class BaseApi extends BaseModule return $request; } - /** - * Get post data that is transmitted as JSON - * - * @return array request data - */ - public static function getJsonPostData() - { - $postdata = Network::postdata(); - if (empty($postdata)) { - return []; - } - - return json_decode($postdata, true); - } - - /** - * Get request data for put requests - * - * @return array request data - */ - public static function getPutData() - { - $rawdata = Network::postdata(); - if (empty($rawdata)) { - return []; - } - - $putdata = []; - - foreach (explode('&', $rawdata) as $value) { - $data = explode('=', $value); - if (count($data) == 2) { - $putdata[$data[0]] = urldecode($data[1]); - } - } - - return $putdata; - } - /** * Log in user via OAuth1 or Simple HTTP Auth. * @@ -394,7 +358,7 @@ class BaseApi extends BaseModule Logger::warning('Requested token scope is not allowed for the application', ['token' => $fields, 'application' => $application]); } } - + if (!DBA::insert('application-token', $fields, Database::INSERT_UPDATE)) { return []; } From fb81be0f4b2a0ce53a45617236c5a897783efd56 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 28 May 2021 12:37:00 +0000 Subject: [PATCH 2/4] Moved API request --- doc/API-Mastodon.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/API-Mastodon.md b/doc/API-Mastodon.md index 37f62e0607..6bb17642da 100644 --- a/doc/API-Mastodon.md +++ b/doc/API-Mastodon.md @@ -122,7 +122,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/) -- [`GET /api/v1/instance/activity`](https://docs.joinmastodon.org/methods/instance#weekly-activity) ## Dummy endpoints @@ -139,7 +138,7 @@ They refer to features that don't exist in Friendica yet. ## Non supportable endpoints These endpoints won't be implemented at the moment. -They refer to features that don't exist in Friendica yet. +They refer to features or data that don't exist in Friendica yet. - [`POST /api/v1/accounts`](https://docs.joinmastodon.org/methods/accounts/) - [`POST /api/v1/accounts/:id/pin`](https://docs.joinmastodon.org/methods/accounts/) @@ -164,6 +163,7 @@ They refer to features that don't exist in Friendica yet. - [`POST /api/v1/filters/:id`](https://docs.joinmastodon.org/methods/accounts/filters/) - [`PUT /api/v1/filters/:id`](https://docs.joinmastodon.org/methods/accounts/filters/) - [`DELETE /api/v1/filters/:id`](https://docs.joinmastodon.org/methods/accounts/filters/) +- [`GET /api/v1/instance/activity`](https://docs.joinmastodon.org/methods/instance#weekly-activity) - [`POST /api/v1/markers`](https://docs.joinmastodon.org/methods/timelines/markers/) - [`GET /api/v1/polls/:id`](https://docs.joinmastodon.org/methods/statuses/polls/) - [`POST /api/v1/polls/:id/votes`](https://docs.joinmastodon.org/methods/statuses/polls/) From c3c6f3c8d3f8bf9d1d0971dd3dacfb07a10654e0 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 29 May 2021 10:40:47 +0000 Subject: [PATCH 3/4] Code style conventions --- src/Module/BaseApi.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Module/BaseApi.php b/src/Module/BaseApi.php index aca6070ae1..7735b9802c 100644 --- a/src/Module/BaseApi.php +++ b/src/Module/BaseApi.php @@ -141,7 +141,8 @@ class BaseApi extends BaseModule * * @return array request data */ - public static function getRequest(array $defaults) { + public static function getRequest(array $defaults) + { $httpinput = HTTPInputData::process(); $input = array_merge($httpinput['variables'], $httpinput['files'], $_REQUEST); From 1ca58968f273df183fdd81f15b65d3fdb1914964 Mon Sep 17 00:00:00 2001 From: Michael Vogel Date: Sat, 29 May 2021 14:32:31 +0200 Subject: [PATCH 4/4] Apply suggestions from code review Co-authored-by: Hypolite Petovan --- src/Module/Api/Mastodon/Lists.php | 2 +- src/Module/Api/Mastodon/Statuses.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Module/Api/Mastodon/Lists.php b/src/Module/Api/Mastodon/Lists.php index 38a9c16640..67db0b81f2 100644 --- a/src/Module/Api/Mastodon/Lists.php +++ b/src/Module/Api/Mastodon/Lists.php @@ -80,7 +80,7 @@ class Lists extends BaseApi { $request = self::getRequest([ 'title' => '', // The title of the list to be updated. - 'replies_policy' => '', // Enumerable oneOf followed list none. + 'replies_policy' => '', // One of: "followed", "list", or "none". ]); if (empty($request['title']) || empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Statuses.php b/src/Module/Api/Mastodon/Statuses.php index 0699c41087..8e13afb2fa 100644 --- a/src/Module/Api/Mastodon/Statuses.php +++ b/src/Module/Api/Mastodon/Statuses.php @@ -53,7 +53,7 @@ class Statuses extends BaseApi 'in_reply_to_id' => 0, // ID of the status being replied to, if status is a reply 'sensitive' => false, // Mark status and attached media as sensitive? 'spoiler_text' => '', // Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field. - 'visibility' => '', // Visibility of the posted status. Enumerable oneOf public, unlisted, private, direct. + 'visibility' => '', // Visibility of the posted status. One of: "public", "unlisted", "private" or "direct". 'scheduled_at' => '', // ISO 8601 Datetime at which to schedule a status. Providing this paramter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future. 'language' => '', // ISO 639 language code for this status. ]);