3 * @copyright Copyright (C) 2010-2021, the Friendica project
5 * @license GNU AGPL version 3 or any later version
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as
9 * published by the Free Software Foundation, either version 3 of the
10 * License, or (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Affero General Public License for more details.
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22 namespace Friendica\Module;
24 use Friendica\BaseModule;
25 use Friendica\Core\Logger;
26 use Friendica\Core\System;
28 use Friendica\Model\Contact;
29 use Friendica\Model\Post;
30 use Friendica\Model\User;
31 use Friendica\Network\HTTPException;
32 use Friendica\Security\BasicAuth;
33 use Friendica\Security\OAuth;
34 use Friendica\Util\DateTimeFormat;
35 use Friendica\Util\HTTPInputData;
37 class BaseApi extends BaseModule
39 const SCOPE_READ = 'read';
40 const SCOPE_WRITE = 'write';
41 const SCOPE_FOLLOW = 'follow';
42 const SCOPE_PUSH = 'push';
47 protected static $boundaries = [];
52 protected static $request = [];
54 public function delete()
56 self::checkAllowedScope(self::SCOPE_WRITE);
58 if (!DI::app()->isLoggedIn()) {
59 throw new HTTPException\ForbiddenException(DI::l10n()->t('Permission denied.'));
63 public function patch()
65 self::checkAllowedScope(self::SCOPE_WRITE);
67 if (!DI::app()->isLoggedIn()) {
68 throw new HTTPException\ForbiddenException(DI::l10n()->t('Permission denied.'));
72 public function post()
74 self::checkAllowedScope(self::SCOPE_WRITE);
76 if (!DI::app()->isLoggedIn()) {
77 throw new HTTPException\ForbiddenException(DI::l10n()->t('Permission denied.'));
83 self::checkAllowedScope(self::SCOPE_WRITE);
85 if (!DI::app()->isLoggedIn()) {
86 throw new HTTPException\ForbiddenException(DI::l10n()->t('Permission denied.'));
91 * Processes data from GET requests and sets defaults
93 * @return array request data
95 public static function getRequest(array $defaults)
97 $httpinput = HTTPInputData::process();
98 $input = array_merge($httpinput['variables'], $httpinput['files'], $_REQUEST);
100 self::$request = $input;
101 self::$boundaries = [];
103 unset(self::$request['pagename']);
107 foreach ($defaults as $parameter => $defaultvalue) {
108 if (is_string($defaultvalue)) {
109 $request[$parameter] = $input[$parameter] ?? $defaultvalue;
110 } elseif (is_int($defaultvalue)) {
111 $request[$parameter] = (int)($input[$parameter] ?? $defaultvalue);
112 } elseif (is_float($defaultvalue)) {
113 $request[$parameter] = (float)($input[$parameter] ?? $defaultvalue);
114 } elseif (is_array($defaultvalue)) {
115 $request[$parameter] = $input[$parameter] ?? [];
116 } elseif (is_bool($defaultvalue)) {
117 $request[$parameter] = in_array(strtolower($input[$parameter] ?? ''), ['true', '1']);
119 Logger::notice('Unhandled default value type', ['parameter' => $parameter, 'type' => gettype($defaultvalue)]);
123 foreach ($input ?? [] as $parameter => $value) {
124 if ($parameter == 'pagename') {
127 if (!in_array($parameter, array_keys($defaults))) {
128 Logger::notice('Unhandled request field', ['parameter' => $parameter, 'value' => $value, 'command' => DI::args()->getCommand()]);
132 Logger::debug('Got request parameters', ['request' => $request, 'command' => DI::args()->getCommand()]);
137 * Set boundaries for the "link" header
138 * @param array $boundaries
141 protected static function setBoundaries(int $id)
143 if (!isset(self::$boundaries['min'])) {
144 self::$boundaries['min'] = $id;
147 if (!isset(self::$boundaries['max'])) {
148 self::$boundaries['max'] = $id;
151 self::$boundaries['min'] = min(self::$boundaries['min'], $id);
152 self::$boundaries['max'] = max(self::$boundaries['max'], $id);
156 * Set the "link" header with "next" and "prev" links
159 protected static function setLinkHeader()
161 if (empty(self::$boundaries)) {
165 $request = self::$request;
167 unset($request['min_id']);
168 unset($request['max_id']);
169 unset($request['since_id']);
171 $prev_request = $next_request = $request;
173 $prev_request['min_id'] = self::$boundaries['max'];
174 $next_request['max_id'] = self::$boundaries['min'];
176 $command = DI::baseUrl() . '/' . DI::args()->getCommand();
178 $prev = $command . '?' . http_build_query($prev_request);
179 $next = $command . '?' . http_build_query($next_request);
181 header('Link: <' . $next . '>; rel="next", <' . $prev . '>; rel="prev"');
185 * Get current application token
187 * @return array token
189 protected static function getCurrentApplication()
191 $token = OAuth::getCurrentApplicationToken();
194 $token = BasicAuth::getCurrentApplicationToken();
201 * Get current user id, returns 0 if not logged in
203 * @return int User ID
205 public static function getCurrentUserID()
207 $uid = OAuth::getCurrentUserID();
210 $uid = BasicAuth::getCurrentUserID(false);
217 * Check if the provided scope does exist.
218 * halts execution on missing scope or when not logged in.
220 * @param string $scope the requested scope (read, write, follow, push)
222 public static function checkAllowedScope(string $scope)
224 $token = self::getCurrentApplication();
227 Logger::notice('Empty application token');
228 DI::mstdnError()->Forbidden();
231 if (!isset($token[$scope])) {
232 Logger::warning('The requested scope does not exist', ['scope' => $scope, 'application' => $token]);
233 DI::mstdnError()->Forbidden();
236 if (empty($token[$scope])) {
237 Logger::warning('The requested scope is not allowed', ['scope' => $scope, 'application' => $token]);
238 DI::mstdnError()->Forbidden();
242 public static function checkThrottleLimit()
244 $uid = self::getCurrentUserID();
246 // Check for throttling (maximum posts per day, week and month)
247 $throttle_day = DI::config()->get('system', 'throttle_limit_day');
248 if ($throttle_day > 0) {
249 $datefrom = date(DateTimeFormat::MYSQL, time() - 24*60*60);
251 $condition = ["`gravity` = ? AND `uid` = ? AND `wall` AND `received` > ?", GRAVITY_PARENT, $uid, $datefrom];
252 $posts_day = Post::countThread($condition);
254 if ($posts_day > $throttle_day) {
255 Logger::info('Daily posting limit reached', ['uid' => $uid, 'posts' => $posts_day, 'limit' => $throttle_day]);
256 $error = DI::l10n()->t('Too Many Requests');
257 $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);
258 $errorobj = new \Friendica\Object\Api\Mastodon\Error($error, $error_description);
259 System::jsonError(429, $errorobj->toArray());
263 $throttle_week = DI::config()->get('system', 'throttle_limit_week');
264 if ($throttle_week > 0) {
265 $datefrom = date(DateTimeFormat::MYSQL, time() - 24*60*60*7);
267 $condition = ["`gravity` = ? AND `uid` = ? AND `wall` AND `received` > ?", GRAVITY_PARENT, $uid, $datefrom];
268 $posts_week = Post::countThread($condition);
270 if ($posts_week > $throttle_week) {
271 Logger::info('Weekly posting limit reached', ['uid' => $uid, 'posts' => $posts_week, 'limit' => $throttle_week]);
272 $error = DI::l10n()->t('Too Many Requests');
273 $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);
274 $errorobj = new \Friendica\Object\Api\Mastodon\Error($error, $error_description);
275 System::jsonError(429, $errorobj->toArray());
279 $throttle_month = DI::config()->get('system', 'throttle_limit_month');
280 if ($throttle_month > 0) {
281 $datefrom = date(DateTimeFormat::MYSQL, time() - 24*60*60*30);
283 $condition = ["`gravity` = ? AND `uid` = ? AND `wall` AND `received` > ?", GRAVITY_PARENT, $uid, $datefrom];
284 $posts_month = Post::countThread($condition);
286 if ($posts_month > $throttle_month) {
287 Logger::info('Monthly posting limit reached', ['uid' => $uid, 'posts' => $posts_month, 'limit' => $throttle_month]);
288 $error = DI::l10n()->t('Too Many Requests');
289 $error_description = DI::l10n()->t("Monthly posting limit of %d post reached. The post was rejected.", "Monthly posting limit of %d posts reached. The post was rejected.", $throttle_month);
290 $errorobj = new \Friendica\Object\Api\Mastodon\Error($error, $error_description);
291 System::jsonError(429, $errorobj->toArray());
296 public static function getContactIDForSearchterm(string $screen_name, int $cid, int $uid)
302 if (strpos($screen_name, '@') !== false) {
303 $cid = Contact::getIdForURL($screen_name, 0, false);
305 $user = User::getByNickname($screen_name, ['uid']);
306 if (!empty($user['uid'])) {
307 $cid = Contact::getPublicIdByUserId($user['uid']);
311 if (empty($cid) && ($uid != 0)) {
312 $cid = Contact::getPublicIdByUserId($uid);
319 * Set values for RSS template
321 * @param array $arr Array to be passed to template
322 * @param int $cid Contact ID of template
325 public static function addRSSValues(array $arr, int $cid)
331 $user_info = DI::twitterUser()->createFromContactId($cid)->toArray();
333 $arr['$user'] = $user_info;
335 'alternate' => $user_info['url'],
336 'self' => DI::baseUrl() . '/' . DI::args()->getQueryString(),
337 'base' => DI::baseUrl(),
338 'updated' => DateTimeFormat::utc(null, DateTimeFormat::API),
339 'atom_updated' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
340 'language' => $user_info['lang'],
341 'logo' => DI::baseUrl() . '/images/friendica-32.png',