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