X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FModule%2FBaseApi.php;h=d6e5f748f23d35976b5cf99eb84e2ddffb0ed520;hb=7fd1db0ec6b3c729e347f94ca961421fb4f5070e;hp=d2240fcd68b231886b272756d7cca0d46148bc1a;hpb=f6faed9fdc6ad80c56f51e88e8ddb45e88580bfb;p=friendica.git diff --git a/src/Module/BaseApi.php b/src/Module/BaseApi.php index d2240fcd68..d6e5f748f2 100644 --- a/src/Module/BaseApi.php +++ b/src/Module/BaseApi.php @@ -1,6 +1,6 @@ getCommand(), -4) === '.xml') { - self::$format = 'xml'; - } - if (substr($arguments->getCommand(), -4) === '.rss') { - self::$format = 'rss'; - } - if (substr($arguments->getCommand(), -4) === '.atom') { - self::$format = 'atom'; - } + $this->app = $app; } - public static function delete(array $parameters = []) + /** + * Additionally checks, if the caller is permitted to do this action + * + * {@inheritDoc} + * + * @throws HTTPException\ForbiddenException + */ + public function run(ModuleHTTPException $httpException, array $request = [], bool $scopecheck = true): ResponseInterface { - if (!api_user()) { - throw new HTTPException\UnauthorizedException(DI::l10n()->t('Permission denied.')); + if ($scopecheck) { + switch ($this->args->getMethod()) { + case Router::DELETE: + case Router::PATCH: + case Router::POST: + case Router::PUT: + self::checkAllowedScope(self::SCOPE_WRITE); + + if (!self::getCurrentUserID()) { + throw new HTTPException\ForbiddenException($this->t('Permission denied.')); + } + break; + } } - $a = DI::app(); - - if (!empty($a->user['uid']) && $a->user['uid'] != api_user()) { - throw new HTTPException\ForbiddenException(DI::l10n()->t('Permission denied.')); - } + return parent::run($httpException, $request); } - public static function patch(array $parameters = []) + /** + * Processes data from GET requests and sets paging conditions + * + * @param array $request Custom REQUEST array + * @param array $condition Existing conditions to merge + * @return array paging data condition parameters data + * @throws \Exception + */ + protected function addPagingConditions(array $request, array $condition): array { - if (!api_user()) { - throw new HTTPException\UnauthorizedException(DI::l10n()->t('Permission denied.')); + $requested_order = $request['friendica_order']; + if ($requested_order == TimelineOrderByTypes::ID) { + if (!empty($request['max_id'])) { + $condition = DBA::mergeConditions($condition, ["`uri-id` < ?", intval($request['max_id'])]); + } + + if (!empty($request['since_id'])) { + $condition = DBA::mergeConditions($condition, ["`uri-id` > ?", intval($request['since_id'])]); + } + + if (!empty($request['min_id'])) { + $condition = DBA::mergeConditions($condition, ["`uri-id` > ?", intval($request['min_id'])]); + } + } else { + switch ($requested_order) { + case TimelineOrderByTypes::RECEIVED: + case TimelineOrderByTypes::CHANGED: + case TimelineOrderByTypes::EDITED: + case TimelineOrderByTypes::CREATED: + case TimelineOrderByTypes::COMMENTED: + $order_field = $requested_order; + break; + default: + throw new \Exception("Unrecognized request order: $requested_order"); + } + + if (!empty($request['max_id'])) { + $condition = DBA::mergeConditions($condition, ["`$order_field` < ?", DateTimeFormat::convert($request['max_id'], DateTimeFormat::MYSQL)]); + } + + if (!empty($request['since_id'])) { + $condition = DBA::mergeConditions($condition, ["`$order_field` > ?", DateTimeFormat::convert($request['since_id'], DateTimeFormat::MYSQL)]); + } + + if (!empty($request['min_id'])) { + $condition = DBA::mergeConditions($condition, ["`$order_field` > ?", DateTimeFormat::convert($request['min_id'], DateTimeFormat::MYSQL)]); + } } - $a = DI::app(); - - if (!empty($a->user['uid']) && $a->user['uid'] != api_user()) { - throw new HTTPException\ForbiddenException(DI::l10n()->t('Permission denied.')); - } + return $condition; } - public static function post(array $parameters = []) + /** + * Processes data from GET requests and sets paging conditions + * + * @param array $request Custom REQUEST array + * @param array $params Existing $params element to build on + * @return array ordering data added to the params blocks that was passed in + * @throws \Exception + */ + protected function buildOrderAndLimitParams(array $request, array $params = []): array { - if (!api_user()) { - throw new HTTPException\UnauthorizedException(DI::l10n()->t('Permission denied.')); + $requested_order = $request['friendica_order']; + switch ($requested_order) { + case TimelineOrderByTypes::CHANGED: + case TimelineOrderByTypes::CREATED: + case TimelineOrderByTypes::COMMENTED: + case TimelineOrderByTypes::EDITED: + case TimelineOrderByTypes::RECEIVED: + $order_field = $requested_order; + break; + case TimelineOrderByTypes::ID: + default: + $order_field = 'uri-id'; } - $a = DI::app(); - - if (!empty($a->user['uid']) && $a->user['uid'] != api_user()) { - throw new HTTPException\ForbiddenException(DI::l10n()->t('Permission denied.')); + if (!empty($request['min_id'])) { + $params['order'] = [$order_field]; + } else { + $params['order'] = [$order_field => true]; } - } - public static function put(array $parameters = []) - { - if (!api_user()) { - throw new HTTPException\UnauthorizedException(DI::l10n()->t('Permission denied.')); - } + $params['limit'] = $request['limit']; - $a = DI::app(); + return $params; + } - if (!empty($a->user['uid']) && $a->user['uid'] != api_user()) { - throw new HTTPException\ForbiddenException(DI::l10n()->t('Permission denied.')); + /** + * Update the ID/time boundaries for this result set. Used for building Link Headers + * + * @param Status $status + * @param array $post_item + * @param string $order + * @return void + * @throws \Exception + */ + protected function updateBoundaries(Status $status, array $post_item, string $order) + { + try { + switch ($order) { + case TimelineOrderByTypes::CHANGED: + if (!empty($status->friendicaExtension()->changedAt())) { + self::setBoundaries(new DateTime(DateTimeFormat::utc($status->friendicaExtension()->changedAt(), DateTimeFormat::JSON))); + } + break; + case TimelineOrderByTypes::CREATED: + if (!empty($status->createdAt())) { + self::setBoundaries(new DateTime(DateTimeFormat::utc($status->createdAt(), DateTimeFormat::JSON))); + } + break; + case TimelineOrderByTypes::COMMENTED: + if (!empty($status->friendicaExtension()->commentedAt())) { + self::setBoundaries(new DateTime(DateTimeFormat::utc($status->friendicaExtension()->commentedAt(), DateTimeFormat::JSON))); + } + break; + case TimelineOrderByTypes::EDITED: + if (!empty($status->editedAt())) { + self::setBoundaries(new DateTime(DateTimeFormat::utc($status->editedAt(), DateTimeFormat::JSON))); + } + break; + case TimelineOrderByTypes::RECEIVED: + if (!empty($status->friendicaExtension()->receivedAt())) { + self::setBoundaries(new DateTime(DateTimeFormat::utc($status->friendicaExtension()->receivedAt(), DateTimeFormat::JSON))); + } + break; + case TimelineOrderByTypes::ID: + default: + self::setBoundaries($post_item['uri-id']); + } + } catch (\Exception $e) { + Logger::debug('Error processing page boundary calculation, skipping', ['error' => $e]); } } - public static function unsupported(string $method = 'all') + /** + * Processes data from GET requests and sets defaults + * + * @param array $defaults Associative array of expected request keys and their default typed value. A null + * value will remove the request key from the resulting value array. + * @param array $request Custom REQUEST array, superglobal instead + * @return array request data + * @throws \Exception + */ + public function getRequest(array $defaults, array $request): array { - $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.');; - $errorobj = new \Friendica\Object\Api\Mastodon\Error($error, $error_description); - System::jsonError(501, $errorobj->toArray()); + self::$request = $request; + self::$boundaries = []; + + unset(self::$request['pagename']); + + return $this->checkDefaults($defaults, $request); } /** - * Log in user via OAuth1 or Simple HTTP Auth. - * - * Simple Auth allow username in form of
user@server
, ignoring server part - * - * @return bool Was a user authenticated? - * @throws HTTPException\ForbiddenException - * @throws HTTPException\UnauthorizedException - * @throws HTTPException\InternalServerErrorException - * @hook 'authenticate' - * array $addon_auth - * 'username' => username from login form - * 'password' => password from login form - * 'authenticated' => return status, - * 'user_record' => return authenticated user record + * Set boundaries for the "link" header + * @param array $boundaries + * @param int|\DateTime $id */ - protected static function login() + protected static function setBoundaries($id) { - if (empty(self::$current_user_id)) { - self::$current_user_id = self::getUserByBearer(); + if (!isset(self::$boundaries['min'])) { + self::$boundaries['min'] = $id; } - if (empty(self::$current_user_id)) { - api_login(DI::app()); + if (!isset(self::$boundaries['max'])) { + self::$boundaries['max'] = $id; } - self::$current_user_id = api_user(); - - return (bool)self::$current_user_id; + self::$boundaries['min'] = min(self::$boundaries['min'], $id); + self::$boundaries['max'] = max(self::$boundaries['max'], $id); } /** - * Get current user id, returns 0 if not logged in - * - * @return int User ID + * Get the "link" header with "next" and "prev" links + * @return string */ - protected static function getCurrentUserID() + protected static function getLinkHeader(bool $asDate = false): string { - if (empty(self::$current_user_id)) { - self::$current_user_id = self::getUserByBearer(); + if (empty(self::$boundaries)) { + return ''; } - if (empty(self::$current_user_id)) { - api_login(DI::app(), false); + $request = self::$request; + + unset($request['min_id']); + unset($request['max_id']); + unset($request['since_id']); - self::$current_user_id = api_user(); + $prev_request = $next_request = $request; + + if ($asDate) { + $max_date = self::$boundaries['max']; + $min_date = self::$boundaries['min']; + $prev_request['min_id'] = $max_date->format(DateTimeFormat::JSON); + $next_request['max_id'] = $min_date->format(DateTimeFormat::JSON); + } else { + $prev_request['min_id'] = self::$boundaries['max']; + $next_request['max_id'] = self::$boundaries['min']; } - return (int)self::$current_user_id; + $command = DI::baseUrl() . '/' . DI::args()->getCommand(); + + $prev = $command . '?' . http_build_query($prev_request); + $next = $command . '?' . http_build_query($next_request); + + return 'Link: <' . $next . '>; rel="next", <' . $prev . '>; rel="prev"'; } - private static function getUserByBearer() + /** + * Get the "link" header with "next" and "prev" links for an offset/limit type call + * @return string + */ + protected static function getOffsetAndLimitLinkHeader(int $offset, int $limit): string { - $authorization = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; - $authorization = $_SERVER['AUTHORIZATION'] ?? $authorization; + $request = self::$request; - if (substr($authorization, 0, 7) != 'Bearer ') { - return 0; - } + unset($request['offset']); + $request['limit'] = $limit; - $bearer = trim(substr($authorization, 7)); - $condition = ['access_token' => $bearer]; - $token = DBA::selectFirst('application-token', ['uid'], $condition); - if (!DBA::isResult($token)) { - Logger::warning('Token not found', $condition); - return 0; - } - Logger::info('Token found', $token); - return $token['uid']; - } + $prev_request = $next_request = $request; - public static function getApplication() - { - $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']; + $prev_request['offset'] = $offset - $limit; + $next_request['offset'] = $offset + $limit; - if ((empty($redirect_uri) && empty($client_secret)) || empty($client_id)) { - Logger::warning('Incomplete request', ['request' => $_REQUEST]); - return []; - } + $command = DI::baseUrl() . '/' . DI::args()->getCommand(); - $condition = ['client_id' => $client_id]; - if (!empty($client_secret)) { - $condition['client_secret'] = $client_secret; - } - if (!empty($redirect_uri)) { - $condition['redirect_uri'] = $redirect_uri; + $prev = $command . '?' . http_build_query($prev_request); + $next = $command . '?' . http_build_query($next_request); + + if ($prev_request['offset'] >= 0) { + return 'Link: <' . $next . '>; rel="next", <' . $prev . '>; rel="prev"'; + } else { + return 'Link: <' . $next . '>; rel="next"'; } + } - $application = DBA::selectFirst('application', [], $condition); - if (!DBA::isResult($application)) { - Logger::warning('Application not found', $condition); - return []; + /** + * Set the "link" header with "next" and "prev" links + * @return void + */ + protected static function setLinkHeader(bool $asDate = false) + { + $header = self::getLinkHeader($asDate); + if (!empty($header)) { + header($header); } - return $application; } - public static function existsTokenForUser(array $application, int $uid) + /** + * Set the "link" header with "next" and "prev" links + * @return void + */ + protected static function setLinkHeaderByOffsetLimit(int $offset, int $limit) { - return DBA::exists('application-token', ['application-id' => $application['id'], 'uid' => $uid]); + $header = self::getOffsetAndLimitLinkHeader($offset, $limit); + if (!empty($header)) { + header($header); + } } - public static function getTokenForUser(array $application, int $uid) + /** + * Check if the app is known to support quoted posts + * + * @return bool + */ + public static function appSupportsQuotes(): bool { - return DBA::selectFirst('application-token', [], ['application-id' => $application['id'], 'uid' => $uid]); + $token = OAuth::getCurrentApplicationToken(); + return (!empty($token['name']) && in_array($token['name'], ['Fedilab'])); } - public static function createTokenForUser(array $application, int $uid) + /** + * Get current application token + * + * @return array token + */ + public static function getCurrentApplication() { - $code = bin2hex(random_bytes(32)); - $access_token = bin2hex(random_bytes(32)); + $token = OAuth::getCurrentApplicationToken(); - $fields = ['application-id' => $application['id'], 'uid' => $uid, 'code' => $code, 'access_token' => $access_token, 'created_at' => DateTimeFormat::utcNow(DateTimeFormat::MYSQL)]; - if (!DBA::insert('application-token', $fields, Database::INSERT_UPDATE)) { - return []; + if (empty($token)) { + $token = BasicAuth::getCurrentApplicationToken(); } - return DBA::selectFirst('application-token', [], ['application-id' => $application['id'], 'uid' => $uid]); + return $token; } /** - * Get user info array. + * Get current user id, returns 0 if not logged in * - * @param int|string $contact_id Contact ID or URL - * @return array|bool - * @throws HTTPException\BadRequestException - * @throws HTTPException\InternalServerErrorException - * @throws HTTPException\UnauthorizedException - * @throws \ImagickException + * @return int User ID */ - protected static function getUser($contact_id = null) + public static function getCurrentUserID() { - return api_get_user(DI::app(), $contact_id); + $uid = OAuth::getCurrentUserID(); + + if (empty($uid)) { + $uid = BasicAuth::getCurrentUserID(false); + } + + return (int)$uid; } /** - * Formats the data according to the data type + * Check if the provided scope does exist. + * halts execution on missing scope or when not logged in. * - * @param string $root_element - * @param array $data An array with a single element containing the returned result - * @return false|string + * @param string $scope the requested scope (read, write, follow, push) */ - protected static function format(string $root_element, array $data) + public static function checkAllowedScope(string $scope) { - $return = api_format_data($root_element, self::$format, $data); + $token = self::getCurrentApplication(); - switch (self::$format) { - case "xml": - header("Content-Type: text/xml"); - break; - case "json": - header("Content-Type: application/json"); - if (!empty($return)) { - $json = json_encode(end($return)); - if (!empty($_GET['callback'])) { - $json = $_GET['callback'] . "(" . $json . ")"; - } - $return = $json; - } - break; - case "rss": - header("Content-Type: application/rss+xml"); - $return = '' . "\n" . $return; - break; - case "atom": - header("Content-Type: application/atom+xml"); - $return = '' . "\n" . $return; - break; + if (empty($token)) { + Logger::notice('Empty application token'); + DI::mstdnError()->Forbidden(); + } + + if (!isset($token[$scope])) { + Logger::warning('The requested scope does not exist', ['scope' => $scope, 'application' => $token]); + DI::mstdnError()->Forbidden(); } - return $return; + if (empty($token[$scope])) { + Logger::warning('The requested scope is not allowed', ['scope' => $scope, 'application' => $token]); + DI::mstdnError()->Forbidden(); + } } - /** - * Creates the XML from a JSON style array - * - * @param $data - * @param $root_element - * @return string - */ - protected static function createXml($data, $root_element) + public static function checkThrottleLimit() { - return api_create_xml($data, $root_element); + $uid = self::getCurrentUserID(); + + // Check for throttling (maximum posts per day, week and month) + $throttle_day = DI::config()->get('system', 'throttle_limit_day'); + if ($throttle_day > 0) { + $datefrom = date(DateTimeFormat::MYSQL, time() - 24*60*60); + + $condition = ["`gravity` = ? AND `uid` = ? AND `wall` AND `received` > ?", Item::GRAVITY_PARENT, $uid, $datefrom]; + $posts_day = Post::countThread($condition); + + if ($posts_day > $throttle_day) { + Logger::notice('Daily posting limit reached', ['uid' => $uid, 'posts' => $posts_day, 'limit' => $throttle_day]); + $error = DI::l10n()->t('Too Many Requests'); + $error_description = DI::l10n()->tt("Daily posting limit of %d post reached. The post was rejected.", "Daily posting limit of %d posts reached. The post was rejected.", $throttle_day); + $errorobj = new \Friendica\Object\Api\Mastodon\Error($error, $error_description); + System::jsonError(429, $errorobj->toArray()); + } + } + + $throttle_week = DI::config()->get('system', 'throttle_limit_week'); + if ($throttle_week > 0) { + $datefrom = date(DateTimeFormat::MYSQL, time() - 24*60*60*7); + + $condition = ["`gravity` = ? AND `uid` = ? AND `wall` AND `received` > ?", Item::GRAVITY_PARENT, $uid, $datefrom]; + $posts_week = Post::countThread($condition); + + if ($posts_week > $throttle_week) { + Logger::notice('Weekly posting limit reached', ['uid' => $uid, 'posts' => $posts_week, 'limit' => $throttle_week]); + $error = DI::l10n()->t('Too Many Requests'); + $error_description = DI::l10n()->tt("Weekly posting limit of %d post reached. The post was rejected.", "Weekly posting limit of %d posts reached. The post was rejected.", $throttle_week); + $errorobj = new \Friendica\Object\Api\Mastodon\Error($error, $error_description); + System::jsonError(429, $errorobj->toArray()); + } + } + + $throttle_month = DI::config()->get('system', 'throttle_limit_month'); + if ($throttle_month > 0) { + $datefrom = date(DateTimeFormat::MYSQL, time() - 24*60*60*30); + + $condition = ["`gravity` = ? AND `uid` = ? AND `wall` AND `received` > ?", Item::GRAVITY_PARENT, $uid, $datefrom]; + $posts_month = Post::countThread($condition); + + if ($posts_month > $throttle_month) { + Logger::notice('Monthly posting limit reached', ['uid' => $uid, 'posts' => $posts_month, 'limit' => $throttle_month]); + $error = DI::l10n()->t('Too Many Requests'); + $error_description = DI::l10n()->tt('Monthly posting limit of %d post reached. The post was rejected.', 'Monthly posting limit of %d posts reached. The post was rejected.', $throttle_month); + $errorobj = new \Friendica\Object\Api\Mastodon\Error($error, $error_description); + System::jsonError(429, $errorobj->toArray()); + } + } + } + + public static function getContactIDForSearchterm(string $screen_name = null, string $profileurl = null, int $cid = null, int $uid) + { + if (!empty($cid)) { + return $cid; + } + + if (!empty($profileurl)) { + return Contact::getIdForURL($profileurl); + } + + if (empty($cid) && !empty($screen_name)) { + if (strpos($screen_name, '@') !== false) { + return Contact::getIdForURL($screen_name, 0, false); + } + + $user = User::getByNickname($screen_name, ['uid']); + if (!empty($user['uid'])) { + return Contact::getPublicIdByUserId($user['uid']); + } + } + + if ($uid != 0) { + return Contact::getPublicIdByUserId($uid); + } + + return null; } }