]> git.mxchange.org Git - friendica.git/blob - include/api.php
API: Set "dismissed" instead of "seen"
[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\Core\Logger;
28 use Friendica\Core\System;
29 use Friendica\Database\DBA;
30 use Friendica\DI;
31 use Friendica\Model\Contact;
32 use Friendica\Model\Group;
33 use Friendica\Model\Item;
34 use Friendica\Model\Photo;
35 use Friendica\Model\Post;
36 use Friendica\Model\Profile;
37 use Friendica\Module\BaseApi;
38 use Friendica\Network\HTTPException;
39 use Friendica\Network\HTTPException\BadRequestException;
40 use Friendica\Network\HTTPException\ForbiddenException;
41 use Friendica\Network\HTTPException\InternalServerErrorException;
42 use Friendica\Network\HTTPException\NotFoundException;
43 use Friendica\Network\HTTPException\UnauthorizedException;
44 use Friendica\Object\Image;
45 use Friendica\Util\Images;
46 use Friendica\Util\Strings;
47
48 $API = [];
49
50 /**
51  * Register a function to be the endpoint for defined API path.
52  *
53  * @param string $path   API URL path, relative to DI::baseUrl()
54  * @param string $func   Function name to call on path request
55  */
56 function api_register_func($path, $func)
57 {
58         global $API;
59
60         $API[$path] = [
61                 'func'   => $func,
62         ];
63
64         // Workaround for hotot
65         $path = str_replace("api/", "api/1.1/", $path);
66
67         $API[$path] = [
68                 'func'   => $func,
69         ];
70 }
71
72 /**
73  * Main API entry point
74  *
75  * Authenticate user, call registered API function, set HTTP headers
76  *
77  * @param App\Arguments $args The app arguments (optional, will retrieved by the DI-Container in case of missing)
78  * @return string|array API call result
79  * @throws Exception
80  */
81 function api_call($command, $extension)
82 {
83         global $API;
84
85         Logger::info('Legacy API call', ['command' => $command, 'extension' => $extension]);
86
87         try {
88                 foreach ($API as $p => $info) {
89                         if (strpos($command, $p) === 0) {
90                                 Logger::debug(BaseApi::LOG_PREFIX . 'parameters', ['module' => 'api', 'action' => 'call', 'parameters' => $_REQUEST]);
91
92                                 $stamp =  microtime(true);
93                                 $return = call_user_func($info['func'], $extension);
94                                 $duration = floatval(microtime(true) - $stamp);
95
96                                 Logger::info(BaseApi::LOG_PREFIX . 'duration {duration}', ['module' => 'api', 'action' => 'call', 'duration' => round($duration, 2)]);
97
98                                 DI::profiler()->saveLog(DI::logger(), BaseApi::LOG_PREFIX . 'performance');
99
100                                 if (false === $return) {
101                                         /*
102                                                 * api function returned false withour throw an
103                                                 * exception. This should not happend, throw a 500
104                                                 */
105                                         throw new InternalServerErrorException();
106                                 }
107
108                                 switch ($extension) {
109                                         case "xml":
110                                                 header("Content-Type: text/xml");
111                                                 break;
112                                         case "json":
113                                                 header("Content-Type: application/json");
114                                                 if (!empty($return)) {
115                                                         $json = json_encode(end($return));
116                                                         if (!empty($_GET['callback'])) {
117                                                                 $json = $_GET['callback'] . "(" . $json . ")";
118                                                         }
119                                                         $return = $json;
120                                                 }
121                                                 break;
122                                         case "rss":
123                                                 header("Content-Type: application/rss+xml");
124                                                 $return  = '<?xml version="1.0" encoding="UTF-8"?>' . "\n" . $return;
125                                                 break;
126                                         case "atom":
127                                                 header("Content-Type: application/atom+xml");
128                                                 $return = '<?xml version="1.0" encoding="UTF-8"?>' . "\n" . $return;
129                                                 break;
130                                 }
131                                 return $return;
132                         }
133                 }
134
135                 Logger::warning(BaseApi::LOG_PREFIX . 'not implemented', ['module' => 'api', 'action' => 'call', 'query' => DI::args()->getQueryString()]);
136                 throw new NotFoundException();
137         } catch (HTTPException $e) {
138                 Logger::notice(BaseApi::LOG_PREFIX . 'got exception', ['module' => 'api', 'action' => 'call', 'query' => DI::args()->getQueryString(), 'error' => $e]);
139                 DI::apiResponse()->error($e->getCode(), $e->getDescription(), $e->getMessage(), $extension);
140         }
141 }
142
143 /**
144  *
145  * @param string $acl_string
146  * @param int    $uid
147  * @return bool
148  * @throws Exception
149  */
150 function check_acl_input($acl_string, $uid)
151 {
152         if (empty($acl_string)) {
153                 return false;
154         }
155
156         $contact_not_found = false;
157
158         // split <x><y><z> into array of cid's
159         preg_match_all("/<[A-Za-z0-9]+>/", $acl_string, $array);
160
161         // check for each cid if it is available on server
162         $cid_array = $array[0];
163         foreach ($cid_array as $cid) {
164                 $cid = str_replace("<", "", $cid);
165                 $cid = str_replace(">", "", $cid);
166                 $condition = ['id' => $cid, 'uid' => $uid];
167                 $contact_not_found |= !DBA::exists('contact', $condition);
168         }
169         return $contact_not_found;
170 }
171
172 /**
173  * @param string  $mediatype
174  * @param array   $media
175  * @param string  $type
176  * @param string  $album
177  * @param string  $allow_cid
178  * @param string  $deny_cid
179  * @param string  $allow_gid
180  * @param string  $deny_gid
181  * @param string  $desc
182  * @param integer $phototype
183  * @param boolean $visibility
184  * @param string  $photo_id
185  * @param int     $uid
186  * @return array
187  * @throws BadRequestException
188  * @throws ForbiddenException
189  * @throws ImagickException
190  * @throws InternalServerErrorException
191  * @throws NotFoundException
192  * @throws UnauthorizedException
193  */
194 function save_media_to_database($mediatype, $media, $type, $album, $allow_cid, $deny_cid, $allow_gid, $deny_gid, $desc, $phototype, $visibility, $photo_id, $uid)
195 {
196         $visitor   = 0;
197         $src = "";
198         $filetype = "";
199         $filename = "";
200         $filesize = 0;
201
202         if (is_array($media)) {
203                 if (is_array($media['tmp_name'])) {
204                         $src = $media['tmp_name'][0];
205                 } else {
206                         $src = $media['tmp_name'];
207                 }
208                 if (is_array($media['name'])) {
209                         $filename = basename($media['name'][0]);
210                 } else {
211                         $filename = basename($media['name']);
212                 }
213                 if (is_array($media['size'])) {
214                         $filesize = intval($media['size'][0]);
215                 } else {
216                         $filesize = intval($media['size']);
217                 }
218                 if (is_array($media['type'])) {
219                         $filetype = $media['type'][0];
220                 } else {
221                         $filetype = $media['type'];
222                 }
223         }
224
225         $filetype = Images::getMimeTypeBySource($src, $filename, $filetype);
226
227         logger::info(
228                 "File upload src: " . $src . " - filename: " . $filename .
229                 " - size: " . $filesize . " - type: " . $filetype);
230
231         // check if there was a php upload error
232         if ($filesize == 0 && $media['error'] == 1) {
233                 throw new InternalServerErrorException("image size exceeds PHP config settings, file was rejected by server");
234         }
235         // check against max upload size within Friendica instance
236         $maximagesize = DI::config()->get('system', 'maximagesize');
237         if ($maximagesize && ($filesize > $maximagesize)) {
238                 $formattedBytes = Strings::formatBytes($maximagesize);
239                 throw new InternalServerErrorException("image size exceeds Friendica config setting (uploaded size: $formattedBytes)");
240         }
241
242         // create Photo instance with the data of the image
243         $imagedata = @file_get_contents($src);
244         $Image = new Image($imagedata, $filetype);
245         if (!$Image->isValid()) {
246                 throw new InternalServerErrorException("unable to process image data");
247         }
248
249         // check orientation of image
250         $Image->orient($src);
251         @unlink($src);
252
253         // check max length of images on server
254         $max_length = DI::config()->get('system', 'max_image_length');
255         if ($max_length > 0) {
256                 $Image->scaleDown($max_length);
257                 logger::info("File upload: Scaling picture to new size " . $max_length);
258         }
259         $width = $Image->getWidth();
260         $height = $Image->getHeight();
261
262         // create a new resource-id if not already provided
263         $resource_id = ($photo_id == null) ? Photo::newResource() : $photo_id;
264
265         if ($mediatype == "photo") {
266                 // upload normal image (scales 0, 1, 2)
267                 logger::info("photo upload: starting new photo upload");
268
269                 $r = Photo::store($Image, $uid, $visitor, $resource_id, $filename, $album, 0, Photo::DEFAULT, $allow_cid, $allow_gid, $deny_cid, $deny_gid, $desc);
270                 if (!$r) {
271                         logger::notice("photo upload: image upload with scale 0 (original size) failed");
272                 }
273                 if ($width > 640 || $height > 640) {
274                         $Image->scaleDown(640);
275                         $r = Photo::store($Image, $uid, $visitor, $resource_id, $filename, $album, 1, Photo::DEFAULT, $allow_cid, $allow_gid, $deny_cid, $deny_gid, $desc);
276                         if (!$r) {
277                                 logger::notice("photo upload: image upload with scale 1 (640x640) failed");
278                         }
279                 }
280
281                 if ($width > 320 || $height > 320) {
282                         $Image->scaleDown(320);
283                         $r = Photo::store($Image, $uid, $visitor, $resource_id, $filename, $album, 2, Photo::DEFAULT, $allow_cid, $allow_gid, $deny_cid, $deny_gid, $desc);
284                         if (!$r) {
285                                 logger::notice("photo upload: image upload with scale 2 (320x320) failed");
286                         }
287                 }
288                 logger::info("photo upload: new photo upload ended");
289         } elseif ($mediatype == "profileimage") {
290                 // upload profile image (scales 4, 5, 6)
291                 logger::info("photo upload: starting new profile image upload");
292
293                 if ($width > 300 || $height > 300) {
294                         $Image->scaleDown(300);
295                         $r = Photo::store($Image, $uid, $visitor, $resource_id, $filename, $album, 4, $phototype, $allow_cid, $allow_gid, $deny_cid, $deny_gid, $desc);
296                         if (!$r) {
297                                 logger::notice("photo upload: profile image upload with scale 4 (300x300) failed");
298                         }
299                 }
300
301                 if ($width > 80 || $height > 80) {
302                         $Image->scaleDown(80);
303                         $r = Photo::store($Image, $uid, $visitor, $resource_id, $filename, $album, 5, $phototype, $allow_cid, $allow_gid, $deny_cid, $deny_gid, $desc);
304                         if (!$r) {
305                                 logger::notice("photo upload: profile image upload with scale 5 (80x80) failed");
306                         }
307                 }
308
309                 if ($width > 48 || $height > 48) {
310                         $Image->scaleDown(48);
311                         $r = Photo::store($Image, $uid, $visitor, $resource_id, $filename, $album, 6, $phototype, $allow_cid, $allow_gid, $deny_cid, $deny_gid, $desc);
312                         if (!$r) {
313                                 logger::notice("photo upload: profile image upload with scale 6 (48x48) failed");
314                         }
315                 }
316                 $Image->__destruct();
317                 logger::info("photo upload: new profile image upload ended");
318         }
319
320         if (!empty($r)) {
321                 // create entry in 'item'-table on new uploads to enable users to comment/like/dislike the photo
322                 if ($photo_id == null && $mediatype == "photo") {
323                         post_photo_item($resource_id, $allow_cid, $deny_cid, $allow_gid, $deny_gid, $filetype, $visibility, $uid);
324                 }
325                 // on success return image data in json/xml format (like /api/friendica/photo does when no scale is given)
326                 return prepare_photo_data($type, false, $resource_id, $uid);
327         } else {
328                 throw new InternalServerErrorException("image upload failed");
329                 DI::page()->exit(DI::apiResponse());
330         }
331 }
332
333 /**
334  *
335  * @param string  $hash
336  * @param string  $allow_cid
337  * @param string  $deny_cid
338  * @param string  $allow_gid
339  * @param string  $deny_gid
340  * @param string  $filetype
341  * @param boolean $visibility
342  * @param int     $uid
343  * @throws InternalServerErrorException
344  */
345 function post_photo_item($hash, $allow_cid, $deny_cid, $allow_gid, $deny_gid, $filetype, $visibility, $uid)
346 {
347         // get data about the api authenticated user
348         $uri = Item::newURI(intval($uid));
349         $owner_record = DBA::selectFirst('contact', [], ['uid' => $uid, 'self' => true]);
350
351         $arr = [];
352         $arr['guid']          = System::createUUID();
353         $arr['uid']           = $uid;
354         $arr['uri']           = $uri;
355         $arr['post-type']     = Item::PT_IMAGE;
356         $arr['wall']          = 1;
357         $arr['resource-id']   = $hash;
358         $arr['contact-id']    = $owner_record['id'];
359         $arr['owner-name']    = $owner_record['name'];
360         $arr['owner-link']    = $owner_record['url'];
361         $arr['owner-avatar']  = $owner_record['thumb'];
362         $arr['author-name']   = $owner_record['name'];
363         $arr['author-link']   = $owner_record['url'];
364         $arr['author-avatar'] = $owner_record['thumb'];
365         $arr['title']         = '';
366         $arr['allow_cid']     = $allow_cid;
367         $arr['allow_gid']     = $allow_gid;
368         $arr['deny_cid']      = $deny_cid;
369         $arr['deny_gid']      = $deny_gid;
370         $arr['visible']       = $visibility;
371         $arr['origin']        = 1;
372
373         $typetoext = Images::supportedTypes();
374
375         // adds link to the thumbnail scale photo
376         $arr['body'] = '[url=' . DI::baseUrl() . '/photos/' . $owner_record['nick'] . '/image/' . $hash . ']'
377                                 . '[img]' . DI::baseUrl() . '/photo/' . $hash . '-' . "2" . '.'. $typetoext[$filetype] . '[/img]'
378                                 . '[/url]';
379
380         // do the magic for storing the item in the database and trigger the federation to other contacts
381         Item::insert($arr);
382 }
383
384 /**
385  *
386  * @param string $type
387  * @param int    $scale
388  * @param string $photo_id
389  *
390  * @return array
391  * @throws BadRequestException
392  * @throws ForbiddenException
393  * @throws ImagickException
394  * @throws InternalServerErrorException
395  * @throws NotFoundException
396  * @throws UnauthorizedException
397  */
398 function prepare_photo_data($type, $scale, $photo_id, $uid)
399 {
400         $scale_sql = ($scale === false ? "" : sprintf("AND scale=%d", intval($scale)));
401         $data_sql = ($scale === false ? "" : "data, ");
402
403         // added allow_cid, allow_gid, deny_cid, deny_gid to output as string like stored in database
404         // clients needs to convert this in their way for further processing
405         $r = DBA::toArray(DBA::p(
406                 "SELECT $data_sql `resource-id`, `created`, `edited`, `title`, `desc`, `album`, `filename`,
407                                         `type`, `height`, `width`, `datasize`, `profile`, `allow_cid`, `deny_cid`, `allow_gid`, `deny_gid`,
408                                         MIN(`scale`) AS `minscale`, MAX(`scale`) AS `maxscale`
409                         FROM `photo` WHERE `uid` = ? AND `resource-id` = ? $scale_sql GROUP BY
410                                    `resource-id`, `created`, `edited`, `title`, `desc`, `album`, `filename`,
411                                    `type`, `height`, `width`, `datasize`, `profile`, `allow_cid`, `deny_cid`, `allow_gid`, `deny_gid`",
412                 $uid,
413                 $photo_id
414         ));
415
416         $typetoext = [
417                 'image/jpeg' => 'jpg',
418                 'image/png' => 'png',
419                 'image/gif' => 'gif'
420         ];
421
422         // prepare output data for photo
423         if (DBA::isResult($r)) {
424                 $data = ['photo' => $r[0]];
425                 $data['photo']['id'] = $data['photo']['resource-id'];
426                 if ($scale !== false) {
427                         $data['photo']['data'] = base64_encode($data['photo']['data']);
428                 } else {
429                         unset($data['photo']['datasize']); //needed only with scale param
430                 }
431                 if ($type == "xml") {
432                         $data['photo']['links'] = [];
433                         for ($k = intval($data['photo']['minscale']); $k <= intval($data['photo']['maxscale']); $k++) {
434                                 $data['photo']['links'][$k . ":link"]["@attributes"] = ["type" => $data['photo']['type'],
435                                                                                 "scale" => $k,
436                                                                                 "href" => DI::baseUrl() . "/photo/" . $data['photo']['resource-id'] . "-" . $k . "." . $typetoext[$data['photo']['type']]];
437                         }
438                 } else {
439                         $data['photo']['link'] = [];
440                         // when we have profile images we could have only scales from 4 to 6, but index of array always needs to start with 0
441                         $i = 0;
442                         for ($k = intval($data['photo']['minscale']); $k <= intval($data['photo']['maxscale']); $k++) {
443                                 $data['photo']['link'][$i] = DI::baseUrl() . "/photo/" . $data['photo']['resource-id'] . "-" . $k . "." . $typetoext[$data['photo']['type']];
444                                 $i++;
445                         }
446                 }
447                 unset($data['photo']['resource-id']);
448                 unset($data['photo']['minscale']);
449                 unset($data['photo']['maxscale']);
450         } else {
451                 throw new NotFoundException();
452         }
453
454         // retrieve item element for getting activities (like, dislike etc.) related to photo
455         $condition = ['uid' => $uid, 'resource-id' => $photo_id];
456         $item = Post::selectFirst(['id', 'uid', 'uri', 'uri-id', 'parent', 'allow_cid', 'deny_cid', 'allow_gid', 'deny_gid'], $condition);
457         if (!DBA::isResult($item)) {
458                 throw new NotFoundException('Photo-related item not found.');
459         }
460
461         $data['photo']['friendica_activities'] = DI::friendicaActivities()->createFromUriId($item['uri-id'], $item['uid'], $type);
462
463         // retrieve comments on photo
464         $condition = ["`parent` = ? AND `uid` = ? AND `gravity` IN (?, ?)",
465                 $item['parent'], $uid, GRAVITY_PARENT, GRAVITY_COMMENT];
466
467         $statuses = Post::selectForUser($uid, [], $condition);
468
469         // prepare output of comments
470         $commentData = [];
471         while ($status = DBA::fetch($statuses)) {
472                 $commentData[] = DI::twitterStatus()->createFromUriId($status['uri-id'], $status['uid'])->toArray();
473         }
474         DBA::close($statuses);
475
476         $comments = [];
477         if ($type == "xml") {
478                 $k = 0;
479                 foreach ($commentData as $comment) {
480                         $comments[$k++ . ":comment"] = $comment;
481                 }
482         } else {
483                 foreach ($commentData as $comment) {
484                         $comments[] = $comment;
485                 }
486         }
487         $data['photo']['friendica_comments'] = $comments;
488
489         // include info if rights on photo and rights on item are mismatching
490         $rights_mismatch = $data['photo']['allow_cid'] != $item['allow_cid'] ||
491                 $data['photo']['deny_cid'] != $item['deny_cid'] ||
492                 $data['photo']['allow_gid'] != $item['allow_gid'] ||
493                 $data['photo']['deny_gid'] != $item['deny_gid'];
494         $data['photo']['rights_mismatch'] = $rights_mismatch;
495
496         return $data;
497 }
498
499 /**
500  * Add a new group to the database.
501  *
502  * @param  string $name  Group name
503  * @param  int    $uid   User ID
504  * @param  array  $users List of users to add to the group
505  *
506  * @return array
507  * @throws BadRequestException
508  */
509 function group_create($name, $uid, $users = [])
510 {
511         // error if no name specified
512         if ($name == "") {
513                 throw new BadRequestException('group name not specified');
514         }
515
516         // error message if specified group name already exists
517         if (DBA::exists('group', ['uid' => $uid, 'name' => $name, 'deleted' => false])) {
518                 throw new BadRequestException('group name already exists');
519         }
520
521         // Check if the group needs to be reactivated
522         if (DBA::exists('group', ['uid' => $uid, 'name' => $name, 'deleted' => true])) {
523                 $reactivate_group = true;
524         }
525
526         // create group
527         $ret = Group::create($uid, $name);
528         if ($ret) {
529                 $gid = Group::getIdByName($uid, $name);
530         } else {
531                 throw new BadRequestException('other API error');
532         }
533
534         // add members
535         $erroraddinguser = false;
536         $errorusers = [];
537         foreach ($users as $user) {
538                 $cid = $user['cid'];
539                 if (DBA::exists('contact', ['id' => $cid, 'uid' => $uid])) {
540                         Group::addMember($gid, $cid);
541                 } else {
542                         $erroraddinguser = true;
543                         $errorusers[] = $cid;
544                 }
545         }
546
547         // return success message incl. missing users in array
548         $status = ($erroraddinguser ? "missing user" : ((isset($reactivate_group) && $reactivate_group) ? "reactivated" : "ok"));
549
550         return ['success' => true, 'gid' => $gid, 'name' => $name, 'status' => $status, 'wrong users' => $errorusers];
551 }
552
553 /**
554  * TWITTER API
555  */
556
557 /**
558  * Returns all lists the user subscribes to.
559  *
560  * @param string $type Return type (atom, rss, xml, json)
561  *
562  * @return array|string
563  * @see https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-list
564  */
565 function api_lists_list($type)
566 {
567         BaseApi::checkAllowedScope(BaseApi::SCOPE_READ);
568         $ret = [];
569         /// @TODO $ret is not filled here?
570         return DI::apiResponse()->formatData('lists', $type, ["lists_list" => $ret]);
571 }
572
573 api_register_func('api/lists/list', 'api_lists_list', true);
574 api_register_func('api/lists/subscriptions', 'api_lists_list', true);
575
576 /**
577  * Returns all groups the user owns.
578  *
579  * @param string $type Return type (atom, rss, xml, json)
580  *
581  * @return array|string
582  * @throws BadRequestException
583  * @throws ForbiddenException
584  * @throws ImagickException
585  * @throws InternalServerErrorException
586  * @throws UnauthorizedException
587  * @see https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-ownerships
588  */
589 function api_lists_ownerships($type)
590 {
591         BaseApi::checkAllowedScope(BaseApi::SCOPE_READ);
592         $uid = BaseApi::getCurrentUserID();
593
594         // params
595         $user_info = DI::twitterUser()->createFromUserId($uid, true)->toArray();
596
597         $groups = DBA::select('group', [], ['deleted' => 0, 'uid' => $uid]);
598
599         // loop through all groups
600         $lists = [];
601         foreach ($groups as $group) {
602                 if ($group['visible']) {
603                         $mode = 'public';
604                 } else {
605                         $mode = 'private';
606                 }
607                 $lists[] = [
608                         'name' => $group['name'],
609                         'id' => intval($group['id']),
610                         'id_str' => (string) $group['id'],
611                         'user' => $user_info,
612                         'mode' => $mode
613                 ];
614         }
615         return DI::apiResponse()->formatData("lists", $type, ['lists' => ['lists' => $lists]]);
616 }
617
618 api_register_func('api/lists/ownerships', 'api_lists_ownerships', true);
619
620 /**
621  * list all photos of the authenticated user
622  *
623  * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
624  * @return string|array
625  * @throws ForbiddenException
626  * @throws InternalServerErrorException
627  */
628 function api_fr_photos_list($type)
629 {
630         BaseApi::checkAllowedScope(BaseApi::SCOPE_READ);
631         $uid = BaseApi::getCurrentUserID();
632
633         $r = DBA::toArray(DBA::p(
634                 "SELECT `resource-id`, MAX(scale) AS `scale`, `album`, `filename`, `type`, MAX(`created`) AS `created`,
635                 MAX(`edited`) AS `edited`, MAX(`desc`) AS `desc` FROM `photo`
636                 WHERE `uid` = ? AND NOT `photo-type` IN (?, ?) GROUP BY `resource-id`, `album`, `filename`, `type`",
637                 $uid, Photo::CONTACT_AVATAR, Photo::CONTACT_BANNER
638         ));
639         $typetoext = [
640                 'image/jpeg' => 'jpg',
641                 'image/png' => 'png',
642                 'image/gif' => 'gif'
643         ];
644         $data = ['photo'=>[]];
645         if (DBA::isResult($r)) {
646                 foreach ($r as $rr) {
647                         $photo = [];
648                         $photo['id'] = $rr['resource-id'];
649                         $photo['album'] = $rr['album'];
650                         $photo['filename'] = $rr['filename'];
651                         $photo['type'] = $rr['type'];
652                         $thumb = DI::baseUrl() . "/photo/" . $rr['resource-id'] . "-" . $rr['scale'] . "." . $typetoext[$rr['type']];
653                         $photo['created'] = $rr['created'];
654                         $photo['edited'] = $rr['edited'];
655                         $photo['desc'] = $rr['desc'];
656
657                         if ($type == "xml") {
658                                 $data['photo'][] = ["@attributes" => $photo, "1" => $thumb];
659                         } else {
660                                 $photo['thumb'] = $thumb;
661                                 $data['photo'][] = $photo;
662                         }
663                 }
664         }
665         return DI::apiResponse()->formatData("photos", $type, $data);
666 }
667
668 api_register_func('api/friendica/photos/list', 'api_fr_photos_list', true);
669
670 /**
671  * upload a new photo or change an existing photo
672  *
673  * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
674  * @return string|array
675  * @throws BadRequestException
676  * @throws ForbiddenException
677  * @throws ImagickException
678  * @throws InternalServerErrorException
679  * @throws NotFoundException
680  */
681 function api_fr_photo_create_update($type)
682 {
683         BaseApi::checkAllowedScope(BaseApi::SCOPE_WRITE);
684         $uid = BaseApi::getCurrentUserID();
685
686         // input params
687         $photo_id  = $_REQUEST['photo_id']  ?? null;
688         $desc      = $_REQUEST['desc']      ?? null;
689         $album     = $_REQUEST['album']     ?? null;
690         $album_new = $_REQUEST['album_new'] ?? null;
691         $allow_cid = $_REQUEST['allow_cid'] ?? null;
692         $deny_cid  = $_REQUEST['deny_cid' ] ?? null;
693         $allow_gid = $_REQUEST['allow_gid'] ?? null;
694         $deny_gid  = $_REQUEST['deny_gid' ] ?? null;
695         // Pictures uploaded via API never get posted as a visible status
696         // See https://github.com/friendica/friendica/issues/10990
697         $visibility = false;
698
699         // do several checks on input parameters
700         // we do not allow calls without album string
701         if ($album == null) {
702                 throw new BadRequestException("no albumname specified");
703         }
704         // if photo_id == null --> we are uploading a new photo
705         if ($photo_id == null) {
706                 $mode = "create";
707
708                 // error if no media posted in create-mode
709                 if (empty($_FILES['media'])) {
710                         // Output error
711                         throw new BadRequestException("no media data submitted");
712                 }
713
714                 // album_new will be ignored in create-mode
715                 $album_new = "";
716         } else {
717                 $mode = "update";
718
719                 // check if photo is existing in databasei
720                 if (!Photo::exists(['resource-id' => $photo_id, 'uid' => $uid, 'album' => $album])) {
721                         throw new BadRequestException("photo not available");
722                 }
723         }
724
725         // checks on acl strings provided by clients
726         $acl_input_error = false;
727         $acl_input_error |= check_acl_input($allow_cid, $uid);
728         $acl_input_error |= check_acl_input($deny_cid, $uid);
729         $acl_input_error |= check_acl_input($allow_gid, $uid);
730         $acl_input_error |= check_acl_input($deny_gid, $uid);
731         if ($acl_input_error) {
732                 throw new BadRequestException("acl data invalid");
733         }
734         // now let's upload the new media in create-mode
735         if ($mode == "create") {
736                 $media = $_FILES['media'];
737                 $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, null, $uid);
738
739                 // return success of updating or error message
740                 if (!is_null($data)) {
741                         return DI::apiResponse()->formatData("photo_create", $type, $data);
742                 } else {
743                         throw new InternalServerErrorException("unknown error - uploading photo failed, see Friendica log for more information");
744                 }
745         }
746
747         // now let's do the changes in update-mode
748         if ($mode == "update") {
749                 $updated_fields = [];
750
751                 if (!is_null($desc)) {
752                         $updated_fields['desc'] = $desc;
753                 }
754
755                 if (!is_null($album_new)) {
756                         $updated_fields['album'] = $album_new;
757                 }
758
759                 if (!is_null($allow_cid)) {
760                         $allow_cid = trim($allow_cid);
761                         $updated_fields['allow_cid'] = $allow_cid;
762                 }
763
764                 if (!is_null($deny_cid)) {
765                         $deny_cid = trim($deny_cid);
766                         $updated_fields['deny_cid'] = $deny_cid;
767                 }
768
769                 if (!is_null($allow_gid)) {
770                         $allow_gid = trim($allow_gid);
771                         $updated_fields['allow_gid'] = $allow_gid;
772                 }
773
774                 if (!is_null($deny_gid)) {
775                         $deny_gid = trim($deny_gid);
776                         $updated_fields['deny_gid'] = $deny_gid;
777                 }
778
779                 $result = false;
780                 if (count($updated_fields) > 0) {
781                         $nothingtodo = false;
782                         $result = Photo::update($updated_fields, ['uid' => $uid, 'resource-id' => $photo_id, 'album' => $album]);
783                 } else {
784                         $nothingtodo = true;
785                 }
786
787                 if (!empty($_FILES['media'])) {
788                         $nothingtodo = false;
789                         $media = $_FILES['media'];
790                         $data = save_media_to_database("photo", $media, $type, $album, $allow_cid, $deny_cid, $allow_gid, $deny_gid, $desc, Photo::DEFAULT, $visibility, $photo_id, $uid);
791                         if (!is_null($data)) {
792                                 return DI::apiResponse()->formatData("photo_update", $type, $data);
793                         }
794                 }
795
796                 // return success of updating or error message
797                 if ($result) {
798                         $answer = ['result' => 'updated', 'message' => 'Image id `' . $photo_id . '` has been updated.'];
799                         return DI::apiResponse()->formatData("photo_update", $type, ['$result' => $answer]);
800                 } else {
801                         if ($nothingtodo) {
802                                 $answer = ['result' => 'cancelled', 'message' => 'Nothing to update for image id `' . $photo_id . '`.'];
803                                 return DI::apiResponse()->formatData("photo_update", $type, ['$result' => $answer]);
804                         }
805                         throw new InternalServerErrorException("unknown error - update photo entry in database failed");
806                 }
807         }
808         throw new InternalServerErrorException("unknown error - this error on uploading or updating a photo should never happen");
809 }
810
811 api_register_func('api/friendica/photo/create', 'api_fr_photo_create_update', true);
812 api_register_func('api/friendica/photo/update', 'api_fr_photo_create_update', true);
813
814 /**
815  * returns the details of a specified photo id, if scale is given, returns the photo data in base 64
816  *
817  * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
818  * @return string|array
819  * @throws BadRequestException
820  * @throws ForbiddenException
821  * @throws InternalServerErrorException
822  * @throws NotFoundException
823  */
824 function api_fr_photo_detail($type)
825 {
826         BaseApi::checkAllowedScope(BaseApi::SCOPE_READ);
827         $uid = BaseApi::getCurrentUserID();
828
829         if (empty($_REQUEST['photo_id'])) {
830                 throw new BadRequestException("No photo id.");
831         }
832
833         $scale = (!empty($_REQUEST['scale']) ? intval($_REQUEST['scale']) : false);
834         $photo_id = $_REQUEST['photo_id'];
835
836         // prepare json/xml output with data from database for the requested photo
837         $data = prepare_photo_data($type, $scale, $photo_id, $uid);
838
839         return DI::apiResponse()->formatData("photo_detail", $type, $data);
840 }
841
842 api_register_func('api/friendica/photo', 'api_fr_photo_detail', true);
843
844 /**
845  * updates the profile image for the user (either a specified profile or the default profile)
846  *
847  * @param string $type Known types are 'atom', 'rss', 'xml' and 'json'
848  *
849  * @return string|array
850  * @throws BadRequestException
851  * @throws ForbiddenException
852  * @throws ImagickException
853  * @throws InternalServerErrorException
854  * @throws NotFoundException
855  * @see   https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile_image
856  */
857 function api_account_update_profile_image($type)
858 {
859         BaseApi::checkAllowedScope(BaseApi::SCOPE_WRITE);
860         $uid = BaseApi::getCurrentUserID();
861
862         // input params
863         $profile_id = $_REQUEST['profile_id'] ?? 0;
864
865         // error if image data is missing
866         if (empty($_FILES['image'])) {
867                 throw new BadRequestException("no media data submitted");
868         }
869
870         // check if specified profile id is valid
871         if ($profile_id != 0) {
872                 $profile = DBA::selectFirst('profile', ['is-default'], ['uid' => $uid, 'id' => $profile_id]);
873                 // error message if specified profile id is not in database
874                 if (!DBA::isResult($profile)) {
875                         throw new BadRequestException("profile_id not available");
876                 }
877                 $is_default_profile = $profile['is-default'];
878         } else {
879                 $is_default_profile = 1;
880         }
881
882         // get mediadata from image or media (Twitter call api/account/update_profile_image provides image)
883         $media = null;
884         if (!empty($_FILES['image'])) {
885                 $media = $_FILES['image'];
886         } elseif (!empty($_FILES['media'])) {
887                 $media = $_FILES['media'];
888         }
889         // save new profile image
890         $data = save_media_to_database("profileimage", $media, $type, DI::l10n()->t(Photo::PROFILE_PHOTOS), "", "", "", "", "", Photo::USER_AVATAR, false, null, $uid);
891
892         // get filetype
893         if (is_array($media['type'])) {
894                 $filetype = $media['type'][0];
895         } else {
896                 $filetype = $media['type'];
897         }
898         if ($filetype == "image/jpeg") {
899                 $fileext = "jpg";
900         } elseif ($filetype == "image/png") {
901                 $fileext = "png";
902         } else {
903                 throw new InternalServerErrorException('Unsupported filetype');
904         }
905
906         // change specified profile or all profiles to the new resource-id
907         if ($is_default_profile) {
908                 $condition = ["`profile` AND `resource-id` != ? AND `uid` = ?", $data['photo']['id'], $uid];
909                 Photo::update(['profile' => false, 'photo-type' => Photo::DEFAULT], $condition);
910         } else {
911                 $fields = ['photo' => DI::baseUrl() . '/photo/' . $data['photo']['id'] . '-4.' . $fileext,
912                         'thumb' => DI::baseUrl() . '/photo/' . $data['photo']['id'] . '-5.' . $fileext];
913                 DBA::update('profile', $fields, ['id' => $_REQUEST['profile'], 'uid' => $uid]);
914         }
915
916         Contact::updateSelfFromUserID($uid, true);
917
918         // Update global directory in background
919         Profile::publishUpdate($uid);
920
921         // output for client
922         if ($data) {
923                 $skip_status = $_REQUEST['skip_status'] ?? false;
924
925                 $user_info = DI::twitterUser()->createFromUserId($uid, $skip_status)->toArray();
926
927                 // "verified" isn't used here in the standard
928                 unset($user_info["verified"]);
929
930                 // "uid" is only needed for some internal stuff, so remove it from here
931                 unset($user_info['uid']);
932
933                 return DI::apiResponse()->formatData("user", $type, ['user' => $user_info]);
934         } else {
935                 // SaveMediaToDatabase failed for some reason
936                 throw new InternalServerErrorException("image upload failed");
937         }
938 }
939
940 api_register_func('api/account/update_profile_image', 'api_account_update_profile_image', true);
941
942 /**
943  * Return all or a specified group of the user with the containing contacts.
944  *
945  * @param string $type Return type (atom, rss, xml, json)
946  *
947  * @return array|string
948  * @throws BadRequestException
949  * @throws ForbiddenException
950  * @throws ImagickException
951  * @throws InternalServerErrorException
952  * @throws UnauthorizedException
953  */
954 function api_friendica_group_show($type)
955 {
956         BaseApi::checkAllowedScope(BaseApi::SCOPE_READ);
957         $uid = BaseApi::getCurrentUserID();
958
959         // params
960         $gid = $_REQUEST['gid'] ?? 0;
961
962         // get data of the specified group id or all groups if not specified
963         if ($gid != 0) {
964                 $groups = DBA::selectToArray('group', [], ['deleted' => false, 'uid' => $uid, 'id' => $gid]);
965
966                 // error message if specified gid is not in database
967                 if (!DBA::isResult($groups)) {
968                         throw new BadRequestException("gid not available");
969                 }
970         } else {
971                 $groups = DBA::selectToArray('group', [], ['deleted' => false, 'uid' => $uid]);
972         }
973
974         // loop through all groups and retrieve all members for adding data in the user array
975         $grps = [];
976         foreach ($groups as $rr) {
977                 $members = Contact\Group::getById($rr['id']);
978                 $users = [];
979
980                 if ($type == "xml") {
981                         $user_element = "users";
982                         $k = 0;
983                         foreach ($members as $member) {
984                                 $user = DI::twitterUser()->createFromContactId($member['contact-id'], $uid, true)->toArray();
985                                 $users[$k++.":user"] = $user;
986                         }
987                 } else {
988                         $user_element = "user";
989                         foreach ($members as $member) {
990                                 $user = DI::twitterUser()->createFromContactId($member['contact-id'], $uid, true)->toArray();
991                                 $users[] = $user;
992                         }
993                 }
994                 $grps[] = ['name' => $rr['name'], 'gid' => $rr['id'], $user_element => $users];
995         }
996         return DI::apiResponse()->formatData("groups", $type, ['group' => $grps]);
997 }
998
999 api_register_func('api/friendica/group_show', 'api_friendica_group_show', true);
1000
1001 /**
1002  * Delete a group.
1003  *
1004  * @param string $type Return type (atom, rss, xml, json)
1005  *
1006  * @return array|string
1007  * @throws BadRequestException
1008  * @throws ForbiddenException
1009  * @throws ImagickException
1010  * @throws InternalServerErrorException
1011  * @throws UnauthorizedException
1012  * @see https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-destroy
1013  */
1014 function api_lists_destroy($type)
1015 {
1016         BaseApi::checkAllowedScope(BaseApi::SCOPE_WRITE);
1017         $uid = BaseApi::getCurrentUserID();
1018
1019         // params
1020         $gid = $_REQUEST['list_id'] ?? 0;
1021
1022         // error if no gid specified
1023         if ($gid == 0) {
1024                 throw new BadRequestException('gid not specified');
1025         }
1026
1027         // get data of the specified group id
1028         $group = DBA::selectFirst('group', [], ['uid' => $uid, 'id' => $gid]);
1029         // error message if specified gid is not in database
1030         if (!$group) {
1031                 throw new BadRequestException('gid not available');
1032         }
1033
1034         if (Group::remove($gid)) {
1035                 $list = [
1036                         'name' => $group['name'],
1037                         'id' => intval($gid),
1038                         'id_str' => (string) $gid,
1039                         'user' => DI::twitterUser()->createFromUserId($uid, true)->toArray()
1040                 ];
1041
1042                 return DI::apiResponse()->formatData("lists", $type, ['lists' => $list]);
1043         }
1044 }
1045
1046 api_register_func('api/lists/destroy', 'api_lists_destroy', true);
1047
1048 /**
1049  * Create the specified group with the posted array of contacts.
1050  *
1051  * @param string $type Return type (atom, rss, xml, json)
1052  *
1053  * @return array|string
1054  * @throws BadRequestException
1055  * @throws ForbiddenException
1056  * @throws ImagickException
1057  * @throws InternalServerErrorException
1058  * @throws UnauthorizedException
1059  */
1060 function api_friendica_group_create($type)
1061 {
1062         BaseApi::checkAllowedScope(BaseApi::SCOPE_WRITE);
1063         $uid = BaseApi::getCurrentUserID();
1064
1065         // params
1066         $name = $_REQUEST['name'] ?? '';
1067         $json = json_decode($_POST['json'], true);
1068         $users = $json['user'];
1069
1070         $success = group_create($name, $uid, $users);
1071
1072         return DI::apiResponse()->formatData("group_create", $type, ['result' => $success]);
1073 }
1074
1075 api_register_func('api/friendica/group_create', 'api_friendica_group_create', true);
1076
1077 /**
1078  * Create a new group.
1079  *
1080  * @param string $type Return type (atom, rss, xml, json)
1081  *
1082  * @return array|string
1083  * @throws BadRequestException
1084  * @throws ForbiddenException
1085  * @throws ImagickException
1086  * @throws InternalServerErrorException
1087  * @throws UnauthorizedException
1088  * @see https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-create
1089  */
1090 function api_lists_create($type)
1091 {
1092         BaseApi::checkAllowedScope(BaseApi::SCOPE_WRITE);
1093         $uid = BaseApi::getCurrentUserID();
1094
1095         // params
1096         $name = $_REQUEST['name'] ?? '';
1097
1098         $success = group_create($name, $uid);
1099         if ($success['success']) {
1100                 $grp = [
1101                         'name' => $success['name'],
1102                         'id' => intval($success['gid']),
1103                         'id_str' => (string) $success['gid'],
1104                         'user' => DI::twitterUser()->createFromUserId($uid, true)->toArray()
1105                 ];
1106
1107                 return DI::apiResponse()->formatData("lists", $type, ['lists' => $grp]);
1108         }
1109 }
1110
1111 api_register_func('api/lists/create', 'api_lists_create', true);
1112
1113 /**
1114  * Update information about a group.
1115  *
1116  * @param string $type Return type (atom, rss, xml, json)
1117  *
1118  * @return array|string
1119  * @throws BadRequestException
1120  * @throws ForbiddenException
1121  * @throws ImagickException
1122  * @throws InternalServerErrorException
1123  * @throws UnauthorizedException
1124  * @see https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-update
1125  */
1126 function api_lists_update($type)
1127 {
1128         BaseApi::checkAllowedScope(BaseApi::SCOPE_WRITE);
1129         $uid = BaseApi::getCurrentUserID();
1130
1131         // params
1132         $gid = $_REQUEST['list_id'] ?? 0;
1133         $name = $_REQUEST['name'] ?? '';
1134
1135         // error if no gid specified
1136         if ($gid == 0) {
1137                 throw new BadRequestException('gid not specified');
1138         }
1139
1140         // get data of the specified group id
1141         $group = DBA::selectFirst('group', [], ['uid' => $uid, 'id' => $gid]);
1142         // error message if specified gid is not in database
1143         if (!$group) {
1144                 throw new BadRequestException('gid not available');
1145         }
1146
1147         if (Group::update($gid, $name)) {
1148                 $list = [
1149                         'name' => $name,
1150                         'id' => intval($gid),
1151                         'id_str' => (string) $gid,
1152                         'user' => DI::twitterUser()->createFromUserId($uid, true)->toArray()
1153                 ];
1154
1155                 return DI::apiResponse()->formatData("lists", $type, ['lists' => $list]);
1156         }
1157 }
1158
1159 api_register_func('api/lists/update', 'api_lists_update', true);