]> git.mxchange.org Git - friendica.git/blob - include/api.php
API call rate_limit_status moved
[friendica.git] / include / api.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2021, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
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.
11  *
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.
16  *
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/>.
19  *
20  * Friendica implementation of statusnet/twitter API
21  *
22  * @file include/api.php
23  * @todo Automatically detect if incoming data is HTML or BBCode
24  */
25
26 use Friendica\App;
27 use Friendica\Collection\Api\Notifications as ApiNotifications;
28 use Friendica\Content\ContactSelector;
29 use Friendica\Content\Text\BBCode;
30 use Friendica\Content\Text\HTML;
31 use Friendica\Core\Logger;
32 use Friendica\Core\Protocol;
33 use Friendica\Core\System;
34 use Friendica\Database\DBA;
35 use Friendica\DI;
36 use Friendica\Model\Contact;
37 use Friendica\Model\Group;
38 use Friendica\Model\Item;
39 use Friendica\Model\Mail;
40 use Friendica\Model\Notification;
41 use Friendica\Model\Photo;
42 use Friendica\Model\Post;
43 use Friendica\Model\Profile;
44 use Friendica\Model\User;
45 use Friendica\Model\Verb;
46 use Friendica\Module\BaseApi;
47 use Friendica\Network\HTTPException;
48 use Friendica\Network\HTTPException\BadRequestException;
49 use Friendica\Network\HTTPException\ForbiddenException;
50 use Friendica\Network\HTTPException\InternalServerErrorException;
51 use Friendica\Network\HTTPException\MethodNotAllowedException;
52 use Friendica\Network\HTTPException\NotFoundException;
53 use Friendica\Network\HTTPException\TooManyRequestsException;
54 use Friendica\Network\HTTPException\UnauthorizedException;
55 use Friendica\Object\Api\Friendica\Notification as ApiNotification;
56 use Friendica\Object\Image;
57 use Friendica\Protocol\Activity;
58 use Friendica\Security\BasicAuth;
59 use Friendica\Security\OAuth;
60 use Friendica\Util\DateTimeFormat;
61 use Friendica\Util\Images;
62 use Friendica\Util\Network;
63 use Friendica\Util\Proxy;
64 use Friendica\Util\Strings;
65
66 require_once __DIR__ . '/../mod/item.php';
67 require_once __DIR__ . '/../mod/wall_upload.php';
68
69 define('API_METHOD_ANY', '*');
70 define('API_METHOD_GET', 'GET');
71 define('API_METHOD_POST', 'POST,PUT');
72 define('API_METHOD_DELETE', 'POST,DELETE');
73
74 define('API_LOG_PREFIX', 'API {action} - ');
75
76 $API = [];
77 $called_api = [];
78
79 /**
80  * Auth API user
81  *
82  * It is not sufficient to use local_user() to check whether someone is allowed to use the API,
83  * because this will open CSRF holes (just embed an image with src=friendicasite.com/api/statuses/update?status=CSRF
84  * into a page, and visitors will post something without noticing it).
85  */
86 function api_user()
87 {
88         $user = OAuth::getCurrentUserID();
89         if (!empty($user)) {
90                 return $user;
91         }
92
93         if (!empty($_SESSION['allow_api'])) {
94                 return local_user();
95         }
96
97         return false;
98 }
99
100 /**
101  * Get source name from API client
102  *
103  * Clients can send 'source' parameter to be show in post metadata
104  * as "sent via <source>".
105  * Some clients doesn't send a source param, we support ones we know
106  * (only Twidere, atm)
107  *
108  * @return string
109  *        Client source name, default to "api" if unset/unknown
110  * @throws Exception
111  */
112 function api_source()
113 {
114         if (requestdata('source')) {
115                 return requestdata('source');
116         }
117
118         // Support for known clients that doesn't send a source name
119         if (!empty($_SERVER['HTTP_USER_AGENT'])) {
120                 if(strpos($_SERVER['HTTP_USER_AGENT'], "Twidere") !== false) {
121                         return "Twidere";
122                 }
123
124                 Logger::info(API_LOG_PREFIX . 'Unrecognized user-agent', ['module' => 'api', 'action' => 'source', 'http_user_agent' => $_SERVER['HTTP_USER_AGENT']]);
125         } else {
126                 Logger::info(API_LOG_PREFIX . 'Empty user-agent', ['module' => 'api', 'action' => 'source']);
127         }
128
129         return "api";
130 }
131
132 /**
133  * Format date for API
134  *
135  * @param string $str Source date, as UTC
136  * @return string Date in UTC formatted as "D M d H:i:s +0000 Y"
137  * @throws Exception
138  */
139 function api_date($str)
140 {
141         // Wed May 23 06:01:13 +0000 2007
142         return DateTimeFormat::utc($str, "D M d H:i:s +0000 Y");
143 }
144
145 /**
146  * Register a function to be the endpoint for defined API path.
147  *
148  * @param string $path   API URL path, relative to DI::baseUrl()
149  * @param string $func   Function name to call on path request
150  * @param bool   $auth   API need logged user
151  * @param string $method HTTP method reqiured to call this endpoint.
152  *                       One of API_METHOD_ANY, API_METHOD_GET, API_METHOD_POST.
153  *                       Default to API_METHOD_ANY
154  */
155 function api_register_func($path, $func, $auth = false, $method = API_METHOD_ANY)
156 {
157         global $API;
158
159         $API[$path] = [
160                 'func'   => $func,
161                 'auth'   => $auth,
162                 'method' => $method,
163         ];
164
165         // Workaround for hotot
166         $path = str_replace("api/", "api/1.1/", $path);
167
168         $API[$path] = [
169                 'func'   => $func,
170                 'auth'   => $auth,
171                 'method' => $method,
172         ];
173 }
174
175 /**
176  * Check HTTP method of called API
177  *
178  * API endpoints can define which HTTP method to accept when called.
179  * This function check the current HTTP method agains endpoint
180  * registered method.
181  *
182  * @param string $method Required methods, uppercase, separated by comma
183  * @return bool
184  */
185 function api_check_method($method)
186 {
187         if ($method == "*") {
188                 return true;
189         }
190         return (stripos($method, $_SERVER['REQUEST_METHOD'] ?? 'GET') !== false);
191 }
192
193 /**
194  * Main API entry point
195  *
196  * Authenticate user, call registered API function, set HTTP headers
197  *
198  * @param App $a App
199  * @param App\Arguments $args The app arguments (optional, will retrieved by the DI-Container in case of missing)
200  * @return string|array API call result
201  * @throws Exception
202  */
203 function api_call(App $a, App\Arguments $args = null)
204 {
205         global $API, $called_api;
206
207         if ($args == null) {
208                 $args = DI::args();
209         }
210
211         $type = "json";
212         if (strpos($args->getCommand(), ".xml") > 0) {
213                 $type = "xml";
214         }
215         if (strpos($args->getCommand(), ".json") > 0) {
216                 $type = "json";
217         }
218         if (strpos($args->getCommand(), ".rss") > 0) {
219                 $type = "rss";
220         }
221         if (strpos($args->getCommand(), ".atom") > 0) {
222                 $type = "atom";
223         }
224
225         try {
226                 foreach ($API as $p => $info) {
227                         if (strpos($args->getCommand(), $p) === 0) {
228                                 if (!api_check_method($info['method'])) {
229                                         throw new MethodNotAllowedException();
230                                 }
231
232                                 $called_api = explode("/", $p);
233
234                                 if (!empty($info['auth']) && api_user() === false) {
235                                         BasicAuth::getCurrentUserID(true);
236                                         Logger::info(API_LOG_PREFIX . 'nickname {nickname}', ['module' => 'api', 'action' => 'call', 'nickname' => $a->getLoggedInUserNickname()]);
237                                 }
238
239                                 Logger::debug(API_LOG_PREFIX . 'parameters', ['module' => 'api', 'action' => 'call', 'parameters' => $_REQUEST]);
240
241                                 $stamp =  microtime(true);
242                                 $return = call_user_func($info['func'], $type);
243                                 $duration = floatval(microtime(true) - $stamp);
244
245                                 Logger::info(API_LOG_PREFIX . 'duration {duration}', ['module' => 'api', 'action' => 'call', 'duration' => round($duration, 2)]);
246
247                                 DI::profiler()->saveLog(DI::logger(), API_LOG_PREFIX . 'performance');
248
249                                 if (false === $return) {
250                                         /*
251                                                 * api function returned false withour throw an
252                                                 * exception. This should not happend, throw a 500
253                                                 */
254                                         throw new InternalServerErrorException();
255                                 }
256
257                                 switch ($type) {
258                                         case "xml":
259                                                 header("Content-Type: text/xml");
260                                                 break;
261                                         case "json":
262                                                 header("Content-Type: application/json");
263                                                 if (!empty($return)) {
264                                                         $json = json_encode(end($return));
265                                                         if (!empty($_GET['callback'])) {
266                                                                 $json = $_GET['callback'] . "(" . $json . ")";
267                                                         }
268                                                         $return = $json;
269                                                 }
270                                                 break;
271                                         case "rss":
272                                                 header("Content-Type: application/rss+xml");
273                                                 $return  = '<?xml version="1.0" encoding="UTF-8"?>' . "\n" . $return;
274                                                 break;
275                                         case "atom":
276                                                 header("Content-Type: application/atom+xml");
277                                                 $return = '<?xml version="1.0" encoding="UTF-8"?>' . "\n" . $return;
278                                                 break;
279                                 }
280                                 return $return;
281                         }
282                 }
283
284                 Logger::warning(API_LOG_PREFIX . 'not implemented', ['module' => 'api', 'action' => 'call', 'query' => DI::args()->getQueryString()]);
285                 throw new NotFoundException();
286         } catch (HTTPException $e) {
287                 header("HTTP/1.1 {$e->getCode()} {$e->getDescription()}");
288                 return api_error($type, $e, $args);
289         }
290 }
291
292 /**
293  * Format API error string
294  *
295  * @param string $type Return type (xml, json, rss, as)
296  * @param object $e    HTTPException Error object
297  * @param App\Arguments $args The App arguments
298  * @return string|array error message formatted as $type
299  */
300 function api_error($type, $e, App\Arguments $args)
301 {
302         $error = ($e->getMessage() !== "" ? $e->getMessage() : $e->getDescription());
303         /// @TODO:  https://dev.twitter.com/overview/api/response-codes
304
305         $error = ["error" => $error,
306                         "code" => $e->getCode() . " " . $e->getDescription(),
307                         "request" => $args->getQueryString()];
308
309         $return = BaseApi::formatData('status', $type, ['status' => $error]);
310
311         switch ($type) {
312                 case "xml":
313                         header("Content-Type: text/xml");
314                         break;
315                 case "json":
316                         header("Content-Type: application/json");
317                         $return = json_encode($return);
318                         break;
319                 case "rss":
320                         header("Content-Type: application/rss+xml");
321                         break;
322                 case "atom":
323                         header("Content-Type: application/atom+xml");
324                         break;
325         }
326
327         return $return;
328 }
329
330 /**
331  * Set values for RSS template
332  *
333  * @param App   $a
334  * @param array $arr       Array to be passed to template
335  * @param array $user_info User info
336  * @return array
337  * @throws BadRequestException
338  * @throws ImagickException
339  * @throws InternalServerErrorException
340  * @throws UnauthorizedException
341  * @todo  find proper type-hints
342  */
343 function api_rss_extra(App $a, $arr, $user_info)
344 {
345         if (is_null($user_info)) {
346                 $user_info = api_get_user();
347         }
348
349         $arr['$user'] = $user_info;
350         $arr['$rss'] = [
351                 'alternate'    => $user_info['url'],
352                 'self'         => DI::baseUrl() . "/" . DI::args()->getQueryString(),
353                 'base'         => DI::baseUrl(),
354                 'updated'      => api_date(null),
355                 'atom_updated' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
356                 'language'     => $user_info['lang'],
357                 'logo'         => DI::baseUrl() . "/images/friendica-32.png",
358         ];
359
360         return $arr;
361 }
362
363
364 /**
365  * Unique contact to contact url.
366  *
367  * @param int $id Contact id
368  * @return bool|string
369  *                Contact url or False if contact id is unknown
370  * @throws Exception
371  */
372 function api_unique_id_to_nurl($id)
373 {
374         $r = DBA::selectFirst('contact', ['nurl'], ['id' => $id]);
375
376         if (DBA::isResult($r)) {
377                 return $r["nurl"];
378         } else {
379                 return false;
380         }
381 }
382
383 /**
384  * Get user info array.
385  *
386  * @param App        $a          App
387  * @param int|string $contact_id Contact ID or URL
388  * @return array|bool
389  * @throws BadRequestException
390  * @throws ImagickException
391  * @throws InternalServerErrorException
392  * @throws UnauthorizedException
393  */
394 function api_get_user($contact_id = null)
395 {
396         global $called_api;
397
398         $user = null;
399         $extra_query = "";
400         $url = "";
401
402         Logger::info(API_LOG_PREFIX . 'Fetching data for user {user}', ['module' => 'api', 'action' => 'get_user', 'user' => $contact_id]);
403
404         // Searching for contact URL
405         if (!is_null($contact_id) && (intval($contact_id) == 0)) {
406                 $user = Strings::normaliseLink($contact_id);
407                 $url = $user;
408                 $extra_query = "AND `contact`.`nurl` = ? ";
409                 if (api_user() !== false) {
410                         $extra_query .= "AND `contact`.`uid`=" . intval(api_user());
411                 }
412         }
413
414         // Searching for contact id with uid = 0
415         if (!is_null($contact_id) && (intval($contact_id) != 0)) {
416                 $user = api_unique_id_to_nurl(intval($contact_id));
417
418                 if ($user == "") {
419                         throw new BadRequestException("User ID ".$contact_id." not found.");
420                 }
421
422                 $url = $user;
423                 $extra_query = "AND `contact`.`nurl` = ? ";
424                 if (api_user() !== false) {
425                         $extra_query .= "AND `contact`.`uid`=" . intval(api_user());
426                 }
427         }
428
429         if (is_null($user) && !empty($_GET['user_id'])) {
430                 $user = api_unique_id_to_nurl($_GET['user_id']);
431
432                 if ($user == "") {
433                         throw new BadRequestException("User ID ".$_GET['user_id']." not found.");
434                 }
435
436                 $url = $user;
437                 $extra_query = "AND `contact`.`nurl` = ? ";
438                 if (api_user() !== false) {
439                         $extra_query .= "AND `contact`.`uid`=" . intval(api_user());
440                 }
441         }
442         if (is_null($user) && !empty($_GET['screen_name'])) {
443                 $user = $_GET['screen_name'];
444                 $extra_query = "AND `contact`.`nick` = ? ";
445                 if (api_user() !== false) {
446                         $extra_query .= "AND `contact`.`uid`=".intval(api_user());
447                 }
448         }
449
450         if (is_null($user) && !empty($_GET['profileurl'])) {
451                 $user = Strings::normaliseLink($_GET['profileurl']);
452                 $extra_query = "AND `contact`.`nurl` = ? ";
453                 if (api_user() !== false) {
454                         $extra_query .= "AND `contact`.`uid`=".intval(api_user());
455                 }
456         }
457
458         // $called_api is the API path exploded on / and is expected to have at least 2 elements
459         if (is_null($user) && (DI::args()->getArgc() > (count($called_api) - 1)) && (count($called_api) > 0)) {
460                 $argid = count($called_api);
461                 if (!empty(DI::args()->getArgv()[$argid])) {
462                         $data = explode(".", DI::args()->getArgv()[$argid]);
463                         if (count($data) > 1) {
464                                 list($user, $null) = $data;
465                         }
466                 }
467                 if (is_numeric($user)) {
468                         $user = api_unique_id_to_nurl(intval($user));
469
470                         if ($user != "") {
471                                 $url = $user;
472                                 $extra_query = "AND `contact`.`nurl` = ? ";
473                                 if (api_user() !== false) {
474                                         $extra_query .= "AND `contact`.`uid`=" . intval(api_user());
475                                 }
476                         }
477                 } else {
478                         $extra_query = "AND `contact`.`nick` = ? ";
479                         if (api_user() !== false) {
480                                 $extra_query .= "AND `contact`.`uid`=" . intval(api_user());
481                         }
482                 }
483         }
484
485         Logger::info(API_LOG_PREFIX . 'getting user {user}', ['module' => 'api', 'action' => 'get_user', 'user' => $user]);
486
487         if (!$user) {
488                 if (api_user() === false) {
489                         BasicAuth::getCurrentUserID(true);
490                         return false;
491                 } else {
492                         $user = api_user();
493                         $extra_query = "AND `contact`.`uid` = ? AND `contact`.`self` ";
494                 }
495         }
496
497         Logger::info(API_LOG_PREFIX . 'found user {user}', ['module' => 'api', 'action' => 'get_user', 'user' => $user, 'extra_query' => $extra_query]);
498
499         // user info
500         $uinfo = DBA::toArray(DBA::p(
501                 "SELECT *, `contact`.`id` AS `cid` FROM `contact`
502                         WHERE 1
503                 $extra_query",
504                 $user
505         ));
506
507         // Selecting the id by priority, friendica first
508         if (is_array($uinfo)) {
509                 api_best_nickname($uinfo);
510         }
511
512         // if the contact wasn't found, fetch it from the contacts with uid = 0
513         if (!DBA::isResult($uinfo)) {
514                 if ($url == "") {
515                         throw new BadRequestException("User not found.");
516                 }
517
518                 $contact = DBA::selectFirst('contact', [], ['uid' => 0, 'nurl' => Strings::normaliseLink($url)]);
519
520                 if (DBA::isResult($contact)) {
521                         $ret = [
522                                 'id' => $contact["id"],
523                                 'id_str' => (string) $contact["id"],
524                                 'name' => $contact["name"],
525                                 'screen_name' => (($contact['nick']) ? $contact['nick'] : $contact['name']),
526                                 'location' => ($contact["location"] != "") ? $contact["location"] : ContactSelector::networkToName($contact['network'], $contact['url'], $contact['protocol']),
527                                 'description' => BBCode::toPlaintext($contact["about"] ?? ''),
528                                 'profile_image_url' => Contact::getAvatarUrlForUrl($contact['url'], api_user(), Proxy::SIZE_MICRO),
529                                 'profile_image_url_https' => Contact::getAvatarUrlForUrl($contact['url'], api_user(), Proxy::SIZE_MICRO),
530                                 'profile_image_url_profile_size' => Contact::getAvatarUrlForUrl($contact['url'], api_user(), Proxy::SIZE_THUMB),
531                                 'profile_image_url_large' => Contact::getAvatarUrlForUrl($contact['url'], api_user(), Proxy::SIZE_SMALL),
532                                 'url' => $contact["url"],
533                                 'protected' => false,
534                                 'followers_count' => 0,
535                                 'friends_count' => 0,
536                                 'listed_count' => 0,
537                                 'created_at' => api_date($contact["created"]),
538                                 'favourites_count' => 0,
539                                 'utc_offset' => 0,
540                                 'time_zone' => 'UTC',
541                                 'geo_enabled' => false,
542                                 'verified' => false,
543                                 'statuses_count' => 0,
544                                 'lang' => '',
545                                 'contributors_enabled' => false,
546                                 'is_translator' => false,
547                                 'is_translation_enabled' => false,
548                                 'following' => false,
549                                 'follow_request_sent' => false,
550                                 'statusnet_blocking' => false,
551                                 'notifications' => false,
552                                 'statusnet_profile_url' => $contact["url"],
553                                 'uid' => 0,
554                                 'cid' => Contact::getIdForURL($contact["url"], api_user(), false),
555                                 'pid' => Contact::getIdForURL($contact["url"], 0, false),
556                                 'self' => 0,
557                                 'network' => $contact["network"],
558                         ];
559
560                         return $ret;
561                 } else {
562                         throw new BadRequestException("User ".$url." not found.");
563                 }
564         }
565
566         if ($uinfo[0]['self']) {
567                 if ($uinfo[0]['network'] == "") {
568                         $uinfo[0]['network'] = Protocol::DFRN;
569                 }
570
571                 $usr = DBA::selectFirst('user', ['default-location'], ['uid' => api_user()]);
572                 $profile = DBA::selectFirst('profile', ['about'], ['uid' => api_user(), 'is-default' => true]);
573         }
574         $countitems = 0;
575         $countfriends = 0;
576         $countfollowers = 0;
577         $starred = 0;
578
579         $pcontact_id  = Contact::getIdForURL($uinfo[0]['url'], 0, false);
580
581         if (!empty($profile['about'])) {
582                 $description = $profile['about'];
583         } else {
584                 $description = $uinfo[0]["about"];
585         }
586
587         if (!empty($usr['default-location'])) {
588                 $location = $usr['default-location'];
589         } elseif (!empty($uinfo[0]["location"])) {
590                 $location = $uinfo[0]["location"];
591         } else {
592                 $location = ContactSelector::networkToName($uinfo[0]['network'], $uinfo[0]['url'], $uinfo[0]['protocol']);
593         }
594
595         $ret = [
596                 'id' => intval($pcontact_id),
597                 'id_str' => (string) intval($pcontact_id),
598                 'name' => (($uinfo[0]['name']) ? $uinfo[0]['name'] : $uinfo[0]['nick']),
599                 'screen_name' => (($uinfo[0]['nick']) ? $uinfo[0]['nick'] : $uinfo[0]['name']),
600                 'location' => $location,
601                 'description' => BBCode::toPlaintext($description ?? ''),
602                 'profile_image_url' => Contact::getAvatarUrlForUrl($uinfo[0]['url'], api_user(), Proxy::SIZE_MICRO),
603                 'profile_image_url_https' => Contact::getAvatarUrlForUrl($uinfo[0]['url'], api_user(), Proxy::SIZE_MICRO),
604                 'profile_image_url_profile_size' => Contact::getAvatarUrlForUrl($uinfo[0]['url'], api_user(), Proxy::SIZE_THUMB),
605                 'profile_image_url_large' => Contact::getAvatarUrlForUrl($uinfo[0]['url'], api_user(), Proxy::SIZE_SMALL),
606                 'url' => $uinfo[0]['url'],
607                 'protected' => false,
608                 'followers_count' => intval($countfollowers),
609                 'friends_count' => intval($countfriends),
610                 'listed_count' => 0,
611                 'created_at' => api_date($uinfo[0]['created']),
612                 'favourites_count' => intval($starred),
613                 'utc_offset' => "0",
614                 'time_zone' => 'UTC',
615                 'geo_enabled' => false,
616                 'verified' => true,
617                 'statuses_count' => intval($countitems),
618                 'lang' => '',
619                 'contributors_enabled' => false,
620                 'is_translator' => false,
621                 'is_translation_enabled' => false,
622                 'following' => (($uinfo[0]['rel'] == Contact::FOLLOWER) || ($uinfo[0]['rel'] == Contact::FRIEND)),
623                 'follow_request_sent' => false,
624                 'statusnet_blocking' => false,
625                 'notifications' => false,
626                 /// @TODO old way?
627                 //'statusnet_profile_url' => DI::baseUrl()."/contact/".$uinfo[0]['cid'],
628                 'statusnet_profile_url' => $uinfo[0]['url'],
629                 'uid' => intval($uinfo[0]['uid']),
630                 'cid' => intval($uinfo[0]['cid']),
631                 'pid' => Contact::getIdForURL($uinfo[0]["url"], 0, false),
632                 'self' => $uinfo[0]['self'],
633                 'network' => $uinfo[0]['network'],
634         ];
635
636         // If this is a local user and it uses Frio, we can get its color preferences.
637         if ($ret['self']) {
638                 $theme_info = DBA::selectFirst('user', ['theme'], ['uid' => $ret['uid']]);
639                 if ($theme_info['theme'] === 'frio') {
640                         $schema = DI::pConfig()->get($ret['uid'], 'frio', 'schema');
641
642                         if ($schema && ($schema != '---')) {
643                                 if (file_exists('view/theme/frio/schema/'.$schema.'.php')) {
644                                         $schemefile = 'view/theme/frio/schema/'.$schema.'.php';
645                                         require_once $schemefile;
646                                 }
647                         } else {
648                                 $nav_bg = DI::pConfig()->get($ret['uid'], 'frio', 'nav_bg');
649                                 $link_color = DI::pConfig()->get($ret['uid'], 'frio', 'link_color');
650                                 $bgcolor = DI::pConfig()->get($ret['uid'], 'frio', 'background_color');
651                         }
652                         if (empty($nav_bg)) {
653                                 $nav_bg = "#708fa0";
654                         }
655                         if (empty($link_color)) {
656                                 $link_color = "#6fdbe8";
657                         }
658                         if (empty($bgcolor)) {
659                                 $bgcolor = "#ededed";
660                         }
661
662                         $ret['profile_sidebar_fill_color'] = str_replace('#', '', $nav_bg);
663                         $ret['profile_link_color'] = str_replace('#', '', $link_color);
664                         $ret['profile_background_color'] = str_replace('#', '', $bgcolor);
665                 }
666         }
667
668         return $ret;
669 }
670
671 /**
672  * return api-formatted array for item's author and owner
673  *
674  * @param App   $a    App
675  * @param array $item item from db
676  * @return array(array:author, array:owner)
677  * @throws BadRequestException
678  * @throws ImagickException
679  * @throws InternalServerErrorException
680  * @throws UnauthorizedException
681  */
682 function api_item_get_user(App $a, $item)
683 {
684         $status_user = api_get_user($item['author-id'] ?? null);
685
686         $author_user = $status_user;
687
688         $status_user["protected"] = isset($item['private']) && ($item['private'] == Item::PRIVATE);
689
690         if (($item['thr-parent'] ?? '') == ($item['uri'] ?? '')) {
691                 $owner_user = api_get_user($item['owner-id'] ?? null);
692         } else {
693                 $owner_user = $author_user;
694         }
695
696         return ([$status_user, $author_user, $owner_user]);
697 }
698
699 /**
700  * TWITTER API
701  */
702
703 /**
704  * Returns an HTTP 200 OK response code and a representation of the requesting user if authentication was successful;
705  * returns a 401 status code and an error message if not.
706  *
707  * @see https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/get-account-verify_credentials
708  *
709  * @param string $type Return type (atom, rss, xml, json)
710  * @return array|string
711  * @throws BadRequestException
712  * @throws ForbiddenException
713  * @throws ImagickException
714  * @throws InternalServerErrorException
715  * @throws UnauthorizedException
716  */
717 function api_account_verify_credentials($type)
718 {
719         $a = DI::app();
720
721         if (api_user() === false) {
722                 throw new ForbiddenException();
723         }
724
725         unset($_REQUEST["user_id"]);
726         unset($_GET["user_id"]);
727
728         unset($_REQUEST["screen_name"]);
729         unset($_GET["screen_name"]);
730
731         $skip_status = $_REQUEST['skip_status'] ?? false;
732
733         $user_info = api_get_user();
734
735         // "verified" isn't used here in the standard
736         unset($user_info["verified"]);
737
738         // - Adding last status
739         if (!$skip_status) {
740                 $item = api_get_last_status($user_info['pid'], $user_info['uid']);
741                 if (!empty($item)) {
742                         $user_info['status'] = api_format_item($item, $type);
743                 }
744         }
745
746         // "uid" and "self" are only needed for some internal stuff, so remove it from here
747         unset($user_info["uid"]);
748         unset($user_info["self"]);
749
750         return BaseApi::formatData("user", $type, ['user' => $user_info]);
751 }
752
753 /// @TODO move to top of file or somewhere better
754 api_register_func('api/account/verify_credentials', 'api_account_verify_credentials', true);
755
756 /**
757  * Get data from $_POST or $_GET
758  *
759  * @param string $k
760  * @return null
761  */
762 function requestdata($k)
763 {
764         if (!empty($_POST[$k])) {
765                 return $_POST[$k];
766         }
767         if (!empty($_GET[$k])) {
768                 return $_GET[$k];
769         }
770         return null;
771 }
772
773 /**
774  * Deprecated function to upload media.
775  *
776  * @param string $type Return type (atom, rss, xml, json)
777  *
778  * @return array|string
779  * @throws BadRequestException
780  * @throws ForbiddenException
781  * @throws ImagickException
782  * @throws InternalServerErrorException
783  * @throws UnauthorizedException
784  */
785 function api_statuses_mediap($type)
786 {
787         $a = DI::app();
788
789         if (api_user() === false) {
790                 logger::notice('api_statuses_update: no user');
791                 throw new ForbiddenException();
792         }
793         $user_info = api_get_user();
794
795         $_REQUEST['profile_uid'] = api_user();
796         $_REQUEST['api_source'] = true;
797         $txt = requestdata('status') ?? '';
798         /// @TODO old-lost code?
799         //$txt = urldecode(requestdata('status'));
800
801         if ((strpos($txt, '<') !== false) || (strpos($txt, '>') !== false)) {
802                 $txt = HTML::toBBCodeVideo($txt);
803                 $config = HTMLPurifier_Config::createDefault();
804                 $config->set('Cache.DefinitionImpl', null);
805                 $purifier = new HTMLPurifier($config);
806                 $txt = $purifier->purify($txt);
807         }
808         $txt = HTML::toBBCode($txt);
809
810         DI::args()->getArgv()[1] = $user_info['screen_name']; //should be set to username?
811
812         $picture = wall_upload_post($a, false);
813
814         // now that we have the img url in bbcode we can add it to the status and insert the wall item.
815         $_REQUEST['body'] = $txt . "\n\n" . '[url=' . $picture["albumpage"] . '][img]' . $picture["preview"] . "[/img][/url]";
816         $item_id = item_post($a);
817
818         // output the post that we just posted.
819         return api_status_show($type, $item_id);
820 }
821
822 /// @TODO move this to top of file or somewhere better!
823 api_register_func('api/statuses/mediap', 'api_statuses_mediap', true, API_METHOD_POST);
824
825 /**
826  * Updates the user’s current status.
827  *
828  * @param string $type Return type (atom, rss, xml, json)
829  *
830  * @return array|string
831  * @throws BadRequestException
832  * @throws ForbiddenException
833  * @throws ImagickException
834  * @throws InternalServerErrorException
835  * @throws TooManyRequestsException
836  * @throws UnauthorizedException
837  * @see https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update
838  */
839 function api_statuses_update($type)
840 {
841         $a = DI::app();
842
843         if (api_user() === false) {
844                 logger::notice('api_statuses_update: no user');
845                 throw new ForbiddenException();
846         }
847
848         api_get_user();
849
850         // convert $_POST array items to the form we use for web posts.
851         if (requestdata('htmlstatus')) {
852                 $txt = requestdata('htmlstatus') ?? '';
853                 if ((strpos($txt, '<') !== false) || (strpos($txt, '>') !== false)) {
854                         $txt = HTML::toBBCodeVideo($txt);
855
856                         $config = HTMLPurifier_Config::createDefault();
857                         $config->set('Cache.DefinitionImpl', null);
858
859                         $purifier = new HTMLPurifier($config);
860                         $txt = $purifier->purify($txt);
861
862                         $_REQUEST['body'] = HTML::toBBCode($txt);
863                 }
864         } else {
865                 $_REQUEST['body'] = requestdata('status');
866         }
867
868         $_REQUEST['title'] = requestdata('title');
869
870         $parent = requestdata('in_reply_to_status_id');
871
872         // Twidere sends "-1" if it is no reply ...
873         if ($parent == -1) {
874                 $parent = "";
875         }
876
877         if (ctype_digit($parent)) {
878                 $_REQUEST['parent'] = $parent;
879         } else {
880                 $_REQUEST['parent_uri'] = $parent;
881         }
882
883         if (requestdata('lat') && requestdata('long')) {
884                 $_REQUEST['coord'] = sprintf("%s %s", requestdata('lat'), requestdata('long'));
885         }
886         $_REQUEST['profile_uid'] = api_user();
887
888         if (!$parent) {
889                 // Check for throttling (maximum posts per day, week and month)
890                 $throttle_day = DI::config()->get('system', 'throttle_limit_day');
891                 if ($throttle_day > 0) {
892                         $datefrom = date(DateTimeFormat::MYSQL, time() - 24*60*60);
893
894                         $condition = ["`gravity` = ? AND `uid` = ? AND `wall` AND `received` > ?", GRAVITY_PARENT, api_user(), $datefrom];
895                         $posts_day = Post::count($condition);
896
897                         if ($posts_day > $throttle_day) {
898                                 logger::info('Daily posting limit reached for user '.api_user());
899                                 // die(api_error($type, DI::l10n()->t("Daily posting limit of %d posts reached. The post was rejected.", $throttle_day));
900                                 throw new TooManyRequestsException(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));
901                         }
902                 }
903
904                 $throttle_week = DI::config()->get('system', 'throttle_limit_week');
905                 if ($throttle_week > 0) {
906                         $datefrom = date(DateTimeFormat::MYSQL, time() - 24*60*60*7);
907
908                         $condition = ["`gravity` = ? AND `uid` = ? AND `wall` AND `received` > ?", GRAVITY_PARENT, api_user(), $datefrom];
909                         $posts_week = Post::count($condition);
910
911                         if ($posts_week > $throttle_week) {
912                                 logger::info('Weekly posting limit reached for user '.api_user());
913                                 // die(api_error($type, DI::l10n()->t("Weekly posting limit of %d posts reached. The post was rejected.", $throttle_week)));
914                                 throw new TooManyRequestsException(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));
915                         }
916                 }
917
918                 $throttle_month = DI::config()->get('system', 'throttle_limit_month');
919                 if ($throttle_month > 0) {
920                         $datefrom = date(DateTimeFormat::MYSQL, time() - 24*60*60*30);
921
922                         $condition = ["`gravity` = ? AND `uid` = ? AND `wall` AND `received` > ?", GRAVITY_PARENT, api_user(), $datefrom];
923                         $posts_month = Post::count($condition);
924
925                         if ($posts_month > $throttle_month) {
926                                 logger::info('Monthly posting limit reached for user '.api_user());
927                                 // die(api_error($type, DI::l10n()->t("Monthly posting limit of %d posts reached. The post was rejected.", $throttle_month));
928                                 throw new TooManyRequestsException(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));
929                         }
930                 }
931         }
932
933         if (requestdata('media_ids')) {
934                 $ids = explode(',', requestdata('media_ids') ?? '');
935         } elseif (!empty($_FILES['media'])) {
936                 // upload the image if we have one
937                 $picture = wall_upload_post($a, false);
938                 if (is_array($picture)) {
939                         $ids[] = $picture['id'];
940                 }
941         }
942
943         $attachments = [];
944         $ressources = [];
945
946         if (!empty($ids)) {
947                 foreach ($ids as $id) {
948                         $media = DBA::toArray(DBA::p("SELECT `resource-id`, `scale`, `nickname`, `type`, `desc`, `filename`, `datasize`, `width`, `height` FROM `photo`
949                                         INNER JOIN `user` ON `user`.`uid` = `photo`.`uid` WHERE `resource-id` IN
950                                                 (SELECT `resource-id` FROM `photo` WHERE `id` = ?) AND `photo`.`uid` = ?
951                                         ORDER BY `photo`.`width` DESC LIMIT 2", $id, api_user()));
952
953                         if (!empty($media)) {
954                                 $ressources[] = $media[0]['resource-id'];
955                                 $phototypes = Images::supportedTypes();
956                                 $ext = $phototypes[$media[0]['type']];
957
958                                 $attachment = ['type' => Post\Media::IMAGE, 'mimetype' => $media[0]['type'],
959                                         'url' => DI::baseUrl() . '/photo/' . $media[0]['resource-id'] . '-' . $media[0]['scale'] . '.' . $ext,
960                                         'size' => $media[0]['datasize'],
961                                         'name' => $media[0]['filename'] ?: $media[0]['resource-id'],
962                                         'description' => $media[0]['desc'] ?? '',
963                                         'width' => $media[0]['width'],
964                                         'height' => $media[0]['height']];
965
966                                 if (count($media) > 1) {
967                                         $attachment['preview'] = DI::baseUrl() . '/photo/' . $media[1]['resource-id'] . '-' . $media[1]['scale'] . '.' . $ext;
968                                         $attachment['preview-width'] = $media[1]['width'];
969                                         $attachment['preview-height'] = $media[1]['height'];
970                                 }
971                                 $attachments[] = $attachment;
972                         }
973                 }
974
975                 // We have to avoid that the post is rejected because of an empty body
976                 if (empty($_REQUEST['body'])) {
977                         $_REQUEST['body'] = '[hr]';
978                 }
979         }
980
981         if (!empty($attachments)) {
982                 $_REQUEST['attachments'] = $attachments;
983         }
984
985         // set this so that the item_post() function is quiet and doesn't redirect or emit json
986
987         $_REQUEST['api_source'] = true;
988
989         if (empty($_REQUEST['source'])) {
990                 $_REQUEST["source"] = api_source();
991         }
992
993         // call out normal post function
994         $item_id = item_post($a);
995
996         if (!empty($ressources) && !empty($item_id)) {
997                 $item = Post::selectFirst(['uri-id', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid'], ['id' => $item_id]);
998                 foreach ($ressources as $ressource) {
999                         Photo::setPermissionForRessource($ressource, api_user(), $item['allow_cid'], $item['allow_gid'], $item['deny_cid'], $item['deny_gid']);
1000                 }
1001         }
1002
1003         // output the post that we just posted.
1004         return api_status_show($type, $item_id);
1005 }
1006
1007 /// @TODO move to top of file or somewhere better
1008 api_register_func('api/statuses/update', 'api_statuses_update', true, API_METHOD_POST);
1009 api_register_func('api/statuses/update_with_media', 'api_statuses_update', true, API_METHOD_POST);
1010
1011 /**
1012  * Uploads an image to Friendica.
1013  *
1014  * @return array
1015  * @throws BadRequestException
1016  * @throws ForbiddenException
1017  * @throws ImagickException
1018  * @throws InternalServerErrorException
1019  * @throws UnauthorizedException
1020  * @see https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-upload
1021  */
1022 function api_media_upload()
1023 {
1024         $a = DI::app();
1025
1026         if (api_user() === false) {
1027                 logger::notice('no user');
1028                 throw new ForbiddenException();
1029         }
1030
1031         api_get_user();
1032
1033         if (empty($_FILES['media'])) {
1034                 // Output error
1035                 throw new BadRequestException("No media.");
1036         }
1037
1038         $media = wall_upload_post($a, false);
1039         if (!$media) {
1040                 // Output error
1041                 throw new InternalServerErrorException();
1042         }
1043
1044         $returndata = [];
1045         $returndata["media_id"] = $media["id"];
1046         $returndata["media_id_string"] = (string)$media["id"];
1047         $returndata["size"] = $media["size"];
1048         $returndata["image"] = ["w" => $media["width"],
1049                                 "h" => $media["height"],
1050                                 "image_type" => $media["type"],
1051                                 "friendica_preview_url" => $media["preview"]];
1052
1053         Logger::info('Media uploaded', ['return' => $returndata]);
1054
1055         return ["media" => $returndata];
1056 }
1057
1058 /// @TODO move to top of file or somewhere better
1059 api_register_func('api/media/upload', 'api_media_upload', true, API_METHOD_POST);
1060
1061 /**
1062  * Updates media meta data (picture descriptions)
1063  *
1064  * @param string $type Return type (atom, rss, xml, json)
1065  *
1066  * @return array|string
1067  * @throws BadRequestException
1068  * @throws ForbiddenException
1069  * @throws ImagickException
1070  * @throws InternalServerErrorException
1071  * @throws TooManyRequestsException
1072  * @throws UnauthorizedException
1073  * @see https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update
1074  *
1075  * @todo Compare the corresponding Twitter function for correct return values
1076  */
1077 function api_media_metadata_create($type)
1078 {
1079         $a = DI::app();
1080
1081         if (api_user() === false) {
1082                 Logger::info('no user');
1083                 throw new ForbiddenException();
1084         }
1085
1086         api_get_user();
1087
1088         $postdata = Network::postdata();
1089
1090         if (empty($postdata)) {
1091                 throw new BadRequestException("No post data");
1092         }
1093
1094         $data = json_decode($postdata, true);
1095         if (empty($data)) {
1096                 throw new BadRequestException("Invalid post data");
1097         }
1098
1099         if (empty($data['media_id']) || empty($data['alt_text'])) {
1100                 throw new BadRequestException("Missing post data values");
1101         }
1102
1103         if (empty($data['alt_text']['text'])) {
1104                 throw new BadRequestException("No alt text.");
1105         }
1106
1107         Logger::info('Updating metadata', ['media_id' => $data['media_id']]);
1108
1109         $condition =  ['id' => $data['media_id'], 'uid' => api_user()];
1110         $photo = DBA::selectFirst('photo', ['resource-id'], $condition);
1111         if (!DBA::isResult($photo)) {
1112                 throw new BadRequestException("Metadata not found.");
1113         }
1114
1115         DBA::update('photo', ['desc' => $data['alt_text']['text']], ['resource-id' => $photo['resource-id']]);
1116 }
1117
1118 api_register_func('api/media/metadata/create', 'api_media_metadata_create', true, API_METHOD_POST);
1119
1120 /**
1121  * @param string $type    Return format (atom, rss, xml, json)
1122  * @param int    $item_id
1123  * @return array|string
1124  * @throws Exception
1125  */
1126 function api_status_show($type, $item_id)
1127 {
1128         Logger::info(API_LOG_PREFIX . 'Start', ['action' => 'status_show', 'type' => $type, 'item_id' => $item_id]);
1129
1130         $status_info = [];
1131
1132         $item = api_get_item(['id' => $item_id]);
1133         if (!empty($item)) {
1134                 $status_info = api_format_item($item, $type);
1135         }
1136
1137         Logger::info(API_LOG_PREFIX . 'End', ['action' => 'get_status', 'status_info' => $status_info]);
1138
1139         return BaseApi::formatData('statuses', $type, ['status' => $status_info]);
1140 }
1141
1142 /**
1143  * Retrieves the last public status of the provided user info
1144  *
1145  * @param int    $ownerId Public contact Id
1146  * @param int    $uid     User Id
1147  * @return array
1148  * @throws Exception
1149  */
1150 function api_get_last_status($ownerId, $uid)
1151 {
1152         $condition = [
1153                 'author-id'=> $ownerId,
1154                 'uid'      => $uid,
1155                 'gravity'  => [GRAVITY_PARENT, GRAVITY_COMMENT],
1156                 'private'  => [Item::PUBLIC, Item::UNLISTED]
1157         ];
1158
1159         $item = api_get_item($condition);
1160
1161         return $item;
1162 }
1163
1164 /**
1165  * Retrieves a single item record based on the provided condition and converts it for API use.
1166  *
1167  * @param array $condition Item table condition array
1168  * @return array
1169  * @throws Exception
1170  */
1171 function api_get_item(array $condition)
1172 {
1173         $item = Post::selectFirst(Item::DISPLAY_FIELDLIST, $condition, ['order' => ['id' => true]]);
1174
1175         return $item;
1176 }
1177
1178 /**
1179  * Returns extended information of a given user, specified by ID or screen name as per the required id parameter.
1180  * The author's most recent status will be returned inline.
1181  *
1182  * @param string $type Return type (atom, rss, xml, json)
1183  * @return array|string
1184  * @throws BadRequestException
1185  * @throws ImagickException
1186  * @throws InternalServerErrorException
1187  * @throws UnauthorizedException
1188  * @see https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-show
1189  */
1190 function api_users_show($type)
1191 {
1192         $a = Friendica\DI::app();
1193
1194         $user_info = api_get_user();
1195
1196         $item = api_get_last_status($user_info['pid'], $user_info['uid']);
1197         if (!empty($item)) {
1198                 $user_info['status'] = api_format_item($item, $type);
1199         }
1200
1201         // "uid" and "self" are only needed for some internal stuff, so remove it from here
1202         unset($user_info['uid']);
1203         unset($user_info['self']);
1204
1205         return BaseApi::formatData('user', $type, ['user' => $user_info]);
1206 }
1207
1208 /// @TODO move to top of file or somewhere better
1209 api_register_func('api/users/show', 'api_users_show');
1210 api_register_func('api/externalprofile/show', 'api_users_show');
1211
1212 /**
1213  * Search a public user account.
1214  *
1215  * @param string $type Return type (atom, rss, xml, json)
1216  *
1217  * @return array|string
1218  * @throws BadRequestException
1219  * @throws ImagickException
1220  * @throws InternalServerErrorException
1221  * @throws UnauthorizedException
1222  * @see https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-search
1223  */
1224 function api_users_search($type)
1225 {
1226         $a = DI::app();
1227
1228         $userlist = [];
1229
1230         if (!empty($_GET['q'])) {
1231                 $contacts = Contact::selectToArray(
1232                         ['id'],
1233                         [
1234                                 '`uid` = 0 AND (`name` = ? OR `nick` = ? OR `url` = ? OR `addr` = ?)',
1235                                 $_GET['q'],
1236                                 $_GET['q'],
1237                                 $_GET['q'],
1238                                 $_GET['q'],
1239                         ]
1240                 );
1241
1242                 if (DBA::isResult($contacts)) {
1243                         $k = 0;
1244                         foreach ($contacts as $contact) {
1245                                 $user_info = api_get_user($contact['id']);
1246
1247                                 if ($type == 'xml') {
1248                                         $userlist[$k++ . ':user'] = $user_info;
1249                                 } else {
1250                                         $userlist[] = $user_info;
1251                                 }
1252                         }
1253                         $userlist = ['users' => $userlist];
1254                 } else {
1255                         throw new NotFoundException('User ' . $_GET['q'] . ' not found.');
1256                 }
1257         } else {
1258                 throw new BadRequestException('No search term specified.');
1259         }
1260
1261         return BaseApi::formatData('users', $type, $userlist);
1262 }
1263
1264 /// @TODO move to top of file or somewhere better
1265 api_register_func('api/users/search', 'api_users_search');
1266
1267 /**
1268  * Return user objects
1269  *
1270  * @see https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-lookup
1271  *
1272  * @param string $type Return format: json or xml
1273  *
1274  * @return array|string
1275  * @throws BadRequestException
1276  * @throws ImagickException
1277  * @throws InternalServerErrorException
1278  * @throws NotFoundException if the results are empty.
1279  * @throws UnauthorizedException
1280  */
1281 function api_users_lookup($type)
1282 {
1283         $users = [];
1284
1285         if (!empty($_REQUEST['user_id'])) {
1286                 foreach (explode(',', $_REQUEST['user_id']) as $id) {
1287                         if (!empty($id)) {
1288                                 $users[] = api_get_user($id);
1289                         }
1290                 }
1291         }
1292
1293         if (empty($users)) {
1294                 throw new NotFoundException;
1295         }
1296
1297         return BaseApi::formatData("users", $type, ['users' => $users]);
1298 }
1299
1300 /// @TODO move to top of file or somewhere better
1301 api_register_func('api/users/lookup', 'api_users_lookup', true);
1302
1303 /**
1304  * Returns statuses that match a specified query.
1305  *
1306  * @see https://developer.twitter.com/en/docs/tweets/search/api-reference/get-search-tweets
1307  *
1308  * @param string $type Return format: json, xml, atom, rss
1309  *
1310  * @return array|string
1311  * @throws BadRequestException if the "q" parameter is missing.
1312  * @throws ForbiddenException
1313  * @throws ImagickException
1314  * @throws InternalServerErrorException
1315  * @throws UnauthorizedException
1316  */
1317 function api_search($type)
1318 {
1319         $a = DI::app();
1320         $user_info = api_get_user();
1321
1322         if (api_user() === false || $user_info === false) {
1323                 throw new ForbiddenException();
1324         }
1325
1326         if (empty($_REQUEST['q'])) {
1327                 throw new BadRequestException('q parameter is required.');
1328         }
1329
1330         $searchTerm = trim(rawurldecode($_REQUEST['q']));
1331
1332         $data = [];
1333         $data['status'] = [];
1334         $count = 15;
1335         $exclude_replies = !empty($_REQUEST['exclude_replies']);
1336         if (!empty($_REQUEST['rpp'])) {
1337                 $count = $_REQUEST['rpp'];
1338         } elseif (!empty($_REQUEST['count'])) {
1339                 $count = $_REQUEST['count'];
1340         }
1341
1342         $since_id = $_REQUEST['since_id'] ?? 0;
1343         $max_id = $_REQUEST['max_id'] ?? 0;
1344         $page = $_REQUEST['page'] ?? 1;
1345
1346         $start = max(0, ($page - 1) * $count);
1347
1348         $params = ['order' => ['id' => true], 'limit' => [$start, $count]];
1349         if (preg_match('/^#(\w+)$/', $searchTerm, $matches) === 1 && isset($matches[1])) {
1350                 $searchTerm = $matches[1];
1351                 $condition = ["`iid` > ? AND `name` = ? AND (NOT `private` OR (`private` AND `uid` = ?))", $since_id, $searchTerm, local_user()];
1352                 $tags = DBA::select('tag-search-view', ['uri-id'], $condition);
1353                 $uriids = [];
1354                 while ($tag = DBA::fetch($tags)) {
1355                         $uriids[] = $tag['uri-id'];
1356                 }
1357                 DBA::close($tags);
1358
1359                 if (empty($uriids)) {
1360                         return BaseApi::formatData('statuses', $type, $data);
1361                 }
1362
1363                 $condition = ['uri-id' => $uriids];
1364                 if ($exclude_replies) {
1365                         $condition['gravity'] = GRAVITY_PARENT;
1366                 }
1367
1368                 $params['group_by'] = ['uri-id'];
1369         } else {
1370                 $condition = ["`id` > ?
1371                         " . ($exclude_replies ? " AND `gravity` = " . GRAVITY_PARENT : ' ') . "
1372                         AND (`uid` = 0 OR (`uid` = ? AND NOT `global`))
1373                         AND `body` LIKE CONCAT('%',?,'%')",
1374                         $since_id, api_user(), $_REQUEST['q']];
1375                 if ($max_id > 0) {
1376                         $condition[0] .= ' AND `id` <= ?';
1377                         $condition[] = $max_id;
1378                 }
1379         }
1380
1381         $statuses = [];
1382
1383         if (parse_url($searchTerm, PHP_URL_SCHEME) != '') {
1384                 $id = Item::fetchByLink($searchTerm, api_user());
1385                 if (!$id) {
1386                         // Public post
1387                         $id = Item::fetchByLink($searchTerm);
1388                 }
1389
1390                 if (!empty($id)) {
1391                         $statuses = Post::select([], ['id' => $id]);
1392                 }
1393         }
1394
1395         $statuses = $statuses ?: Post::selectForUser(api_user(), [], $condition, $params);
1396
1397         $data['status'] = api_format_items(Post::toArray($statuses), $user_info);
1398
1399         bindComments($data['status']);
1400
1401         return BaseApi::formatData('statuses', $type, $data);
1402 }
1403
1404 /// @TODO move to top of file or somewhere better
1405 api_register_func('api/search/tweets', 'api_search', true);
1406 api_register_func('api/search', 'api_search', true);
1407
1408 /**
1409  * Returns the most recent statuses posted by the user and the users they follow.
1410  *
1411  * @see  https://developer.twitter.com/en/docs/tweets/timelines/api-reference/get-statuses-home_timeline
1412  *
1413  * @param string $type Return type (atom, rss, xml, json)
1414  *
1415  * @return array|string
1416  * @throws BadRequestException
1417  * @throws ForbiddenException
1418  * @throws ImagickException
1419  * @throws InternalServerErrorException
1420  * @throws UnauthorizedException
1421  * @todo Optional parameters
1422  * @todo Add reply info
1423  */
1424 function api_statuses_home_timeline($type)
1425 {
1426         $a = DI::app();
1427         $user_info = api_get_user();
1428
1429         if (api_user() === false || $user_info === false) {
1430                 throw new ForbiddenException();
1431         }
1432
1433         unset($_REQUEST["user_id"]);
1434         unset($_GET["user_id"]);
1435
1436         unset($_REQUEST["screen_name"]);
1437         unset($_GET["screen_name"]);
1438
1439         // get last network messages
1440
1441         // params
1442         $count = $_REQUEST['count'] ?? 20;
1443         $page = $_REQUEST['page']?? 0;
1444         $since_id = $_REQUEST['since_id'] ?? 0;
1445         $max_id = $_REQUEST['max_id'] ?? 0;
1446         $exclude_replies = !empty($_REQUEST['exclude_replies']);
1447         $conversation_id = $_REQUEST['conversation_id'] ?? 0;
1448
1449         $start = max(0, ($page - 1) * $count);
1450
1451         $condition = ["`uid` = ? AND `gravity` IN (?, ?) AND `id` > ?",
1452                 api_user(), GRAVITY_PARENT, GRAVITY_COMMENT, $since_id];
1453
1454         if ($max_id > 0) {
1455                 $condition[0] .= " AND `id` <= ?";
1456                 $condition[] = $max_id;
1457         }
1458         if ($exclude_replies) {
1459                 $condition[0] .= ' AND `gravity` = ?';
1460                 $condition[] = GRAVITY_PARENT;
1461         }
1462         if ($conversation_id > 0) {
1463                 $condition[0] .= " AND `parent` = ?";
1464                 $condition[] = $conversation_id;
1465         }
1466
1467         $params = ['order' => ['id' => true], 'limit' => [$start, $count]];
1468         $statuses = Post::selectForUser(api_user(), [], $condition, $params);
1469
1470         $items = Post::toArray($statuses);
1471
1472         $ret = api_format_items($items, $user_info, false, $type);
1473
1474         // Set all posts from the query above to seen
1475         $idarray = [];
1476         foreach ($items as $item) {
1477                 $idarray[] = intval($item["id"]);
1478         }
1479
1480         if (!empty($idarray)) {
1481                 $unseen = Post::exists(['unseen' => true, 'id' => $idarray]);
1482                 if ($unseen) {
1483                         Item::update(['unseen' => false], ['unseen' => true, 'id' => $idarray]);
1484                 }
1485         }
1486
1487         bindComments($ret);
1488
1489         $data = ['status' => $ret];
1490         switch ($type) {
1491                 case "atom":
1492                         break;
1493                 case "rss":
1494                         $data = api_rss_extra($a, $data, $user_info);
1495                         break;
1496         }
1497
1498         return BaseApi::formatData("statuses", $type, $data);
1499 }
1500
1501
1502 /// @TODO move to top of file or somewhere better
1503 api_register_func('api/statuses/home_timeline', 'api_statuses_home_timeline', true);
1504 api_register_func('api/statuses/friends_timeline', 'api_statuses_home_timeline', true);
1505
1506 /**
1507  * Returns the most recent statuses from public users.
1508  *
1509  * @param string $type Return type (atom, rss, xml, json)
1510  *
1511  * @return array|string
1512  * @throws BadRequestException
1513  * @throws ForbiddenException
1514  * @throws ImagickException
1515  * @throws InternalServerErrorException
1516  * @throws UnauthorizedException
1517  */
1518 function api_statuses_public_timeline($type)
1519 {
1520         $a = DI::app();
1521         $user_info = api_get_user();
1522
1523         if (api_user() === false || $user_info === false) {
1524                 throw new ForbiddenException();
1525         }
1526
1527         // get last network messages
1528
1529         // params
1530         $count = $_REQUEST['count'] ?? 20;
1531         $page = $_REQUEST['page'] ?? 1;
1532         $since_id = $_REQUEST['since_id'] ?? 0;
1533         $max_id = $_REQUEST['max_id'] ?? 0;
1534         $exclude_replies = (!empty($_REQUEST['exclude_replies']) ? 1 : 0);
1535         $conversation_id = $_REQUEST['conversation_id'] ?? 0;
1536
1537         $start = max(0, ($page - 1) * $count);
1538
1539         if ($exclude_replies && !$conversation_id) {
1540                 $condition = ["`gravity` = ? AND `id` > ? AND `private` = ? AND `wall` AND NOT `author-hidden`",
1541                         GRAVITY_PARENT, $since_id, Item::PUBLIC];
1542
1543                 if ($max_id > 0) {
1544                         $condition[0] .= " AND `id` <= ?";
1545                         $condition[] = $max_id;
1546                 }
1547
1548                 $params = ['order' => ['id' => true], 'limit' => [$start, $count]];
1549                 $statuses = Post::selectForUser(api_user(), [], $condition, $params);
1550
1551                 $r = Post::toArray($statuses);
1552         } else {
1553                 $condition = ["`gravity` IN (?, ?) AND `id` > ? AND `private` = ? AND `wall` AND `origin` AND NOT `author-hidden`",
1554                         GRAVITY_PARENT, GRAVITY_COMMENT, $since_id, Item::PUBLIC];
1555
1556                 if ($max_id > 0) {
1557                         $condition[0] .= " AND `id` <= ?";
1558                         $condition[] = $max_id;
1559                 }
1560                 if ($conversation_id > 0) {
1561                         $condition[0] .= " AND `parent` = ?";
1562                         $condition[] = $conversation_id;
1563                 }
1564
1565                 $params = ['order' => ['id' => true], 'limit' => [$start, $count]];
1566                 $statuses = Post::selectForUser(api_user(), [], $condition, $params);
1567
1568                 $r = Post::toArray($statuses);
1569         }
1570
1571         $ret = api_format_items($r, $user_info, false, $type);
1572
1573         bindComments($ret);
1574
1575         $data = ['status' => $ret];
1576         switch ($type) {
1577                 case "atom":
1578                         break;
1579                 case "rss":
1580                         $data = api_rss_extra($a, $data, $user_info);
1581                         break;
1582         }
1583
1584         return BaseApi::formatData("statuses", $type, $data);
1585 }
1586
1587 /// @TODO move to top of file or somewhere better
1588 api_register_func('api/statuses/public_timeline', 'api_statuses_public_timeline', true);
1589
1590 /**
1591  * Returns the most recent statuses posted by users this node knows about.
1592  *
1593  * @param string $type Return format: json, xml, atom, rss
1594  * @return array|string
1595  * @throws BadRequestException
1596  * @throws ForbiddenException
1597  * @throws ImagickException
1598  * @throws InternalServerErrorException
1599  * @throws UnauthorizedException
1600  */
1601 function api_statuses_networkpublic_timeline($type)
1602 {
1603         $a = DI::app();
1604         $user_info = api_get_user();
1605
1606         if (api_user() === false || $user_info === false) {
1607                 throw new ForbiddenException();
1608         }
1609
1610         $since_id        = $_REQUEST['since_id'] ?? 0;
1611         $max_id          = $_REQUEST['max_id'] ?? 0;
1612
1613         // pagination
1614         $count = $_REQUEST['count'] ?? 20;
1615         $page  = $_REQUEST['page'] ?? 1;
1616
1617         $start = max(0, ($page - 1) * $count);
1618
1619         $condition = ["`uid` = 0 AND `gravity` IN (?, ?) AND `id` > ? AND `private` = ?",
1620                 GRAVITY_PARENT, GRAVITY_COMMENT, $since_id, Item::PUBLIC];
1621
1622         if ($max_id > 0) {
1623                 $condition[0] .= " AND `id` <= ?";
1624                 $condition[] = $max_id;
1625         }
1626
1627         $params = ['order' => ['id' => true], 'limit' => [$start, $count]];
1628         $statuses = Post::toArray(Post::selectForUser(api_user(), Item::DISPLAY_FIELDLIST, $condition, $params));
1629
1630         $ret = api_format_items($statuses, $user_info, false, $type);
1631
1632         bindComments($ret);
1633
1634         $data = ['status' => $ret];
1635         switch ($type) {
1636                 case "atom":
1637                         break;
1638                 case "rss":
1639                         $data = api_rss_extra($a, $data, $user_info);
1640                         break;
1641         }
1642
1643         return BaseApi::formatData("statuses", $type, $data);
1644 }
1645
1646 /// @TODO move to top of file or somewhere better
1647 api_register_func('api/statuses/networkpublic_timeline', 'api_statuses_networkpublic_timeline', true);
1648
1649 /**
1650  * Returns a single status.
1651  *
1652  * @param string $type Return type (atom, rss, xml, json)
1653  *
1654  * @return array|string
1655  * @throws BadRequestException
1656  * @throws ForbiddenException
1657  * @throws ImagickException
1658  * @throws InternalServerErrorException
1659  * @throws UnauthorizedException
1660  * @see https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-show-id
1661  */
1662 function api_statuses_show($type)
1663 {
1664         $a = DI::app();
1665         $user_info = api_get_user();
1666
1667         if (api_user() === false || $user_info === false) {
1668                 throw new ForbiddenException();
1669         }
1670
1671         // params
1672         $id = intval(DI::args()->getArgv()[3] ?? 0);
1673
1674         if ($id == 0) {
1675                 $id = intval($_REQUEST['id'] ?? 0);
1676         }
1677
1678         // Hotot workaround
1679         if ($id == 0) {
1680                 $id = intval(DI::args()->getArgv()[4] ?? 0);
1681         }
1682
1683         logger::notice('API: api_statuses_show: ' . $id);
1684
1685         $conversation = !empty($_REQUEST['conversation']);
1686
1687         // try to fetch the item for the local user - or the public item, if there is no local one
1688         $uri_item = Post::selectFirst(['uri-id'], ['id' => $id]);
1689         if (!DBA::isResult($uri_item)) {
1690                 throw new BadRequestException(sprintf("There is no status with the id %d", $id));
1691         }
1692
1693         $item = Post::selectFirst(['id'], ['uri-id' => $uri_item['uri-id'], 'uid' => [0, api_user()]], ['order' => ['uid' => true]]);
1694         if (!DBA::isResult($item)) {
1695                 throw new BadRequestException(sprintf("There is no status with the uri-id %d for the given user.", $uri_item['uri-id']));
1696         }
1697
1698         $id = $item['id'];
1699
1700         if ($conversation) {
1701                 $condition = ['parent' => $id, 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT]];
1702                 $params = ['order' => ['id' => true]];
1703         } else {
1704                 $condition = ['id' => $id, 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT]];
1705                 $params = [];
1706         }
1707
1708         $statuses = Post::selectForUser(api_user(), [], $condition, $params);
1709
1710         /// @TODO How about copying this to above methods which don't check $r ?
1711         if (!DBA::isResult($statuses)) {
1712                 throw new BadRequestException(sprintf("There is no status or conversation with the id %d.", $id));
1713         }
1714
1715         $ret = api_format_items(Post::toArray($statuses), $user_info, false, $type);
1716
1717         if ($conversation) {
1718                 $data = ['status' => $ret];
1719                 return BaseApi::formatData("statuses", $type, $data);
1720         } else {
1721                 $data = ['status' => $ret[0]];
1722                 return BaseApi::formatData("status", $type, $data);
1723         }
1724 }
1725
1726 /// @TODO move to top of file or somewhere better
1727 api_register_func('api/statuses/show', 'api_statuses_show', true);
1728
1729 /**
1730  *
1731  * @param string $type Return type (atom, rss, xml, json)
1732  *
1733  * @return array|string
1734  * @throws BadRequestException
1735  * @throws ForbiddenException
1736  * @throws ImagickException
1737  * @throws InternalServerErrorException
1738  * @throws UnauthorizedException
1739  * @todo nothing to say?
1740  */
1741 function api_conversation_show($type)
1742 {
1743         $a = DI::app();
1744         $user_info = api_get_user();
1745
1746         if (api_user() === false || $user_info === false) {
1747                 throw new ForbiddenException();
1748         }
1749
1750         // params
1751         $id       = intval(DI::args()->getArgv()[3]           ?? 0);
1752         $since_id = intval($_REQUEST['since_id'] ?? 0);
1753         $max_id   = intval($_REQUEST['max_id']   ?? 0);
1754         $count    = intval($_REQUEST['count']    ?? 20);
1755         $page     = intval($_REQUEST['page']     ?? 1);
1756
1757         $start = max(0, ($page - 1) * $count);
1758
1759         if ($id == 0) {
1760                 $id = intval($_REQUEST['id'] ?? 0);
1761         }
1762
1763         // Hotot workaround
1764         if ($id == 0) {
1765                 $id = intval(DI::args()->getArgv()[4] ?? 0);
1766         }
1767
1768         Logger::info(API_LOG_PREFIX . '{subaction}', ['module' => 'api', 'action' => 'conversation', 'subaction' => 'show', 'id' => $id]);
1769
1770         // try to fetch the item for the local user - or the public item, if there is no local one
1771         $item = Post::selectFirst(['parent-uri-id'], ['id' => $id]);
1772         if (!DBA::isResult($item)) {
1773                 throw new BadRequestException("There is no status with the id $id.");
1774         }
1775
1776         $parent = Post::selectFirst(['id'], ['uri-id' => $item['parent-uri-id'], 'uid' => [0, api_user()]], ['order' => ['uid' => true]]);
1777         if (!DBA::isResult($parent)) {
1778                 throw new BadRequestException("There is no status with this id.");
1779         }
1780
1781         $id = $parent['id'];
1782
1783         $condition = ["`parent` = ? AND `uid` IN (0, ?) AND `gravity` IN (?, ?) AND `id` > ?",
1784                 $id, api_user(), GRAVITY_PARENT, GRAVITY_COMMENT, $since_id];
1785
1786         if ($max_id > 0) {
1787                 $condition[0] .= " AND `id` <= ?";
1788                 $condition[] = $max_id;
1789         }
1790
1791         $params = ['order' => ['id' => true], 'limit' => [$start, $count]];
1792         $statuses = Post::selectForUser(api_user(), [], $condition, $params);
1793
1794         if (!DBA::isResult($statuses)) {
1795                 throw new BadRequestException("There is no status with id $id.");
1796         }
1797
1798         $ret = api_format_items(Post::toArray($statuses), $user_info, false, $type);
1799
1800         $data = ['status' => $ret];
1801         return BaseApi::formatData("statuses", $type, $data);
1802 }
1803
1804 /// @TODO move to top of file or somewhere better
1805 api_register_func('api/conversation/show', 'api_conversation_show', true);
1806 api_register_func('api/statusnet/conversation', 'api_conversation_show', true);
1807
1808 /**
1809  * Repeats a status.
1810  *
1811  * @param string $type Return type (atom, rss, xml, json)
1812  *
1813  * @return array|string
1814  * @throws BadRequestException
1815  * @throws ForbiddenException
1816  * @throws ImagickException
1817  * @throws InternalServerErrorException
1818  * @throws UnauthorizedException
1819  * @see https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-retweet-id
1820  */
1821 function api_statuses_repeat($type)
1822 {
1823         global $called_api;
1824
1825         $a = DI::app();
1826
1827         if (api_user() === false) {
1828                 throw new ForbiddenException();
1829         }
1830
1831         api_get_user();
1832
1833         // params
1834         $id = intval(DI::args()->getArgv()[3] ?? 0);
1835
1836         if ($id == 0) {
1837                 $id = intval($_REQUEST['id'] ?? 0);
1838         }
1839
1840         // Hotot workaround
1841         if ($id == 0) {
1842                 $id = intval(DI::args()->getArgv()[4] ?? 0);
1843         }
1844
1845         logger::notice('API: api_statuses_repeat: ' . $id);
1846
1847         $fields = ['uri-id', 'network', 'body', 'title', 'author-name', 'author-link', 'author-avatar', 'guid', 'created', 'plink'];
1848         $item = Post::selectFirst($fields, ['id' => $id, 'private' => [Item::PUBLIC, Item::UNLISTED]]);
1849
1850         if (DBA::isResult($item) && !empty($item['body'])) {
1851                 if (in_array($item['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::TWITTER])) {
1852                         if (!Item::performActivity($id, 'announce', local_user())) {
1853                                 throw new InternalServerErrorException();
1854                         }
1855
1856                         $item_id = $id;
1857                 } else {
1858                         if (strpos($item['body'], "[/share]") !== false) {
1859                                 $pos = strpos($item['body'], "[share");
1860                                 $post = substr($item['body'], $pos);
1861                         } else {
1862                                 $post = BBCode::getShareOpeningTag($item['author-name'], $item['author-link'], $item['author-avatar'], $item['plink'], $item['created'], $item['guid']);
1863
1864                                 if (!empty($item['title'])) {
1865                                         $post .= '[h3]' . $item['title'] . "[/h3]\n";
1866                                 }
1867
1868                                 $post .= $item['body'];
1869                                 $post .= "[/share]";
1870                         }
1871                         $_REQUEST['body'] = $post;
1872                         $_REQUEST['profile_uid'] = api_user();
1873                         $_REQUEST['api_source'] = true;
1874
1875                         if (empty($_REQUEST['source'])) {
1876                                 $_REQUEST["source"] = api_source();
1877                         }
1878
1879                         $item_id = item_post($a);
1880                 }
1881         } else {
1882                 throw new ForbiddenException();
1883         }
1884
1885         // output the post that we just posted.
1886         $called_api = [];
1887         return api_status_show($type, $item_id);
1888 }
1889
1890 /// @TODO move to top of file or somewhere better
1891 api_register_func('api/statuses/retweet', 'api_statuses_repeat', true, API_METHOD_POST);
1892
1893 /**
1894  * Destroys a specific status.
1895  *
1896  * @param string $type Return type (atom, rss, xml, json)
1897  *
1898  * @return array|string
1899  * @throws BadRequestException
1900  * @throws ForbiddenException
1901  * @throws ImagickException
1902  * @throws InternalServerErrorException
1903  * @throws UnauthorizedException
1904  * @see https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-destroy-id
1905  */
1906 function api_statuses_destroy($type)
1907 {
1908         $a = DI::app();
1909
1910         if (api_user() === false) {
1911                 throw new ForbiddenException();
1912         }
1913
1914         api_get_user();
1915
1916         // params
1917         $id = intval(DI::args()->getArgv()[3] ?? 0);
1918
1919         if ($id == 0) {
1920                 $id = intval($_REQUEST['id'] ?? 0);
1921         }
1922
1923         // Hotot workaround
1924         if ($id == 0) {
1925                 $id = intval(DI::args()->getArgv()[4] ?? 0);
1926         }
1927
1928         logger::notice('API: api_statuses_destroy: ' . $id);
1929
1930         $ret = api_statuses_show($type);
1931
1932         Item::deleteForUser(['id' => $id], api_user());
1933
1934         return $ret;
1935 }
1936
1937 /// @TODO move to top of file or somewhere better
1938 api_register_func('api/statuses/destroy', 'api_statuses_destroy', true, API_METHOD_DELETE);
1939
1940 /**
1941  * Returns the most recent mentions.
1942  *
1943  * @param string $type Return type (atom, rss, xml, json)
1944  *
1945  * @return array|string
1946  * @throws BadRequestException
1947  * @throws ForbiddenException
1948  * @throws ImagickException
1949  * @throws InternalServerErrorException
1950  * @throws UnauthorizedException
1951  * @see http://developer.twitter.com/doc/get/statuses/mentions
1952  */
1953 function api_statuses_mentions($type)
1954 {
1955         $a = DI::app();
1956         $user_info = api_get_user();
1957
1958         if (api_user() === false || $user_info === false) {
1959                 throw new ForbiddenException();
1960         }
1961
1962         unset($_REQUEST["user_id"]);
1963         unset($_GET["user_id"]);
1964
1965         unset($_REQUEST["screen_name"]);
1966         unset($_GET["screen_name"]);
1967
1968         // get last network messages
1969
1970         // params
1971         $since_id = intval($_REQUEST['since_id'] ?? 0);
1972         $max_id   = intval($_REQUEST['max_id']   ?? 0);
1973         $count    = intval($_REQUEST['count']    ?? 20);
1974         $page     = intval($_REQUEST['page']     ?? 1);
1975
1976         $start = max(0, ($page - 1) * $count);
1977
1978         $query = "`gravity` IN (?, ?) AND `uri-id` IN
1979                 (SELECT `uri-id` FROM `post-user-notification` WHERE `uid` = ? AND `notification-type` & ? != 0 ORDER BY `uri-id`)
1980                 AND (`uid` = 0 OR (`uid` = ? AND NOT `global`)) AND `id` > ?";
1981
1982         $condition = [
1983                 GRAVITY_PARENT, GRAVITY_COMMENT,
1984                 api_user(),
1985                 Post\UserNotification::TYPE_EXPLICIT_TAGGED | Post\UserNotification::TYPE_IMPLICIT_TAGGED |
1986                 Post\UserNotification::TYPE_THREAD_COMMENT | Post\UserNotification::TYPE_DIRECT_COMMENT |
1987                 Post\UserNotification::TYPE_DIRECT_THREAD_COMMENT,
1988                 api_user(), $since_id,
1989         ];
1990
1991         if ($max_id > 0) {
1992                 $query .= " AND `id` <= ?";
1993                 $condition[] = $max_id;
1994         }
1995
1996         array_unshift($condition, $query);
1997
1998         $params = ['order' => ['id' => true], 'limit' => [$start, $count]];
1999         $statuses = Post::selectForUser(api_user(), [], $condition, $params);
2000
2001         $ret = api_format_items(Post::toArray($statuses), $user_info, false, $type);
2002
2003         $data = ['status' => $ret];
2004         switch ($type) {
2005                 case "atom":
2006                         break;
2007                 case "rss":
2008                         $data = api_rss_extra($a, $data, $user_info);
2009                         break;
2010         }
2011
2012         return BaseApi::formatData("statuses", $type, $data);
2013 }
2014
2015 /// @TODO move to top of file or somewhere better
2016 api_register_func('api/statuses/mentions', 'api_statuses_mentions', true);
2017 api_register_func('api/statuses/replies', 'api_statuses_mentions', true);
2018
2019 /**
2020  * Returns the most recent statuses posted by the user.
2021  *
2022  * @param string $type Either "json" or "xml"
2023  * @return string|array
2024  * @throws BadRequestException
2025  * @throws ForbiddenException
2026  * @throws ImagickException
2027  * @throws InternalServerErrorException
2028  * @throws UnauthorizedException
2029  * @see   https://developer.twitter.com/en/docs/tweets/timelines/api-reference/get-statuses-user_timeline
2030  */
2031 function api_statuses_user_timeline($type)
2032 {
2033         $a = DI::app();
2034         $user_info = api_get_user();
2035
2036         if (api_user() === false || $user_info === false) {
2037                 throw new ForbiddenException();
2038         }
2039
2040         Logger::info('api_statuses_user_timeline', ['api_user' => api_user(), 'user_info' => $user_info, '_REQUEST' => $_REQUEST]);
2041
2042         $since_id        = $_REQUEST['since_id'] ?? 0;
2043         $max_id          = $_REQUEST['max_id'] ?? 0;
2044         $exclude_replies = !empty($_REQUEST['exclude_replies']);
2045         $conversation_id = $_REQUEST['conversation_id'] ?? 0;
2046
2047         // pagination
2048         $count = $_REQUEST['count'] ?? 20;
2049         $page  = $_REQUEST['page'] ?? 1;
2050
2051         $start = max(0, ($page - 1) * $count);
2052
2053         $condition = ["`uid` = ? AND `gravity` IN (?, ?) AND `id` > ? AND `contact-id` = ?",
2054                 api_user(), GRAVITY_PARENT, GRAVITY_COMMENT, $since_id, $user_info['cid']];
2055
2056         if ($user_info['self'] == 1) {
2057                 $condition[0] .= ' AND `wall` ';
2058         }
2059
2060         if ($exclude_replies) {
2061                 $condition[0] .= ' AND `gravity` = ?';
2062                 $condition[] = GRAVITY_PARENT;
2063         }
2064
2065         if ($conversation_id > 0) {
2066                 $condition[0] .= " AND `parent` = ?";
2067                 $condition[] = $conversation_id;
2068         }
2069
2070         if ($max_id > 0) {
2071                 $condition[0] .= " AND `id` <= ?";
2072                 $condition[] = $max_id;
2073         }
2074         $params = ['order' => ['id' => true], 'limit' => [$start, $count]];
2075         $statuses = Post::selectForUser(api_user(), [], $condition, $params);
2076
2077         $ret = api_format_items(Post::toArray($statuses), $user_info, true, $type);
2078
2079         bindComments($ret);
2080
2081         $data = ['status' => $ret];
2082         switch ($type) {
2083                 case "atom":
2084                         break;
2085                 case "rss":
2086                         $data = api_rss_extra($a, $data, $user_info);
2087                         break;
2088         }
2089
2090         return BaseApi::formatData("statuses", $type, $data);
2091 }
2092
2093 /// @TODO move to top of file or somewhere better
2094 api_register_func('api/statuses/user_timeline', 'api_statuses_user_timeline', true);
2095
2096 /**
2097  * Star/unstar an item.
2098  * param: id : id of the item
2099  *
2100  * @param string $type Return type (atom, rss, xml, json)
2101  *
2102  * @return array|string
2103  * @throws BadRequestException
2104  * @throws ForbiddenException
2105  * @throws ImagickException
2106  * @throws InternalServerErrorException
2107  * @throws UnauthorizedException
2108  * @see https://web.archive.org/web/20131019055350/https://dev.twitter.com/docs/api/1/post/favorites/create/%3Aid
2109  */
2110 function api_favorites_create_destroy($type)
2111 {
2112         $a = DI::app();
2113
2114         if (api_user() === false) {
2115                 throw new ForbiddenException();
2116         }
2117
2118         // for versioned api.
2119         /// @TODO We need a better global soluton
2120         $action_argv_id = 2;
2121         if (count(DI::args()->getArgv()) > 1 && DI::args()->getArgv()[1] == "1.1") {
2122                 $action_argv_id = 3;
2123         }
2124
2125         if (DI::args()->getArgc() <= $action_argv_id) {
2126                 throw new BadRequestException("Invalid request.");
2127         }
2128         $action = str_replace("." . $type, "", DI::args()->getArgv()[$action_argv_id]);
2129         if (DI::args()->getArgc() == $action_argv_id + 2) {
2130                 $itemid = intval(DI::args()->getArgv()[$action_argv_id + 1] ?? 0);
2131         } else {
2132                 $itemid = intval($_REQUEST['id'] ?? 0);
2133         }
2134
2135         $item = Post::selectFirstForUser(api_user(), [], ['id' => $itemid, 'uid' => api_user()]);
2136
2137         if (!DBA::isResult($item)) {
2138                 throw new BadRequestException("Invalid item.");
2139         }
2140
2141         switch ($action) {
2142                 case "create":
2143                         $item['starred'] = 1;
2144                         break;
2145                 case "destroy":
2146                         $item['starred'] = 0;
2147                         break;
2148                 default:
2149                         throw new BadRequestException("Invalid action ".$action);
2150         }
2151
2152         $r = Item::update(['starred' => $item['starred']], ['id' => $itemid]);
2153
2154         if ($r === false) {
2155                 throw new InternalServerErrorException("DB error");
2156         }
2157
2158
2159         $user_info = api_get_user();
2160         $rets = api_format_items([$item], $user_info, false, $type);
2161         $ret = $rets[0];
2162
2163         $data = ['status' => $ret];
2164         switch ($type) {
2165                 case "atom":
2166                         break;
2167                 case "rss":
2168                         $data = api_rss_extra($a, $data, $user_info);
2169                         break;
2170         }
2171
2172         return BaseApi::formatData("status", $type, $data);
2173 }
2174
2175 /// @TODO move to top of file or somewhere better
2176 api_register_func('api/favorites/create', 'api_favorites_create_destroy', true, API_METHOD_POST);
2177 api_register_func('api/favorites/destroy', 'api_favorites_create_destroy', true, API_METHOD_DELETE);
2178
2179 /**
2180  * Returns the most recent favorite statuses.
2181  *
2182  * @param string $type Return type (atom, rss, xml, json)
2183  *
2184  * @return string|array
2185  * @throws BadRequestException
2186  * @throws ForbiddenException
2187  * @throws ImagickException
2188  * @throws InternalServerErrorException
2189  * @throws UnauthorizedException
2190  */
2191 function api_favorites($type)
2192 {
2193         global $called_api;
2194
2195         $a = DI::app();
2196         $user_info = api_get_user();
2197
2198         if (api_user() === false || $user_info === false) {
2199                 throw new ForbiddenException();
2200         }
2201
2202         $called_api = [];
2203
2204         // in friendica starred item are private
2205         // return favorites only for self
2206         Logger::info(API_LOG_PREFIX . 'for {self}', ['module' => 'api', 'action' => 'favorites', 'self' => $user_info['self']]);
2207
2208         if ($user_info['self'] == 0) {
2209                 $ret = [];
2210         } else {
2211                 // params
2212                 $since_id = $_REQUEST['since_id'] ?? 0;
2213                 $max_id = $_REQUEST['max_id'] ?? 0;
2214                 $count = $_GET['count'] ?? 20;
2215                 $page = $_REQUEST['page'] ?? 1;
2216
2217                 $start = max(0, ($page - 1) * $count);
2218
2219                 $condition = ["`uid` = ? AND `gravity` IN (?, ?) AND `id` > ? AND `starred`",
2220                         api_user(), GRAVITY_PARENT, GRAVITY_COMMENT, $since_id];
2221
2222                 $params = ['order' => ['id' => true], 'limit' => [$start, $count]];
2223
2224                 if ($max_id > 0) {
2225                         $condition[0] .= " AND `id` <= ?";
2226                         $condition[] = $max_id;
2227                 }
2228
2229                 $statuses = Post::selectForUser(api_user(), [], $condition, $params);
2230
2231                 $ret = api_format_items(Post::toArray($statuses), $user_info, false, $type);
2232         }
2233
2234         bindComments($ret);
2235
2236         $data = ['status' => $ret];
2237         switch ($type) {
2238                 case "atom":
2239                         break;
2240                 case "rss":
2241                         $data = api_rss_extra($a, $data, $user_info);
2242                         break;
2243         }
2244
2245         return BaseApi::formatData("statuses", $type, $data);
2246 }
2247
2248 /// @TODO move to top of file or somewhere better
2249 api_register_func('api/favorites', 'api_favorites', true);
2250
2251 /**
2252  *
2253  * @param array $item
2254  * @param array $recipient
2255  * @param array $sender
2256  *
2257  * @return array
2258  * @throws InternalServerErrorException
2259  */
2260 function api_format_messages($item, $recipient, $sender)
2261 {
2262         // standard meta information
2263         $ret = [
2264                 'id'                    => $item['id'],
2265                 'sender_id'             => $sender['id'],
2266                 'text'                  => "",
2267                 'recipient_id'          => $recipient['id'],
2268                 'created_at'            => api_date($item['created'] ?? DateTimeFormat::utcNow()),
2269                 'sender_screen_name'    => $sender['screen_name'],
2270                 'recipient_screen_name' => $recipient['screen_name'],
2271                 'sender'                => $sender,
2272                 'recipient'             => $recipient,
2273                 'title'                 => "",
2274                 'friendica_seen'        => $item['seen'] ?? 0,
2275                 'friendica_parent_uri'  => $item['parent-uri'] ?? '',
2276         ];
2277
2278         // "uid" and "self" are only needed for some internal stuff, so remove it from here
2279         if (isset($ret['sender']['uid'])) {
2280                 unset($ret['sender']['uid']);
2281         }
2282         if (isset($ret['sender']['self'])) {
2283                 unset($ret['sender']['self']);
2284         }
2285         if (isset($ret['recipient']['uid'])) {
2286                 unset($ret['recipient']['uid']);
2287         }
2288         if (isset($ret['recipient']['self'])) {
2289                 unset($ret['recipient']['self']);
2290         }
2291
2292         //don't send title to regular StatusNET requests to avoid confusing these apps
2293         if (!empty($_GET['getText'])) {
2294                 $ret['title'] = $item['title'];
2295                 if ($_GET['getText'] == 'html') {
2296                         $ret['text'] = BBCode::convertForUriId($item['uri-id'], $item['body'], BBCode::API);
2297                 } elseif ($_GET['getText'] == 'plain') {
2298                         $ret['text'] = trim(HTML::toPlaintext(BBCode::convertForUriId($item['uri-id'], api_clean_plain_items($item['body']), BBCode::API), 0));
2299                 }
2300         } else {
2301                 $ret['text'] = $item['title'] . "\n" . HTML::toPlaintext(BBCode::convertForUriId($item['uri-id'], api_clean_plain_items($item['body']), BBCode::API), 0);
2302         }
2303         if (!empty($_GET['getUserObjects']) && $_GET['getUserObjects'] == 'false') {
2304                 unset($ret['sender']);
2305                 unset($ret['recipient']);
2306         }
2307
2308         return $ret;
2309 }
2310
2311 /**
2312  *
2313  * @param array $item
2314  *
2315  * @return array
2316  * @throws InternalServerErrorException
2317  */
2318 function api_convert_item($item)
2319 {
2320         $body = api_add_attachments_to_body($item);
2321
2322         $entities = api_get_entitities($statustext, $body, $item['uri-id']);
2323
2324         // Add pictures to the attachment array and remove them from the body
2325         $attachments = api_get_attachments($body, $item['uri-id']);
2326
2327         // Workaround for ostatus messages where the title is identically to the body
2328         $html = BBCode::convertForUriId($item['uri-id'], api_clean_plain_items($body), BBCode::API);
2329         $statusbody = trim(HTML::toPlaintext($html, 0));
2330
2331         // handle data: images
2332         $statusbody = api_format_items_embeded_images($item, $statusbody);
2333
2334         $statustitle = trim($item['title']);
2335
2336         if (($statustitle != '') && (strpos($statusbody, $statustitle) !== false)) {
2337                 $statustext = trim($statusbody);
2338         } else {
2339                 $statustext = trim($statustitle."\n\n".$statusbody);
2340         }
2341
2342         if ((($item['network'] ?? Protocol::PHANTOM) == Protocol::FEED) && (mb_strlen($statustext)> 1000)) {
2343                 $statustext = mb_substr($statustext, 0, 1000) . "... \n" . ($item['plink'] ?? '');
2344         }
2345
2346         $statushtml = BBCode::convertForUriId($item['uri-id'], BBCode::removeAttachment($body), BBCode::API);
2347
2348         // Workaround for clients with limited HTML parser functionality
2349         $search = ["<br>", "<blockquote>", "</blockquote>",
2350                         "<h1>", "</h1>", "<h2>", "</h2>",
2351                         "<h3>", "</h3>", "<h4>", "</h4>",
2352                         "<h5>", "</h5>", "<h6>", "</h6>"];
2353         $replace = ["<br>", "<br><blockquote>", "</blockquote><br>",
2354                         "<br><h1>", "</h1><br>", "<br><h2>", "</h2><br>",
2355                         "<br><h3>", "</h3><br>", "<br><h4>", "</h4><br>",
2356                         "<br><h5>", "</h5><br>", "<br><h6>", "</h6><br>"];
2357         $statushtml = str_replace($search, $replace, $statushtml);
2358
2359         if ($item['title'] != "") {
2360                 $statushtml = "<br><h4>" . BBCode::convertForUriId($item['uri-id'], $item['title']) . "</h4><br>" . $statushtml;
2361         }
2362
2363         do {
2364                 $oldtext = $statushtml;
2365                 $statushtml = str_replace("<br><br>", "<br>", $statushtml);
2366         } while ($oldtext != $statushtml);
2367
2368         if (substr($statushtml, 0, 4) == '<br>') {
2369                 $statushtml = substr($statushtml, 4);
2370         }
2371
2372         if (substr($statushtml, 0, -4) == '<br>') {
2373                 $statushtml = substr($statushtml, -4);
2374         }
2375
2376         // feeds without body should contain the link
2377         if ((($item['network'] ?? Protocol::PHANTOM) == Protocol::FEED) && (strlen($item['body']) == 0)) {
2378                 $statushtml .= BBCode::convertForUriId($item['uri-id'], $item['plink']);
2379         }
2380
2381         return [
2382                 "text" => $statustext,
2383                 "html" => $statushtml,
2384                 "attachments" => $attachments,
2385                 "entities" => $entities
2386         ];
2387 }
2388
2389 /**
2390  * Add media attachments to the body
2391  *
2392  * @param array $item
2393  * @return string body with added media
2394  */
2395 function api_add_attachments_to_body(array $item)
2396 {
2397         $body = Post\Media::addAttachmentsToBody($item['uri-id'], $item['body']);
2398
2399         if (strpos($body, '[/img]') !== false) {
2400                 return $body;
2401         }
2402
2403         foreach (Post\Media::getByURIId($item['uri-id'], [Post\Media::HTML]) as $media) {
2404                 if (!empty($media['preview'])) {
2405                         $description = $media['description'] ?: $media['name'];
2406                         if (!empty($description)) {
2407                                 $body .= "\n[img=" . $media['preview'] . ']' . $description .'[/img]';
2408                         } else {
2409                                 $body .= "\n[img]" . $media['preview'] .'[/img]';
2410                         }
2411                 }
2412         }
2413
2414         return $body;
2415 }
2416
2417 /**
2418  *
2419  * @param string $body
2420  * @param int    $uriid
2421  *
2422  * @return array
2423  * @throws InternalServerErrorException
2424  */
2425 function api_get_attachments(&$body, $uriid)
2426 {
2427         $body = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $body);
2428         $body = preg_replace("/\[img\=(.*?)\](.*?)\[\/img\]/ism", '[img]$1[/img]', $body);
2429
2430         $URLSearchString = "^\[\]";
2431         if (!preg_match_all("/\[img\]([$URLSearchString]*)\[\/img\]/ism", $body, $images)) {
2432                 return [];
2433         }
2434
2435         // Remove all embedded pictures, since they are added as attachments
2436         foreach ($images[0] as $orig) {
2437                 $body = str_replace($orig, '', $body);
2438         }
2439
2440         $attachments = [];
2441
2442         foreach ($images[1] as $image) {
2443                 $imagedata = Images::getInfoFromURLCached($image);
2444
2445                 if ($imagedata) {
2446                         $attachments[] = ["url" => Post\Link::getByLink($uriid, $image), "mimetype" => $imagedata["mime"], "size" => $imagedata["size"]];
2447                 }
2448         }
2449
2450         return $attachments;
2451 }
2452
2453 /**
2454  *
2455  * @param string $text
2456  * @param string $bbcode
2457  *
2458  * @return array
2459  * @throws InternalServerErrorException
2460  * @todo Links at the first character of the post
2461  */
2462 function api_get_entitities(&$text, $bbcode, $uriid)
2463 {
2464         $include_entities = strtolower($_REQUEST['include_entities'] ?? 'false');
2465
2466         if ($include_entities != "true") {
2467                 preg_match_all("/\[img](.*?)\[\/img\]/ism", $bbcode, $images);
2468
2469                 foreach ($images[1] as $image) {
2470                         $replace = Post\Link::getByLink($uriid, $image);
2471                         $text = str_replace($image, $replace, $text);
2472                 }
2473                 return [];
2474         }
2475
2476         $bbcode = BBCode::cleanPictureLinks($bbcode);
2477
2478         // Change pure links in text to bbcode uris
2479         $bbcode = preg_replace("/([^\]\='".'"'."]|^)(https?\:\/\/[a-zA-Z0-9\:\/\-\?\&\;\.\=\_\~\#\%\$\!\+\,]+)/ism", '$1[url=$2]$2[/url]', $bbcode);
2480
2481         $entities = [];
2482         $entities["hashtags"] = [];
2483         $entities["symbols"] = [];
2484         $entities["urls"] = [];
2485         $entities["user_mentions"] = [];
2486
2487         $URLSearchString = "^\[\]";
2488
2489         $bbcode = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", '#$2', $bbcode);
2490
2491         $bbcode = preg_replace("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism", '[url=$1]$2[/url]', $bbcode);
2492         $bbcode = preg_replace("/\[video\](.*?)\[\/video\]/ism", '[url=$1]$1[/url]', $bbcode);
2493
2494         $bbcode = preg_replace(
2495                 "/\[youtube\]([A-Za-z0-9\-_=]+)(.*?)\[\/youtube\]/ism",
2496                 '[url=https://www.youtube.com/watch?v=$1]https://www.youtube.com/watch?v=$1[/url]',
2497                 $bbcode
2498         );
2499         $bbcode = preg_replace("/\[youtube\](.*?)\[\/youtube\]/ism", '[url=$1]$1[/url]', $bbcode);
2500
2501         $bbcode = preg_replace(
2502                 "/\[vimeo\]([0-9]+)(.*?)\[\/vimeo\]/ism",
2503                 '[url=https://vimeo.com/$1]https://vimeo.com/$1[/url]',
2504                 $bbcode
2505         );
2506         $bbcode = preg_replace("/\[vimeo\](.*?)\[\/vimeo\]/ism", '[url=$1]$1[/url]', $bbcode);
2507
2508         $bbcode = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $bbcode);
2509
2510         preg_match_all("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", $bbcode, $urls);
2511
2512         $ordered_urls = [];
2513         foreach ($urls[1] as $id => $url) {
2514                 $start = iconv_strpos($text, $url, 0, "UTF-8");
2515                 if (!($start === false)) {
2516                         $ordered_urls[$start] = ["url" => $url, "title" => $urls[2][$id]];
2517                 }
2518         }
2519
2520         ksort($ordered_urls);
2521
2522         $offset = 0;
2523
2524         foreach ($ordered_urls as $url) {
2525                 if ((substr($url["title"], 0, 7) != "http://") && (substr($url["title"], 0, 8) != "https://")
2526                         && !strpos($url["title"], "http://") && !strpos($url["title"], "https://")
2527                 ) {
2528                         $display_url = $url["title"];
2529                 } else {
2530                         $display_url = str_replace(["http://www.", "https://www."], ["", ""], $url["url"]);
2531                         $display_url = str_replace(["http://", "https://"], ["", ""], $display_url);
2532
2533                         if (strlen($display_url) > 26) {
2534                                 $display_url = substr($display_url, 0, 25)."…";
2535                         }
2536                 }
2537
2538                 $start = iconv_strpos($text, $url["url"], $offset, "UTF-8");
2539                 if (!($start === false)) {
2540                         $entities["urls"][] = ["url" => $url["url"],
2541                                                         "expanded_url" => $url["url"],
2542                                                         "display_url" => $display_url,
2543                                                         "indices" => [$start, $start+strlen($url["url"])]];
2544                         $offset = $start + 1;
2545                 }
2546         }
2547
2548         preg_match_all("/\[img\=(.*?)\](.*?)\[\/img\]/ism", $bbcode, $images, PREG_SET_ORDER);
2549         $ordered_images = [];
2550         foreach ($images as $image) {
2551                 $start = iconv_strpos($text, $image[1], 0, "UTF-8");
2552                 if (!($start === false)) {
2553                         $ordered_images[$start] = ['url' => $image[1], 'alt' => $image[2]];
2554                 }
2555         }
2556
2557         preg_match_all("/\[img](.*?)\[\/img\]/ism", $bbcode, $images);
2558         foreach ($images[1] as $image) {
2559                 $start = iconv_strpos($text, $image, 0, "UTF-8");
2560                 if (!($start === false)) {
2561                         $ordered_images[$start] = ['url' => $image, 'alt' => ''];
2562                 }
2563         }
2564
2565         $offset = 0;
2566
2567         foreach ($ordered_images as $image) {
2568                 $url = $image['url'];
2569                 $ext_alt_text = $image['alt'];
2570
2571                 $display_url = str_replace(["http://www.", "https://www."], ["", ""], $url);
2572                 $display_url = str_replace(["http://", "https://"], ["", ""], $display_url);
2573
2574                 if (strlen($display_url) > 26) {
2575                         $display_url = substr($display_url, 0, 25)."…";
2576                 }
2577
2578                 $start = iconv_strpos($text, $url, $offset, "UTF-8");
2579                 if (!($start === false)) {
2580                         $image = Images::getInfoFromURLCached($url);
2581                         if ($image) {
2582                                 $media_url = Post\Link::getByLink($uriid, $url);
2583                                 $sizes["medium"] = ["w" => $image[0], "h" => $image[1], "resize" => "fit"];
2584
2585                                 $entities["media"][] = [
2586                                                         "id" => $start+1,
2587                                                         "id_str" => (string) ($start + 1),
2588                                                         "indices" => [$start, $start+strlen($url)],
2589                                                         "media_url" => Strings::normaliseLink($media_url),
2590                                                         "media_url_https" => $media_url,
2591                                                         "url" => $url,
2592                                                         "display_url" => $display_url,
2593                                                         "expanded_url" => $url,
2594                                                         "ext_alt_text" => $ext_alt_text,
2595                                                         "type" => "photo",
2596                                                         "sizes" => $sizes];
2597                         }
2598                         $offset = $start + 1;
2599                 }
2600         }
2601
2602         return $entities;
2603 }
2604
2605 /**
2606  *
2607  * @param array $item
2608  * @param string $text
2609  *
2610  * @return string
2611  */
2612 function api_format_items_embeded_images($item, $text)
2613 {
2614         $text = preg_replace_callback(
2615                 '|data:image/([^;]+)[^=]+=*|m',
2616                 function () use ($item) {
2617                         return DI::baseUrl() . '/display/' . $item['guid'];
2618                 },
2619                 $text
2620         );
2621         return $text;
2622 }
2623
2624 /**
2625  * return <a href='url'>name</a> as array
2626  *
2627  * @param string $txt text
2628  * @return array
2629  *                      'name' => 'name',
2630  *                      'url => 'url'
2631  */
2632 function api_contactlink_to_array($txt)
2633 {
2634         $match = [];
2635         $r = preg_match_all('|<a href="([^"]*)">([^<]*)</a>|', $txt, $match);
2636         if ($r && count($match)==3) {
2637                 $res = [
2638                         'name' => $match[2],
2639                         'url' => $match[1]
2640                 ];
2641         } else {
2642                 $res = [
2643                         'name' => $txt,
2644                         'url' => ""
2645                 ];
2646         }
2647         return $res;
2648 }
2649
2650
2651 /**
2652  * return likes, dislikes and attend status for item
2653  *
2654  * @param array  $item array
2655  * @param string $type Return type (atom, rss, xml, json)
2656  *
2657  * @return array
2658  *            likes => int count,
2659  *            dislikes => int count
2660  * @throws BadRequestException
2661  * @throws ImagickException
2662  * @throws InternalServerErrorException
2663  * @throws UnauthorizedException
2664  */
2665 function api_format_items_activities($item, $type = "json")
2666 {
2667         $a = DI::app();
2668
2669         $activities = [
2670                 'like' => [],
2671                 'dislike' => [],
2672                 'attendyes' => [],
2673                 'attendno' => [],
2674                 'attendmaybe' => [],
2675                 'announce' => [],
2676         ];
2677
2678         $condition = ['uid' => $item['uid'], 'thr-parent' => $item['uri'], 'gravity' => GRAVITY_ACTIVITY];
2679         $ret = Post::selectForUser($item['uid'], ['author-id', 'verb'], $condition);
2680
2681         while ($parent_item = Post::fetch($ret)) {
2682                 // not used as result should be structured like other user data
2683                 //builtin_activity_puller($i, $activities);
2684
2685                 // get user data and add it to the array of the activity
2686                 $user = api_get_user($parent_item['author-id']);
2687                 switch ($parent_item['verb']) {
2688                         case Activity::LIKE:
2689                                 $activities['like'][] = $user;
2690                                 break;
2691                         case Activity::DISLIKE:
2692                                 $activities['dislike'][] = $user;
2693                                 break;
2694                         case Activity::ATTEND:
2695                                 $activities['attendyes'][] = $user;
2696                                 break;
2697                         case Activity::ATTENDNO:
2698                                 $activities['attendno'][] = $user;
2699                                 break;
2700                         case Activity::ATTENDMAYBE:
2701                                 $activities['attendmaybe'][] = $user;
2702                                 break;
2703                         case Activity::ANNOUNCE:
2704                                 $activities['announce'][] = $user;
2705                                 break;
2706                         default:
2707                                 break;
2708                 }
2709         }
2710
2711         DBA::close($ret);
2712
2713         if ($type == "xml") {
2714                 $xml_activities = [];
2715                 foreach ($activities as $k => $v) {
2716                         // change xml element from "like" to "friendica:like"
2717                         $xml_activities["friendica:".$k] = $v;
2718                         // add user data into xml output
2719                         $k_user = 0;
2720                         foreach ($v as $user) {
2721                                 $xml_activities["friendica:".$k][$k_user++.":user"] = $user;
2722                         }
2723                 }
2724                 $activities = $xml_activities;
2725         }
2726
2727         return $activities;
2728 }
2729
2730 /**
2731  * format items to be returned by api
2732  *
2733  * @param array  $items       array of items
2734  * @param array  $user_info
2735  * @param bool   $filter_user filter items by $user_info
2736  * @param string $type        Return type (atom, rss, xml, json)
2737  * @return array
2738  * @throws BadRequestException
2739  * @throws ImagickException
2740  * @throws InternalServerErrorException
2741  * @throws UnauthorizedException
2742  */
2743 function api_format_items($items, $user_info, $filter_user = false, $type = "json")
2744 {
2745         $a = Friendica\DI::app();
2746
2747         $ret = [];
2748
2749         if (empty($items)) {
2750                 return $ret;
2751         }
2752
2753         foreach ((array)$items as $item) {
2754                 list($status_user, $author_user, $owner_user) = api_item_get_user($a, $item);
2755
2756                 // Look if the posts are matching if they should be filtered by user id
2757                 if ($filter_user && ($status_user["id"] != $user_info["id"])) {
2758                         continue;
2759                 }
2760
2761                 $status = api_format_item($item, $type, $status_user, $author_user, $owner_user);
2762
2763                 $ret[] = $status;
2764         }
2765
2766         return $ret;
2767 }
2768
2769 /**
2770  * @param array  $item       Item record
2771  * @param string $type       Return format (atom, rss, xml, json)
2772  * @param array $status_user User record of the item author, can be provided by api_item_get_user()
2773  * @param array $author_user User record of the item author, can be provided by api_item_get_user()
2774  * @param array $owner_user  User record of the item owner, can be provided by api_item_get_user()
2775  * @return array API-formatted status
2776  * @throws BadRequestException
2777  * @throws ImagickException
2778  * @throws InternalServerErrorException
2779  * @throws UnauthorizedException
2780  */
2781 function api_format_item($item, $type = "json", $status_user = null, $author_user = null, $owner_user = null)
2782 {
2783         $a = Friendica\DI::app();
2784
2785         if (empty($status_user) || empty($author_user) || empty($owner_user)) {
2786                 list($status_user, $author_user, $owner_user) = api_item_get_user($a, $item);
2787         }
2788
2789         DI::contentItem()->localize($item);
2790
2791         $in_reply_to = api_in_reply_to($item);
2792
2793         $converted = api_convert_item($item);
2794
2795         if ($type == "xml") {
2796                 $geo = "georss:point";
2797         } else {
2798                 $geo = "geo";
2799         }
2800
2801         $status = [
2802                 'text'          => $converted["text"],
2803                 'truncated' => false,
2804                 'created_at'=> api_date($item['created']),
2805                 'in_reply_to_status_id' => $in_reply_to['status_id'],
2806                 'in_reply_to_status_id_str' => $in_reply_to['status_id_str'],
2807                 'source'    => (($item['app']) ? $item['app'] : 'web'),
2808                 'id'            => intval($item['id']),
2809                 'id_str'        => (string) intval($item['id']),
2810                 'in_reply_to_user_id' => $in_reply_to['user_id'],
2811                 'in_reply_to_user_id_str' => $in_reply_to['user_id_str'],
2812                 'in_reply_to_screen_name' => $in_reply_to['screen_name'],
2813                 $geo => null,
2814                 'favorited' => $item['starred'] ? true : false,
2815                 'user' =>  $status_user,
2816                 'friendica_author' => $author_user,
2817                 'friendica_owner' => $owner_user,
2818                 'friendica_private' => $item['private'] == Item::PRIVATE,
2819                 //'entities' => NULL,
2820                 'statusnet_html' => $converted["html"],
2821                 'statusnet_conversation_id' => $item['parent'],
2822                 'external_url' => DI::baseUrl() . "/display/" . $item['guid'],
2823                 'friendica_activities' => api_format_items_activities($item, $type),
2824                 'friendica_title' => $item['title'],
2825                 'friendica_html' => BBCode::convertForUriId($item['uri-id'], $item['body'], BBCode::EXTERNAL)
2826         ];
2827
2828         if (count($converted["attachments"]) > 0) {
2829                 $status["attachments"] = $converted["attachments"];
2830         }
2831
2832         if (count($converted["entities"]) > 0) {
2833                 $status["entities"] = $converted["entities"];
2834         }
2835
2836         if ($status["source"] == 'web') {
2837                 $status["source"] = ContactSelector::networkToName($item['author-network'], $item['author-link'], $item['network']);
2838         } elseif (ContactSelector::networkToName($item['author-network'], $item['author-link'], $item['network']) != $status["source"]) {
2839                 $status["source"] = trim($status["source"].' ('.ContactSelector::networkToName($item['author-network'], $item['author-link'], $item['network']).')');
2840         }
2841
2842         $retweeted_item = [];
2843         $quoted_item = [];
2844
2845         if (empty($retweeted_item) && ($item['owner-id'] == $item['author-id'])) {
2846                 $announce = api_get_announce($item);
2847                 if (!empty($announce)) {
2848                         $retweeted_item = $item;
2849                         $item = $announce;
2850                         $status['friendica_owner'] = api_get_user($announce['author-id']);
2851                 }
2852         }
2853
2854         if (!empty($quoted_item)) {
2855                 if ($quoted_item['id'] != $item['id']) {
2856                         $quoted_status = api_format_item($quoted_item);
2857                         /// @todo Only remove the attachments that are also contained in the quotes status
2858                         unset($status['attachments']);
2859                         unset($status['entities']);
2860                 } else {
2861                         $conv_quoted = api_convert_item($quoted_item);
2862                         $quoted_status = $status;
2863                         unset($quoted_status['attachments']);
2864                         unset($quoted_status['entities']);
2865                         unset($quoted_status['statusnet_conversation_id']);
2866                         $quoted_status['text'] = $conv_quoted['text'];
2867                         $quoted_status['statusnet_html'] = $conv_quoted['html'];
2868                         try {
2869                                 $quoted_status["user"] = api_get_user($quoted_item["author-id"]);
2870                         } catch (BadRequestException $e) {
2871                                 // user not found. should be found?
2872                                 /// @todo check if the user should be always found
2873                                 $quoted_status["user"] = [];
2874                         }
2875                 }
2876                 unset($quoted_status['friendica_author']);
2877                 unset($quoted_status['friendica_owner']);
2878                 unset($quoted_status['friendica_activities']);
2879                 unset($quoted_status['friendica_private']);
2880         }
2881
2882         if (!empty($retweeted_item)) {
2883                 $retweeted_status = $status;
2884                 unset($retweeted_status['friendica_author']);
2885                 unset($retweeted_status['friendica_owner']);
2886                 unset($retweeted_status['friendica_activities']);
2887                 unset($retweeted_status['friendica_private']);
2888                 unset($retweeted_status['statusnet_conversation_id']);
2889                 $status['user'] = $status['friendica_owner'];
2890                 try {
2891                         $retweeted_status["user"] = api_get_user($retweeted_item["author-id"]);
2892                 } catch (BadRequestException $e) {
2893                         // user not found. should be found?
2894                         /// @todo check if the user should be always found
2895                         $retweeted_status["user"] = [];
2896                 }
2897
2898                 $rt_converted = api_convert_item($retweeted_item);
2899
2900                 $retweeted_status['text'] = $rt_converted["text"];
2901                 $retweeted_status['statusnet_html'] = $rt_converted["html"];
2902                 $retweeted_status['friendica_html'] = $rt_converted["html"];
2903                 $retweeted_status['created_at'] =  api_date($retweeted_item['created']);
2904
2905                 if (!empty($quoted_status)) {
2906                         $retweeted_status['quoted_status'] = $quoted_status;
2907                 }
2908
2909                 $status['friendica_author'] = $retweeted_status['user'];
2910                 $status['retweeted_status'] = $retweeted_status;
2911         } elseif (!empty($quoted_status)) {
2912                 $root_status = api_convert_item($item);
2913
2914                 $status['text'] = $root_status["text"];
2915                 $status['statusnet_html'] = $root_status["html"];
2916                 $status['quoted_status'] = $quoted_status;
2917         }
2918
2919         // "uid" and "self" are only needed for some internal stuff, so remove it from here
2920         unset($status["user"]["uid"]);
2921         unset($status["user"]["self"]);
2922
2923         if ($item["coord"] != "") {
2924                 $coords = explode(' ', $item["coord"]);
2925                 if (count($coords) == 2) {
2926                         if ($type == "json") {
2927                                 $status["geo"] = ['type' => 'Point',
2928                                         'coordinates' => [(float) $coords[0],
2929                                                 (float) $coords[1]]];
2930                         } else {// Not sure if this is the official format - if someone founds a documentation we can check
2931                                 $status["georss:point"] = $item["coord"];
2932                         }
2933                 }
2934         }
2935
2936         return $status;
2937 }
2938
2939 /**
2940  * Returns all lists the user subscribes to.
2941  *
2942  * @param string $type Return type (atom, rss, xml, json)
2943  *
2944  * @return array|string
2945  * @see https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-list
2946  */
2947 function api_lists_list($type)
2948 {
2949         $ret = [];
2950         /// @TODO $ret is not filled here?
2951         return BaseApi::formatData('lists', $type, ["lists_list" => $ret]);
2952 }
2953
2954 /// @TODO move to top of file or somewhere better
2955 api_register_func('api/lists/list', 'api_lists_list', true);
2956 api_register_func('api/lists/subscriptions', 'api_lists_list', true);
2957
2958 /**
2959  * Returns all groups the user owns.
2960  *
2961  * @param string $type Return type (atom, rss, xml, json)
2962  *
2963  * @return array|string
2964  * @throws BadRequestException
2965  * @throws ForbiddenException
2966  * @throws ImagickException
2967  * @throws InternalServerErrorException
2968  * @throws UnauthorizedException
2969  * @see https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-ownerships
2970  */
2971 function api_lists_ownerships($type)
2972 {
2973         $a = DI::app();
2974
2975         if (api_user() === false) {
2976                 throw new ForbiddenException();
2977         }
2978
2979         // params
2980         $user_info = api_get_user();
2981         $uid = $user_info['uid'];
2982
2983         $groups = DBA::select('group', [], ['deleted' => 0, 'uid' => $uid]);
2984
2985         // loop through all groups
2986         $lists = [];
2987         foreach ($groups as $group) {
2988                 if ($group['visible']) {
2989                         $mode = 'public';
2990                 } else {
2991                         $mode = 'private';
2992                 }
2993                 $lists[] = [
2994                         'name' => $group['name'],
2995                         'id' => intval($group['id']),
2996                         'id_str' => (string) $group['id'],
2997                         'user' => $user_info,
2998                         'mode' => $mode
2999                 ];
3000         }
3001         return BaseApi::formatData("lists", $type, ['lists' => ['lists' => $lists]]);
3002 }
3003
3004 /// @TODO move to top of file or somewhere better
3005 api_register_func('api/lists/ownerships', 'api_lists_ownerships', true);
3006
3007 /**
3008  * Returns recent statuses from users in the specified group.
3009  *
3010  * @param string $type Return type (atom, rss, xml, json)
3011  *
3012  * @return array|string
3013  * @throws BadRequestException
3014  * @throws ForbiddenException
3015  * @throws ImagickException
3016  * @throws InternalServerErrorException
3017  * @throws UnauthorizedException
3018  * @see https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-ownerships
3019  */
3020 function api_lists_statuses($type)
3021 {
3022         $a = DI::app();
3023
3024         $user_info = api_get_user();
3025         if (api_user() === false || $user_info === false) {
3026                 throw new ForbiddenException();
3027         }
3028
3029         unset($_REQUEST["user_id"]);
3030         unset($_GET["user_id"]);
3031
3032         unset($_REQUEST["screen_name"]);
3033         unset($_GET["screen_name"]);
3034
3035         if (empty($_REQUEST['list_id'])) {
3036                 throw new BadRequestException('list_id not specified');
3037         }
3038
3039         // params
3040         $count = $_REQUEST['count'] ?? 20;
3041         $page = $_REQUEST['page'] ?? 1;
3042         $since_id = $_REQUEST['since_id'] ?? 0;
3043         $max_id = $_REQUEST['max_id'] ?? 0;
3044         $exclude_replies = (!empty($_REQUEST['exclude_replies']) ? 1 : 0);
3045         $conversation_id = $_REQUEST['conversation_id'] ?? 0;
3046
3047         $start = max(0, ($page - 1) * $count);
3048
3049         $groups = DBA::selectToArray('group_member', ['contact-id'], ['gid' => 1]);
3050         $gids = array_column($groups, 'contact-id');
3051         $condition = ['uid' => api_user(), 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT], 'group-id' => $gids];
3052         $condition = DBA::mergeConditions($condition, ["`id` > ?", $since_id]);
3053
3054         if ($max_id > 0) {
3055                 $condition[0] .= " AND `id` <= ?";
3056                 $condition[] = $max_id;
3057         }
3058         if ($exclude_replies > 0) {
3059                 $condition[0] .= ' AND `gravity` = ?';
3060                 $condition[] = GRAVITY_PARENT;
3061         }
3062         if ($conversation_id > 0) {
3063                 $condition[0] .= " AND `parent` = ?";
3064                 $condition[] = $conversation_id;
3065         }
3066
3067         $params = ['order' => ['id' => true], 'limit' => [$start, $count]];
3068         $statuses = Post::selectForUser(api_user(), [], $condition, $params);
3069
3070         $items = api_format_items(Post::toArray($statuses), $user_info, false, $type);
3071
3072         $data = ['status' => $items];
3073         switch ($type) {
3074                 case "atom":
3075                         break;
3076                 case "rss":
3077                         $data = api_rss_extra($a, $data, $user_info);
3078                         break;
3079         }
3080
3081         return BaseApi::formatData("statuses", $type, $data);
3082 }
3083
3084 /// @TODO move to top of file or somewhere better
3085 api_register_func('api/lists/statuses', 'api_lists_statuses', true);
3086
3087 /**
3088  * Returns either the friends of the follower list
3089  *
3090  * Considers friends and followers lists to be private and won't return
3091  * anything if any user_id parameter is passed.
3092  *
3093  * @param string $qtype Either "friends" or "followers"
3094  * @return boolean|array
3095  * @throws BadRequestException
3096  * @throws ForbiddenException
3097  * @throws ImagickException
3098  * @throws InternalServerErrorException
3099  * @throws UnauthorizedException
3100  */
3101 function api_statuses_f($qtype)
3102 {
3103         $a = DI::app();
3104
3105         if (api_user() === false) {
3106                 throw new ForbiddenException();
3107         }
3108
3109         // pagination
3110         $count = $_GET['count'] ?? 20;
3111         $page = $_GET['page'] ?? 1;
3112
3113         $start = max(0, ($page - 1) * $count);
3114
3115         $user_info = api_get_user();
3116
3117         if (!empty($_GET['cursor']) && $_GET['cursor'] == 'undefined') {
3118                 /* this is to stop Hotot to load friends multiple times
3119                 *  I'm not sure if I'm missing return something or
3120                 *  is a bug in hotot. Workaround, meantime
3121                 */
3122
3123                 /*$ret=Array();
3124                 return array('$users' => $ret);*/
3125                 return false;
3126         }
3127
3128         $sql_extra = '';
3129         if ($qtype == 'friends') {
3130                 $sql_extra = sprintf(" AND ( `rel` = %d OR `rel` = %d ) ", intval(Contact::SHARING), intval(Contact::FRIEND));
3131         } elseif ($qtype == 'followers') {
3132                 $sql_extra = sprintf(" AND ( `rel` = %d OR `rel` = %d ) ", intval(Contact::FOLLOWER), intval(Contact::FRIEND));
3133         }
3134
3135         // friends and followers only for self
3136         if ($user_info['self'] == 0) {
3137                 $sql_extra = " AND false ";
3138         }
3139
3140         if ($qtype == 'blocks') {
3141                 $sql_filter = 'AND `blocked` AND NOT `pending`';
3142         } elseif ($qtype == 'incoming') {
3143                 $sql_filter = 'AND `pending`';
3144         } else {
3145                 $sql_filter = 'AND (NOT `blocked` OR `pending`)';
3146         }
3147
3148         // @todo This query most likely can be replaced with a Contact::select...
3149         $r = DBA::toArray(DBA::p(
3150                 "SELECT `nurl`
3151                 FROM `contact`
3152                 WHERE `uid` = ?
3153                 AND NOT `self`
3154                 $sql_filter
3155                 $sql_extra
3156                 ORDER BY `nick`
3157                 LIMIT ?, ?",
3158                 api_user(),
3159                 $start,
3160                 $count
3161         ));
3162
3163         $ret = [];
3164         foreach ($r as $cid) {
3165                 $user = api_get_user($cid['nurl']);
3166                 // "uid" and "self" are only needed for some internal stuff, so remove it from here
3167                 unset($user["uid"]);
3168                 unset($user["self"]);
3169
3170                 if ($user) {
3171                         $ret[] = $user;
3172                 }
3173         }
3174
3175         return ['user' => $ret];
3176 }
3177
3178
3179 /**
3180  * Returns the list of friends of the provided user
3181  *
3182  * @deprecated By Twitter API in favor of friends/list
3183  *
3184  * @param string $type Either "json" or "xml"
3185  * @return boolean|string|array
3186  * @throws BadRequestException
3187  * @throws ForbiddenException
3188  */
3189 function api_statuses_friends($type)
3190 {
3191         $data =  api_statuses_f("friends");
3192         if ($data === false) {
3193                 return false;
3194         }
3195         return BaseApi::formatData("users", $type, $data);
3196 }
3197
3198 /**
3199  * Returns the list of followers of the provided user
3200  *
3201  * @deprecated By Twitter API in favor of friends/list
3202  *
3203  * @param string $type Either "json" or "xml"
3204  * @return boolean|string|array
3205  * @throws BadRequestException
3206  * @throws ForbiddenException
3207  */
3208 function api_statuses_followers($type)
3209 {
3210         $data = api_statuses_f("followers");
3211         if ($data === false) {
3212                 return false;
3213         }
3214         return BaseApi::formatData("users", $type, $data);
3215 }
3216
3217 /// @TODO move to top of file or somewhere better
3218 api_register_func('api/statuses/friends', 'api_statuses_friends', true);
3219 api_register_func('api/statuses/followers', 'api_statuses_followers', true);
3220
3221 /**
3222  * Returns the list of blocked users
3223  *
3224  * @see https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/get-blocks-list
3225  *
3226  * @param string $type Either "json" or "xml"
3227  *
3228  * @return boolean|string|array
3229  * @throws BadRequestException
3230  * @throws ForbiddenException
3231  */
3232 function api_blocks_list($type)
3233 {
3234         $data =  api_statuses_f('blocks');
3235         if ($data === false) {
3236                 return false;
3237         }
3238         return BaseApi::formatData("users", $type, $data);
3239 }
3240
3241 /// @TODO move to top of file or somewhere better
3242 api_register_func('api/blocks/list', 'api_blocks_list', true);
3243
3244 /**
3245  * Returns the list of pending users IDs
3246  *
3247  * @see https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friendships-incoming
3248  *
3249  * @param string $type Either "json" or "xml"
3250  *
3251  * @return boolean|string|array
3252  * @throws BadRequestException
3253  * @throws ForbiddenException
3254  */
3255 function api_friendships_incoming($type)
3256 {
3257         $data =  api_statuses_f('incoming');
3258         if ($data === false) {
3259                 return false;
3260         }
3261
3262         $ids = [];
3263         foreach ($data['user'] as $user) {
3264                 $ids[] = $user['id'];
3265         }
3266
3267         return BaseApi::formatData("ids", $type, ['id' => $ids]);
3268 }
3269
3270 /// @TODO move to top of file or somewhere better
3271 api_register_func('api/friendships/incoming', 'api_friendships_incoming', true);
3272
3273 /**
3274  * Returns the instance's configuration information.
3275  *
3276  * @param string $type Return type (atom, rss, xml, json)
3277  *
3278  * @return array|string
3279  * @throws InternalServerErrorException
3280  */
3281 function api_statusnet_config($type)
3282 {
3283         $name      = DI::config()->get('config', 'sitename');
3284         $server    = DI::baseUrl()->getHostname();
3285         $logo      = DI::baseUrl() . '/images/friendica-64.png';
3286         $email     = DI::config()->get('config', 'admin_email');
3287         $closed    = intval(DI::config()->get('config', 'register_policy')) === \Friendica\Module\Register::CLOSED ? 'true' : 'false';
3288         $private   = DI::config()->get('system', 'block_public') ? 'true' : 'false';
3289         $textlimit = (string) DI::config()->get('config', 'api_import_size', DI::config()->get('config', 'max_import_size', 200000));
3290         $ssl       = DI::config()->get('system', 'have_ssl') ? 'true' : 'false';
3291         $sslserver = DI::config()->get('system', 'have_ssl') ? str_replace('http:', 'https:', DI::baseUrl()) : '';
3292
3293         $config = [
3294                 'site' => ['name' => $name,'server' => $server, 'theme' => 'default', 'path' => '',
3295                         'logo' => $logo, 'fancy' => true, 'language' => 'en', 'email' => $email, 'broughtby' => '',
3296                         'broughtbyurl' => '', 'timezone' => 'UTC', 'closed' => $closed, 'inviteonly' => false,
3297                         'private' => $private, 'textlimit' => $textlimit, 'sslserver' => $sslserver, 'ssl' => $ssl,
3298                         'shorturllength' => '30',
3299                         'friendica' => [
3300                                         'FRIENDICA_PLATFORM' => FRIENDICA_PLATFORM,
3301                                         'FRIENDICA_VERSION' => FRIENDICA_VERSION,
3302                                         'DFRN_PROTOCOL_VERSION' => DFRN_PROTOCOL_VERSION,
3303                                         'DB_UPDATE_VERSION' => DB_UPDATE_VERSION
3304                                         ]
3305                 ],
3306         ];
3307
3308         return BaseApi::formatData('config', $type, ['config' => $config]);
3309 }
3310
3311 /// @TODO move to top of file or somewhere better
3312 api_register_func('api/gnusocial/config', 'api_statusnet_config', false);
3313 api_register_func('api/statusnet/config', 'api_statusnet_config', false);
3314
3315 /**
3316  * Sends a new direct message.
3317  *
3318  * @param string $type Return type (atom, rss, xml, json)
3319  *
3320  * @return array|string
3321  * @throws BadRequestException
3322  * @throws ForbiddenException
3323  * @throws ImagickException
3324  * @throws InternalServerErrorException
3325  * @throws NotFoundException
3326  * @throws UnauthorizedException
3327  * @see https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/new-message
3328  */
3329 function api_direct_messages_new($type)
3330 {
3331         $a = DI::app();
3332
3333         if (api_user() === false) {
3334                 throw new ForbiddenException();
3335         }
3336
3337         if (empty($_POST["text"]) || empty($_POST["screen_name"]) && empty($_POST["user_id"])) {
3338                 return;
3339         }
3340
3341         $sender = api_get_user();
3342
3343         $recipient = null;
3344         if (!empty($_POST['screen_name'])) {
3345                 $contacts = Contact::selectToArray(['id', 'nurl', 'network'], ['uid' => api_user(), 'nick' => $_POST['screen_name']]);
3346                 if (DBA::isResult($contacts)) {
3347                         // Selecting the id by priority, friendica first
3348                         api_best_nickname($contacts);
3349
3350                         $recipient = api_get_user($contacts[0]['nurl']);
3351                 }
3352         } else {
3353                 $recipient = api_get_user($_POST['user_id']);
3354         }
3355
3356         if (empty($recipient)) {
3357                 throw new NotFoundException('Recipient not found');
3358         }
3359
3360         $replyto = '';
3361         if (!empty($_REQUEST['replyto'])) {
3362                 $mail = DBA::selectFirst('mail', ['parent-uri', 'title'], ['uid' => api_user(), 'id' => $_REQUEST['replyto']]);
3363                 $replyto = $mail['parent-uri'];
3364                 $sub     = $mail['title'];
3365         } else {
3366                 if (!empty($_REQUEST['title'])) {
3367                         $sub = $_REQUEST['title'];
3368                 } else {
3369                         $sub = ((strlen($_POST['text'])>10) ? substr($_POST['text'], 0, 10)."...":$_POST['text']);
3370                 }
3371         }
3372
3373         $id = Mail::send($recipient['cid'], $_POST['text'], $sub, $replyto);
3374
3375         if ($id > -1) {
3376                 $mail = DBA::selectFirst('mail', [], ['id' => $id]);
3377                 $ret = api_format_messages($mail, $recipient, $sender);
3378         } else {
3379                 $ret = ["error" => $id];
3380         }
3381
3382         $data = ['direct_message'=>$ret];
3383
3384         switch ($type) {
3385                 case "atom":
3386                         break;
3387                 case "rss":
3388                         $data = api_rss_extra($a, $data, $sender);
3389                         break;
3390         }
3391
3392         return BaseApi::formatData("direct-messages", $type, $data);
3393 }
3394
3395 /// @TODO move to top of file or somewhere better
3396 api_register_func('api/direct_messages/new', 'api_direct_messages_new', true, API_METHOD_POST);
3397
3398 /**
3399  * delete a direct_message from mail table through api
3400  *
3401  * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
3402  * @return string|array
3403  * @throws BadRequestException
3404  * @throws ForbiddenException
3405  * @throws ImagickException
3406  * @throws InternalServerErrorException
3407  * @throws UnauthorizedException
3408  * @see   https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/delete-message
3409  */
3410 function api_direct_messages_destroy($type)
3411 {
3412         $a = DI::app();
3413
3414         if (api_user() === false) {
3415                 throw new ForbiddenException();
3416         }
3417
3418         // params
3419         $user_info = api_get_user();
3420         //required
3421         $id = $_REQUEST['id'] ?? 0;
3422         // optional
3423         $parenturi = $_REQUEST['friendica_parenturi'] ?? '';
3424         $verbose = (!empty($_GET['friendica_verbose']) ? strtolower($_GET['friendica_verbose']) : "false");
3425         /// @todo optional parameter 'include_entities' from Twitter API not yet implemented
3426
3427         $uid = $user_info['uid'];
3428         // error if no id or parenturi specified (for clients posting parent-uri as well)
3429         if ($verbose == "true" && ($id == 0 || $parenturi == "")) {
3430                 $answer = ['result' => 'error', 'message' => 'message id or parenturi not specified'];
3431                 return BaseApi::formatData("direct_messages_delete", $type, ['$result' => $answer]);
3432         }
3433
3434         // BadRequestException if no id specified (for clients using Twitter API)
3435         if ($id == 0) {
3436                 throw new BadRequestException('Message id not specified');
3437         }
3438
3439         // add parent-uri to sql command if specified by calling app
3440         $sql_extra = ($parenturi != "" ? " AND `parent-uri` = '" . DBA::escape($parenturi) . "'" : "");
3441
3442         // error message if specified id is not in database
3443         if (!DBA::exists('mail', ["`uid` = ? AND `id` = ? " . $sql_extra, $uid, $id])) {
3444                 if ($verbose == "true") {
3445                         $answer = ['result' => 'error', 'message' => 'message id not in database'];
3446                         return BaseApi::formatData("direct_messages_delete", $type, ['$result' => $answer]);
3447                 }
3448                 /// @todo BadRequestException ok for Twitter API clients?
3449                 throw new BadRequestException('message id not in database');
3450         }
3451
3452         // delete message
3453         $result = DBA::delete('mail', ["`uid` = ? AND `id` = ? " . $sql_extra, $uid, $id]);
3454
3455         if ($verbose == "true") {
3456                 if ($result) {
3457                         // return success
3458                         $answer = ['result' => 'ok', 'message' => 'message deleted'];
3459                         return BaseApi::formatData("direct_message_delete", $type, ['$result' => $answer]);
3460                 } else {
3461                         $answer = ['result' => 'error', 'message' => 'unknown error'];
3462                         return BaseApi::formatData("direct_messages_delete", $type, ['$result' => $answer]);
3463                 }
3464         }
3465         /// @todo return JSON data like Twitter API not yet implemented
3466 }
3467
3468 /// @TODO move to top of file or somewhere better
3469 api_register_func('api/direct_messages/destroy', 'api_direct_messages_destroy', true, API_METHOD_DELETE);
3470
3471 /**
3472  * Unfollow Contact
3473  *
3474  * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
3475  * @return string|array
3476  * @throws HTTPException\BadRequestException
3477  * @throws HTTPException\ExpectationFailedException
3478  * @throws HTTPException\ForbiddenException
3479  * @throws HTTPException\InternalServerErrorException
3480  * @throws HTTPException\NotFoundException
3481  * @see   https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/post-friendships-destroy.html
3482  */
3483 function api_friendships_destroy($type)
3484 {
3485         $uid = api_user();
3486
3487         if ($uid === false) {
3488                 throw new HTTPException\ForbiddenException();
3489         }
3490
3491         $owner = User::getOwnerDataById($uid);
3492         if (!$owner) {
3493                 Logger::notice(API_LOG_PREFIX . 'No owner {uid} found', ['module' => 'api', 'action' => 'friendships_destroy', 'uid' => $uid]);
3494                 throw new HTTPException\NotFoundException('Error Processing Request');
3495         }
3496
3497         $contact_id = $_REQUEST['user_id'] ?? 0;
3498
3499         if (empty($contact_id)) {
3500                 Logger::notice(API_LOG_PREFIX . 'No user_id specified', ['module' => 'api', 'action' => 'friendships_destroy']);
3501                 throw new HTTPException\BadRequestException('no user_id specified');
3502         }
3503
3504         // Get Contact by given id
3505         $contact = DBA::selectFirst('contact', ['url'], ['id' => $contact_id, 'uid' => 0, 'self' => false]);
3506
3507         if(!DBA::isResult($contact)) {
3508                 Logger::notice(API_LOG_PREFIX . 'No public contact found for ID {contact}', ['module' => 'api', 'action' => 'friendships_destroy', 'contact' => $contact_id]);
3509                 throw new HTTPException\NotFoundException('no contact found to given ID');
3510         }
3511
3512         $url = $contact['url'];
3513
3514         $condition = ["`uid` = ? AND (`rel` = ? OR `rel` = ?) AND (`nurl` = ? OR `alias` = ? OR `alias` = ?)",
3515                         $uid, Contact::SHARING, Contact::FRIEND, Strings::normaliseLink($url),
3516                         Strings::normaliseLink($url), $url];
3517         $contact = DBA::selectFirst('contact', [], $condition);
3518
3519         if (!DBA::isResult($contact)) {
3520                 Logger::notice(API_LOG_PREFIX . 'Not following contact', ['module' => 'api', 'action' => 'friendships_destroy']);
3521                 throw new HTTPException\NotFoundException('Not following Contact');
3522         }
3523
3524         try {
3525                 $result = Contact::terminateFriendship($owner, $contact);
3526
3527                 if ($result === null) {
3528                         Logger::notice(API_LOG_PREFIX . 'Not supported for {network}', ['module' => 'api', 'action' => 'friendships_destroy', 'network' => $contact['network']]);
3529                         throw new HTTPException\ExpectationFailedException('Unfollowing is currently not supported by this contact\'s network.');
3530                 }
3531
3532                 if ($result === false) {
3533                         throw new HTTPException\ServiceUnavailableException('Unable to unfollow this contact, please retry in a few minutes or contact your administrator.');
3534                 }
3535         } catch (Exception $e) {
3536                 Logger::error(API_LOG_PREFIX . $e->getMessage(), ['owner' => $owner, 'contact' => $contact]);
3537                 throw new HTTPException\InternalServerErrorException('Unable to unfollow this contact, please contact your administrator');
3538         }
3539
3540         // "uid" and "self" are only needed for some internal stuff, so remove it from here
3541         unset($contact['uid']);
3542         unset($contact['self']);
3543
3544         // Set screen_name since Twidere requests it
3545         $contact['screen_name'] = $contact['nick'];
3546
3547         return BaseApi::formatData('friendships-destroy', $type, ['user' => $contact]);
3548 }
3549 api_register_func('api/friendships/destroy', 'api_friendships_destroy', true, API_METHOD_POST);
3550
3551 /**
3552  *
3553  * @param string $type Return type (atom, rss, xml, json)
3554  * @param string $box
3555  * @param string $verbose
3556  *
3557  * @return array|string
3558  * @throws BadRequestException
3559  * @throws ForbiddenException
3560  * @throws ImagickException
3561  * @throws InternalServerErrorException
3562  * @throws UnauthorizedException
3563  */
3564 function api_direct_messages_box($type, $box, $verbose)
3565 {
3566         $a = DI::app();
3567         if (api_user() === false) {
3568                 throw new ForbiddenException();
3569         }
3570         // params
3571         $count = $_GET['count'] ?? 20;
3572         $page = $_REQUEST['page'] ?? 1;
3573
3574         $since_id = $_REQUEST['since_id'] ?? 0;
3575         $max_id = $_REQUEST['max_id'] ?? 0;
3576
3577         $user_id = $_REQUEST['user_id'] ?? '';
3578         $screen_name = $_REQUEST['screen_name'] ?? '';
3579
3580         //  caller user info
3581         unset($_REQUEST["user_id"]);
3582         unset($_GET["user_id"]);
3583
3584         unset($_REQUEST["screen_name"]);
3585         unset($_GET["screen_name"]);
3586
3587         $user_info = api_get_user();
3588         if ($user_info === false) {
3589                 throw new ForbiddenException();
3590         }
3591         $profile_url = $user_info["url"];
3592
3593         // pagination
3594         $start = max(0, ($page - 1) * $count);
3595
3596         $sql_extra = "";
3597
3598         // filters
3599         if ($box=="sentbox") {
3600                 $sql_extra = "`mail`.`from-url`='" . DBA::escape($profile_url) . "'";
3601         } elseif ($box == "conversation") {
3602                 $sql_extra = "`mail`.`parent-uri`='" . DBA::escape($_GET['uri'] ?? '')  . "'";
3603         } elseif ($box == "all") {
3604                 $sql_extra = "true";
3605         } elseif ($box == "inbox") {
3606                 $sql_extra = "`mail`.`from-url`!='" . DBA::escape($profile_url) . "'";
3607         }
3608
3609         if ($max_id > 0) {
3610                 $sql_extra .= ' AND `mail`.`id` <= ' . intval($max_id);
3611         }
3612
3613         if ($user_id != "") {
3614                 $sql_extra .= ' AND `mail`.`contact-id` = ' . intval($user_id);
3615         } elseif ($screen_name !="") {
3616                 $sql_extra .= " AND `contact`.`nick` = '" . DBA::escape($screen_name). "'";
3617         }
3618
3619         $r = DBA::toArray(DBA::p(
3620                 "SELECT `mail`.*, `contact`.`nurl` AS `contact-url` FROM `mail`,`contact` WHERE `mail`.`contact-id` = `contact`.`id` AND `mail`.`uid` = ? AND $sql_extra AND `mail`.`id` > ? ORDER BY `mail`.`id` DESC LIMIT ?,?",
3621                 api_user(),
3622                 $since_id,
3623                 $start,
3624                 $count
3625         ));
3626         if ($verbose == "true" && !DBA::isResult($r)) {
3627                 $answer = ['result' => 'error', 'message' => 'no mails available'];
3628                 return BaseApi::formatData("direct_messages_all", $type, ['$result' => $answer]);
3629         }
3630
3631         $ret = [];
3632         foreach ($r as $item) {
3633                 if ($box == "inbox" || $item['from-url'] != $profile_url) {
3634                         $recipient = $user_info;
3635                         $sender = api_get_user(Strings::normaliseLink($item['contact-url']));
3636                 } elseif ($box == "sentbox" || $item['from-url'] == $profile_url) {
3637                         $recipient = api_get_user(Strings::normaliseLink($item['contact-url']));
3638                         $sender = $user_info;
3639                 }
3640
3641                 if (isset($recipient) && isset($sender)) {
3642                         $ret[] = api_format_messages($item, $recipient, $sender);
3643                 }
3644         }
3645
3646
3647         $data = ['direct_message' => $ret];
3648         switch ($type) {
3649                 case "atom":
3650                         break;
3651                 case "rss":
3652                         $data = api_rss_extra($a, $data, $user_info);
3653                         break;
3654         }
3655
3656         return BaseApi::formatData("direct-messages", $type, $data);
3657 }
3658
3659 /**
3660  * Returns the most recent direct messages sent by the user.
3661  *
3662  * @param string $type Return type (atom, rss, xml, json)
3663  *
3664  * @return array|string
3665  * @throws BadRequestException
3666  * @throws ForbiddenException
3667  * @see https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/get-sent-message
3668  */
3669 function api_direct_messages_sentbox($type)
3670 {
3671         $verbose = !empty($_GET['friendica_verbose']) ? strtolower($_GET['friendica_verbose']) : "false";
3672         return api_direct_messages_box($type, "sentbox", $verbose);
3673 }
3674
3675 /**
3676  * Returns the most recent direct messages sent to the user.
3677  *
3678  * @param string $type Return type (atom, rss, xml, json)
3679  *
3680  * @return array|string
3681  * @throws BadRequestException
3682  * @throws ForbiddenException
3683  * @see https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/get-messages
3684  */
3685 function api_direct_messages_inbox($type)
3686 {
3687         $verbose = !empty($_GET['friendica_verbose']) ? strtolower($_GET['friendica_verbose']) : "false";
3688         return api_direct_messages_box($type, "inbox", $verbose);
3689 }
3690
3691 /**
3692  *
3693  * @param string $type Return type (atom, rss, xml, json)
3694  *
3695  * @return array|string
3696  * @throws BadRequestException
3697  * @throws ForbiddenException
3698  */
3699 function api_direct_messages_all($type)
3700 {
3701         $verbose = !empty($_GET['friendica_verbose']) ? strtolower($_GET['friendica_verbose']) : "false";
3702         return api_direct_messages_box($type, "all", $verbose);
3703 }
3704
3705 /**
3706  *
3707  * @param string $type Return type (atom, rss, xml, json)
3708  *
3709  * @return array|string
3710  * @throws BadRequestException
3711  * @throws ForbiddenException
3712  */
3713 function api_direct_messages_conversation($type)
3714 {
3715         $verbose = !empty($_GET['friendica_verbose']) ? strtolower($_GET['friendica_verbose']) : "false";
3716         return api_direct_messages_box($type, "conversation", $verbose);
3717 }
3718
3719 /// @TODO move to top of file or somewhere better
3720 api_register_func('api/direct_messages/conversation', 'api_direct_messages_conversation', true);
3721 api_register_func('api/direct_messages/all', 'api_direct_messages_all', true);
3722 api_register_func('api/direct_messages/sent', 'api_direct_messages_sentbox', true);
3723 api_register_func('api/direct_messages', 'api_direct_messages_inbox', true);
3724
3725 /**
3726  * delete a complete photoalbum with all containing photos from database through api
3727  *
3728  * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
3729  * @return string|array
3730  * @throws BadRequestException
3731  * @throws ForbiddenException
3732  * @throws InternalServerErrorException
3733  */
3734 function api_fr_photoalbum_delete($type)
3735 {
3736         if (api_user() === false) {
3737                 throw new ForbiddenException();
3738         }
3739         // input params
3740         $album = $_REQUEST['album'] ?? '';
3741
3742         // we do not allow calls without album string
3743         if ($album == "") {
3744                 throw new BadRequestException("no albumname specified");
3745         }
3746         // check if album is existing
3747
3748         $photos = DBA::selectToArray('photo', ['resource-id'], ['uid' => api_user(), 'album' => $album], ['group_by' => ['resource-id']]);
3749         if (!DBA::isResult($photos)) {
3750                 throw new BadRequestException("album not available");
3751         }
3752
3753         $resourceIds = array_column($photos, 'resource-id');
3754
3755         // function for setting the items to "deleted = 1" which ensures that comments, likes etc. are not shown anymore
3756         // to the user and the contacts of the users (drop_items() performs the federation of the deletion to other networks
3757         $condition = ['uid' => api_user(), 'resource-id' => $resourceIds, 'type' => 'photo'];
3758         Item::deleteForUser($condition, api_user());
3759
3760         // now let's delete all photos from the album
3761         $result = Photo::delete(['uid' => api_user(), 'album' => $album]);
3762
3763         // return success of deletion or error message
3764         if ($result) {
3765                 $answer = ['result' => 'deleted', 'message' => 'album `' . $album . '` with all containing photos has been deleted.'];
3766                 return BaseApi::formatData("photoalbum_delete", $type, ['$result' => $answer]);
3767         } else {
3768                 throw new InternalServerErrorException("unknown error - deleting from database failed");
3769         }
3770 }
3771
3772 /**
3773  * update the name of the album for all photos of an album
3774  *
3775  * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
3776  * @return string|array
3777  * @throws BadRequestException
3778  * @throws ForbiddenException
3779  * @throws InternalServerErrorException
3780  */
3781 function api_fr_photoalbum_update($type)
3782 {
3783         if (api_user() === false) {
3784                 throw new ForbiddenException();
3785         }
3786         // input params
3787         $album = $_REQUEST['album'] ?? '';
3788         $album_new = $_REQUEST['album_new'] ?? '';
3789
3790         // we do not allow calls without album string
3791         if ($album == "") {
3792                 throw new BadRequestException("no albumname specified");
3793         }
3794         if ($album_new == "") {
3795                 throw new BadRequestException("no new albumname specified");
3796         }
3797         // check if album is existing
3798         if (!Photo::exists(['uid' => api_user(), 'album' => $album])) {
3799                 throw new BadRequestException("album not available");
3800         }
3801         // now let's update all photos to the albumname
3802         $result = Photo::update(['album' => $album_new], ['uid' => api_user(), 'album' => $album]);
3803
3804         // return success of updating or error message
3805         if ($result) {
3806                 $answer = ['result' => 'updated', 'message' => 'album `' . $album . '` with all containing photos has been renamed to `' . $album_new . '`.'];
3807                 return BaseApi::formatData("photoalbum_update", $type, ['$result' => $answer]);
3808         } else {
3809                 throw new InternalServerErrorException("unknown error - updating in database failed");
3810         }
3811 }
3812
3813
3814 /**
3815  * list all photos of the authenticated user
3816  *
3817  * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
3818  * @return string|array
3819  * @throws ForbiddenException
3820  * @throws InternalServerErrorException
3821  */
3822 function api_fr_photos_list($type)
3823 {
3824         if (api_user() === false) {
3825                 throw new ForbiddenException();
3826         }
3827         $r = DBA::toArray(DBA::p(
3828                 "SELECT `resource-id`, MAX(scale) AS `scale`, `album`, `filename`, `type`, MAX(`created`) AS `created`,
3829                 MAX(`edited`) AS `edited`, MAX(`desc`) AS `desc` FROM `photo`
3830                 WHERE `uid` = ? AND NOT `photo-type` IN (?, ?) GROUP BY `resource-id`, `album`, `filename`, `type`",
3831                 local_user(), Photo::CONTACT_AVATAR, Photo::CONTACT_BANNER
3832         ));
3833         $typetoext = [
3834                 'image/jpeg' => 'jpg',
3835                 'image/png' => 'png',
3836                 'image/gif' => 'gif'
3837         ];
3838         $data = ['photo'=>[]];
3839         if (DBA::isResult($r)) {
3840                 foreach ($r as $rr) {
3841                         $photo = [];
3842                         $photo['id'] = $rr['resource-id'];
3843                         $photo['album'] = $rr['album'];
3844                         $photo['filename'] = $rr['filename'];
3845                         $photo['type'] = $rr['type'];
3846                         $thumb = DI::baseUrl() . "/photo/" . $rr['resource-id'] . "-" . $rr['scale'] . "." . $typetoext[$rr['type']];
3847                         $photo['created'] = $rr['created'];
3848                         $photo['edited'] = $rr['edited'];
3849                         $photo['desc'] = $rr['desc'];
3850
3851                         if ($type == "xml") {
3852                                 $data['photo'][] = ["@attributes" => $photo, "1" => $thumb];
3853                         } else {
3854                                 $photo['thumb'] = $thumb;
3855                                 $data['photo'][] = $photo;
3856                         }
3857                 }
3858         }
3859         return BaseApi::formatData("photos", $type, $data);
3860 }
3861
3862 /**
3863  * upload a new photo or change an existing photo
3864  *
3865  * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
3866  * @return string|array
3867  * @throws BadRequestException
3868  * @throws ForbiddenException
3869  * @throws ImagickException
3870  * @throws InternalServerErrorException
3871  * @throws NotFoundException
3872  */
3873 function api_fr_photo_create_update($type)
3874 {
3875         if (api_user() === false) {
3876                 throw new ForbiddenException();
3877         }
3878         // input params
3879         $photo_id  = $_REQUEST['photo_id']  ?? null;
3880         $desc      = $_REQUEST['desc']      ?? null;
3881         $album     = $_REQUEST['album']     ?? null;
3882         $album_new = $_REQUEST['album_new'] ?? null;
3883         $allow_cid = $_REQUEST['allow_cid'] ?? null;
3884         $deny_cid  = $_REQUEST['deny_cid' ] ?? null;
3885         $allow_gid = $_REQUEST['allow_gid'] ?? null;
3886         $deny_gid  = $_REQUEST['deny_gid' ] ?? null;
3887         $visibility = !$allow_cid && !$deny_cid && !$allow_gid && !$deny_gid;
3888
3889         // do several checks on input parameters
3890         // we do not allow calls without album string
3891         if ($album == null) {
3892                 throw new BadRequestException("no albumname specified");
3893         }
3894         // if photo_id == null --> we are uploading a new photo
3895         if ($photo_id == null) {
3896                 $mode = "create";
3897
3898                 // error if no media posted in create-mode
3899                 if (empty($_FILES['media'])) {
3900                         // Output error
3901                         throw new BadRequestException("no media data submitted");
3902                 }
3903
3904                 // album_new will be ignored in create-mode
3905                 $album_new = "";
3906         } else {
3907                 $mode = "update";
3908
3909                 // check if photo is existing in databasei
3910                 if (!Photo::exists(['resource-id' => $photo_id, 'uid' => api_user(), 'album' => $album])) {
3911                         throw new BadRequestException("photo not available");
3912                 }
3913         }
3914
3915         // checks on acl strings provided by clients
3916         $acl_input_error = false;
3917         $acl_input_error |= check_acl_input($allow_cid);
3918         $acl_input_error |= check_acl_input($deny_cid);
3919         $acl_input_error |= check_acl_input($allow_gid);
3920         $acl_input_error |= check_acl_input($deny_gid);
3921         if ($acl_input_error) {
3922                 throw new BadRequestException("acl data invalid");
3923         }
3924         // now let's upload the new media in create-mode
3925         if ($mode == "create") {
3926                 $media = $_FILES['media'];
3927                 $data = save_media_to_database("photo", $media, $type, $album, trim($allow_cid), trim($deny_cid), trim($allow_gid), trim($deny_gid), $desc, Photo::DEFAULT, $visibility);
3928
3929                 // return success of updating or error message
3930                 if (!is_null($data)) {
3931                         return BaseApi::formatData("photo_create", $type, $data);
3932                 } else {
3933                         throw new InternalServerErrorException("unknown error - uploading photo failed, see Friendica log for more information");
3934                 }
3935         }
3936
3937         // now let's do the changes in update-mode
3938         if ($mode == "update") {
3939                 $updated_fields = [];
3940
3941                 if (!is_null($desc)) {
3942                         $updated_fields['desc'] = $desc;
3943                 }
3944
3945                 if (!is_null($album_new)) {
3946                         $updated_fields['album'] = $album_new;
3947                 }
3948
3949                 if (!is_null($allow_cid)) {
3950                         $allow_cid = trim($allow_cid);
3951                         $updated_fields['allow_cid'] = $allow_cid;
3952                 }
3953
3954                 if (!is_null($deny_cid)) {
3955                         $deny_cid = trim($deny_cid);
3956                         $updated_fields['deny_cid'] = $deny_cid;
3957                 }
3958
3959                 if (!is_null($allow_gid)) {
3960                         $allow_gid = trim($allow_gid);
3961                         $updated_fields['allow_gid'] = $allow_gid;
3962                 }
3963
3964                 if (!is_null($deny_gid)) {
3965                         $deny_gid = trim($deny_gid);
3966                         $updated_fields['deny_gid'] = $deny_gid;
3967                 }
3968
3969                 $result = false;
3970                 if (count($updated_fields) > 0) {
3971                         $nothingtodo = false;
3972                         $result = Photo::update($updated_fields, ['uid' => api_user(), 'resource-id' => $photo_id, 'album' => $album]);
3973                 } else {
3974                         $nothingtodo = true;
3975                 }
3976
3977                 if (!empty($_FILES['media'])) {
3978                         $nothingtodo = false;
3979                         $media = $_FILES['media'];
3980                         $data = save_media_to_database("photo", $media, $type, $album, $allow_cid, $deny_cid, $allow_gid, $deny_gid, $desc, Photo::DEFAULT, $visibility, $photo_id);
3981                         if (!is_null($data)) {
3982                                 return BaseApi::formatData("photo_update", $type, $data);
3983                         }
3984                 }
3985
3986                 // return success of updating or error message
3987                 if ($result) {
3988                         $answer = ['result' => 'updated', 'message' => 'Image id `' . $photo_id . '` has been updated.'];
3989                         return BaseApi::formatData("photo_update", $type, ['$result' => $answer]);
3990                 } else {
3991                         if ($nothingtodo) {
3992                                 $answer = ['result' => 'cancelled', 'message' => 'Nothing to update for image id `' . $photo_id . '`.'];
3993                                 return BaseApi::formatData("photo_update", $type, ['$result' => $answer]);
3994                         }
3995                         throw new InternalServerErrorException("unknown error - update photo entry in database failed");
3996                 }
3997         }
3998         throw new InternalServerErrorException("unknown error - this error on uploading or updating a photo should never happen");
3999 }
4000
4001 /**
4002  * delete a single photo from the database through api
4003  *
4004  * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
4005  * @return string|array
4006  * @throws BadRequestException
4007  * @throws ForbiddenException
4008  * @throws InternalServerErrorException
4009  */
4010 function api_fr_photo_delete($type)
4011 {
4012         if (api_user() === false) {
4013                 throw new ForbiddenException();
4014         }
4015
4016         // input params
4017         $photo_id = $_REQUEST['photo_id'] ?? null;
4018
4019         // do several checks on input parameters
4020         // we do not allow calls without photo id
4021         if ($photo_id == null) {
4022                 throw new BadRequestException("no photo_id specified");
4023         }
4024
4025         // check if photo is existing in database
4026         if (!Photo::exists(['resource-id' => $photo_id, 'uid' => api_user()])) {
4027                 throw new BadRequestException("photo not available");
4028         }
4029
4030         // now we can perform on the deletion of the photo
4031         $result = Photo::delete(['uid' => api_user(), 'resource-id' => $photo_id]);
4032
4033         // return success of deletion or error message
4034         if ($result) {
4035                 // function for setting the items to "deleted = 1" which ensures that comments, likes etc. are not shown anymore
4036                 // to the user and the contacts of the users (drop_items() do all the necessary magic to avoid orphans in database and federate deletion)
4037                 $condition = ['uid' => api_user(), 'resource-id' => $photo_id, 'type' => 'photo'];
4038                 Item::deleteForUser($condition, api_user());
4039
4040                 $result = ['result' => 'deleted', 'message' => 'photo with id `' . $photo_id . '` has been deleted from server.'];
4041                 return BaseApi::formatData("photo_delete", $type, ['$result' => $result]);
4042         } else {
4043                 throw new InternalServerErrorException("unknown error on deleting photo from database table");
4044         }
4045 }
4046
4047
4048 /**
4049  * returns the details of a specified photo id, if scale is given, returns the photo data in base 64
4050  *
4051  * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
4052  * @return string|array
4053  * @throws BadRequestException
4054  * @throws ForbiddenException
4055  * @throws InternalServerErrorException
4056  * @throws NotFoundException
4057  */
4058 function api_fr_photo_detail($type)
4059 {
4060         if (api_user() === false) {
4061                 throw new ForbiddenException();
4062         }
4063         if (empty($_REQUEST['photo_id'])) {
4064                 throw new BadRequestException("No photo id.");
4065         }
4066
4067         $scale = (!empty($_REQUEST['scale']) ? intval($_REQUEST['scale']) : false);
4068         $photo_id = $_REQUEST['photo_id'];
4069
4070         // prepare json/xml output with data from database for the requested photo
4071         $data = prepare_photo_data($type, $scale, $photo_id);
4072
4073         return BaseApi::formatData("photo_detail", $type, $data);
4074 }
4075
4076
4077 /**
4078  * updates the profile image for the user (either a specified profile or the default profile)
4079  *
4080  * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
4081  *
4082  * @return string|array
4083  * @throws BadRequestException
4084  * @throws ForbiddenException
4085  * @throws ImagickException
4086  * @throws InternalServerErrorException
4087  * @throws NotFoundException
4088  * @see   https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile_image
4089  */
4090 function api_account_update_profile_image($type)
4091 {
4092         if (api_user() === false) {
4093                 throw new ForbiddenException();
4094         }
4095         // input params
4096         $profile_id = $_REQUEST['profile_id'] ?? 0;
4097
4098         // error if image data is missing
4099         if (empty($_FILES['image'])) {
4100                 throw new BadRequestException("no media data submitted");
4101         }
4102
4103         // check if specified profile id is valid
4104         if ($profile_id != 0) {
4105                 $profile = DBA::selectFirst('profile', ['is-default'], ['uid' => api_user(), 'id' => $profile_id]);
4106                 // error message if specified profile id is not in database
4107                 if (!DBA::isResult($profile)) {
4108                         throw new BadRequestException("profile_id not available");
4109                 }
4110                 $is_default_profile = $profile['is-default'];
4111         } else {
4112                 $is_default_profile = 1;
4113         }
4114
4115         // get mediadata from image or media (Twitter call api/account/update_profile_image provides image)
4116         $media = null;
4117         if (!empty($_FILES['image'])) {
4118                 $media = $_FILES['image'];
4119         } elseif (!empty($_FILES['media'])) {
4120                 $media = $_FILES['media'];
4121         }
4122         // save new profile image
4123         $data = save_media_to_database("profileimage", $media, $type, DI::l10n()->t(Photo::PROFILE_PHOTOS), "", "", "", "", "", Photo::USER_AVATAR);
4124
4125         // get filetype
4126         if (is_array($media['type'])) {
4127                 $filetype = $media['type'][0];
4128         } else {
4129                 $filetype = $media['type'];
4130         }
4131         if ($filetype == "image/jpeg") {
4132                 $fileext = "jpg";
4133         } elseif ($filetype == "image/png") {
4134                 $fileext = "png";
4135         } else {
4136                 throw new InternalServerErrorException('Unsupported filetype');
4137         }
4138
4139         // change specified profile or all profiles to the new resource-id
4140         if ($is_default_profile) {
4141                 $condition = ["`profile` AND `resource-id` != ? AND `uid` = ?", $data['photo']['id'], api_user()];
4142                 Photo::update(['profile' => false, 'photo-type' => Photo::DEFAULT], $condition);
4143         } else {
4144                 $fields = ['photo' => DI::baseUrl() . '/photo/' . $data['photo']['id'] . '-4.' . $fileext,
4145                         'thumb' => DI::baseUrl() . '/photo/' . $data['photo']['id'] . '-5.' . $fileext];
4146                 DBA::update('profile', $fields, ['id' => $_REQUEST['profile'], 'uid' => api_user()]);
4147         }
4148
4149         Contact::updateSelfFromUserID(api_user(), true);
4150
4151         // Update global directory in background
4152         Profile::publishUpdate(api_user());
4153
4154         // output for client
4155         if ($data) {
4156                 return api_account_verify_credentials($type);
4157         } else {
4158                 // SaveMediaToDatabase failed for some reason
4159                 throw new InternalServerErrorException("image upload failed");
4160         }
4161 }
4162
4163 // place api-register for photoalbum calls before 'api/friendica/photo', otherwise this function is never reached
4164 api_register_func('api/friendica/photoalbum/delete', 'api_fr_photoalbum_delete', true, API_METHOD_DELETE);
4165 api_register_func('api/friendica/photoalbum/update', 'api_fr_photoalbum_update', true, API_METHOD_POST);
4166 api_register_func('api/friendica/photos/list', 'api_fr_photos_list', true);
4167 api_register_func('api/friendica/photo/create', 'api_fr_photo_create_update', true, API_METHOD_POST);
4168 api_register_func('api/friendica/photo/update', 'api_fr_photo_create_update', true, API_METHOD_POST);
4169 api_register_func('api/friendica/photo/delete', 'api_fr_photo_delete', true, API_METHOD_DELETE);
4170 api_register_func('api/friendica/photo', 'api_fr_photo_detail', true);
4171 api_register_func('api/account/update_profile_image', 'api_account_update_profile_image', true, API_METHOD_POST);
4172
4173 /**
4174  * Update user profile
4175  *
4176  * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
4177  *
4178  * @return array|string
4179  * @throws BadRequestException
4180  * @throws ForbiddenException
4181  * @throws ImagickException
4182  * @throws InternalServerErrorException
4183  * @throws UnauthorizedException
4184  */
4185 function api_account_update_profile($type)
4186 {
4187         $local_user = api_user();
4188         $api_user = api_get_user();
4189
4190         if (!empty($_POST['name'])) {
4191                 DBA::update('profile', ['name' => $_POST['name']], ['uid' => $local_user]);
4192                 DBA::update('user', ['username' => $_POST['name']], ['uid' => $local_user]);
4193                 Contact::update(['name' => $_POST['name']], ['uid' => $local_user, 'self' => 1]);
4194                 Contact::update(['name' => $_POST['name']], ['id' => $api_user['id']]);
4195         }
4196
4197         if (isset($_POST['description'])) {
4198                 DBA::update('profile', ['about' => $_POST['description']], ['uid' => $local_user]);
4199                 Contact::update(['about' => $_POST['description']], ['uid' => $local_user, 'self' => 1]);
4200                 Contact::update(['about' => $_POST['description']], ['id' => $api_user['id']]);
4201         }
4202
4203         Profile::publishUpdate($local_user);
4204
4205         return api_account_verify_credentials($type);
4206 }
4207
4208 /// @TODO move to top of file or somewhere better
4209 api_register_func('api/account/update_profile', 'api_account_update_profile', true, API_METHOD_POST);
4210
4211 /**
4212  *
4213  * @param string $acl_string
4214  * @return bool
4215  * @throws Exception
4216  */
4217 function check_acl_input($acl_string)
4218 {
4219         if (empty($acl_string)) {
4220                 return false;
4221         }
4222
4223         $contact_not_found = false;
4224
4225         // split <x><y><z> into array of cid's
4226         preg_match_all("/<[A-Za-z0-9]+>/", $acl_string, $array);
4227
4228         // check for each cid if it is available on server
4229         $cid_array = $array[0];
4230         foreach ($cid_array as $cid) {
4231                 $cid = str_replace("<", "", $cid);
4232                 $cid = str_replace(">", "", $cid);
4233                 $condition = ['id' => $cid, 'uid' => api_user()];
4234                 $contact_not_found |= !DBA::exists('contact', $condition);
4235         }
4236         return $contact_not_found;
4237 }
4238
4239 /**
4240  * @param string  $mediatype
4241  * @param array   $media
4242  * @param string  $type
4243  * @param string  $album
4244  * @param string  $allow_cid
4245  * @param string  $deny_cid
4246  * @param string  $allow_gid
4247  * @param string  $deny_gid
4248  * @param string  $desc
4249  * @param integer $phototype
4250  * @param boolean $visibility
4251  * @param string  $photo_id
4252  * @return array
4253  * @throws BadRequestException
4254  * @throws ForbiddenException
4255  * @throws ImagickException
4256  * @throws InternalServerErrorException
4257  * @throws NotFoundException
4258  * @throws UnauthorizedException
4259  */
4260 function save_media_to_database($mediatype, $media, $type, $album, $allow_cid, $deny_cid, $allow_gid, $deny_gid, $desc, $phototype = 0, $visibility = false, $photo_id = null)
4261 {
4262         $visitor   = 0;
4263         $src = "";
4264         $filetype = "";
4265         $filename = "";
4266         $filesize = 0;
4267
4268         if (is_array($media)) {
4269                 if (is_array($media['tmp_name'])) {
4270                         $src = $media['tmp_name'][0];
4271                 } else {
4272                         $src = $media['tmp_name'];
4273                 }
4274                 if (is_array($media['name'])) {
4275                         $filename = basename($media['name'][0]);
4276                 } else {
4277                         $filename = basename($media['name']);
4278                 }
4279                 if (is_array($media['size'])) {
4280                         $filesize = intval($media['size'][0]);
4281                 } else {
4282                         $filesize = intval($media['size']);
4283                 }
4284                 if (is_array($media['type'])) {
4285                         $filetype = $media['type'][0];
4286                 } else {
4287                         $filetype = $media['type'];
4288                 }
4289         }
4290
4291         $filetype = Images::getMimeTypeBySource($src, $filename, $filetype);
4292
4293         logger::info(
4294                 "File upload src: " . $src . " - filename: " . $filename .
4295                 " - size: " . $filesize . " - type: " . $filetype);
4296
4297         // check if there was a php upload error
4298         if ($filesize == 0 && $media['error'] == 1) {
4299                 throw new InternalServerErrorException("image size exceeds PHP config settings, file was rejected by server");
4300         }
4301         // check against max upload size within Friendica instance
4302         $maximagesize = DI::config()->get('system', 'maximagesize');
4303         if ($maximagesize && ($filesize > $maximagesize)) {
4304                 $formattedBytes = Strings::formatBytes($maximagesize);
4305                 throw new InternalServerErrorException("image size exceeds Friendica config setting (uploaded size: $formattedBytes)");
4306         }
4307
4308         // create Photo instance with the data of the image
4309         $imagedata = @file_get_contents($src);
4310         $Image = new Image($imagedata, $filetype);
4311         if (!$Image->isValid()) {
4312                 throw new InternalServerErrorException("unable to process image data");
4313         }
4314
4315         // check orientation of image
4316         $Image->orient($src);
4317         @unlink($src);
4318
4319         // check max length of images on server
4320         $max_length = DI::config()->get('system', 'max_image_length');
4321         if ($max_length > 0) {
4322                 $Image->scaleDown($max_length);
4323                 logger::info("File upload: Scaling picture to new size " . $max_length);
4324         }
4325         $width = $Image->getWidth();
4326         $height = $Image->getHeight();
4327
4328         // create a new resource-id if not already provided
4329         $resource_id = ($photo_id == null) ? Photo::newResource() : $photo_id;
4330
4331         if ($mediatype == "photo") {
4332                 // upload normal image (scales 0, 1, 2)
4333                 logger::info("photo upload: starting new photo upload");
4334
4335                 $r = Photo::store($Image, local_user(), $visitor, $resource_id, $filename, $album, 0, Photo::DEFAULT, $allow_cid, $allow_gid, $deny_cid, $deny_gid, $desc);
4336                 if (!$r) {
4337                         logger::notice("photo upload: image upload with scale 0 (original size) failed");
4338                 }
4339                 if ($width > 640 || $height > 640) {
4340                         $Image->scaleDown(640);
4341                         $r = Photo::store($Image, local_user(), $visitor, $resource_id, $filename, $album, 1, Photo::DEFAULT, $allow_cid, $allow_gid, $deny_cid, $deny_gid, $desc);
4342                         if (!$r) {
4343                                 logger::notice("photo upload: image upload with scale 1 (640x640) failed");
4344                         }
4345                 }
4346
4347                 if ($width > 320 || $height > 320) {
4348                         $Image->scaleDown(320);
4349                         $r = Photo::store($Image, local_user(), $visitor, $resource_id, $filename, $album, 2, Photo::DEFAULT, $allow_cid, $allow_gid, $deny_cid, $deny_gid, $desc);
4350                         if (!$r) {
4351                                 logger::notice("photo upload: image upload with scale 2 (320x320) failed");
4352                         }
4353                 }
4354                 logger::info("photo upload: new photo upload ended");
4355         } elseif ($mediatype == "profileimage") {
4356                 // upload profile image (scales 4, 5, 6)
4357                 logger::info("photo upload: starting new profile image upload");
4358
4359                 if ($width > 300 || $height > 300) {
4360                         $Image->scaleDown(300);
4361                         $r = Photo::store($Image, local_user(), $visitor, $resource_id, $filename, $album, 4, $phototype, $allow_cid, $allow_gid, $deny_cid, $deny_gid, $desc);
4362                         if (!$r) {
4363                                 logger::notice("photo upload: profile image upload with scale 4 (300x300) failed");
4364                         }
4365                 }
4366
4367                 if ($width > 80 || $height > 80) {
4368                         $Image->scaleDown(80);
4369                         $r = Photo::store($Image, local_user(), $visitor, $resource_id, $filename, $album, 5, $phototype, $allow_cid, $allow_gid, $deny_cid, $deny_gid, $desc);
4370                         if (!$r) {
4371                                 logger::notice("photo upload: profile image upload with scale 5 (80x80) failed");
4372                         }
4373                 }
4374
4375                 if ($width > 48 || $height > 48) {
4376                         $Image->scaleDown(48);
4377                         $r = Photo::store($Image, local_user(), $visitor, $resource_id, $filename, $album, 6, $phototype, $allow_cid, $allow_gid, $deny_cid, $deny_gid, $desc);
4378                         if (!$r) {
4379                                 logger::notice("photo upload: profile image upload with scale 6 (48x48) failed");
4380                         }
4381                 }
4382                 $Image->__destruct();
4383                 logger::info("photo upload: new profile image upload ended");
4384         }
4385
4386         if (!empty($r)) {
4387                 // create entry in 'item'-table on new uploads to enable users to comment/like/dislike the photo
4388                 if ($photo_id == null && $mediatype == "photo") {
4389                         post_photo_item($resource_id, $allow_cid, $deny_cid, $allow_gid, $deny_gid, $filetype, $visibility);
4390                 }
4391                 // on success return image data in json/xml format (like /api/friendica/photo does when no scale is given)
4392                 return prepare_photo_data($type, false, $resource_id);
4393         } else {
4394                 throw new InternalServerErrorException("image upload failed");
4395         }
4396 }
4397
4398 /**
4399  *
4400  * @param string  $hash
4401  * @param string  $allow_cid
4402  * @param string  $deny_cid
4403  * @param string  $allow_gid
4404  * @param string  $deny_gid
4405  * @param string  $filetype
4406  * @param boolean $visibility
4407  * @throws InternalServerErrorException
4408  */
4409 function post_photo_item($hash, $allow_cid, $deny_cid, $allow_gid, $deny_gid, $filetype, $visibility = false)
4410 {
4411         // get data about the api authenticated user
4412         $uri = Item::newURI(intval(api_user()));
4413         $owner_record = DBA::selectFirst('contact', [], ['uid' => api_user(), 'self' => true]);
4414
4415         $arr = [];
4416         $arr['guid']          = System::createUUID();
4417         $arr['uid']           = intval(api_user());
4418         $arr['uri']           = $uri;
4419         $arr['type']          = 'photo';
4420         $arr['wall']          = 1;
4421         $arr['resource-id']   = $hash;
4422         $arr['contact-id']    = $owner_record['id'];
4423         $arr['owner-name']    = $owner_record['name'];
4424         $arr['owner-link']    = $owner_record['url'];
4425         $arr['owner-avatar']  = $owner_record['thumb'];
4426         $arr['author-name']   = $owner_record['name'];
4427         $arr['author-link']   = $owner_record['url'];
4428         $arr['author-avatar'] = $owner_record['thumb'];
4429         $arr['title']         = "";
4430         $arr['allow_cid']     = $allow_cid;
4431         $arr['allow_gid']     = $allow_gid;
4432         $arr['deny_cid']      = $deny_cid;
4433         $arr['deny_gid']      = $deny_gid;
4434         $arr['visible']       = $visibility;
4435         $arr['origin']        = 1;
4436
4437         $typetoext = [
4438                         'image/jpeg' => 'jpg',
4439                         'image/png' => 'png',
4440                         'image/gif' => 'gif'
4441                         ];
4442
4443         // adds link to the thumbnail scale photo
4444         $arr['body'] = '[url=' . DI::baseUrl() . '/photos/' . $owner_record['nick'] . '/image/' . $hash . ']'
4445                                 . '[img]' . DI::baseUrl() . '/photo/' . $hash . '-' . "2" . '.'. $typetoext[$filetype] . '[/img]'
4446                                 . '[/url]';
4447
4448         // do the magic for storing the item in the database and trigger the federation to other contacts
4449         Item::insert($arr);
4450 }
4451
4452 /**
4453  *
4454  * @param string $type
4455  * @param int    $scale
4456  * @param string $photo_id
4457  *
4458  * @return array
4459  * @throws BadRequestException
4460  * @throws ForbiddenException
4461  * @throws ImagickException
4462  * @throws InternalServerErrorException
4463  * @throws NotFoundException
4464  * @throws UnauthorizedException
4465  */
4466 function prepare_photo_data($type, $scale, $photo_id)
4467 {
4468         $a = DI::app();
4469         $user_info = api_get_user();
4470
4471         if ($user_info === false) {
4472                 throw new ForbiddenException();
4473         }
4474
4475         $scale_sql = ($scale === false ? "" : sprintf("AND scale=%d", intval($scale)));
4476         $data_sql = ($scale === false ? "" : "data, ");
4477
4478         // added allow_cid, allow_gid, deny_cid, deny_gid to output as string like stored in database
4479         // clients needs to convert this in their way for further processing
4480         $r = DBA::toArray(DBA::p(
4481                 "SELECT $data_sql `resource-id`, `created`, `edited`, `title`, `desc`, `album`, `filename`,
4482                                         `type`, `height`, `width`, `datasize`, `profile`, `allow_cid`, `deny_cid`, `allow_gid`, `deny_gid`,
4483                                         MIN(`scale`) AS `minscale`, MAX(`scale`) AS `maxscale`
4484                         FROM `photo` WHERE `uid` = ? AND `resource-id` = ? $scale_sql GROUP BY
4485                                    `resource-id`, `created`, `edited`, `title`, `desc`, `album`, `filename`,
4486                                    `type`, `height`, `width`, `datasize`, `profile`, `allow_cid`, `deny_cid`, `allow_gid`, `deny_gid`",
4487                 local_user(),
4488                 $photo_id
4489         ));
4490
4491         $typetoext = [
4492                 'image/jpeg' => 'jpg',
4493                 'image/png' => 'png',
4494                 'image/gif' => 'gif'
4495         ];
4496
4497         // prepare output data for photo
4498         if (DBA::isResult($r)) {
4499                 $data = ['photo' => $r[0]];
4500                 $data['photo']['id'] = $data['photo']['resource-id'];
4501                 if ($scale !== false) {
4502                         $data['photo']['data'] = base64_encode($data['photo']['data']);
4503                 } else {
4504                         unset($data['photo']['datasize']); //needed only with scale param
4505                 }
4506                 if ($type == "xml") {
4507                         $data['photo']['links'] = [];
4508                         for ($k = intval($data['photo']['minscale']); $k <= intval($data['photo']['maxscale']); $k++) {
4509                                 $data['photo']['links'][$k . ":link"]["@attributes"] = ["type" => $data['photo']['type'],
4510                                                                                 "scale" => $k,
4511                                                                                 "href" => DI::baseUrl() . "/photo/" . $data['photo']['resource-id'] . "-" . $k . "." . $typetoext[$data['photo']['type']]];
4512                         }
4513                 } else {
4514                         $data['photo']['link'] = [];
4515                         // when we have profile images we could have only scales from 4 to 6, but index of array always needs to start with 0
4516                         $i = 0;
4517                         for ($k = intval($data['photo']['minscale']); $k <= intval($data['photo']['maxscale']); $k++) {
4518                                 $data['photo']['link'][$i] = DI::baseUrl() . "/photo/" . $data['photo']['resource-id'] . "-" . $k . "." . $typetoext[$data['photo']['type']];
4519                                 $i++;
4520                         }
4521                 }
4522                 unset($data['photo']['resource-id']);
4523                 unset($data['photo']['minscale']);
4524                 unset($data['photo']['maxscale']);
4525         } else {
4526                 throw new NotFoundException();
4527         }
4528
4529         // retrieve item element for getting activities (like, dislike etc.) related to photo
4530         $condition = ['uid' => api_user(), 'resource-id' => $photo_id];
4531         $item = Post::selectFirst(['id', 'uid', 'uri', 'parent', 'allow_cid', 'deny_cid', 'allow_gid', 'deny_gid'], $condition);
4532         if (!DBA::isResult($item)) {
4533                 throw new NotFoundException('Photo-related item not found.');
4534         }
4535
4536         $data['photo']['friendica_activities'] = api_format_items_activities($item, $type);
4537
4538         // retrieve comments on photo
4539         $condition = ["`parent` = ? AND `uid` = ? AND `gravity` IN (?, ?)",
4540                 $item['parent'], api_user(), GRAVITY_PARENT, GRAVITY_COMMENT];
4541
4542         $statuses = Post::selectForUser(api_user(), [], $condition);
4543
4544         // prepare output of comments
4545         $commentData = api_format_items(Post::toArray($statuses), $user_info, false, $type);
4546         $comments = [];
4547         if ($type == "xml") {
4548                 $k = 0;
4549                 foreach ($commentData as $comment) {
4550                         $comments[$k++ . ":comment"] = $comment;
4551                 }
4552         } else {
4553                 foreach ($commentData as $comment) {
4554                         $comments[] = $comment;
4555                 }
4556         }
4557         $data['photo']['friendica_comments'] = $comments;
4558
4559         // include info if rights on photo and rights on item are mismatching
4560         $rights_mismatch = $data['photo']['allow_cid'] != $item['allow_cid'] ||
4561                 $data['photo']['deny_cid'] != $item['deny_cid'] ||
4562                 $data['photo']['allow_gid'] != $item['allow_gid'] ||
4563                 $data['photo']['deny_gid'] != $item['deny_gid'];
4564         $data['photo']['rights_mismatch'] = $rights_mismatch;
4565
4566         return $data;
4567 }
4568
4569 /**
4570  * Return an item with announcer data if it had been announced
4571  *
4572  * @param array $item Item array
4573  * @return array Item array with announce data
4574  */
4575 function api_get_announce($item)
4576 {
4577         // Quit if the item already has got a different owner and author
4578         if ($item['owner-id'] != $item['author-id']) {
4579                 return [];
4580         }
4581
4582         // Don't change original or Diaspora posts
4583         if ($item['origin'] || in_array($item['network'], [Protocol::DIASPORA])) {
4584                 return [];
4585         }
4586
4587         // Quit if we do now the original author and it had been a post from a native network
4588         if (!empty($item['contact-uid']) && in_array($item['network'], Protocol::NATIVE_SUPPORT)) {
4589                 return [];
4590         }
4591
4592         $fields = ['author-id', 'author-name', 'author-link', 'author-avatar'];
4593         $condition = ['parent-uri' => $item['uri'], 'gravity' => GRAVITY_ACTIVITY, 'uid' => [0, $item['uid']], 'vid' => Verb::getID(Activity::ANNOUNCE)];
4594         $announce = Post::selectFirstForUser($item['uid'], $fields, $condition, ['order' => ['received' => true]]);
4595         if (!DBA::isResult($announce)) {
4596                 return [];
4597         }
4598
4599         return array_merge($item, $announce);
4600 }
4601
4602 /**
4603  *
4604  * @param array $item
4605  *
4606  * @return array
4607  * @throws Exception
4608  */
4609 function api_in_reply_to($item)
4610 {
4611         $in_reply_to = [];
4612
4613         $in_reply_to['status_id'] = null;
4614         $in_reply_to['user_id'] = null;
4615         $in_reply_to['status_id_str'] = null;
4616         $in_reply_to['user_id_str'] = null;
4617         $in_reply_to['screen_name'] = null;
4618
4619         if (($item['thr-parent'] != $item['uri']) && ($item['gravity'] != GRAVITY_PARENT)) {
4620                 $parent = Post::selectFirst(['id'], ['uid' => $item['uid'], 'uri' => $item['thr-parent']]);
4621                 if (DBA::isResult($parent)) {
4622                         $in_reply_to['status_id'] = intval($parent['id']);
4623                 } else {
4624                         $in_reply_to['status_id'] = intval($item['parent']);
4625                 }
4626
4627                 $in_reply_to['status_id_str'] = (string) intval($in_reply_to['status_id']);
4628
4629                 $fields = ['author-nick', 'author-name', 'author-id', 'author-link'];
4630                 $parent = Post::selectFirst($fields, ['id' => $in_reply_to['status_id']]);
4631
4632                 if (DBA::isResult($parent)) {
4633                         $in_reply_to['screen_name'] = (($parent['author-nick']) ? $parent['author-nick'] : $parent['author-name']);
4634                         $in_reply_to['user_id'] = intval($parent['author-id']);
4635                         $in_reply_to['user_id_str'] = (string) intval($parent['author-id']);
4636                 }
4637
4638                 // There seems to be situation, where both fields are identical:
4639                 // https://github.com/friendica/friendica/issues/1010
4640                 // This is a bugfix for that.
4641                 if (intval($in_reply_to['status_id']) == intval($item['id'])) {
4642                         Logger::warning(API_LOG_PREFIX . 'ID {id} is similar to reply-to {reply-to}', ['module' => 'api', 'action' => 'in_reply_to', 'id' => $item['id'], 'reply-to' => $in_reply_to['status_id']]);
4643                         $in_reply_to['status_id'] = null;
4644                         $in_reply_to['user_id'] = null;
4645                         $in_reply_to['status_id_str'] = null;
4646                         $in_reply_to['user_id_str'] = null;
4647                         $in_reply_to['screen_name'] = null;
4648                 }
4649         }
4650
4651         return $in_reply_to;
4652 }
4653
4654 /**
4655  *
4656  * @param string $text
4657  *
4658  * @return string
4659  * @throws InternalServerErrorException
4660  */
4661 function api_clean_plain_items($text)
4662 {
4663         $include_entities = strtolower($_REQUEST['include_entities'] ?? 'false');
4664
4665         $text = BBCode::cleanPictureLinks($text);
4666         $URLSearchString = "^\[\]";
4667
4668         $text = preg_replace("/([!#@])\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", '$1$3', $text);
4669
4670         if ($include_entities == "true") {
4671                 $text = preg_replace("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", '[url=$1]$1[/url]', $text);
4672         }
4673
4674         // Simplify "attachment" element
4675         $text = BBCode::removeAttachment($text);
4676
4677         return $text;
4678 }
4679
4680 /**
4681  *
4682  * @param array $contacts
4683  *
4684  * @return void
4685  */
4686 function api_best_nickname(&$contacts)
4687 {
4688         $best_contact = [];
4689
4690         if (count($contacts) == 0) {
4691                 return;
4692         }
4693
4694         foreach ($contacts as $contact) {
4695                 if ($contact["network"] == "") {
4696                         $contact["network"] = "dfrn";
4697                         $best_contact = [$contact];
4698                 }
4699         }
4700
4701         if (sizeof($best_contact) == 0) {
4702                 foreach ($contacts as $contact) {
4703                         if ($contact["network"] == "dfrn") {
4704                                 $best_contact = [$contact];
4705                         }
4706                 }
4707         }
4708
4709         if (sizeof($best_contact) == 0) {
4710                 foreach ($contacts as $contact) {
4711                         if ($contact["network"] == "dspr") {
4712                                 $best_contact = [$contact];
4713                         }
4714                 }
4715         }
4716
4717         if (sizeof($best_contact) == 0) {
4718                 foreach ($contacts as $contact) {
4719                         if ($contact["network"] == "stat") {
4720                                 $best_contact = [$contact];
4721                         }
4722                 }
4723         }
4724
4725         if (sizeof($best_contact) == 0) {
4726                 foreach ($contacts as $contact) {
4727                         if ($contact["network"] == "pump") {
4728                                 $best_contact = [$contact];
4729                         }
4730                 }
4731         }
4732
4733         if (sizeof($best_contact) == 0) {
4734                 foreach ($contacts as $contact) {
4735                         if ($contact["network"] == "twit") {
4736                                 $best_contact = [$contact];
4737                         }
4738                 }
4739         }
4740
4741         if (sizeof($best_contact) == 1) {
4742                 $contacts = $best_contact;
4743         } else {
4744                 $contacts = [$contacts[0]];
4745         }
4746 }
4747
4748 /**
4749  * Return all or a specified group of the user with the containing contacts.
4750  *
4751  * @param string $type Return type (atom, rss, xml, json)
4752  *
4753  * @return array|string
4754  * @throws BadRequestException
4755  * @throws ForbiddenException
4756  * @throws ImagickException
4757  * @throws InternalServerErrorException
4758  * @throws UnauthorizedException
4759  */
4760 function api_friendica_group_show($type)
4761 {
4762         $a = DI::app();
4763
4764         if (api_user() === false) {
4765                 throw new ForbiddenException();
4766         }
4767
4768         // params
4769         $user_info = api_get_user();
4770         $gid = $_REQUEST['gid'] ?? 0;
4771         $uid = $user_info['uid'];
4772
4773         // get data of the specified group id or all groups if not specified
4774         if ($gid != 0) {
4775                 $groups = DBA::selectToArray('group', [], ['deleted' => false, 'uid' => $uid, 'id' => $gid]);
4776
4777                 // error message if specified gid is not in database
4778                 if (!DBA::isResult($groups)) {
4779                         throw new BadRequestException("gid not available");
4780                 }
4781         } else {
4782                 $groups = DBA::selectToArray('group', [], ['deleted' => false, 'uid' => $uid]);
4783         }
4784
4785         // loop through all groups and retrieve all members for adding data in the user array
4786         $grps = [];
4787         foreach ($groups as $rr) {
4788                 $members = Contact\Group::getById($rr['id']);
4789                 $users = [];
4790
4791                 if ($type == "xml") {
4792                         $user_element = "users";
4793                         $k = 0;
4794                         foreach ($members as $member) {
4795                                 $user = api_get_user($member['nurl']);
4796                                 $users[$k++.":user"] = $user;
4797                         }
4798                 } else {
4799                         $user_element = "user";
4800                         foreach ($members as $member) {
4801                                 $user = api_get_user($member['nurl']);
4802                                 $users[] = $user;
4803                         }
4804                 }
4805                 $grps[] = ['name' => $rr['name'], 'gid' => $rr['id'], $user_element => $users];
4806         }
4807         return BaseApi::formatData("groups", $type, ['group' => $grps]);
4808 }
4809 api_register_func('api/friendica/group_show', 'api_friendica_group_show', true);
4810
4811
4812 /**
4813  * Delete the specified group of the user.
4814  *
4815  * @param string $type Return type (atom, rss, xml, json)
4816  *
4817  * @return array|string
4818  * @throws BadRequestException
4819  * @throws ForbiddenException
4820  * @throws ImagickException
4821  * @throws InternalServerErrorException
4822  * @throws UnauthorizedException
4823  */
4824 function api_friendica_group_delete($type)
4825 {
4826         $a = DI::app();
4827
4828         if (api_user() === false) {
4829                 throw new ForbiddenException();
4830         }
4831
4832         // params
4833         $user_info = api_get_user();
4834         $gid = $_REQUEST['gid'] ?? 0;
4835         $name = $_REQUEST['name'] ?? '';
4836         $uid = $user_info['uid'];
4837
4838         // error if no gid specified
4839         if ($gid == 0 || $name == "") {
4840                 throw new BadRequestException('gid or name not specified');
4841         }
4842
4843         // error message if specified gid is not in database
4844         if (!DBA::exists('group', ['uid' => $uid, 'id' => $gid])) {
4845                 throw new BadRequestException('gid not available');
4846         }
4847
4848         // error message if specified gid is not in database
4849         if (!DBA::exists('group', ['uid' => $uid, 'id' => $gid, 'name' => $name])) {
4850                 throw new BadRequestException('wrong group name');
4851         }
4852
4853         // delete group
4854         $gid = Group::getIdByName($uid, $name);
4855         if (empty($gid)) {
4856                 throw new BadRequestException('other API error');
4857         }
4858
4859         $ret = Group::remove($gid);
4860
4861         if ($ret) {
4862                 // return success
4863                 $success = ['success' => $ret, 'gid' => $gid, 'name' => $name, 'status' => 'deleted', 'wrong users' => []];
4864                 return BaseApi::formatData("group_delete", $type, ['result' => $success]);
4865         } else {
4866                 throw new BadRequestException('other API error');
4867         }
4868 }
4869 api_register_func('api/friendica/group_delete', 'api_friendica_group_delete', true, API_METHOD_DELETE);
4870
4871 /**
4872  * Delete a group.
4873  *
4874  * @param string $type Return type (atom, rss, xml, json)
4875  *
4876  * @return array|string
4877  * @throws BadRequestException
4878  * @throws ForbiddenException
4879  * @throws ImagickException
4880  * @throws InternalServerErrorException
4881  * @throws UnauthorizedException
4882  * @see https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-destroy
4883  */
4884 function api_lists_destroy($type)
4885 {
4886         $a = DI::app();
4887
4888         if (api_user() === false) {
4889                 throw new ForbiddenException();
4890         }
4891
4892         // params
4893         $user_info = api_get_user();
4894         $gid = $_REQUEST['list_id'] ?? 0;
4895         $uid = $user_info['uid'];
4896
4897         // error if no gid specified
4898         if ($gid == 0) {
4899                 throw new BadRequestException('gid not specified');
4900         }
4901
4902         // get data of the specified group id
4903         $group = DBA::selectFirst('group', [], ['uid' => $uid, 'id' => $gid]);
4904         // error message if specified gid is not in database
4905         if (!$group) {
4906                 throw new BadRequestException('gid not available');
4907         }
4908
4909         if (Group::remove($gid)) {
4910                 $list = [
4911                         'name' => $group['name'],
4912                         'id' => intval($gid),
4913                         'id_str' => (string) $gid,
4914                         'user' => $user_info
4915                 ];
4916
4917                 return BaseApi::formatData("lists", $type, ['lists' => $list]);
4918         }
4919 }
4920 api_register_func('api/lists/destroy', 'api_lists_destroy', true, API_METHOD_DELETE);
4921
4922 /**
4923  * Add a new group to the database.
4924  *
4925  * @param  string $name  Group name
4926  * @param  int    $uid   User ID
4927  * @param  array  $users List of users to add to the group
4928  *
4929  * @return array
4930  * @throws BadRequestException
4931  */
4932 function group_create($name, $uid, $users = [])
4933 {
4934         // error if no name specified
4935         if ($name == "") {
4936                 throw new BadRequestException('group name not specified');
4937         }
4938
4939         // error message if specified group name already exists
4940         if (DBA::exists('group', ['uid' => $uid, 'name' => $name, 'deleted' => false])) {
4941                 throw new BadRequestException('group name already exists');
4942         }
4943
4944         // Check if the group needs to be reactivated
4945         if (DBA::exists('group', ['uid' => $uid, 'name' => $name, 'deleted' => true])) {
4946                 $reactivate_group = true;
4947         }
4948
4949         // create group
4950         $ret = Group::create($uid, $name);
4951         if ($ret) {
4952                 $gid = Group::getIdByName($uid, $name);
4953         } else {
4954                 throw new BadRequestException('other API error');
4955         }
4956
4957         // add members
4958         $erroraddinguser = false;
4959         $errorusers = [];
4960         foreach ($users as $user) {
4961                 $cid = $user['cid'];
4962                 if (DBA::exists('contact', ['id' => $cid, 'uid' => $uid])) {
4963                         Group::addMember($gid, $cid);
4964                 } else {
4965                         $erroraddinguser = true;
4966                         $errorusers[] = $cid;
4967                 }
4968         }
4969
4970         // return success message incl. missing users in array
4971         $status = ($erroraddinguser ? "missing user" : ((isset($reactivate_group) && $reactivate_group) ? "reactivated" : "ok"));
4972
4973         return ['success' => true, 'gid' => $gid, 'name' => $name, 'status' => $status, 'wrong users' => $errorusers];
4974 }
4975
4976 /**
4977  * Create the specified group with the posted array of contacts.
4978  *
4979  * @param string $type Return type (atom, rss, xml, json)
4980  *
4981  * @return array|string
4982  * @throws BadRequestException
4983  * @throws ForbiddenException
4984  * @throws ImagickException
4985  * @throws InternalServerErrorException
4986  * @throws UnauthorizedException
4987  */
4988 function api_friendica_group_create($type)
4989 {
4990         $a = DI::app();
4991
4992         if (api_user() === false) {
4993                 throw new ForbiddenException();
4994         }
4995
4996         // params
4997         $user_info = api_get_user();
4998         $name = $_REQUEST['name'] ?? '';
4999         $uid = $user_info['uid'];
5000         $json = json_decode($_POST['json'], true);
5001         $users = $json['user'];
5002
5003         $success = group_create($name, $uid, $users);
5004
5005         return BaseApi::formatData("group_create", $type, ['result' => $success]);
5006 }
5007 api_register_func('api/friendica/group_create', 'api_friendica_group_create', true, API_METHOD_POST);
5008
5009 /**
5010  * Create a new group.
5011  *
5012  * @param string $type Return type (atom, rss, xml, json)
5013  *
5014  * @return array|string
5015  * @throws BadRequestException
5016  * @throws ForbiddenException
5017  * @throws ImagickException
5018  * @throws InternalServerErrorException
5019  * @throws UnauthorizedException
5020  * @see https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-create
5021  */
5022 function api_lists_create($type)
5023 {
5024         $a = DI::app();
5025
5026         if (api_user() === false) {
5027                 throw new ForbiddenException();
5028         }
5029
5030         // params
5031         $user_info = api_get_user();
5032         $name = $_REQUEST['name'] ?? '';
5033         $uid = $user_info['uid'];
5034
5035         $success = group_create($name, $uid);
5036         if ($success['success']) {
5037                 $grp = [
5038                         'name' => $success['name'],
5039                         'id' => intval($success['gid']),
5040                         'id_str' => (string) $success['gid'],
5041                         'user' => $user_info
5042                 ];
5043
5044                 return BaseApi::formatData("lists", $type, ['lists'=>$grp]);
5045         }
5046 }
5047 api_register_func('api/lists/create', 'api_lists_create', true, API_METHOD_POST);
5048
5049 /**
5050  * Update the specified group with the posted array of contacts.
5051  *
5052  * @param string $type Return type (atom, rss, xml, json)
5053  *
5054  * @return array|string
5055  * @throws BadRequestException
5056  * @throws ForbiddenException
5057  * @throws ImagickException
5058  * @throws InternalServerErrorException
5059  * @throws UnauthorizedException
5060  */
5061 function api_friendica_group_update($type)
5062 {
5063         $a = DI::app();
5064
5065         if (api_user() === false) {
5066                 throw new ForbiddenException();
5067         }
5068
5069         // params
5070         $user_info = api_get_user();
5071         $uid = $user_info['uid'];
5072         $gid = $_REQUEST['gid'] ?? 0;
5073         $name = $_REQUEST['name'] ?? '';
5074         $json = json_decode($_POST['json'], true);
5075         $users = $json['user'];
5076
5077         // error if no name specified
5078         if ($name == "") {
5079                 throw new BadRequestException('group name not specified');
5080         }
5081
5082         // error if no gid specified
5083         if ($gid == "") {
5084                 throw new BadRequestException('gid not specified');
5085         }
5086
5087         // remove members
5088         $members = Contact\Group::getById($gid);
5089         foreach ($members as $member) {
5090                 $cid = $member['id'];
5091                 foreach ($users as $user) {
5092                         $found = ($user['cid'] == $cid ? true : false);
5093                 }
5094                 if (!isset($found) || !$found) {
5095                         $gid = Group::getIdByName($uid, $name);
5096                         Group::removeMember($gid, $cid);
5097                 }
5098         }
5099
5100         // add members
5101         $erroraddinguser = false;
5102         $errorusers = [];
5103         foreach ($users as $user) {
5104                 $cid = $user['cid'];
5105
5106                 if (DBA::exists('contact', ['id' => $cid, 'uid' => $uid])) {
5107                         Group::addMember($gid, $cid);
5108                 } else {
5109                         $erroraddinguser = true;
5110                         $errorusers[] = $cid;
5111                 }
5112         }
5113
5114         // return success message incl. missing users in array
5115         $status = ($erroraddinguser ? "missing user" : "ok");
5116         $success = ['success' => true, 'gid' => $gid, 'name' => $name, 'status' => $status, 'wrong users' => $errorusers];
5117         return BaseApi::formatData("group_update", $type, ['result' => $success]);
5118 }
5119
5120 api_register_func('api/friendica/group_update', 'api_friendica_group_update', true, API_METHOD_POST);
5121
5122 /**
5123  * Update information about a group.
5124  *
5125  * @param string $type Return type (atom, rss, xml, json)
5126  *
5127  * @return array|string
5128  * @throws BadRequestException
5129  * @throws ForbiddenException
5130  * @throws ImagickException
5131  * @throws InternalServerErrorException
5132  * @throws UnauthorizedException
5133  * @see https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-update
5134  */
5135 function api_lists_update($type)
5136 {
5137         $a = DI::app();
5138
5139         if (api_user() === false) {
5140                 throw new ForbiddenException();
5141         }
5142
5143         // params
5144         $user_info = api_get_user();
5145         $gid = $_REQUEST['list_id'] ?? 0;
5146         $name = $_REQUEST['name'] ?? '';
5147         $uid = $user_info['uid'];
5148
5149         // error if no gid specified
5150         if ($gid == 0) {
5151                 throw new BadRequestException('gid not specified');
5152         }
5153
5154         // get data of the specified group id
5155         $group = DBA::selectFirst('group', [], ['uid' => $uid, 'id' => $gid]);
5156         // error message if specified gid is not in database
5157         if (!$group) {
5158                 throw new BadRequestException('gid not available');
5159         }
5160
5161         if (Group::update($gid, $name)) {
5162                 $list = [
5163                         'name' => $name,
5164                         'id' => intval($gid),
5165                         'id_str' => (string) $gid,
5166                         'user' => $user_info
5167                 ];
5168
5169                 return BaseApi::formatData("lists", $type, ['lists' => $list]);
5170         }
5171 }
5172
5173 api_register_func('api/lists/update', 'api_lists_update', true, API_METHOD_POST);
5174
5175 /**
5176  *
5177  * @param string $type Return type (atom, rss, xml, json)
5178  *
5179  * @return array|string
5180  * @throws BadRequestException
5181  * @throws ForbiddenException
5182  * @throws ImagickException
5183  * @throws InternalServerErrorException
5184  */
5185 function api_friendica_activity($type)
5186 {
5187         $a = DI::app();
5188
5189         if (api_user() === false) {
5190                 throw new ForbiddenException();
5191         }
5192         $verb = strtolower(DI::args()->getArgv()[3]);
5193         $verb = preg_replace("|\..*$|", "", $verb);
5194
5195         $id = $_REQUEST['id'] ?? 0;
5196
5197         $res = Item::performActivity($id, $verb, api_user());
5198
5199         if ($res) {
5200                 if ($type == "xml") {
5201                         $ok = "true";
5202                 } else {
5203                         $ok = "ok";
5204                 }
5205                 return BaseApi::formatData('ok', $type, ['ok' => $ok]);
5206         } else {
5207                 throw new BadRequestException('Error adding activity');
5208         }
5209 }
5210
5211 /// @TODO move to top of file or somewhere better
5212 api_register_func('api/friendica/activity/like', 'api_friendica_activity', true, API_METHOD_POST);
5213 api_register_func('api/friendica/activity/dislike', 'api_friendica_activity', true, API_METHOD_POST);
5214 api_register_func('api/friendica/activity/attendyes', 'api_friendica_activity', true, API_METHOD_POST);
5215 api_register_func('api/friendica/activity/attendno', 'api_friendica_activity', true, API_METHOD_POST);
5216 api_register_func('api/friendica/activity/attendmaybe', 'api_friendica_activity', true, API_METHOD_POST);
5217 api_register_func('api/friendica/activity/unlike', 'api_friendica_activity', true, API_METHOD_POST);
5218 api_register_func('api/friendica/activity/undislike', 'api_friendica_activity', true, API_METHOD_POST);
5219 api_register_func('api/friendica/activity/unattendyes', 'api_friendica_activity', true, API_METHOD_POST);
5220 api_register_func('api/friendica/activity/unattendno', 'api_friendica_activity', true, API_METHOD_POST);
5221 api_register_func('api/friendica/activity/unattendmaybe', 'api_friendica_activity', true, API_METHOD_POST);
5222
5223 /**
5224  * Returns notifications
5225  *
5226  * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
5227  *
5228  * @return string|array
5229  * @throws ForbiddenException
5230  * @throws BadRequestException
5231  * @throws Exception
5232  */
5233 function api_friendica_notification($type)
5234 {
5235         if (api_user() === false) {
5236                 throw new ForbiddenException();
5237         }
5238         if (DI::args()->getArgc()!==3) {
5239                 throw new BadRequestException('Invalid argument count');
5240         }
5241
5242         $Notifies = DI::notify()->selectAllForUser(local_user(), 50);
5243
5244         $notifications = new ApiNotifications();
5245         foreach ($Notifies as $Notify) {
5246                 $notifications[] = new ApiNotification($Notify);
5247         }
5248
5249         if ($type == 'xml') {
5250                 $xmlnotes = [];
5251                 foreach ($notifications as $notification) {
5252                         $xmlnotes[] = ['@attributes' => $notification->toArray()];
5253                 }
5254
5255                 $result = $xmlnotes;
5256         } elseif (count($notifications) > 0) {
5257                 $result = $notifications->getArrayCopy();
5258         } else {
5259                 $result = false;
5260         }
5261
5262         return BaseApi::formatData('notes', $type, ['note' => $result]);
5263 }
5264
5265 /**
5266  * Set notification as seen and returns associated item (if possible)
5267  *
5268  * POST request with 'id' param as notification id
5269  *
5270  * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
5271  * @return string|array
5272  * @throws BadRequestException
5273  * @throws ForbiddenException
5274  * @throws ImagickException
5275  * @throws InternalServerErrorException
5276  * @throws UnauthorizedException
5277  */
5278 function api_friendica_notification_seen($type)
5279 {
5280         $a         = DI::app();
5281         $user_info = api_get_user();
5282
5283         if (api_user() === false || $user_info === false) {
5284                 throw new ForbiddenException();
5285         }
5286         if (DI::args()->getArgc() !== 4) {
5287                 throw new BadRequestException('Invalid argument count');
5288         }
5289
5290         $id = intval($_REQUEST['id'] ?? 0);
5291
5292         try {
5293                 $Notify = DI::notify()->selectOneById($id);
5294                 if ($Notify->uid !== api_user()) {
5295                         throw new NotFoundException();
5296                 }
5297
5298                 if ($Notify->uriId) {
5299                         DI::notification()->setAllSeenForUser($Notify->uid, ['target-uri-id' => $Notify->uriId]);
5300                 }
5301
5302                 $Notify->setSeen();
5303                 DI::notify()->save($Notify);
5304
5305                 if ($Notify->otype === Notification\ObjectType::ITEM) {
5306                         $item = Post::selectFirstForUser(api_user(), [], ['id' => $Notify->iid, 'uid' => api_user()]);
5307                         if (DBA::isResult($item)) {
5308                                 // we found the item, return it to the user
5309                                 $ret  = api_format_items([$item], $user_info, false, $type);
5310                                 $data = ['status' => $ret];
5311                                 return BaseApi::formatData('status', $type, $data);
5312                         }
5313                         // the item can't be found, but we set the notification as seen, so we count this as a success
5314                 }
5315
5316                 return BaseApi::formatData('result', $type, ['result' => 'success']);
5317         } catch (NotFoundException $e) {
5318                 throw new BadRequestException('Invalid argument', $e);
5319         } catch (Exception $e) {
5320                 throw new InternalServerErrorException('Internal Server exception', $e);
5321         }
5322 }
5323
5324 /// @TODO move to top of file or somewhere better
5325 api_register_func('api/friendica/notification/seen', 'api_friendica_notification_seen', true, API_METHOD_POST);
5326 api_register_func('api/friendica/notification', 'api_friendica_notification', true, API_METHOD_GET);
5327
5328 /**
5329  * update a direct_message to seen state
5330  *
5331  * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
5332  * @return string|array (success result=ok, error result=error with error message)
5333  * @throws BadRequestException
5334  * @throws ForbiddenException
5335  * @throws ImagickException
5336  * @throws InternalServerErrorException
5337  * @throws UnauthorizedException
5338  */
5339 function api_friendica_direct_messages_setseen($type)
5340 {
5341         $a = DI::app();
5342         if (api_user() === false) {
5343                 throw new ForbiddenException();
5344         }
5345
5346         // params
5347         $user_info = api_get_user();
5348         $uid = $user_info['uid'];
5349         $id = $_REQUEST['id'] ?? 0;
5350
5351         // return error if id is zero
5352         if ($id == "") {
5353                 $answer = ['result' => 'error', 'message' => 'message id not specified'];
5354                 return BaseApi::formatData("direct_messages_setseen", $type, ['$result' => $answer]);
5355         }
5356
5357         // error message if specified id is not in database
5358         if (!DBA::exists('mail', ['id' => $id, 'uid' => $uid])) {
5359                 $answer = ['result' => 'error', 'message' => 'message id not in database'];
5360                 return BaseApi::formatData("direct_messages_setseen", $type, ['$result' => $answer]);
5361         }
5362
5363         // update seen indicator
5364         $result = DBA::update('mail', ['seen' => true], ['id' => $id]);
5365
5366         if ($result) {
5367                 // return success
5368                 $answer = ['result' => 'ok', 'message' => 'message set to seen'];
5369                 return BaseApi::formatData("direct_message_setseen", $type, ['$result' => $answer]);
5370         } else {
5371                 $answer = ['result' => 'error', 'message' => 'unknown error'];
5372                 return BaseApi::formatData("direct_messages_setseen", $type, ['$result' => $answer]);
5373         }
5374 }
5375
5376 /// @TODO move to top of file or somewhere better
5377 api_register_func('api/friendica/direct_messages_setseen', 'api_friendica_direct_messages_setseen', true);
5378
5379 /**
5380  * search for direct_messages containing a searchstring through api
5381  *
5382  * @param string $type      Known types are 'atom', 'rss', 'xml' and 'json'
5383  * @param string $box
5384  * @return string|array (success: success=true if found and search_result contains found messages,
5385  *                          success=false if nothing was found, search_result='nothing found',
5386  *                          error: result=error with error message)
5387  * @throws BadRequestException
5388  * @throws ForbiddenException
5389  * @throws ImagickException
5390  * @throws InternalServerErrorException
5391  * @throws UnauthorizedException
5392  */
5393 function api_friendica_direct_messages_search($type, $box = "")
5394 {
5395         $a = DI::app();
5396
5397         if (api_user() === false) {
5398                 throw new ForbiddenException();
5399         }
5400
5401         // params
5402         $user_info = api_get_user();
5403         $searchstring = $_REQUEST['searchstring'] ?? '';
5404         $uid = $user_info['uid'];
5405
5406         // error if no searchstring specified
5407         if ($searchstring == "") {
5408                 $answer = ['result' => 'error', 'message' => 'searchstring not specified'];
5409                 return BaseApi::formatData("direct_messages_search", $type, ['$result' => $answer]);
5410         }
5411
5412         // get data for the specified searchstring
5413         $r = DBA::toArray(DBA::p(
5414                 "SELECT `mail`.*, `contact`.`nurl` AS `contact-url` FROM `mail`,`contact` WHERE `mail`.`contact-id` = `contact`.`id` AND `mail`.`uid` = ? AND `body` LIKE ? ORDER BY `mail`.`id` DESC",
5415                 $uid,
5416                 '%'.$searchstring.'%'
5417         ));
5418
5419         $profile_url = $user_info["url"];
5420
5421         // message if nothing was found
5422         if (!DBA::isResult($r)) {
5423                 $success = ['success' => false, 'search_results' => 'problem with query'];
5424         } elseif (count($r) == 0) {
5425                 $success = ['success' => false, 'search_results' => 'nothing found'];
5426         } else {
5427                 $ret = [];
5428                 foreach ($r as $item) {
5429                         if ($box == "inbox" || $item['from-url'] != $profile_url) {
5430                                 $recipient = $user_info;
5431                                 $sender = api_get_user(Strings::normaliseLink($item['contact-url']));
5432                         } elseif ($box == "sentbox" || $item['from-url'] == $profile_url) {
5433                                 $recipient = api_get_user(Strings::normaliseLink($item['contact-url']));
5434                                 $sender = $user_info;
5435                         }
5436
5437                         if (isset($recipient) && isset($sender)) {
5438                                 $ret[] = api_format_messages($item, $recipient, $sender);
5439                         }
5440                 }
5441                 $success = ['success' => true, 'search_results' => $ret];
5442         }
5443
5444         return BaseApi::formatData("direct_message_search", $type, ['$result' => $success]);
5445 }
5446
5447 /// @TODO move to top of file or somewhere better
5448 api_register_func('api/friendica/direct_messages_search', 'api_friendica_direct_messages_search', true);
5449
5450 /**
5451  * Returns a list of saved searches.
5452  *
5453  * @see https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/get-saved_searches-list
5454  *
5455  * @param  string $type Return format: json or xml
5456  *
5457  * @return string|array
5458  * @throws Exception
5459  */
5460 function api_saved_searches_list($type)
5461 {
5462         $terms = DBA::select('search', ['id', 'term'], ['uid' => local_user()]);
5463
5464         $result = [];
5465         while ($term = DBA::fetch($terms)) {
5466                 $result[] = [
5467                         'created_at' => api_date(time()),
5468                         'id' => intval($term['id']),
5469                         'id_str' => $term['id'],
5470                         'name' => $term['term'],
5471                         'position' => null,
5472                         'query' => $term['term']
5473                 ];
5474         }
5475
5476         DBA::close($terms);
5477
5478         return BaseApi::formatData("terms", $type, ['terms' => $result]);
5479 }
5480
5481 /// @TODO move to top of file or somewhere better
5482 api_register_func('api/saved_searches/list', 'api_saved_searches_list', true);
5483
5484 /*
5485  * Number of comments
5486  *
5487  * Bind comment numbers(friendica_comments: Int) on each statuses page of *_timeline / favorites / search
5488  *
5489  * @param object $data [Status, Status]
5490  *
5491  * @return void
5492  */
5493 function bindComments(&$data)
5494 {
5495         if (count($data) == 0) {
5496                 return;
5497         }
5498
5499         $ids = [];
5500         $comments = [];
5501         foreach ($data as $item) {
5502                 $ids[] = $item['id'];
5503         }
5504
5505         $idStr = DBA::escape(implode(', ', $ids));
5506         $sql = "SELECT `parent`, COUNT(*) as comments FROM `post-user-view` WHERE `parent` IN ($idStr) AND `deleted` = ? AND `gravity`= ? GROUP BY `parent`";
5507         $items = DBA::p($sql, 0, GRAVITY_COMMENT);
5508         $itemsData = DBA::toArray($items);
5509
5510         foreach ($itemsData as $item) {
5511                 $comments[$item['parent']] = $item['comments'];
5512         }
5513
5514         foreach ($data as $idx => $item) {
5515                 $id = $item['id'];
5516                 $data[$idx]['friendica_comments'] = isset($comments[$id]) ? $comments[$id] : 0;
5517         }
5518 }
5519
5520 /*
5521 @TODO Maybe open to implement?
5522 To.Do:
5523         [pagename] => api/1.1/statuses/lookup.json
5524         [id] => 605138389168451584
5525         [include_cards] => true
5526         [cards_platform] => Android-12
5527         [include_entities] => true
5528         [include_my_retweet] => 1
5529         [include_rts] => 1
5530         [include_reply_count] => true
5531         [include_descendent_reply_count] => true
5532 (?)
5533
5534
5535 Not implemented by now:
5536 statuses/retweets_of_me
5537 friendships/create
5538 friendships/destroy
5539 friendships/exists
5540 friendships/show
5541 account/update_location
5542 account/update_profile_background_image
5543 blocks/create
5544 blocks/destroy
5545 friendica/profile/update
5546 friendica/profile/create
5547 friendica/profile/delete
5548
5549 Not implemented in status.net:
5550 statuses/retweeted_to_me
5551 statuses/retweeted_by_me
5552 direct_messages/destroy
5553 account/end_session
5554 account/update_delivery_device
5555 notifications/follow
5556 notifications/leave
5557 blocks/exists
5558 blocks/blocking
5559 lists
5560 */