Friendica Communications Platform (please note that this is a clone of the repository at github, issues are handled there) https://friendi.ca
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

6440 lignes
185KB

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