Friendica Communications Platform (please note that this is a clone of the repository at github, issues are handled there) https://friendi.ca
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

3233 lines
105KB

  1. <?php
  2. /**
  3. * @file src/Model/Item.php
  4. */
  5. namespace Friendica\Model;
  6. use Friendica\BaseObject;
  7. use Friendica\Content\Text\BBCode;
  8. use Friendica\Core\Addon;
  9. use Friendica\Core\Config;
  10. use Friendica\Core\Lock;
  11. use Friendica\Core\PConfig;
  12. use Friendica\Core\Protocol;
  13. use Friendica\Core\System;
  14. use Friendica\Core\Worker;
  15. use Friendica\Database\DBA;
  16. use Friendica\Model\Contact;
  17. use Friendica\Model\PermissionSet;
  18. use Friendica\Model\ItemURI;
  19. use Friendica\Object\Image;
  20. use Friendica\Protocol\Diaspora;
  21. use Friendica\Protocol\OStatus;
  22. use Friendica\Util\DateTimeFormat;
  23. use Friendica\Util\XML;
  24. use Text_LanguageDetect;
  25. require_once 'boot.php';
  26. require_once 'include/items.php';
  27. require_once 'include/text.php';
  28. class Item extends BaseObject
  29. {
  30. // Posting types, inspired by https://www.w3.org/TR/activitystreams-vocabulary/#object-types
  31. const PT_ARTICLE = 0;
  32. const PT_NOTE = 1;
  33. const PT_PAGE = 2;
  34. const PT_IMAGE = 16;
  35. const PT_AUDIO = 17;
  36. const PT_VIDEO = 18;
  37. const PT_DOCUMENT = 19;
  38. const PT_EVENT = 32;
  39. const PT_PERSONAL_NOTE = 128;
  40. // Field list that is used to display the items
  41. const DISPLAY_FIELDLIST = ['uid', 'id', 'parent', 'uri', 'thr-parent', 'parent-uri', 'guid', 'network',
  42. 'commented', 'created', 'edited', 'received', 'verb', 'object-type', 'postopts', 'plink',
  43. 'wall', 'private', 'starred', 'origin', 'title', 'body', 'file', 'attach', 'language',
  44. 'content-warning', 'location', 'coord', 'app', 'rendered-hash', 'rendered-html', 'object',
  45. 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'item_id',
  46. 'author-id', 'author-link', 'author-name', 'author-avatar', 'author-network',
  47. 'owner-id', 'owner-link', 'owner-name', 'owner-avatar', 'owner-network',
  48. 'contact-id', 'contact-link', 'contact-name', 'contact-avatar',
  49. 'writable', 'self', 'cid', 'alias',
  50. 'event-id', 'event-created', 'event-edited', 'event-start', 'event-finish',
  51. 'event-summary', 'event-desc', 'event-location', 'event-type',
  52. 'event-nofinish', 'event-adjust', 'event-ignore', 'event-id'];
  53. // Field list that is used to deliver items via the protocols
  54. const DELIVER_FIELDLIST = ['uid', 'id', 'parent', 'uri', 'thr-parent', 'parent-uri', 'guid',
  55. 'created', 'edited', 'verb', 'object-type', 'object', 'target',
  56. 'private', 'title', 'body', 'location', 'coord', 'app',
  57. 'attach', 'tag', 'deleted', 'extid', 'post-type',
  58. 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid',
  59. 'author-id', 'author-link', 'owner-link', 'contact-uid',
  60. 'signed_text', 'signature', 'signer', 'network'];
  61. // Field list for "item-content" table that is mixed with the item table
  62. const MIXED_CONTENT_FIELDLIST = ['title', 'content-warning', 'body', 'location',
  63. 'coord', 'app', 'rendered-hash', 'rendered-html', 'verb',
  64. 'object-type', 'object', 'target-type', 'target', 'plink'];
  65. // Field list for "item-content" table that is not present in the "item" table
  66. const CONTENT_FIELDLIST = ['language'];
  67. // Field list for additional delivery data
  68. const DELIVERY_DATA_FIELDLIST = ['postopts', 'inform'];
  69. // All fields in the item table
  70. const ITEM_FIELDLIST = ['id', 'uid', 'parent', 'uri', 'parent-uri', 'thr-parent', 'guid',
  71. 'contact-id', 'type', 'wall', 'gravity', 'extid', 'icid', 'iaid', 'psid',
  72. 'uri-hash', 'created', 'edited', 'commented', 'received', 'changed', 'verb',
  73. 'postopts', 'plink', 'resource-id', 'event-id', 'tag', 'attach', 'inform',
  74. 'file', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'post-type',
  75. 'private', 'pubmail', 'moderated', 'visible', 'starred', 'bookmark',
  76. 'unseen', 'deleted', 'origin', 'forum_mode', 'mention', 'global', 'network',
  77. 'title', 'content-warning', 'body', 'location', 'coord', 'app',
  78. 'rendered-hash', 'rendered-html', 'object-type', 'object', 'target-type', 'target',
  79. 'author-id', 'author-link', 'author-name', 'author-avatar',
  80. 'owner-id', 'owner-link', 'owner-name', 'owner-avatar'];
  81. // Never reorder or remove entries from this list. Just add new ones at the end, if needed.
  82. // The item-activity table only stores the index and needs this array to know the matching activity.
  83. const ACTIVITIES = [ACTIVITY_LIKE, ACTIVITY_DISLIKE, ACTIVITY_ATTEND, ACTIVITY_ATTENDNO, ACTIVITY_ATTENDMAYBE];
  84. private static $legacy_mode = null;
  85. public static function isLegacyMode()
  86. {
  87. if (is_null(self::$legacy_mode)) {
  88. self::$legacy_mode = (Config::get("system", "post_update_version") < 1279);
  89. }
  90. return self::$legacy_mode;
  91. }
  92. /**
  93. * @brief returns an activity index from an activity string
  94. *
  95. * @param string $activity activity string
  96. * @return integer Activity index
  97. */
  98. public static function activityToIndex($activity)
  99. {
  100. $index = array_search($activity, self::ACTIVITIES);
  101. if (is_bool($index)) {
  102. $index = -1;
  103. }
  104. return $index;
  105. }
  106. /**
  107. * @brief returns an activity string from an activity index
  108. *
  109. * @param integer $index activity index
  110. * @return string Activity string
  111. */
  112. private static function indexToActivity($index)
  113. {
  114. if (is_null($index) || !array_key_exists($index, self::ACTIVITIES)) {
  115. return '';
  116. }
  117. return self::ACTIVITIES[$index];
  118. }
  119. /**
  120. * @brief Fetch a single item row
  121. *
  122. * @param mixed $stmt statement object
  123. * @return array current row
  124. */
  125. public static function fetch($stmt)
  126. {
  127. $row = DBA::fetch($stmt);
  128. if (is_bool($row)) {
  129. return $row;
  130. }
  131. // ---------------------- Transform item structure data ----------------------
  132. // We prefer the data from the user's contact over the public one
  133. if (!empty($row['author-link']) && !empty($row['contact-link']) &&
  134. ($row['author-link'] == $row['contact-link'])) {
  135. if (isset($row['author-avatar']) && !empty($row['contact-avatar'])) {
  136. $row['author-avatar'] = $row['contact-avatar'];
  137. }
  138. if (isset($row['author-name']) && !empty($row['contact-name'])) {
  139. $row['author-name'] = $row['contact-name'];
  140. }
  141. }
  142. if (!empty($row['owner-link']) && !empty($row['contact-link']) &&
  143. ($row['owner-link'] == $row['contact-link'])) {
  144. if (isset($row['owner-avatar']) && !empty($row['contact-avatar'])) {
  145. $row['owner-avatar'] = $row['contact-avatar'];
  146. }
  147. if (isset($row['owner-name']) && !empty($row['contact-name'])) {
  148. $row['owner-name'] = $row['contact-name'];
  149. }
  150. }
  151. // We can always comment on posts from these networks
  152. if (array_key_exists('writable', $row) &&
  153. in_array($row['internal-network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])) {
  154. $row['writable'] = true;
  155. }
  156. // ---------------------- Transform item content data ----------------------
  157. // Fetch data from the item-content table whenever there is content there
  158. if (self::isLegacyMode()) {
  159. $legacy_fields = array_merge(self::DELIVERY_DATA_FIELDLIST, self::MIXED_CONTENT_FIELDLIST);
  160. foreach ($legacy_fields as $field) {
  161. if (empty($row[$field]) && !empty($row['internal-item-' . $field])) {
  162. $row[$field] = $row['internal-item-' . $field];
  163. }
  164. unset($row['internal-item-' . $field]);
  165. }
  166. }
  167. if (!empty($row['internal-iaid']) && array_key_exists('verb', $row)) {
  168. $row['verb'] = self::indexToActivity($row['internal-activity']);
  169. if (array_key_exists('title', $row)) {
  170. $row['title'] = '';
  171. }
  172. if (array_key_exists('body', $row)) {
  173. $row['body'] = $row['verb'];
  174. }
  175. if (array_key_exists('object', $row)) {
  176. $row['object'] = '';
  177. }
  178. if (array_key_exists('object-type', $row)) {
  179. $row['object-type'] = ACTIVITY_OBJ_NOTE;
  180. }
  181. } elseif (array_key_exists('verb', $row) && in_array($row['verb'], ['', ACTIVITY_POST, ACTIVITY_SHARE])) {
  182. // Posts don't have an object or target - but having tags or files.
  183. // We safe some performance by building tag and file strings only here.
  184. // We remove object and target since they aren't used for this type.
  185. if (array_key_exists('object', $row)) {
  186. $row['object'] = '';
  187. }
  188. if (array_key_exists('target', $row)) {
  189. $row['target'] = '';
  190. }
  191. }
  192. if (!array_key_exists('verb', $row) || in_array($row['verb'], ['', ACTIVITY_POST, ACTIVITY_SHARE])) {
  193. // Build the tag string out of the term entries
  194. if (array_key_exists('tag', $row) && empty($row['tag'])) {
  195. $row['tag'] = Term::tagTextFromItemId($row['internal-iid']);
  196. }
  197. // Build the file string out of the term entries
  198. if (array_key_exists('file', $row) && empty($row['file'])) {
  199. $row['file'] = Term::fileTextFromItemId($row['internal-iid']);
  200. }
  201. }
  202. if (array_key_exists('ignored', $row) && array_key_exists('internal-user-ignored', $row) && !is_null($row['internal-user-ignored'])) {
  203. $row['ignored'] = $row['internal-user-ignored'];
  204. }
  205. // Remove internal fields
  206. unset($row['internal-activity']);
  207. unset($row['internal-network']);
  208. unset($row['internal-iid']);
  209. unset($row['internal-iaid']);
  210. unset($row['internal-icid']);
  211. unset($row['internal-user-ignored']);
  212. return $row;
  213. }
  214. /**
  215. * @brief Fills an array with data from an item query
  216. *
  217. * @param object $stmt statement object
  218. * @return array Data array
  219. */
  220. public static function inArray($stmt, $do_close = true) {
  221. if (is_bool($stmt)) {
  222. return $stmt;
  223. }
  224. $data = [];
  225. while ($row = self::fetch($stmt)) {
  226. $data[] = $row;
  227. }
  228. if ($do_close) {
  229. DBA::close($stmt);
  230. }
  231. return $data;
  232. }
  233. /**
  234. * @brief Check if item data exists
  235. *
  236. * @param array $condition array of fields for condition
  237. *
  238. * @return boolean Are there rows for that condition?
  239. */
  240. public static function exists($condition) {
  241. $stmt = self::select(['id'], $condition, ['limit' => 1]);
  242. if (is_bool($stmt)) {
  243. $retval = $stmt;
  244. } else {
  245. $retval = (DBA::numRows($stmt) > 0);
  246. }
  247. DBA::close($stmt);
  248. return $retval;
  249. }
  250. /**
  251. * Retrieve a single record from the item table for a given user and returns it in an associative array
  252. *
  253. * @brief Retrieve a single record from a table
  254. * @param integer $uid User ID
  255. * @param array $fields
  256. * @param array $condition
  257. * @param array $params
  258. * @return bool|array
  259. * @see DBA::select
  260. */
  261. public static function selectFirstForUser($uid, array $selected = [], array $condition = [], $params = [])
  262. {
  263. $params['uid'] = $uid;
  264. if (empty($selected)) {
  265. $selected = Item::DISPLAY_FIELDLIST;
  266. }
  267. return self::selectFirst($selected, $condition, $params);
  268. }
  269. /**
  270. * @brief Select rows from the item table for a given user
  271. *
  272. * @param integer $uid User ID
  273. * @param array $selected Array of selected fields, empty for all
  274. * @param array $condition Array of fields for condition
  275. * @param array $params Array of several parameters
  276. *
  277. * @return boolean|object
  278. */
  279. public static function selectForUser($uid, array $selected = [], array $condition = [], $params = [])
  280. {
  281. $params['uid'] = $uid;
  282. if (empty($selected)) {
  283. $selected = Item::DISPLAY_FIELDLIST;
  284. }
  285. return self::select($selected, $condition, $params);
  286. }
  287. /**
  288. * Retrieve a single record from the item table and returns it in an associative array
  289. *
  290. * @brief Retrieve a single record from a table
  291. * @param array $fields
  292. * @param array $condition
  293. * @param array $params
  294. * @return bool|array
  295. * @see DBA::select
  296. */
  297. public static function selectFirst(array $fields = [], array $condition = [], $params = [])
  298. {
  299. $params['limit'] = 1;
  300. $result = self::select($fields, $condition, $params);
  301. if (is_bool($result)) {
  302. return $result;
  303. } else {
  304. $row = self::fetch($result);
  305. DBA::close($result);
  306. return $row;
  307. }
  308. }
  309. /**
  310. * @brief Select rows from the item table
  311. *
  312. * @param array $selected Array of selected fields, empty for all
  313. * @param array $condition Array of fields for condition
  314. * @param array $params Array of several parameters
  315. *
  316. * @return boolean|object
  317. */
  318. public static function select(array $selected = [], array $condition = [], $params = [])
  319. {
  320. $uid = 0;
  321. $usermode = false;
  322. if (isset($params['uid'])) {
  323. $uid = $params['uid'];
  324. $usermode = true;
  325. }
  326. $fields = self::fieldlist($selected, $usermode);
  327. $select_fields = self::constructSelectFields($fields, $selected);
  328. $condition_string = DBA::buildCondition($condition);
  329. $condition_string = self::addTablesToFields($condition_string, $fields);
  330. if ($usermode) {
  331. $condition_string = $condition_string . ' AND ' . self::condition(false);
  332. }
  333. $param_string = self::addTablesToFields(DBA::buildParameter($params), $fields);
  334. $table = "`item` " . self::constructJoins($uid, $select_fields . $condition_string . $param_string, false, $usermode);
  335. $sql = "SELECT " . $select_fields . " FROM " . $table . $condition_string . $param_string;
  336. return DBA::p($sql, $condition);
  337. }
  338. /**
  339. * @brief Select rows from the starting post in the item table
  340. *
  341. * @param integer $uid User ID
  342. * @param array $fields Array of selected fields, empty for all
  343. * @param array $condition Array of fields for condition
  344. * @param array $params Array of several parameters
  345. *
  346. * @return boolean|object
  347. */
  348. public static function selectThreadForUser($uid, array $selected = [], array $condition = [], $params = [])
  349. {
  350. $params['uid'] = $uid;
  351. if (empty($selected)) {
  352. $selected = Item::DISPLAY_FIELDLIST;
  353. }
  354. return self::selectThread($selected, $condition, $params);
  355. }
  356. /**
  357. * Retrieve a single record from the starting post in the item table and returns it in an associative array
  358. *
  359. * @brief Retrieve a single record from a table
  360. * @param integer $uid User ID
  361. * @param array $selected
  362. * @param array $condition
  363. * @param array $params
  364. * @return bool|array
  365. * @see DBA::select
  366. */
  367. public static function selectFirstThreadForUser($uid, array $selected = [], array $condition = [], $params = [])
  368. {
  369. $params['uid'] = $uid;
  370. if (empty($selected)) {
  371. $selected = Item::DISPLAY_FIELDLIST;
  372. }
  373. return self::selectFirstThread($selected, $condition, $params);
  374. }
  375. /**
  376. * Retrieve a single record from the starting post in the item table and returns it in an associative array
  377. *
  378. * @brief Retrieve a single record from a table
  379. * @param array $fields
  380. * @param array $condition
  381. * @param array $params
  382. * @return bool|array
  383. * @see DBA::select
  384. */
  385. public static function selectFirstThread(array $fields = [], array $condition = [], $params = [])
  386. {
  387. $params['limit'] = 1;
  388. $result = self::selectThread($fields, $condition, $params);
  389. if (is_bool($result)) {
  390. return $result;
  391. } else {
  392. $row = self::fetch($result);
  393. DBA::close($result);
  394. return $row;
  395. }
  396. }
  397. /**
  398. * @brief Select rows from the starting post in the item table
  399. *
  400. * @param array $selected Array of selected fields, empty for all
  401. * @param array $condition Array of fields for condition
  402. * @param array $params Array of several parameters
  403. *
  404. * @return boolean|object
  405. */
  406. public static function selectThread(array $selected = [], array $condition = [], $params = [])
  407. {
  408. $uid = 0;
  409. $usermode = false;
  410. if (isset($params['uid'])) {
  411. $uid = $params['uid'];
  412. $usermode = true;
  413. }
  414. $fields = self::fieldlist($selected, $usermode);
  415. $fields['thread'] = ['mention', 'ignored', 'iid'];
  416. $threadfields = ['thread' => ['iid', 'uid', 'contact-id', 'owner-id', 'author-id',
  417. 'created', 'edited', 'commented', 'received', 'changed', 'wall', 'private',
  418. 'pubmail', 'moderated', 'visible', 'starred', 'ignored', 'post-type',
  419. 'unseen', 'deleted', 'origin', 'forum_mode', 'mention', 'network']];
  420. $select_fields = self::constructSelectFields($fields, $selected);
  421. $condition_string = DBA::buildCondition($condition);
  422. $condition_string = self::addTablesToFields($condition_string, $threadfields);
  423. $condition_string = self::addTablesToFields($condition_string, $fields);
  424. if ($usermode) {
  425. $condition_string = $condition_string . ' AND ' . self::condition(true);
  426. }
  427. $param_string = DBA::buildParameter($params);
  428. $param_string = self::addTablesToFields($param_string, $threadfields);
  429. $param_string = self::addTablesToFields($param_string, $fields);
  430. $table = "`thread` " . self::constructJoins($uid, $select_fields . $condition_string . $param_string, true, $usermode);
  431. $sql = "SELECT " . $select_fields . " FROM " . $table . $condition_string . $param_string;
  432. return DBA::p($sql, $condition);
  433. }
  434. /**
  435. * @brief Returns a list of fields that are associated with the item table
  436. *
  437. * @return array field list
  438. */
  439. private static function fieldlist($selected, $usermode)
  440. {
  441. $fields = [];
  442. $fields['item'] = ['id', 'uid', 'parent', 'uri', 'parent-uri', 'thr-parent', 'guid',
  443. 'contact-id', 'owner-id', 'author-id', 'type', 'wall', 'gravity', 'extid',
  444. 'created', 'edited', 'commented', 'received', 'changed', 'psid', 'uri-hash',
  445. 'resource-id', 'event-id', 'tag', 'attach', 'post-type', 'file',
  446. 'private', 'pubmail', 'moderated', 'visible', 'starred', 'bookmark',
  447. 'unseen', 'deleted', 'origin', 'forum_mode', 'mention', 'global',
  448. 'id' => 'item_id', 'network', 'icid', 'iaid', 'id' => 'internal-iid',
  449. 'network' => 'internal-network', 'icid' => 'internal-icid',
  450. 'iaid' => 'internal-iaid'];
  451. if ($usermode) {
  452. $fields['user-item'] = ['ignored' => 'internal-user-ignored'];
  453. }
  454. $fields['item-activity'] = ['activity', 'activity' => 'internal-activity'];
  455. $fields['item-content'] = array_merge(self::CONTENT_FIELDLIST, self::MIXED_CONTENT_FIELDLIST);
  456. $fields['item-delivery-data'] = self::DELIVERY_DATA_FIELDLIST;
  457. $fields['permissionset'] = ['allow_cid', 'allow_gid', 'deny_cid', 'deny_gid'];
  458. $fields['author'] = ['url' => 'author-link', 'name' => 'author-name',
  459. 'thumb' => 'author-avatar', 'nick' => 'author-nick', 'network' => 'author-network'];
  460. $fields['owner'] = ['url' => 'owner-link', 'name' => 'owner-name',
  461. 'thumb' => 'owner-avatar', 'nick' => 'owner-nick', 'network' => 'owner-network'];
  462. $fields['contact'] = ['url' => 'contact-link', 'name' => 'contact-name', 'thumb' => 'contact-avatar',
  463. 'writable', 'self', 'id' => 'cid', 'alias', 'uid' => 'contact-uid',
  464. 'photo', 'name-date', 'uri-date', 'avatar-date', 'thumb', 'dfrn-id'];
  465. $fields['parent-item'] = ['guid' => 'parent-guid', 'network' => 'parent-network'];
  466. $fields['parent-item-author'] = ['url' => 'parent-author-link', 'name' => 'parent-author-name'];
  467. $fields['event'] = ['created' => 'event-created', 'edited' => 'event-edited',
  468. 'start' => 'event-start','finish' => 'event-finish',
  469. 'summary' => 'event-summary','desc' => 'event-desc',
  470. 'location' => 'event-location', 'type' => 'event-type',
  471. 'nofinish' => 'event-nofinish','adjust' => 'event-adjust',
  472. 'ignore' => 'event-ignore', 'id' => 'event-id'];
  473. $fields['sign'] = ['signed_text', 'signature', 'signer'];
  474. return $fields;
  475. }
  476. /**
  477. * @brief Returns SQL condition for the "select" functions
  478. *
  479. * @param boolean $thread_mode Called for the items (false) or for the threads (true)
  480. *
  481. * @return string SQL condition
  482. */
  483. private static function condition($thread_mode)
  484. {
  485. if ($thread_mode) {
  486. $master_table = "`thread`";
  487. } else {
  488. $master_table = "`item`";
  489. }
  490. return sprintf("$master_table.`visible` AND NOT $master_table.`deleted` AND NOT $master_table.`moderated`
  491. AND (`user-item`.`hidden` IS NULL OR NOT `user-item`.`hidden`)
  492. AND (`user-author`.`blocked` IS NULL OR NOT `user-author`.`blocked`)
  493. AND (`user-author`.`ignored` IS NULL OR NOT `user-author`.`ignored` OR `item`.`gravity` != %d)
  494. AND (`user-owner`.`blocked` IS NULL OR NOT `user-owner`.`blocked`)
  495. AND (`user-owner`.`ignored` IS NULL OR NOT `user-owner`.`ignored` OR `item`.`gravity` != %d) ",
  496. GRAVITY_PARENT, GRAVITY_PARENT);
  497. }
  498. /**
  499. * @brief Returns all needed "JOIN" commands for the "select" functions
  500. *
  501. * @param integer $uid User ID
  502. * @param string $sql_commands The parts of the built SQL commands in the "select" functions
  503. * @param boolean $thread_mode Called for the items (false) or for the threads (true)
  504. *
  505. * @return string The SQL joins for the "select" functions
  506. */
  507. private static function constructJoins($uid, $sql_commands, $thread_mode, $user_mode)
  508. {
  509. if ($thread_mode) {
  510. $master_table = "`thread`";
  511. $master_table_key = "`thread`.`iid`";
  512. $joins = "STRAIGHT_JOIN `item` ON `item`.`id` = `thread`.`iid` ";
  513. } else {
  514. $master_table = "`item`";
  515. $master_table_key = "`item`.`id`";
  516. $joins = '';
  517. }
  518. if ($user_mode) {
  519. $joins .= sprintf("STRAIGHT_JOIN `contact` ON `contact`.`id` = $master_table.`contact-id`
  520. AND NOT `contact`.`blocked`
  521. AND ((NOT `contact`.`readonly` AND NOT `contact`.`pending` AND (`contact`.`rel` IN (%s, %s)))
  522. OR `contact`.`self` OR `item`.`gravity` != %d OR `contact`.`uid` = 0)
  523. STRAIGHT_JOIN `contact` AS `author` ON `author`.`id` = $master_table.`author-id` AND NOT `author`.`blocked`
  524. STRAIGHT_JOIN `contact` AS `owner` ON `owner`.`id` = $master_table.`owner-id` AND NOT `owner`.`blocked`
  525. LEFT JOIN `user-item` ON `user-item`.`iid` = $master_table_key AND `user-item`.`uid` = %d
  526. LEFT JOIN `user-contact` AS `user-author` ON `user-author`.`cid` = $master_table.`author-id` AND `user-author`.`uid` = %d
  527. LEFT JOIN `user-contact` AS `user-owner` ON `user-owner`.`cid` = $master_table.`owner-id` AND `user-owner`.`uid` = %d",
  528. Contact::SHARING, Contact::FRIEND, GRAVITY_PARENT, intval($uid), intval($uid), intval($uid));
  529. } else {
  530. if (strpos($sql_commands, "`contact`.") !== false) {
  531. $joins .= "LEFT JOIN `contact` ON `contact`.`id` = $master_table.`contact-id`";
  532. }
  533. if (strpos($sql_commands, "`author`.") !== false) {
  534. $joins .= " LEFT JOIN `contact` AS `author` ON `author`.`id` = $master_table.`author-id`";
  535. }
  536. if (strpos($sql_commands, "`owner`.") !== false) {
  537. $joins .= " LEFT JOIN `contact` AS `owner` ON `owner`.`id` = $master_table.`owner-id`";
  538. }
  539. }
  540. if (strpos($sql_commands, "`group_member`.") !== false) {
  541. $joins .= " STRAIGHT_JOIN `group_member` ON `group_member`.`contact-id` = $master_table.`contact-id`";
  542. }
  543. if (strpos($sql_commands, "`user`.") !== false) {
  544. $joins .= " STRAIGHT_JOIN `user` ON `user`.`uid` = $master_table.`uid`";
  545. }
  546. if (strpos($sql_commands, "`event`.") !== false) {
  547. $joins .= " LEFT JOIN `event` ON `event-id` = `event`.`id`";
  548. }
  549. if (strpos($sql_commands, "`sign`.") !== false) {
  550. $joins .= " LEFT JOIN `sign` ON `sign`.`iid` = `item`.`id`";
  551. }
  552. if (strpos($sql_commands, "`item-activity`.") !== false) {
  553. $joins .= " LEFT JOIN `item-activity` ON `item-activity`.`id` = `item`.`iaid`";
  554. }
  555. if (strpos($sql_commands, "`item-content`.") !== false) {
  556. $joins .= " LEFT JOIN `item-content` ON `item-content`.`id` = `item`.`icid`";
  557. }
  558. if (strpos($sql_commands, "`item-delivery-data`.") !== false) {
  559. $joins .= " LEFT JOIN `item-delivery-data` ON `item-delivery-data`.`iid` = `item`.`id`";
  560. }
  561. if (strpos($sql_commands, "`permissionset`.") !== false) {
  562. $joins .= " LEFT JOIN `permissionset` ON `permissionset`.`id` = `item`.`psid`";
  563. }
  564. if ((strpos($sql_commands, "`parent-item`.") !== false) || (strpos($sql_commands, "`parent-author`.") !== false)) {
  565. $joins .= " STRAIGHT_JOIN `item` AS `parent-item` ON `parent-item`.`id` = `item`.`parent`";
  566. }
  567. if (strpos($sql_commands, "`parent-item-author`.") !== false) {
  568. $joins .= " STRAIGHT_JOIN `contact` AS `parent-item-author` ON `parent-item-author`.`id` = `parent-item`.`author-id`";
  569. }
  570. return $joins;
  571. }
  572. /**
  573. * @brief Add the field list for the "select" functions
  574. *
  575. * @param array $fields The field definition array
  576. * @param array $selected The array with the selected fields from the "select" functions
  577. *
  578. * @return string The field list
  579. */
  580. private static function constructSelectFields($fields, $selected)
  581. {
  582. if (!empty($selected)) {
  583. $selected[] = 'internal-iid';
  584. $selected[] = 'internal-iaid';
  585. $selected[] = 'internal-icid';
  586. $selected[] = 'internal-network';
  587. }
  588. if (in_array('verb', $selected)) {
  589. $selected[] = 'internal-activity';
  590. }
  591. if (in_array('ignored', $selected)) {
  592. $selected[] = 'internal-user-ignored';
  593. }
  594. $selection = [];
  595. foreach ($fields as $table => $table_fields) {
  596. foreach ($table_fields as $field => $select) {
  597. if (empty($selected) || in_array($select, $selected)) {
  598. $legacy_fields = array_merge(self::DELIVERY_DATA_FIELDLIST, self::MIXED_CONTENT_FIELDLIST);
  599. if (self::isLegacyMode() && in_array($select, $legacy_fields)) {
  600. $selection[] = "`item`.`".$select."` AS `internal-item-" . $select . "`";
  601. }
  602. if (is_int($field)) {
  603. $selection[] = "`" . $table . "`.`" . $select . "`";
  604. } else {
  605. $selection[] = "`" . $table . "`.`" . $field . "` AS `" . $select . "`";
  606. }
  607. }
  608. }
  609. }
  610. return implode(", ", $selection);
  611. }
  612. /**
  613. * @brief add table definition to fields in an SQL query
  614. *
  615. * @param string $query SQL query
  616. * @param array $fields The field definition array
  617. *
  618. * @return string the changed SQL query
  619. */
  620. private static function addTablesToFields($query, $fields)
  621. {
  622. foreach ($fields as $table => $table_fields) {
  623. foreach ($table_fields as $alias => $field) {
  624. if (is_int($alias)) {
  625. $replace_field = $field;
  626. } else {
  627. $replace_field = $alias;
  628. }
  629. $search = "/([^\.])`" . $field . "`/i";
  630. $replace = "$1`" . $table . "`.`" . $replace_field . "`";
  631. $query = preg_replace($search, $replace, $query);
  632. }
  633. }
  634. return $query;
  635. }
  636. /**
  637. * @brief Generate a server unique item hash for linking between the item tables
  638. *
  639. * @param string $uri Item URI
  640. * @param date $created Item creation date
  641. *
  642. * @return string the item hash
  643. */
  644. private static function itemHash($uri, $created)
  645. {
  646. return round(strtotime($created) / 100) . hash('ripemd128', $uri);
  647. }
  648. /**
  649. * @brief Update existing item entries
  650. *
  651. * @param array $fields The fields that are to be changed
  652. * @param array $condition The condition for finding the item entries
  653. *
  654. * In the future we may have to change permissions as well.
  655. * Then we had to add the user id as third parameter.
  656. *
  657. * A return value of "0" doesn't mean an error - but that 0 rows had been changed.
  658. *
  659. * @return integer|boolean number of affected rows - or "false" if there was an error
  660. */
  661. public static function update(array $fields, array $condition)
  662. {
  663. if (empty($condition) || empty($fields)) {
  664. return false;
  665. }
  666. // To ensure the data integrity we do it in an transaction
  667. DBA::transaction();
  668. // We cannot simply expand the condition to check for origin entries
  669. // The condition needn't to be a simple array but could be a complex condition.
  670. // And we have to execute this query before the update to ensure to fetch the same data.
  671. $items = DBA::select('item', ['id', 'origin', 'uri', 'created', 'uri-hash', 'iaid', 'icid', 'tag', 'file'], $condition);
  672. $content_fields = [];
  673. foreach (array_merge(self::CONTENT_FIELDLIST, self::MIXED_CONTENT_FIELDLIST) as $field) {
  674. if (isset($fields[$field])) {
  675. $content_fields[$field] = $fields[$field];
  676. if (in_array($field, self::CONTENT_FIELDLIST) || !self::isLegacyMode()) {
  677. unset($fields[$field]);
  678. } else {
  679. $fields[$field] = null;
  680. }
  681. }
  682. }
  683. $clear_fields = ['bookmark', 'type', 'author-name', 'author-avatar', 'author-link', 'owner-name', 'owner-avatar', 'owner-link'];
  684. foreach ($clear_fields as $field) {
  685. if (array_key_exists($field, $fields)) {
  686. $fields[$field] = null;
  687. }
  688. }
  689. if (array_key_exists('tag', $fields)) {
  690. $tags = $fields['tag'];
  691. $fields['tag'] = null;
  692. } else {
  693. $tags = '';
  694. }
  695. if (array_key_exists('file', $fields)) {
  696. $files = $fields['file'];
  697. $fields['file'] = null;
  698. } else {
  699. $files = '';
  700. }
  701. $delivery_data = ['postopts' => defaults($fields, 'postopts', ''),
  702. 'inform' => defaults($fields, 'inform', '')];
  703. $fields['postopts'] = null;
  704. $fields['inform'] = null;
  705. if (!empty($fields)) {
  706. $success = DBA::update('item', $fields, $condition);
  707. if (!$success) {
  708. DBA::close($items);
  709. DBA::rollback();
  710. return false;
  711. }
  712. }
  713. // When there is no content for the "old" item table, this will count the fetched items
  714. $rows = DBA::affectedRows();
  715. while ($item = DBA::fetch($items)) {
  716. // This part here can safely be removed when the legacy fields in the item had been removed
  717. if (empty($item['uri-hash']) && !empty($item['uri']) && !empty($item['created'])) {
  718. // Fetch the uri-hash from an existing item entry if there is one
  719. $item_condition = ["`uri` = ? AND `uri-hash` != ''", $item['uri']];
  720. $existing = DBA::selectfirst('item', ['uri-hash'], $item_condition);
  721. if (DBA::isResult($existing)) {
  722. $item['uri-hash'] = $existing['uri-hash'];
  723. } else {
  724. $item['uri-hash'] = self::itemHash($item['uri'], $item['created']);
  725. }
  726. DBA::update('item', ['uri-hash' => $item['uri-hash']], ['id' => $item['id']]);
  727. DBA::update('item-activity', ['uri-hash' => $item['uri-hash']], ["`uri` = ? AND `uri-hash` = ''", $item['uri']]);
  728. DBA::update('item-content', ['uri-plink-hash' => $item['uri-hash']], ["`uri` = ? AND `uri-plink-hash` = ''", $item['uri']]);
  729. }
  730. if (!empty($item['iaid']) || (!empty($content_fields['verb']) && (self::activityToIndex($content_fields['verb']) >= 0))) {
  731. if (!empty($item['iaid'])) {
  732. $update_condition = ['id' => $item['iaid']];
  733. } else {
  734. $update_condition = ['uri-hash' => $item['uri-hash']];
  735. }
  736. self::updateActivity($content_fields, $update_condition);
  737. if (empty($item['iaid'])) {
  738. $item_activity = DBA::selectFirst('item-activity', ['id'], ['uri-hash' => $item['uri-hash']]);
  739. if (DBA::isResult($item_activity)) {
  740. $item_fields = ['iaid' => $item_activity['id'], 'icid' => null];
  741. foreach (self::MIXED_CONTENT_FIELDLIST as $field) {
  742. if (self::isLegacyMode()) {
  743. $item_fields[$field] = null;
  744. } else {
  745. unset($item_fields[$field]);
  746. }
  747. }
  748. DBA::update('item', $item_fields, ['id' => $item['id']]);
  749. if (!empty($item['icid']) && !DBA::exists('item', ['icid' => $item['icid']])) {
  750. DBA::delete('item-content', ['id' => $item['icid']]);
  751. }
  752. }
  753. } elseif (!empty($item['icid'])) {
  754. DBA::update('item', ['icid' => null], ['id' => $item['id']]);
  755. if (!DBA::exists('item', ['icid' => $item['icid']])) {
  756. DBA::delete('item-content', ['id' => $item['icid']]);
  757. }
  758. }
  759. } else {
  760. if (!empty($item['icid'])) {
  761. $update_condition = ['id' => $item['icid']];
  762. } else {
  763. $update_condition = ['uri-plink-hash' => $item['uri-hash']];
  764. }
  765. self::updateContent($content_fields, $update_condition);
  766. if (empty($item['icid'])) {
  767. $item_content = DBA::selectFirst('item-content', [], ['uri-plink-hash' => $item['uri-hash']]);
  768. if (DBA::isResult($item_content)) {
  769. $item_fields = ['icid' => $item_content['id']];
  770. // Clear all fields in the item table that have a content in the item-content table
  771. foreach ($item_content as $field => $content) {
  772. if (in_array($field, self::MIXED_CONTENT_FIELDLIST) && !empty($item_content[$field])) {
  773. if (self::isLegacyMode()) {
  774. $item_fields[$field] = null;
  775. } else {
  776. unset($item_fields[$field]);
  777. }
  778. }
  779. }
  780. DBA::update('item', $item_fields, ['id' => $item['id']]);
  781. }
  782. }
  783. }
  784. if (!empty($tags)) {
  785. Term::insertFromTagFieldByItemId($item['id'], $tags);
  786. if (!empty($item['tag'])) {
  787. DBA::update('item', ['tag' => ''], ['id' => $item['id']]);
  788. }
  789. }
  790. if (!empty($files)) {
  791. Term::insertFromFileFieldByItemId($item['id'], $files);
  792. if (!empty($item['file'])) {
  793. DBA::update('item', ['file' => ''], ['id' => $item['id']]);
  794. }
  795. }
  796. self::updateDeliveryData($item['id'], $delivery_data);
  797. self::updateThread($item['id']);
  798. // We only need to notfiy others when it is an original entry from us.
  799. // Only call the notifier when the item has some content relevant change.
  800. if ($item['origin'] && in_array('edited', array_keys($fields))) {
  801. Worker::add(PRIORITY_HIGH, "Notifier", 'edit_post', $item['id']);
  802. }
  803. }
  804. DBA::close($items);
  805. DBA::commit();
  806. return $rows;
  807. }
  808. /**
  809. * @brief Delete an item and notify others about it - if it was ours
  810. *
  811. * @param array $condition The condition for finding the item entries
  812. * @param integer $priority Priority for the notification
  813. */
  814. public static function delete($condition, $priority = PRIORITY_HIGH)
  815. {
  816. $items = self::select(['id'], $condition);
  817. while ($item = self::fetch($items)) {
  818. self::deleteById($item['id'], $priority);
  819. }
  820. DBA::close($items);
  821. }
  822. /**
  823. * @brief Delete an item for an user and notify others about it - if it was ours
  824. *
  825. * @param array $condition The condition for finding the item entries
  826. * @param integer $uid User who wants to delete this item
  827. */
  828. public static function deleteForUser($condition, $uid)
  829. {
  830. if ($uid == 0) {
  831. return;
  832. }
  833. $items = self::select(['id', 'uid'], $condition);
  834. while ($item = self::fetch($items)) {
  835. // "Deleting" global items just means hiding them
  836. if ($item['uid'] == 0) {
  837. DBA::update('user-item', ['hidden' => true], ['iid' => $item['id'], 'uid' => $uid], true);
  838. } elseif ($item['uid'] == $uid) {
  839. self::deleteById($item['id'], PRIORITY_HIGH);
  840. } else {
  841. logger('Wrong ownership. Not deleting item ' . $item['id']);
  842. }
  843. }
  844. DBA::close($items);
  845. }
  846. /**
  847. * @brief Delete an item and notify others about it - if it was ours
  848. *
  849. * @param integer $item_id Item ID that should be delete
  850. * @param integer $priority Priority for the notification
  851. *
  852. * @return boolean success
  853. */
  854. private static function deleteById($item_id, $priority = PRIORITY_HIGH)
  855. {
  856. // locate item to be deleted
  857. $fields = ['id', 'uri', 'uid', 'parent', 'parent-uri', 'origin',
  858. 'deleted', 'file', 'resource-id', 'event-id', 'attach',
  859. 'verb', 'object-type', 'object', 'target', 'contact-id',
  860. 'icid', 'iaid', 'psid'];
  861. $item = self::selectFirst($fields, ['id' => $item_id]);
  862. if (!DBA::isResult($item)) {
  863. logger('Item with ID ' . $item_id . " hasn't been found.", LOGGER_DEBUG);
  864. return false;
  865. }
  866. if ($item['deleted']) {
  867. logger('Item with ID ' . $item_id . ' has already been deleted.', LOGGER_DEBUG);
  868. return false;
  869. }
  870. $parent = self::selectFirst(['origin'], ['id' => $item['parent']]);
  871. if (!DBA::isResult($parent)) {
  872. $parent = ['origin' => false];
  873. }
  874. // clean up categories and tags so they don't end up as orphans
  875. $matches = false;
  876. $cnt = preg_match_all('/<(.*?)>/', $item['file'], $matches, PREG_SET_ORDER);
  877. if ($cnt) {
  878. foreach ($matches as $mtch) {
  879. file_tag_unsave_file($item['uid'], $item['id'], $mtch[1],true);
  880. }
  881. }
  882. $matches = false;
  883. $cnt = preg_match_all('/\[(.*?)\]/', $item['file'], $matches, PREG_SET_ORDER);
  884. if ($cnt) {
  885. foreach ($matches as $mtch) {
  886. file_tag_unsave_file($item['uid'], $item['id'], $mtch[1],false);
  887. }
  888. }
  889. /*
  890. * If item is a link to a photo resource, nuke all the associated photos
  891. * (visitors will not have photo resources)
  892. * This only applies to photos uploaded from the photos page. Photos inserted into a post do not
  893. * generate a resource-id and therefore aren't intimately linked to the item.
  894. */
  895. if (strlen($item['resource-id'])) {
  896. DBA::delete('photo', ['resource-id' => $item['resource-id'], 'uid' => $item['uid']]);
  897. }
  898. // If item is a link to an event, delete the event.
  899. if (intval($item['event-id'])) {
  900. Event::delete($item['event-id']);
  901. }
  902. // If item has attachments, drop them
  903. foreach (explode(", ", $item['attach']) as $attach) {
  904. preg_match("|attach/(\d+)|", $attach, $matches);
  905. if (is_array($matches) && count($matches) > 1) {
  906. DBA::delete('attach', ['id' => $matches[1], 'uid' => $item['uid']]);
  907. }
  908. }
  909. // Delete tags that had been attached to other items
  910. self::deleteTagsFromItem($item);
  911. // Set the item to "deleted"
  912. $item_fields = ['deleted' => true, 'edited' => DateTimeFormat::utcNow(), 'changed' => DateTimeFormat::utcNow()];
  913. DBA::update('item', $item_fields, ['id' => $item['id']]);
  914. Term::insertFromTagFieldByItemId($item['id'], '');
  915. Term::insertFromFileFieldByItemId($item['id'], '');
  916. self::deleteThread($item['id'], $item['parent-uri']);
  917. if (!self::exists(["`uri` = ? AND `uid` != 0 AND NOT `deleted`", $item['uri']])) {
  918. self::delete(['uri' => $item['uri'], 'uid' => 0, 'deleted' => false], $priority);
  919. }
  920. DBA::delete('item-delivery-data', ['iid' => $item['id']]);
  921. // We don't delete the item-activity here, since we need some of the data for ActivityPub
  922. if (!empty($item['icid']) && !self::exists(['icid' => $item['icid'], 'deleted' => false])) {
  923. DBA::delete('item-content', ['id' => $item['icid']], ['cascade' => false]);
  924. }
  925. // When the permission set will be used in photo and events as well,
  926. // this query here needs to be extended.
  927. if (!empty($item['psid']) && !self::exists(['psid' => $item['psid'], 'deleted' => false])) {
  928. DBA::delete('permissionset', ['id' => $item['psid']], ['cascade' => false]);
  929. }
  930. // If it's the parent of a comment thread, kill all the kids
  931. if ($item['id'] == $item['parent']) {
  932. self::delete(['parent' => $item['parent'], 'deleted' => false], $priority);
  933. }
  934. // Is it our comment and/or our thread?
  935. if ($item['origin'] || $parent['origin']) {
  936. // When we delete the original post we will delete all existing copies on the server as well
  937. self::delete(['uri' => $item['uri'], 'deleted' => false], $priority);
  938. // send the notification upstream/downstream
  939. Worker::add(['priority' => $priority, 'dont_fork' => true], "Notifier", "drop", intval($item['id']));
  940. } elseif ($item['uid'] != 0) {
  941. // When we delete just our local user copy of an item, we have to set a marker to hide it
  942. $global_item = self::selectFirst(['id'], ['uri' => $item['uri'], 'uid' => 0, 'deleted' => false]);
  943. if (DBA::isResult($global_item)) {
  944. DBA::update('user-item', ['hidden' => true], ['iid' => $global_item['id'], 'uid' => $item['uid']], true);
  945. }
  946. }
  947. logger('Item with ID ' . $item_id . " has been deleted.", LOGGER_DEBUG);
  948. return true;
  949. }
  950. private static function deleteTagsFromItem($item)
  951. {
  952. if (($item["verb"] != ACTIVITY_TAG) || ($item["object-type"] != ACTIVITY_OBJ_TAGTERM)) {
  953. return;
  954. }
  955. $xo = XML::parseString($item["object"], false);
  956. $xt = XML::parseString($item["target"], false);
  957. if ($xt->type != ACTIVITY_OBJ_NOTE) {
  958. return;
  959. }
  960. $i = self::selectFirst(['id', 'contact-id', 'tag'], ['uri' => $xt->id, 'uid' => $item['uid']]);
  961. if (!DBA::isResult($i)) {
  962. return;
  963. }
  964. // For tags, the owner cannot remove the tag on the author's copy of the post.
  965. $owner_remove = ($item["contact-id"] == $i["contact-id"]);
  966. $author_copy = $item["origin"];
  967. if (($owner_remove && $author_copy) || !$owner_remove) {
  968. return;
  969. }
  970. $tags = explode(',', $i["tag"]);
  971. $newtags = [];
  972. if (count($tags)) {
  973. foreach ($tags as $tag) {
  974. if (trim($tag) !== trim($xo->body)) {
  975. $newtags[] = trim($tag);
  976. }
  977. }
  978. }
  979. self::update(['tag' => implode(',', $newtags)], ['id' => $i["id"]]);
  980. }
  981. private static function guid($item, $notify)
  982. {
  983. if (!empty($item['guid'])) {
  984. return notags(trim($item['guid']));
  985. }
  986. if ($notify) {
  987. // We have to avoid duplicates. So we create the GUID in form of a hash of the plink or uri.
  988. // We add the hash of our own host because our host is the original creator of the post.
  989. $prefix_host = get_app()->get_hostname();
  990. } else {
  991. $prefix_host = '';
  992. // We are only storing the post so we create a GUID from the original hostname.
  993. if (!empty($item['author-link'])) {
  994. $parsed = parse_url($item['author-link']);
  995. if (!empty($parsed['host'])) {
  996. $prefix_host = $parsed['host'];
  997. }
  998. }
  999. if (empty($prefix_host) && !empty($item['plink'])) {
  1000. $parsed = parse_url($item['plink']);
  1001. if (!empty($parsed['host'])) {
  1002. $prefix_host = $parsed['host'];
  1003. }
  1004. }
  1005. if (empty($prefix_host) && !empty($item['uri'])) {
  1006. $parsed = parse_url($item['uri']);
  1007. if (!empty($parsed['host'])) {
  1008. $prefix_host = $parsed['host'];
  1009. }
  1010. }
  1011. // Is it in the format data@host.tld? - Used for mail contacts
  1012. if (empty($prefix_host) && !empty($item['author-link']) && strstr($item['author-link'], '@')) {
  1013. $mailparts = explode('@', $item['author-link']);
  1014. $prefix_host = array_pop($mailparts);
  1015. }
  1016. }
  1017. if (!empty($item['plink'])) {
  1018. $guid = self::guidFromUri($item['plink'], $prefix_host);
  1019. } elseif (!empty($item['uri'])) {
  1020. $guid = self::guidFromUri($item['uri'], $prefix_host);
  1021. } else {
  1022. $guid = System::createUUID(hash('crc32', $prefix_host));
  1023. }
  1024. return $guid;
  1025. }
  1026. private static function contactId($item)
  1027. {
  1028. $contact_id = (int)$item["contact-id"];
  1029. if (!empty($contact_id)) {
  1030. return $contact_id;
  1031. }
  1032. logger('Missing contact-id. Called by: '.System::callstack(), LOGGER_DEBUG);
  1033. /*
  1034. * First we are looking for a suitable contact that matches with the author of the post
  1035. * This is done only for comments
  1036. */
  1037. if ($item['parent-uri'] != $item['uri']) {
  1038. $contact_id = Contact::getIdForURL($item['author-link'], $item['uid']);
  1039. }
  1040. // If not present then maybe the owner was found
  1041. if ($contact_id == 0) {
  1042. $contact_id = Contact::getIdForURL($item['owner-link'], $item['uid']);
  1043. }
  1044. // Still missing? Then use the "self" contact of the current user
  1045. if ($contact_id == 0) {
  1046. $self = DBA::selectFirst('contact', ['id'], ['self' => true, 'uid' => $item['uid']]);
  1047. if (DBA::isResult($self)) {
  1048. $contact_id = $self["id"];
  1049. }
  1050. }
  1051. logger("Contact-id was missing for post ".$item['guid']." from user id ".$item['uid']." - now set to ".$contact_id, LOGGER_DEBUG);
  1052. return $contact_id;
  1053. }
  1054. // This function will finally cover most of the preparation functionality in mod/item.php
  1055. public static function prepare(&$item)
  1056. {
  1057. $data = BBCode::getAttachmentData($item['body']);
  1058. if ((preg_match_all("/\[bookmark\=([^\]]*)\](.*?)\[\/bookmark\]/ism", $item['body'], $match, PREG_SET_ORDER) || isset($data["type"]))
  1059. && ($posttype != Item::PT_PERSONAL_NOTE)) {
  1060. $posttype = Item::PT_PAGE;
  1061. $objecttype = ACTIVITY_OBJ_BOOKMARK;
  1062. }
  1063. }
  1064. public static function insert($item, $force_parent = false, $notify = false, $dontcache = false)
  1065. {
  1066. $a = get_app();
  1067. // If it is a posting where users should get notifications, then define it as wall posting
  1068. if ($notify) {
  1069. $item['wall'] = 1;
  1070. $item['origin'] = 1;
  1071. $item['network'] = Protocol::DFRN;
  1072. $item['protocol'] = Conversation::PARCEL_DFRN;
  1073. if (is_int($notify)) {
  1074. $priority = $notify;
  1075. } else {
  1076. $priority = PRIORITY_HIGH;
  1077. }
  1078. } else {
  1079. $item['network'] = trim(defaults($item, 'network', Protocol::PHANTOM));
  1080. }
  1081. $item['guid'] = self::guid($item, $notify);
  1082. $item['uri'] = notags(trim(defaults($item, 'uri', self::newURI($item['uid'], $item['guid']))));
  1083. // Store URI data
  1084. $item['uri-id'] = ItemURI::insert(['uri' => $item['uri'], 'guid' => $item['guid']]);
  1085. // Store conversation data
  1086. $item = Conversation::insert($item);
  1087. /*
  1088. * If a Diaspora signature structure was passed in, pull it out of the
  1089. * item array and set it aside for later storage.
  1090. */
  1091. $dsprsig = null;
  1092. if (x($item, 'dsprsig')) {
  1093. $encoded_signature = $item['dsprsig'];
  1094. $dsprsig = json_decode(base64_decode($item['dsprsig']));
  1095. unset($item['dsprsig']);
  1096. }
  1097. if (!empty($item['diaspora_signed_text'])) {
  1098. $diaspora_signed_text = $item['diaspora_signed_text'];
  1099. unset($item['diaspora_signed_text']);
  1100. } else {
  1101. $diaspora_signed_text = '';
  1102. }
  1103. // Converting the plink
  1104. /// @TODO Check if this is really still needed
  1105. if ($item['network'] == Protocol::OSTATUS) {
  1106. if (isset($item['plink'])) {
  1107. $item['plink'] = OStatus::convertHref($item['plink']);
  1108. } elseif (isset($item['uri'])) {
  1109. $item['plink'] = OStatus::convertHref($item['uri']);
  1110. }
  1111. }
  1112. if (!empty($item['thr-parent'])) {
  1113. $item['parent-uri'] = $item['thr-parent'];
  1114. }
  1115. if (isset($item['gravity'])) {
  1116. $item['gravity'] = intval($item['gravity']);
  1117. } elseif ($item['parent-uri'] === $item['uri']) {
  1118. $item['gravity'] = GRAVITY_PARENT;
  1119. } elseif (activity_match($item['verb'], ACTIVITY_POST)) {
  1120. $item['gravity'] = GRAVITY_COMMENT;
  1121. } else {
  1122. $item['gravity'] = GRAVITY_UNKNOWN; // Should not happen
  1123. logger('Unknown gravity for verb: ' . $item['verb'], LOGGER_DEBUG);
  1124. }
  1125. $uid = intval($item['uid']);
  1126. // check for create date and expire time
  1127. $expire_interval = Config::get('system', 'dbclean-expire-days', 0);
  1128. $user = DBA::selectFirst('user', ['expire'], ['uid' => $uid]);
  1129. if (DBA::isResult($user) && ($user['expire'] > 0) && (($user['expire'] < $expire_interval) || ($expire_interval == 0))) {
  1130. $expire_interval = $user['expire'];
  1131. }
  1132. if (($expire_interval > 0) && !empty($item['created'])) {
  1133. $expire_date = time() - ($expire_interval * 86400);
  1134. $created_date = strtotime($item['created']);
  1135. if ($created_date < $expire_date) {
  1136. logger('item-store: item created ('.date('c', $created_date).') before expiration time ('.date('c', $expire_date).'). ignored. ' . print_r($item,true), LOGGER_DEBUG);
  1137. return 0;
  1138. }
  1139. }
  1140. /*
  1141. * Do we already have this item?
  1142. * We have to check several networks since Friendica posts could be repeated
  1143. * via OStatus (maybe Diasporsa as well)
  1144. */
  1145. if (in_array($item['network'], [Protocol::ACTIVITYPUB, Protocol::DIASPORA, Protocol::DFRN, Protocol::OSTATUS, ""])) {
  1146. $condition = ["`uri` = ? AND `uid` = ? AND `network` IN (?, ?, ?)",
  1147. trim($item['uri']), $item['uid'],
  1148. Protocol::DIASPORA, Protocol::DFRN, Protocol::OSTATUS];
  1149. $existing = self::selectFirst(['id', 'network'], $condition);
  1150. if (DBA::isResult($existing)) {
  1151. // We only log the entries with a different user id than 0. Otherwise we would have too many false positives
  1152. if ($uid != 0) {
  1153. logger("Item with uri ".$item['uri']." already existed for user ".$uid." with id ".$existing["id"]." target network ".$existing["network"]." - new network: ".$item['network']);
  1154. }
  1155. return $existing["id"];
  1156. }
  1157. }
  1158. // Ensure to always have the same creation date.
  1159. $existing = self::selectfirst(['created', 'uri-hash'], ['uri' => $item['uri']]);
  1160. if (DBA::isResult($existing)) {
  1161. $item['created'] = $existing['created'];
  1162. $item['uri-hash'] = $existing['uri-hash'];
  1163. }
  1164. $item['wall'] = intval(defaults($item, 'wall', 0));
  1165. $item['extid'] = trim(defaults($item, 'extid', ''));
  1166. $item['author-name'] = trim(defaults($item, 'author-name', ''));
  1167. $item['author-link'] = trim(defaults($item, 'author-link', ''));
  1168. $item['author-avatar'] = trim(defaults($item, 'author-avatar', ''));
  1169. $item['owner-name'] = trim(defaults($item, 'owner-name', ''));
  1170. $item['owner-link'] = trim(defaults($item, 'owner-link', ''));
  1171. $item['owner-avatar'] = trim(defaults($item, 'owner-avatar', ''));
  1172. $item['received'] = ((x($item, 'received') !== false) ? DateTimeFormat::utc($item['received']) : DateTimeFormat::utcNow());
  1173. $item['created'] = ((x($item, 'created') !== false) ? DateTimeFormat::utc($item['created']) : $item['received']);
  1174. $item['edited'] = ((x($item, 'edited') !== false) ? DateTimeFormat::utc($item['edited']) : $item['created']);
  1175. $item['changed'] = ((x($item, 'changed') !== false) ? DateTimeFormat::utc($item['changed']) : $item['created']);
  1176. $item['commented'] = ((x($item, 'commented') !== false) ? DateTimeFormat::utc($item['commented']) : $item['created']);
  1177. $item['title'] = trim(defaults($item, 'title', ''));
  1178. $item['location'] = trim(defaults($item, 'location', ''));
  1179. $item['coord'] = trim(defaults($item, 'coord', ''));
  1180. $item['visible'] = ((x($item, 'visible') !== false) ? intval($item['visible']) : 1);
  1181. $item['deleted'] = 0;
  1182. $item['parent-uri'] = trim(defaults($item, 'parent-uri', $item['uri']));
  1183. $item['post-type'] = defaults($item, 'post-type', self::PT_ARTICLE);
  1184. $item['verb'] = trim(defaults($item, 'verb', ''));
  1185. $item['object-type'] = trim(defaults($item, 'object-type', ''));
  1186. $item['object'] = trim(defaults($item, 'object', ''));
  1187. $item['target-type'] = trim(defaults($item, 'target-type', ''));
  1188. $item['target'] = trim(defaults($item, 'target', ''));
  1189. $item['plink'] = trim(defaults($item, 'plink', ''));
  1190. $item['allow_cid'] = trim(defaults($item, 'allow_cid', ''));
  1191. $item['allow_gid'] = trim(defaults($item, 'allow_gid', ''));
  1192. $item['deny_cid'] = trim(defaults($item, 'deny_cid', ''));
  1193. $item['deny_gid'] = trim(defaults($item, 'deny_gid', ''));
  1194. $item['private'] = intval(defaults($item, 'private', 0));
  1195. $item['body'] = trim(defaults($item, 'body', ''));
  1196. $item['tag'] = trim(defaults($item, 'tag', ''));
  1197. $item['attach'] = trim(defaults($item, 'attach', ''));
  1198. $item['app'] = trim(defaults($item, 'app', ''));
  1199. $item['origin'] = intval(defaults($item, 'origin', 0));
  1200. $item['postopts'] = trim(defaults($item, 'postopts', ''));
  1201. $item['resource-id'] = trim(defaults($item, 'resource-id', ''));
  1202. $item['event-id'] = intval(defaults($item, 'event-id', 0));
  1203. $item['inform'] = trim(defaults($item, 'inform', ''));
  1204. $item['file'] = trim(defaults($item, 'file', ''));
  1205. // Unique identifier to be linked against item-activities and item-content
  1206. $item['uri-hash'] = defaults($item, 'uri-hash', self::itemHash($item['uri'], $item['created']));
  1207. // When there is no content then we don't post it
  1208. if ($item['body'].$item['title'] == '') {
  1209. logger('No body, no title.');
  1210. return 0;
  1211. }
  1212. self::addLanguageToItemArray($item);
  1213. // Items cannot be stored before they happen ...
  1214. if ($item['created'] > DateTimeFormat::utcNow()) {
  1215. $item['created'] = DateTimeFormat::utcNow();
  1216. }
  1217. // We haven't invented time travel by now.
  1218. if ($item['edited'] > DateTimeFormat::utcNow()) {
  1219. $item['edited'] = DateTimeFormat::utcNow();
  1220. }
  1221. $item['plink'] = defaults($item, 'plink', System::baseUrl() . '/display/' . urlencode($item['guid']));
  1222. // The contact-id should be set before "self::insert" was called - but there seems to be issues sometimes
  1223. $item["contact-id"] = self::contactId($item);
  1224. $default = ['url' => $item['author-link'], 'name' => $item['author-name'],
  1225. 'photo' => $item['author-avatar'], 'network' => $item['network']];
  1226. $item['author-id'] = defaults($item, 'author-id', Contact::getIdForURL($item["author-link"], 0, false, $default));
  1227. if (Contact::isBlocked($item["author-id"])) {
  1228. logger('Contact '.$item["author-id"].' is blocked, item '.$item["uri"].' will not be stored');
  1229. return 0;
  1230. }
  1231. $default = ['url' => $item['owner-link'], 'name' => $item['owner-name'],
  1232. 'photo' => $item['owner-avatar'], 'network' => $item['network']];
  1233. $item['owner-id'] = defaults($item, 'owner-id', Contact::getIdForURL($item["owner-link"], 0, false, $default));
  1234. if (Contact::isBlocked($item["owner-id"])) {
  1235. logger('Contact '.$item["owner-id"].' is blocked, item '.$item["uri"].' will not be stored');
  1236. return 0;
  1237. }
  1238. if ($item['network'] == Protocol::PHANTOM) {
  1239. logger('Missing network. Called by: '.System::callstack(), LOGGER_DEBUG);
  1240. $item['network'] = Protocol::DFRN;
  1241. logger("Set network to " . $item["network"] . " for " . $item["uri"], LOGGER_DEBUG);
  1242. }
  1243. // Checking if there is already an item with the same guid
  1244. logger('Checking for an item for user '.$item['uid'].' on network '.$item['network'].' with the guid '.$item['guid'], LOGGER_DEBUG);
  1245. $condition = ['guid' => $item['guid'], 'network' => $item['network'], 'uid' => $item['uid']];
  1246. if (self::exists($condition)) {
  1247. logger('found item with guid '.$item['guid'].' for user '.$item['uid'].' on network '.$item['network'], LOGGER_DEBUG);
  1248. return 0;
  1249. }
  1250. // Check for hashtags in the body and repair or add hashtag links
  1251. self::setHashtags($item);
  1252. $item['thr-parent'] = $item['parent-uri'];
  1253. $notify_type = '';
  1254. $allow_cid = '';
  1255. $allow_gid = '';
  1256. $deny_cid = '';
  1257. $deny_gid = '';
  1258. if ($item['parent-uri'] === $item['uri']) {
  1259. $parent_id = 0;
  1260. $parent_deleted = 0;
  1261. $allow_cid = $item['allow_cid'];
  1262. $allow_gid = $item['allow_gid'];
  1263. $deny_cid = $item['deny_cid'];
  1264. $deny_gid = $item['deny_gid'];
  1265. $notify_type = 'wall-new';
  1266. } else {
  1267. // find the parent and snarf the item id and ACLs
  1268. // and anything else we need to inherit
  1269. $fields = ['uri', 'parent-uri', 'id', 'deleted',
  1270. 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid',
  1271. 'wall', 'private', 'forum_mode', 'origin'];
  1272. $condition = ['uri' => $item['parent-uri'], 'uid' => $item['uid']];
  1273. $params = ['order' => ['id' => false]];
  1274. $parent = self::selectFirst($fields, $condition, $params);
  1275. if (DBA::isResult($parent)) {
  1276. // is the new message multi-level threaded?
  1277. // even though we don't support it now, preserve the info
  1278. // and re-attach to the conversation parent.
  1279. if ($parent['uri'] != $parent['parent-uri']) {
  1280. $item['parent-uri'] = $parent['parent-uri'];
  1281. $condition = ['uri' => $item['parent-uri'],
  1282. 'parent-uri' => $item['parent-uri'],
  1283. 'uid' => $item['uid']];
  1284. $params = ['order' => ['id' => false]];
  1285. $toplevel_parent = self::selectFirst($fields, $condition, $params);
  1286. if (DBA::isResult($toplevel_parent)) {
  1287. $parent = $toplevel_parent;
  1288. }
  1289. }
  1290. $parent_id = $parent['id'];
  1291. $parent_deleted = $parent['deleted'];
  1292. $allow_cid = $parent['allow_cid'];
  1293. $allow_gid = $parent['allow_gid'];
  1294. $deny_cid = $parent['deny_cid'];
  1295. $deny_gid = $parent['deny_gid'];
  1296. $item['wall'] = $parent['wall'];
  1297. $notify_type = 'comment-new';
  1298. /*
  1299. * If the parent is private, force privacy for the entire conversation
  1300. * This differs from the above settings as it subtly allows comments from
  1301. * email correspondents to be private even if the overall thread is not.
  1302. */
  1303. if ($parent['private']) {
  1304. $item['private'] = $parent['private'];
  1305. }
  1306. /*
  1307. * Edge case. We host a public forum that was originally posted to privately.
  1308. * The original author commented, but as this is a comment, the permissions
  1309. * weren't fixed up so it will still show the comment as private unless we fix it here.
  1310. */
  1311. if ((intval($parent['forum_mode']) == 1) && $parent['private']) {
  1312. $item['private'] = 0;
  1313. }
  1314. // If its a post from myself then tag the thread as "mention"
  1315. logger("Checking if parent ".$parent_id." has to be tagged as mention for user ".$item['uid'], LOGGER_DEBUG);
  1316. $user = DBA::selectFirst('user', ['nickname'], ['uid' => $item['uid']]);
  1317. if (DBA::isResult($user)) {
  1318. $self = normalise_link(System::baseUrl() . '/profile/' . $user['nickname']);
  1319. $self_id = Contact::getIdForURL($self, 0, true);
  1320. logger("'myself' is ".$self_id." for parent ".$parent_id." checking against ".$item['author-id']." and ".$item['owner-id'], LOGGER_DEBUG);
  1321. if (($item['author-id'] == $self_id) || ($item['owner-id'] == $self_id)) {
  1322. DBA::update('thread', ['mention' => true], ['iid' => $parent_id]);
  1323. logger("tagged thread ".$parent_id." as mention for user ".$self, LOGGER_DEBUG);
  1324. }
  1325. }
  1326. } else {
  1327. /*
  1328. * Allow one to see reply tweets from status.net even when
  1329. * we don't have or can't see the original post.
  1330. */
  1331. if ($force_parent) {
  1332. logger('$force_parent=true, reply converted to top-level post.');
  1333. $parent_id = 0;
  1334. $item['parent-uri'] = $item['uri'];
  1335. $item['gravity'] = GRAVITY_PARENT;
  1336. } else {
  1337. logger('item parent '.$item['parent-uri'].' for '.$item['uid'].' was not found - ignoring item');
  1338. return 0;
  1339. }
  1340. $parent_deleted = 0;
  1341. }
  1342. }
  1343. $item['parent-uri-id'] = ItemURI::getIdByURI($item['parent-uri']);
  1344. $item['thr-parent-id'] = ItemURI::getIdByURI($item['thr-parent']);
  1345. $condition = ["`uri` = ? AND `network` IN (?, ?) AND `uid` = ?",
  1346. $item['uri'], $item['network'], Protocol::DFRN, $item['uid']];
  1347. if (self::exists($condition)) {
  1348. logger('duplicated item with the same uri found. '.print_r($item,true));
  1349. return 0;
  1350. }
  1351. // On Friendica and Diaspora the GUID is unique
  1352. if (in_array($item['network'], [Protocol::DFRN, Protocol::DIASPORA])) {
  1353. $condition = ['guid' => $item['guid'], 'uid' => $item['uid']];
  1354. if (self::exists($condition)) {
  1355. logger('duplicated item with the same guid found. '.print_r($item,true));
  1356. return 0;
  1357. }
  1358. } else {
  1359. // Check for an existing post with the same content. There seems to be a problem with OStatus.
  1360. $condition = ["`body` = ? AND `network` = ? AND `created` = ? AND `contact-id` = ? AND `uid` = ?",
  1361. $item['body'], $item['network'], $item['created'], $item['contact-id'], $item['uid']];
  1362. if (self::exists($condition)) {
  1363. logger('duplicated item with the same body found. '.print_r($item,true));
  1364. return 0;
  1365. }
  1366. }
  1367. // Is this item available in the global items (with uid=0)?
  1368. if ($item["uid"] == 0) {
  1369. $item["global"] = true;
  1370. // Set the global flag on all items if this was a global item entry
  1371. self::update(['global' => true], ['uri' => $item["uri"]]);
  1372. } else {
  1373. $item["global"] = self::exists(['uid' => 0, 'uri' => $item["uri"]]);
  1374. }
  1375. // ACL settings
  1376. if (strlen($allow_cid) || strlen($allow_gid) || strlen($deny_cid) || strlen($deny_gid)) {
  1377. $private = 1;
  1378. } else {
  1379. $private = $item['private'];
  1380. }
  1381. $item["allow_cid"] = $allow_cid;
  1382. $item["allow_gid"] = $allow_gid;
  1383. $item["deny_cid"] = $deny_cid;
  1384. $item["deny_gid"] = $deny_gid;
  1385. $item["private"] = $private;
  1386. $item["deleted"] = $parent_deleted;
  1387. // Fill the cache field
  1388. put_item_in_cache($item);
  1389. if ($notify) {
  1390. $item['edit'] = false;
  1391. $item['parent'] = $parent_id;
  1392. Addon::callHooks('post_local', $item);
  1393. unset($item['edit']);
  1394. unset($item['parent']);
  1395. } else {
  1396. Addon::callHooks('post_remote', $item);
  1397. }
  1398. // This array field is used to trigger some automatic reactions
  1399. // It is mainly used in the "post_local" hook.
  1400. unset($item['api_source']);
  1401. if (x($item, 'cancel')) {
  1402. logger('post cancelled by addon.');
  1403. return 0;
  1404. }
  1405. /*
  1406. * Check for already added items.
  1407. * There is a timing issue here that sometimes creates double postings.
  1408. * An unique index would help - but the limitations of MySQL (maximum size of index values) prevent this.
  1409. */
  1410. if ($item["uid"] == 0) {
  1411. if (self::exists(['uri' => trim($item['uri']), 'uid' => 0])) {
  1412. logger('Global item already stored. URI: '.$item['uri'].' on network '.$item['network'], LOGGER_DEBUG);
  1413. return 0;
  1414. }
  1415. }
  1416. logger('' . print_r($item,true), LOGGER_DATA);
  1417. if (array_key_exists('tag', $item)) {
  1418. $tags = $item['tag'];
  1419. unset($item['tag']);
  1420. } else {
  1421. $tags = '';
  1422. }
  1423. if (array_key_exists('file', $item)) {
  1424. $files = $item['file'];
  1425. unset($item['file']);
  1426. } else {
  1427. $files = '';
  1428. }
  1429. // Creates or assigns the permission set
  1430. $item['psid'] = PermissionSet::fetchIDForPost($item);
  1431. // We are doing this outside of the transaction to avoid timing problems
  1432. if (!self::insertActivity($item)) {
  1433. self::insertContent($item);
  1434. }
  1435. $delivery_data = ['postopts' => defaults($item, 'postopts', ''),
  1436. 'inform' => defaults($item, 'inform', '')];
  1437. unset($item['postopts']);
  1438. unset($item['inform']);
  1439. // These fields aren't stored anymore in the item table, they are fetched upon request
  1440. unset($item['author-link']);
  1441. unset($item['author-name']);
  1442. unset($item['author-avatar']);
  1443. unset($item['owner-link']);
  1444. unset($item['owner-name']);
  1445. unset($item['owner-avatar']);
  1446. DBA::transaction();
  1447. $ret = DBA::insert('item', $item);
  1448. // When the item was successfully stored we fetch the ID of the item.
  1449. if (DBA::isResult($ret)) {
  1450. $current_post = DBA::lastInsertId();
  1451. } else {
  1452. // This can happen - for example - if there are locking timeouts.
  1453. DBA::rollback();
  1454. // Store the data into a spool file so that we can try again later.
  1455. // At first we restore the Diaspora signature that we removed above.
  1456. if (isset($encoded_signature)) {
  1457. $item['dsprsig'] = $encoded_signature;
  1458. }
  1459. // Now we store the data in the spool directory
  1460. // We use "microtime" to keep the arrival order and "mt_rand" to avoid duplicates
  1461. $file = 'item-'.round(microtime(true) * 10000).'-'.mt_rand().'.msg';
  1462. $spoolpath = get_spoolpath();
  1463. if ($spoolpath != "") {
  1464. $spool = $spoolpath.'/'.$file;
  1465. // Ensure to have the removed data from above again in the item array
  1466. $item = array_merge($item, $delivery_data);
  1467. file_put_contents($spool, json_encode($item));
  1468. logger("Item wasn't stored - Item was spooled into file ".$file, LOGGER_DEBUG);
  1469. }
  1470. return 0;
  1471. }
  1472. if ($current_post == 0) {
  1473. // This is one of these error messages that never should occur.
  1474. logger("couldn't find created item - we better quit now.");
  1475. DBA::rollback();
  1476. return 0;
  1477. }
  1478. // How much entries have we created?
  1479. // We wouldn't need this query when we could use an unique index - but MySQL has length problems with them.
  1480. $entries = DBA::count('item', ['uri' => $item['uri'], 'uid' => $item['uid'], 'network' => $item['network']]);
  1481. if ($entries > 1) {
  1482. // There are duplicates. We delete our just created entry.
  1483. logger('Duplicated post occurred. uri = ' . $item['uri'] . ' uid = ' . $item['uid']);
  1484. // Yes, we could do a rollback here - but we are having many users with MyISAM.
  1485. DBA::delete('item', ['id' => $current_post]);
  1486. DBA::commit();
  1487. return 0;
  1488. } elseif ($entries == 0) {
  1489. // This really should never happen since we quit earlier if there were problems.
  1490. logger("Something is terribly wrong. We haven't found our created entry.");
  1491. DBA::rollback();
  1492. return 0;
  1493. }
  1494. logger('created item '.$current_post);
  1495. self::updateContact($item);
  1496. if (!$parent_id || ($item['parent-uri'] === $item['uri'])) {
  1497. $parent_id = $current_post;
  1498. }
  1499. // Set parent id
  1500. self::update(['parent' => $parent_id], ['id' => $current_post]);
  1501. $item['id'] = $current_post;
  1502. $item['parent'] = $parent_id;
  1503. // update the commented timestamp on the parent
  1504. // Only update "commented" if it is really a comment
  1505. if (($item['gravity'] != GRAVITY_ACTIVITY) || !Config::get("system", "like_no_comment")) {
  1506. self::update(['commented' => DateTimeFormat::utcNow(), 'changed' => DateTimeFormat::utcNow()], ['id' => $parent_id]);
  1507. } else {
  1508. self::update(['changed' => DateTimeFormat::utcNow()], ['id' => $parent_id]);
  1509. }
  1510. if ($dsprsig) {
  1511. /*
  1512. * Friendica servers lower than 3.4.3-2 had double encoded the signature ...
  1513. * We can check for this condition when we decode and encode the stuff again.
  1514. */
  1515. if (base64_encode(base64_decode(base64_decode($dsprsig->signature))) == base64_decode($dsprsig->signature)) {
  1516. $dsprsig->signature = base64_decode($dsprsig->signature);
  1517. logger("Repaired double encoded signature from handle ".$dsprsig->signer, LOGGER_DEBUG);
  1518. }
  1519. DBA::insert('sign', ['iid' => $current_post, 'signed_text' => $dsprsig->signed_text,
  1520. 'signature' => $dsprsig->signature, 'signer' => $dsprsig->signer]);
  1521. }
  1522. if (!empty($diaspora_signed_text)) {
  1523. // Formerly we stored the signed text, the signature and the author in different fields.
  1524. // We now store the raw data so that we are more flexible.
  1525. DBA::insert('sign', ['iid' => $current_post, 'signed_text' => $diaspora_signed_text]);
  1526. }
  1527. $deleted = self::tagDeliver($item['uid'], $current_post);
  1528. /*
  1529. * current post can be deleted if is for a community page and no mention are
  1530. * in it.
  1531. */
  1532. if (!$deleted && !$dontcache) {
  1533. $posted_item = self::selectFirst(self::ITEM_FIELDLIST, ['id' => $current_post]);
  1534. if (DBA::isResult($posted_item)) {
  1535. if ($notify) {
  1536. Addon::callHooks('post_local_end', $posted_item);
  1537. } else {
  1538. Addon::callHooks('post_remote_end', $posted_item);
  1539. }
  1540. } else {
  1541. logger('new item not found in DB, id ' . $current_post);
  1542. }
  1543. }
  1544. if ($item['parent-uri'] === $item['uri']) {
  1545. self::addThread($current_post);
  1546. } else {
  1547. self::updateThread($parent_id);
  1548. }
  1549. $delivery_data['iid'] = $current_post;
  1550. self::insertDeliveryData($delivery_data);
  1551. DBA::commit();
  1552. /*
  1553. * Due to deadlock issues with the "term" table we are doing these steps after the commit.
  1554. * This is not perfect - but a workable solution until we found the reason for the problem.
  1555. */
  1556. if (!empty($tags)) {
  1557. Term::insertFromTagFieldByItemId($current_post, $tags);
  1558. }
  1559. if (!empty($files)) {
  1560. Term::insertFromFileFieldByItemId($current_post, $files);
  1561. }
  1562. if ($item['parent-uri'] === $item['uri']) {
  1563. self::addShadow($current_post);
  1564. } else {
  1565. self::addShadowPost($current_post);
  1566. }
  1567. check_user_notification($current_post);
  1568. if ($notify) {
  1569. Worker::add(['priority' => $priority, 'dont_fork' => true], "Notifier", $notify_type, $current_post);
  1570. } elseif (!empty($parent) && $parent['origin']) {
  1571. Worker::add(['priority' => PRIORITY_HIGH, 'dont_fork' => true], "Notifier", "comment-import", $current_post);
  1572. }
  1573. return $current_post;
  1574. }
  1575. /**
  1576. * @brief Insert a new item delivery data entry
  1577. *
  1578. * @param array $item The item fields that are to be inserted
  1579. */
  1580. private static function insertDeliveryData($delivery_data)
  1581. {
  1582. if (empty($delivery_data['iid']) || (empty($delivery_data['postopts']) && empty($delivery_data['inform']))) {
  1583. return;
  1584. }
  1585. DBA::insert('item-delivery-data', $delivery_data);
  1586. }
  1587. /**
  1588. * @brief Update an existing item delivery data entry
  1589. *
  1590. * @param integer $id The item id that is to be updated
  1591. * @param array $item The item fields that are to be inserted
  1592. */
  1593. private static function updateDeliveryData($id, $delivery_data)
  1594. {
  1595. if (empty($id) || (empty($delivery_data['postopts']) && empty($delivery_data['inform']))) {
  1596. return;
  1597. }
  1598. DBA::update('item-delivery-data', $delivery_data, ['iid' => $id], true);
  1599. }
  1600. /**
  1601. * @brief Insert a new item content entry
  1602. *
  1603. * @param array $item The item fields that are to be inserted
  1604. */
  1605. private static function insertActivity(&$item)
  1606. {
  1607. $activity_index = self::activityToIndex($item['verb']);
  1608. if ($activity_index < 0) {
  1609. return false;
  1610. }
  1611. $fields = ['uri' => $item['uri'], 'activity' => $activity_index,
  1612. 'uri-hash' => $item['uri-hash'], 'uri-id' => $item['uri-id']];
  1613. // We just remove everything that is content
  1614. foreach (array_merge(self::CONTENT_FIELDLIST, self::MIXED_CONTENT_FIELDLIST) as $field) {
  1615. unset($item[$field]);
  1616. }
  1617. // To avoid timing problems, we are using locks.
  1618. $locked = Lock::acquire('item_insert_activity');
  1619. if (!$locked) {
  1620. logger("Couldn't acquire lock for URI " . $item['uri'] . " - proceeding anyway.");
  1621. }
  1622. // Do we already have this content?
  1623. $item_activity = DBA::selectFirst('item-activity', ['id'], ['uri-hash' => $item['uri-hash']]);
  1624. if (DBA::isResult($item_activity)) {
  1625. $item['iaid'] = $item_activity['id'];
  1626. logger('Fetched activity for URI ' . $item['uri'] . ' (' . $item['iaid'] . ')');
  1627. } elseif (DBA::insert('item-activity', $fields)) {
  1628. $item['iaid'] = DBA::lastInsertId();
  1629. logger('Inserted activity for URI ' . $item['uri'] . ' (' . $item['iaid'] . ')');
  1630. } else {
  1631. // This shouldn't happen.
  1632. logger('Could not insert activity for URI ' . $item['uri'] . ' - should not happen');
  1633. Lock::release('item_insert_activity');
  1634. return false;
  1635. }
  1636. if ($locked) {
  1637. Lock::release('item_insert_activity');
  1638. }
  1639. return true;
  1640. }
  1641. /**
  1642. * @brief Insert a new item content entry
  1643. *
  1644. * @param array $item The item fields that are to be inserted
  1645. */
  1646. private static function insertContent(&$item)
  1647. {
  1648. $fields = ['uri' => $item['uri'], 'uri-plink-hash' => $item['uri-hash'],
  1649. 'uri-id' => $item['uri-id']];
  1650. foreach (array_merge(self::CONTENT_FIELDLIST, self::MIXED_CONTENT_FIELDLIST) as $field) {
  1651. if (isset($item[$field])) {
  1652. $fields[$field] = $item[$field];
  1653. unset($item[$field]);
  1654. }
  1655. }
  1656. // To avoid timing problems, we are using locks.
  1657. $locked = Lock::acquire('item_insert_content');
  1658. if (!$locked) {
  1659. logger("Couldn't acquire lock for URI " . $item['uri'] . " - proceeding anyway.");
  1660. }
  1661. // Do we already have this content?
  1662. $item_content = DBA::selectFirst('item-content', ['id'], ['uri-plink-hash' => $item['uri-hash']]);
  1663. if (DBA::isResult($item_content)) {
  1664. $item['icid'] = $item_content['id'];
  1665. logger('Fetched content for URI ' . $item['uri'] . ' (' . $item['icid'] . ')');
  1666. } elseif (DBA::insert('item-content', $fields)) {
  1667. $item['icid'] = DBA::lastInsertId();
  1668. logger('Inserted content for URI ' . $item['uri'] . ' (' . $item['icid'] . ')');
  1669. } else {
  1670. // This shouldn't happen.
  1671. logger('Could not insert content for URI ' . $item['uri'] . ' - should not happen');
  1672. }
  1673. if ($locked) {
  1674. Lock::release('item_insert_content');
  1675. }
  1676. }
  1677. /**
  1678. * @brief Update existing item content entries
  1679. *
  1680. * @param array $item The item fields that are to be changed
  1681. * @param array $condition The condition for finding the item content entries
  1682. */
  1683. private static function updateActivity($item, $condition)
  1684. {
  1685. if (empty($item['verb'])) {
  1686. return false;
  1687. }
  1688. $activity_index = self::activityToIndex($item['verb']);
  1689. if ($activity_index < 0) {
  1690. return false;
  1691. }
  1692. $fields = ['activity' => $activity_index];
  1693. logger('Update activity for ' . json_encode($condition));
  1694. DBA::update('item-activity', $fields, $condition, true);
  1695. return true;
  1696. }
  1697. /**
  1698. * @brief Update existing item content entries
  1699. *
  1700. * @param array $item The item fields that are to be changed
  1701. * @param array $condition The condition for finding the item content entries
  1702. */
  1703. private static function updateContent($item, $condition)
  1704. {
  1705. // We have to select only the fields from the "item-content" table
  1706. $fields = [];
  1707. foreach (array_merge(self::CONTENT_FIELDLIST, self::MIXED_CONTENT_FIELDLIST) as $field) {
  1708. if (isset($item[$field])) {
  1709. $fields[$field] = $item[$field];
  1710. }
  1711. }
  1712. if (empty($fields)) {
  1713. // when there are no fields at all, just use the condition
  1714. // This is to ensure that we always store content.
  1715. $fields = $condition;
  1716. }
  1717. logger('Update content for ' . json_encode($condition));
  1718. DBA::update('item-content', $fields, $condition, true);
  1719. }
  1720. /**
  1721. * @brief Distributes public items to the receivers
  1722. *
  1723. * @param integer $itemid Item ID that should be added
  1724. * @param string $signed_text Original text (for Diaspora signatures), JSON encoded.
  1725. */
  1726. public static function distribute($itemid, $signed_text = '')
  1727. {
  1728. $condition = ["`id` IN (SELECT `parent` FROM `item` WHERE `id` = ?)", $itemid];
  1729. $parent = self::selectFirst(['owner-id'], $condition);
  1730. if (!DBA::isResult($parent)) {
  1731. return;
  1732. }
  1733. // Only distribute public items from native networks
  1734. $condition = ['id' => $itemid, 'uid' => 0,
  1735. 'network' => [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, ""],
  1736. 'visible' => true, 'deleted' => false, 'moderated' => false, 'private' => false];
  1737. $item = self::selectFirst(self::ITEM_FIELDLIST, ['id' => $itemid]);
  1738. if (!DBA::isResult($item)) {
  1739. return;
  1740. }
  1741. $origin = $item['origin'];
  1742. unset($item['id']);
  1743. unset($item['parent']);
  1744. unset($item['mention']);
  1745. unset($item['wall']);
  1746. unset($item['origin']);
  1747. unset($item['starred']);
  1748. $users = [];
  1749. /// @todo add a field "pcid" in the contact table that referrs to the public contact id.
  1750. $owner = DBA::selectFirst('contact', ['url', 'nurl', 'alias'], ['id' => $parent['owner-id']]);
  1751. if (!DBA::isResult($owner)) {
  1752. return;
  1753. }
  1754. $condition = ['nurl' => $owner['nurl'], 'rel' => [Contact::SHARING, Contact::FRIEND]];
  1755. $contacts = DBA::select('contact', ['uid'], $condition);
  1756. while ($contact = DBA::fetch($contacts)) {
  1757. if ($contact['uid'] == 0) {
  1758. continue;
  1759. }
  1760. $users[$contact['uid']] = $contact['uid'];
  1761. }
  1762. DBA::close($contacts);
  1763. $condition = ['alias' => $owner['url'], 'rel' => [Contact::SHARING, Contact::FRIEND]];
  1764. $contacts = DBA::select('contact', ['uid'], $condition);
  1765. while ($contact = DBA::fetch($contacts)) {
  1766. if ($contact['uid'] == 0) {
  1767. continue;
  1768. }
  1769. $users[$contact['uid']] = $contact['uid'];
  1770. }
  1771. DBA::close($contacts);
  1772. if (!empty($owner['alias'])) {
  1773. $condition = ['url' => $owner['alias'], 'rel' => [Contact::SHARING, Contact::FRIEND]];
  1774. $contacts = DBA::select('contact', ['uid'], $condition);
  1775. while ($contact = DBA::fetch($contacts)) {
  1776. if ($contact['uid'] == 0) {
  1777. continue;
  1778. }
  1779. $users[$contact['uid']] = $contact['uid'];
  1780. }
  1781. DBA::close($contacts);
  1782. }
  1783. $origin_uid = 0;
  1784. if ($item['uri'] != $item['parent-uri']) {
  1785. $parents = self::select(['uid', 'origin'], ["`uri` = ? AND `uid` != 0", $item['parent-uri']]);
  1786. while ($parent = self::fetch($parents)) {
  1787. $users[$parent['uid']] = $parent['uid'];
  1788. if ($parent['origin'] && !$origin) {
  1789. $origin_uid = $parent['uid'];
  1790. }
  1791. }
  1792. }
  1793. foreach ($users as $uid) {
  1794. if ($origin_uid == $uid) {
  1795. $item['diaspora_signed_text'] = $signed_text;
  1796. }
  1797. self::storeForUser($itemid, $item, $uid);
  1798. }
  1799. }
  1800. /**
  1801. * @brief Store public items for the receivers
  1802. *
  1803. * @param integer $itemid Item ID that should be added
  1804. * @param array $item The item entry that will be stored
  1805. * @param integer $uid The user that will receive the item entry
  1806. */
  1807. private static function storeForUser($itemid, $item, $uid)
  1808. {
  1809. $item['uid'] = $uid;
  1810. $item['origin'] = 0;
  1811. $item['wall'] = 0;
  1812. if ($item['uri'] == $item['parent-uri']) {
  1813. $item['contact-id'] = Contact::getIdForURL($item['owner-link'], $uid);
  1814. } else {
  1815. $item['contact-id'] = Contact::getIdForURL($item['author-link'], $uid);
  1816. }
  1817. if (empty($item['contact-id'])) {
  1818. $self = DBA::selectFirst('contact', ['id'], ['self' => true, 'uid' => $uid]);
  1819. if (!DBA::isResult($self)) {
  1820. return;
  1821. }
  1822. $item['contact-id'] = $self['id'];
  1823. }
  1824. /// @todo Handling of "event-id"
  1825. $notify = false;
  1826. if ($item['uri'] == $item['parent-uri']) {
  1827. $contact = DBA::selectFirst('contact', [], ['id' => $item['contact-id'], 'self' => false]);
  1828. if (DBA::isResult($contact)) {
  1829. $notify = self::isRemoteSelf($contact, $item);
  1830. }
  1831. }
  1832. $distributed = self::insert($item, false, $notify, true);
  1833. if (!$distributed) {
  1834. logger("Distributed public item " . $itemid . " for user " . $uid . " wasn't stored", LOGGER_DEBUG);
  1835. } else {
  1836. logger("Distributed public item " . $itemid . " for user " . $uid . " with id " . $distributed, LOGGER_DEBUG);
  1837. }
  1838. }
  1839. /**
  1840. * @brief Add a shadow entry for a given item id that is a thread starter
  1841. *
  1842. * We store every public item entry additionally with the user id "0".
  1843. * This is used for the community page and for the search.
  1844. * It is planned that in the future we will store public item entries only once.
  1845. *
  1846. * @param integer $itemid Item ID that should be added
  1847. */
  1848. public static function addShadow($itemid)
  1849. {
  1850. $fields = ['uid', 'private', 'moderated', 'visible', 'deleted', 'network', 'uri'];
  1851. $condition = ['id' => $itemid, 'parent' => [0, $itemid]];
  1852. $item = self::selectFirst($fields, $condition);
  1853. if (!DBA::isResult($item)) {
  1854. return;
  1855. }
  1856. // is it already a copy?
  1857. if (($itemid == 0) || ($item['uid'] == 0)) {
  1858. return;
  1859. }
  1860. // Is it a visible public post?
  1861. if (!$item["visible"] || $item["deleted"] || $item["moderated"] || $item["private"]) {
  1862. return;
  1863. }
  1864. // is it an entry from a connector? Only add an entry for natively connected networks
  1865. if (!in_array($item["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, ""])) {
  1866. return;
  1867. }
  1868. if (self::exists(['uri' => $item['uri'], 'uid' => 0])) {
  1869. return;
  1870. }
  1871. $item = self::selectFirst(self::ITEM_FIELDLIST, ['id' => $itemid]);
  1872. if (DBA::isResult($item)) {
  1873. // Preparing public shadow (removing user specific data)
  1874. $item['uid'] = 0;
  1875. unset($item['id']);
  1876. unset($item['parent']);
  1877. unset($item['wall']);
  1878. unset($item['mention']);
  1879. unset($item['origin']);
  1880. unset($item['starred']);
  1881. unset($item['postopts']);
  1882. unset($item['inform']);
  1883. if ($item['uri'] == $item['parent-uri']) {
  1884. $item['contact-id'] = $item['owner-id'];
  1885. } else {
  1886. $item['contact-id'] = $item['author-id'];
  1887. }
  1888. $public_shadow = self::insert($item, false, false, true);
  1889. logger("Stored public shadow for thread ".$itemid." under id ".$public_shadow, LOGGER_DEBUG);
  1890. }
  1891. }
  1892. /**
  1893. * @brief Add a shadow entry for a given item id that is a comment
  1894. *
  1895. * This function does the same like the function above - but for comments
  1896. *
  1897. * @param integer $itemid Item ID that should be added
  1898. */
  1899. public static function addShadowPost($itemid)
  1900. {
  1901. $item = self::selectFirst(self::ITEM_FIELDLIST, ['id' => $itemid]);
  1902. if (!DBA::isResult($item)) {
  1903. return;
  1904. }
  1905. // Is it a toplevel post?
  1906. if ($item['id'] == $item['parent']) {
  1907. self::addShadow($itemid);
  1908. return;
  1909. }
  1910. // Is this a shadow entry?
  1911. if ($item['uid'] == 0) {
  1912. return;
  1913. }
  1914. // Is there a shadow parent?
  1915. if (!self::exists(['uri' => $item['parent-uri'], 'uid' => 0])) {
  1916. return;
  1917. }
  1918. // Is there already a shadow entry?
  1919. if (self::exists(['uri' => $item['uri'], 'uid' => 0])) {
  1920. return;
  1921. }
  1922. // Save "origin" and "parent" state
  1923. $origin = $item['origin'];
  1924. $parent = $item['parent'];
  1925. // Preparing public shadow (removing user specific data)
  1926. $item['uid'] = 0;
  1927. unset($item['id']);
  1928. unset($item['parent']);
  1929. unset($item['wall']);
  1930. unset($item['mention']);
  1931. unset($item['origin']);
  1932. unset($item['starred']);
  1933. unset($item['postopts']);
  1934. unset($item['inform']);
  1935. $item['contact-id'] = Contact::getIdForURL($item['author-link']);
  1936. $public_shadow = self::insert($item, false, false, true);
  1937. logger("Stored public shadow for comment ".$item['uri']." under id ".$public_shadow, LOGGER_DEBUG);
  1938. // If this was a comment to a Diaspora post we don't get our comment back.
  1939. // This means that we have to distribute the comment by ourselves.
  1940. if ($origin && self::exists(['id' => $parent, 'network' => Protocol::DIASPORA])) {
  1941. self::distribute($public_shadow);
  1942. }
  1943. }
  1944. /**
  1945. * Adds a language specification in a "language" element of given $arr.
  1946. * Expects "body" element to exist in $arr.
  1947. */
  1948. private static function addLanguageToItemArray(&$item)
  1949. {
  1950. $naked_body = BBCode::toPlaintext($item['body'], false);
  1951. $ld = new Text_LanguageDetect();
  1952. $ld->setNameMode(2);
  1953. $languages = $ld->detect($naked_body, 3);
  1954. if (is_array($languages)) {
  1955. $item['language'] = json_encode($languages);
  1956. }
  1957. }
  1958. /**
  1959. * @brief Creates an unique guid out of a given uri
  1960. *
  1961. * @param string $uri uri of an item entry
  1962. * @param string $host hostname for the GUID prefix
  1963. * @return string unique guid
  1964. */
  1965. public static function guidFromUri($uri, $host)
  1966. {
  1967. // Our regular guid routine is using this kind of prefix as well
  1968. // We have to avoid that different routines could accidentally create the same value
  1969. $parsed = parse_url($uri);
  1970. // We use a hash of the hostname as prefix for the guid
  1971. $guid_prefix = hash("crc32", $host);
  1972. // Remove the scheme to make sure that "https" and "http" doesn't make a difference
  1973. unset($parsed["scheme"]);
  1974. // Glue it together to be able to make a hash from it
  1975. $host_id = implode("/", $parsed);
  1976. // We could use any hash algorithm since it isn't a security issue
  1977. $host_hash = hash("ripemd128", $host_id);
  1978. return $guid_prefix.$host_hash;
  1979. }
  1980. /**
  1981. * generate an unique URI
  1982. *
  1983. * @param integer $uid User id
  1984. * @param string $guid An existing GUID (Otherwise it will be generated)
  1985. *
  1986. * @return string
  1987. */
  1988. public static function newURI($uid, $guid = "")
  1989. {
  1990. if ($guid == "") {
  1991. $guid = System::createUUID();
  1992. }
  1993. return self::getApp()->get_baseurl() . '/objects/' . $guid;
  1994. }
  1995. /**
  1996. * @brief Set "success_update" and "last-item" to the date of the last time we heard from this contact
  1997. *
  1998. * This can be used to filter for inactive contacts.
  1999. * Only do this for public postings to avoid privacy problems, since poco data is public.
  2000. * Don't set this value if it isn't from the owner (could be an author that we don't know)
  2001. *
  2002. * @param array $arr Contains the just posted item record
  2003. */
  2004. private static function updateContact($arr)
  2005. {
  2006. // Unarchive the author
  2007. $contact = DBA::selectFirst('contact', [], ['id' => $arr["author-id"]]);
  2008. if (DBA::isResult($contact)) {
  2009. Contact::unmarkForArchival($contact);
  2010. }
  2011. // Unarchive the contact if it's not our own contact
  2012. $contact = DBA::selectFirst('contact', [], ['id' => $arr["contact-id"], 'self' => false]);
  2013. if (DBA::isResult($contact)) {
  2014. Contact::unmarkForArchival($contact);
  2015. }
  2016. $update = (!$arr['private'] && ((defaults($arr, 'author-link', '') === defaults($arr, 'owner-link', '')) || ($arr["parent-uri"] === $arr["uri"])));
  2017. // Is it a forum? Then we don't care about the rules from above
  2018. if (!$update && ($arr["network"] == Protocol::DFRN) && ($arr["parent-uri"] === $arr["uri"])) {
  2019. if (DBA::exists('contact', ['id' => $arr['contact-id'], 'forum' => true])) {
  2020. $update = true;
  2021. }
  2022. }
  2023. if ($update) {
  2024. DBA::update('contact', ['success_update' => $arr['received'], 'last-item' => $arr['received']],
  2025. ['id' => $arr['contact-id']]);
  2026. }
  2027. // Now do the same for the system wide contacts with uid=0
  2028. if (!$arr['private']) {
  2029. DBA::update('contact', ['success_update' => $arr['received'], 'last-item' => $arr['received']],
  2030. ['id' => $arr['owner-id']]);
  2031. if ($arr['owner-id'] != $arr['author-id']) {
  2032. DBA::update('contact', ['success_update' => $arr['received'], 'last-item' => $arr['received']],
  2033. ['id' => $arr['author-id']]);
  2034. }
  2035. }
  2036. }
  2037. public static function setHashtags(&$item)
  2038. {
  2039. $tags = get_tags($item["body"]);
  2040. // No hashtags?
  2041. if (!count($tags)) {
  2042. return false;
  2043. }
  2044. // This sorting is important when there are hashtags that are part of other hashtags
  2045. // Otherwise there could be problems with hashtags like #test and #test2
  2046. rsort($tags);
  2047. $URLSearchString = "^\[\]";
  2048. // All hashtags should point to the home server if "local_tags" is activated
  2049. if (Config::get('system', 'local_tags')) {
  2050. $item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
  2051. "#[url=".System::baseUrl()."/search?tag=$2]$2[/url]", $item["body"]);
  2052. $item["tag"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
  2053. "#[url=".System::baseUrl()."/search?tag=$2]$2[/url]", $item["tag"]);
  2054. }
  2055. // mask hashtags inside of url, bookmarks and attachments to avoid urls in urls
  2056. $item["body"] = preg_replace_callback("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
  2057. function ($match) {
  2058. return ("[url=" . str_replace("#", "&num;", $match[1]) . "]" . str_replace("#", "&num;", $match[2]) . "[/url]");
  2059. }, $item["body"]);
  2060. $item["body"] = preg_replace_callback("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism",
  2061. function ($match) {
  2062. return ("[bookmark=" . str_replace("#", "&num;", $match[1]) . "]" . str_replace("#", "&num;", $match[2]) . "[/bookmark]");
  2063. }, $item["body"]);
  2064. $item["body"] = preg_replace_callback("/\[attachment (.*)\](.*?)\[\/attachment\]/ism",
  2065. function ($match) {
  2066. return ("[attachment " . str_replace("#", "&num;", $match[1]) . "]" . $match[2] . "[/attachment]");
  2067. }, $item["body"]);
  2068. // Repair recursive urls
  2069. $item["body"] = preg_replace("/&num;\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
  2070. "&num;$2", $item["body"]);
  2071. foreach ($tags as $tag) {
  2072. if ((strpos($tag, '#') !== 0) || strpos($tag, '[url=')) {
  2073. continue;
  2074. }
  2075. $basetag = str_replace('_',' ',substr($tag,1));
  2076. $newtag = '#[url=' . System::baseUrl() . '/search?tag=' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
  2077. $item["body"] = str_replace($tag, $newtag, $item["body"]);
  2078. if (!stristr($item["tag"], "/search?tag=" . $basetag . "]" . $basetag . "[/url]")) {
  2079. if (strlen($item["tag"])) {
  2080. $item["tag"] = ','.$item["tag"];
  2081. }
  2082. $item["tag"] = $newtag.$item["tag"];
  2083. }
  2084. }
  2085. // Convert back the masked hashtags
  2086. $item["body"] = str_replace("&num;", "#", $item["body"]);
  2087. }
  2088. public static function getGuidById($id)
  2089. {
  2090. $item = self::selectFirst(['guid'], ['id' => $id]);
  2091. if (DBA::isResult($item)) {
  2092. return $item['guid'];
  2093. } else {
  2094. return '';
  2095. }
  2096. }
  2097. /**
  2098. * This function is only used for the old Friendica app on Android that doesn't like paths with guid
  2099. * @param string $guid item guid
  2100. * @param int $uid user id
  2101. * @return array with id and nick of the item with the given guid
  2102. */
  2103. public static function getIdAndNickByGuid($guid, $uid = 0)
  2104. {
  2105. $nick = "";
  2106. $id = 0;
  2107. if ($uid == 0) {
  2108. $uid == local_user();
  2109. }
  2110. // Does the given user have this item?
  2111. if ($uid) {
  2112. $item = self::selectFirst(['id'], ['guid' => $guid, 'uid' => $uid]);
  2113. if (DBA::isResult($item)) {
  2114. $user = DBA::selectFirst('user', ['nickname'], ['uid' => $uid]);
  2115. if (!DBA::isResult($user)) {
  2116. return;
  2117. }
  2118. $id = $item['id'];
  2119. $nick = $user['nickname'];
  2120. }
  2121. }
  2122. // Or is it anywhere on the server?
  2123. if ($nick == "") {
  2124. $condition = ["`guid` = ? AND `uid` != 0", $guid];
  2125. $item = self::selectFirst(['id', 'uid'], $condition);
  2126. if (DBA::isResult($item)) {
  2127. $user = DBA::selectFirst('user', ['nickname'], ['uid' => $item['uid']]);
  2128. if (!DBA::isResult($user)) {
  2129. return;
  2130. }
  2131. $id = $item['id'];
  2132. $nick = $user['nickname'];
  2133. }
  2134. }
  2135. return ["nick" => $nick, "id" => $id];
  2136. }
  2137. /**
  2138. * look for mention tags and setup a second delivery chain for forum/community posts if appropriate
  2139. * @param int $uid
  2140. * @param int $item_id
  2141. * @return bool true if item was deleted, else false
  2142. */
  2143. private static function tagDeliver($uid, $item_id)
  2144. {
  2145. $mention = false;
  2146. $user = DBA::selectFirst('user', [], ['uid' => $uid]);
  2147. if (!DBA::isResult($user)) {
  2148. return;
  2149. }
  2150. $community_page = (($user['page-flags'] == Contact::PAGE_COMMUNITY) ? true : false);
  2151. $prvgroup = (($user['page-flags'] == Contact::PAGE_PRVGROUP) ? true : false);
  2152. $item = self::selectFirst(self::ITEM_FIELDLIST, ['id' => $item_id]);
  2153. if (!DBA::isResult($item)) {
  2154. return;
  2155. }
  2156. $link = normalise_link(System::baseUrl() . '/profile/' . $user['nickname']);
  2157. /*
  2158. * Diaspora uses their own hardwired link URL in @-tags
  2159. * instead of the one we supply with webfinger
  2160. */
  2161. $dlink = normalise_link(System::baseUrl() . '/u/' . $user['nickname']);
  2162. $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism', $item['body'], $matches, PREG_SET_ORDER);
  2163. if ($cnt) {
  2164. foreach ($matches as $mtch) {
  2165. if (link_compare($link, $mtch[1]) || link_compare($dlink, $mtch[1])) {
  2166. $mention = true;
  2167. logger('mention found: ' . $mtch[2]);
  2168. }
  2169. }
  2170. }
  2171. if (!$mention) {
  2172. if (($community_page || $prvgroup) &&
  2173. !$item['wall'] && !$item['origin'] && ($item['id'] == $item['parent'])) {
  2174. // mmh.. no mention.. community page or private group... no wall.. no origin.. top-post (not a comment)
  2175. // delete it!
  2176. logger("no-mention top-level post to community or private group. delete.");
  2177. DBA::delete('item', ['id' => $item_id]);
  2178. return true;
  2179. }
  2180. return;
  2181. }
  2182. $arr = ['item' => $item, 'user' => $user];
  2183. Addon::callHooks('tagged', $arr);
  2184. if (!$community_page && !$prvgroup) {
  2185. return;
  2186. }
  2187. /*
  2188. * tgroup delivery - setup a second delivery chain
  2189. * prevent delivery looping - only proceed
  2190. * if the message originated elsewhere and is a top-level post
  2191. */
  2192. if ($item['wall'] || $item['origin'] || ($item['id'] != $item['parent'])) {
  2193. return;
  2194. }
  2195. // now change this copy of the post to a forum head message and deliver to all the tgroup members
  2196. $self = DBA::selectFirst('contact', ['id', 'name', 'url', 'thumb'], ['uid' => $uid, 'self' => true]);
  2197. if (!DBA::isResult($self)) {
  2198. return;
  2199. }
  2200. $owner_id = Contact::getIdForURL($self['url']);
  2201. // also reset all the privacy bits to the forum default permissions
  2202. $private = ($user['allow_cid'] || $user['allow_gid'] || $user['deny_cid'] || $user['deny_gid']) ? 1 : 0;
  2203. $psid = PermissionSet::fetchIDForPost($user);
  2204. $forum_mode = ($prvgroup ? 2 : 1);
  2205. $fields = ['wall' => true, 'origin' => true, 'forum_mode' => $forum_mode, 'contact-id' => $self['id'],
  2206. 'owner-id' => $owner_id, 'private' => $private, 'psid' => $psid];
  2207. self::update($fields, ['id' => $item_id]);
  2208. self::updateThread($item_id);
  2209. Worker::add(['priority' => PRIORITY_HIGH, 'dont_fork' => true], 'Notifier', 'tgroup', $item_id);
  2210. }
  2211. public static function isRemoteSelf($contact, &$datarray)
  2212. {
  2213. $a = get_app();
  2214. if (!$contact['remote_self']) {
  2215. return false;
  2216. }
  2217. // Prevent the forwarding of posts that are forwarded
  2218. if (!empty($datarray["extid"]) && ($datarray["extid"] == Protocol::DFRN)) {
  2219. logger('Already forwarded', LOGGER_DEBUG);
  2220. return false;
  2221. }
  2222. // Prevent to forward already forwarded posts
  2223. if ($datarray["app"] == $a->get_hostname()) {
  2224. logger('Already forwarded (second test)', LOGGER_DEBUG);
  2225. return false;
  2226. }
  2227. // Only forward posts
  2228. if ($datarray["verb"] != ACTIVITY_POST) {
  2229. logger('No post', LOGGER_DEBUG);
  2230. return false;
  2231. }
  2232. if (($contact['network'] != Protocol::FEED) && $datarray['private']) {
  2233. logger('Not public', LOGGER_DEBUG);
  2234. return false;
  2235. }
  2236. $datarray2 = $datarray;
  2237. logger('remote-self start - Contact '.$contact['url'].' - '.$contact['remote_self'].' Item '.print_r($datarray, true), LOGGER_DEBUG);
  2238. if ($contact['remote_self'] == 2) {
  2239. $self = DBA::selectFirst('contact', ['id', 'name', 'url', 'thumb'],
  2240. ['uid' => $contact['uid'], 'self' => true]);
  2241. if (DBA::isResult($self)) {
  2242. $datarray['contact-id'] = $self["id"];
  2243. $datarray['owner-name'] = $self["name"];
  2244. $datarray['owner-link'] = $self["url"];
  2245. $datarray['owner-avatar'] = $self["thumb"];
  2246. $datarray['author-name'] = $datarray['owner-name'];
  2247. $datarray['author-link'] = $datarray['owner-link'];
  2248. $datarray['author-avatar'] = $datarray['owner-avatar'];
  2249. unset($datarray['created']);
  2250. unset($datarray['edited']);
  2251. unset($datarray['network']);
  2252. unset($datarray['owner-id']);
  2253. unset($datarray['author-id']);
  2254. }
  2255. if ($contact['network'] != Protocol::FEED) {
  2256. $datarray["guid"] = System::createUUID();
  2257. unset($datarray["plink"]);
  2258. $datarray["uri"] = self::newURI($contact['uid'], $datarray["guid"]);
  2259. $datarray["parent-uri"] = $datarray["uri"];
  2260. $datarray["thr-parent"] = $datarray["uri"];
  2261. $datarray["extid"] = Protocol::DFRN;
  2262. $urlpart = parse_url($datarray2['author-link']);
  2263. $datarray["app"] = $urlpart["host"];
  2264. } else {
  2265. $datarray['private'] = 0;
  2266. }
  2267. }
  2268. if ($contact['network'] != Protocol::FEED) {
  2269. // Store the original post
  2270. $result = self::insert($datarray2, false, false);
  2271. logger('remote-self post original item - Contact '.$contact['url'].' return '.$result.' Item '.print_r($datarray2, true), LOGGER_DEBUG);
  2272. } else {
  2273. $datarray["app"] = "Feed";
  2274. $result = true;
  2275. }
  2276. // Trigger automatic reactions for addons
  2277. $datarray['api_source'] = true;
  2278. // We have to tell the hooks who we are - this really should be improved
  2279. $_SESSION["authenticated"] = true;
  2280. $_SESSION["uid"] = $contact['uid'];
  2281. return $result;
  2282. }
  2283. /**
  2284. *
  2285. * @param string $s
  2286. * @param int $uid
  2287. * @param array $item
  2288. * @param int $cid
  2289. * @return string
  2290. */
  2291. public static function fixPrivatePhotos($s, $uid, $item = null, $cid = 0)
  2292. {
  2293. if (Config::get('system', 'disable_embedded')) {
  2294. return $s;
  2295. }
  2296. logger('check for photos', LOGGER_DEBUG);
  2297. $site = substr(System::baseUrl(), strpos(System::baseUrl(), '://'));
  2298. $orig_body = $s;
  2299. $new_body = '';
  2300. $img_start = strpos($orig_body, '[img');
  2301. $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
  2302. $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
  2303. while (($img_st_close !== false) && ($img_len !== false)) {
  2304. $img_st_close++; // make it point to AFTER the closing bracket
  2305. $image = substr($orig_body, $img_start + $img_st_close, $img_len);
  2306. logger('found photo ' . $image, LOGGER_DEBUG);
  2307. if (stristr($image, $site . '/photo/')) {
  2308. // Only embed locally hosted photos
  2309. $replace = false;
  2310. $i = basename($image);
  2311. $i = str_replace(['.jpg', '.png', '.gif'], ['', '', ''], $i);
  2312. $x = strpos($i, '-');
  2313. if ($x) {
  2314. $res = substr($i, $x + 1);
  2315. $i = substr($i, 0, $x);
  2316. $fields = ['data', 'type', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid'];
  2317. $photo = DBA::selectFirst('photo', $fields, ['resource-id' => $i, 'scale' => $res, 'uid' => $uid]);
  2318. if (DBA::isResult($photo)) {
  2319. /*
  2320. * Check to see if we should replace this photo link with an embedded image
  2321. * 1. No need to do so if the photo is public
  2322. * 2. If there's a contact-id provided, see if they're in the access list
  2323. * for the photo. If so, embed it.
  2324. * 3. Otherwise, if we have an item, see if the item permissions match the photo
  2325. * permissions, regardless of order but first check to see if they're an exact
  2326. * match to save some processing overhead.
  2327. */
  2328. if (self::hasPermissions($photo)) {
  2329. if ($cid) {
  2330. $recips = self::enumeratePermissions($photo);
  2331. if (in_array($cid, $recips)) {
  2332. $replace = true;
  2333. }
  2334. } elseif ($item) {
  2335. if (self::samePermissions($item, $photo)) {
  2336. $replace = true;
  2337. }
  2338. }
  2339. }
  2340. if ($replace) {
  2341. $data = $photo['data'];
  2342. $type = $photo['type'];
  2343. // If a custom width and height were specified, apply before embedding
  2344. if (preg_match("/\[img\=([0-9]*)x([0-9]*)\]/is", substr($orig_body, $img_start, $img_st_close), $match)) {
  2345. logger('scaling photo', LOGGER_DEBUG);
  2346. $width = intval($match[1]);
  2347. $height = intval($match[2]);
  2348. $Image = new Image($data, $type);
  2349. if ($Image->isValid()) {
  2350. $Image->scaleDown(max($width, $height));
  2351. $data = $Image->asString();
  2352. $type = $Image->getType();
  2353. }
  2354. }
  2355. logger('replacing photo', LOGGER_DEBUG);
  2356. $image = 'data:' . $type . ';base64,' . base64_encode($data);
  2357. logger('replaced: ' . $image, LOGGER_DATA);
  2358. }
  2359. }
  2360. }
  2361. }
  2362. $new_body = $new_body . substr($orig_body, 0, $img_start + $img_st_close) . $image . '[/img]';
  2363. $orig_body = substr($orig_body, $img_start + $img_st_close + $img_len + strlen('[/img]'));
  2364. if ($orig_body === false) {
  2365. $orig_body = '';
  2366. }
  2367. $img_start = strpos($orig_body, '[img');
  2368. $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
  2369. $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
  2370. }
  2371. $new_body = $new_body . $orig_body;
  2372. return $new_body;
  2373. }
  2374. private static function hasPermissions($obj)
  2375. {
  2376. return !empty($obj['allow_cid']) || !empty($obj['allow_gid']) ||
  2377. !empty($obj['deny_cid']) || !empty($obj['deny_gid']);
  2378. }
  2379. private static function samePermissions($obj1, $obj2)
  2380. {
  2381. // first part is easy. Check that these are exactly the same.
  2382. if (($obj1['allow_cid'] == $obj2['allow_cid'])
  2383. && ($obj1['allow_gid'] == $obj2['allow_gid'])
  2384. && ($obj1['deny_cid'] == $obj2['deny_cid'])
  2385. && ($obj1['deny_gid'] == $obj2['deny_gid'])) {
  2386. return true;
  2387. }
  2388. // This is harder. Parse all the permissions and compare the resulting set.
  2389. $recipients1 = self::enumeratePermissions($obj1);
  2390. $recipients2 = self::enumeratePermissions($obj2);
  2391. sort($recipients1);
  2392. sort($recipients2);
  2393. /// @TODO Comparison of arrays, maybe use array_diff_assoc() here?
  2394. return ($recipients1 == $recipients2);
  2395. }
  2396. // returns an array of contact-ids that are allowed to see this object
  2397. public static function enumeratePermissions($obj)
  2398. {
  2399. $allow_people = expand_acl($obj['allow_cid']);
  2400. $allow_groups = Group::expand(expand_acl($obj['allow_gid']));
  2401. $deny_people = expand_acl($obj['deny_cid']);
  2402. $deny_groups = Group::expand(expand_acl($obj['deny_gid']));
  2403. $recipients = array_unique(array_merge($allow_people, $allow_groups));
  2404. $deny = array_unique(array_merge($deny_people, $deny_groups));
  2405. $recipients = array_diff($recipients, $deny);
  2406. return $recipients;
  2407. }
  2408. public static function getFeedTags($item)
  2409. {
  2410. $ret = [];
  2411. $matches = false;
  2412. $cnt = preg_match_all('|\#\[url\=(.*?)\](.*?)\[\/url\]|', $item['tag'], $matches);
  2413. if ($cnt) {
  2414. for ($x = 0; $x < $cnt; $x ++) {
  2415. if ($matches[1][$x]) {
  2416. $ret[$matches[2][$x]] = ['#', $matches[1][$x], $matches[2][$x]];
  2417. }
  2418. }
  2419. }
  2420. $matches = false;
  2421. $cnt = preg_match_all('|\@\[url\=(.*?)\](.*?)\[\/url\]|', $item['tag'], $matches);
  2422. if ($cnt) {
  2423. for ($x = 0; $x < $cnt; $x ++) {
  2424. if ($matches[1][$x]) {
  2425. $ret[] = ['@', $matches[1][$x], $matches[2][$x]];
  2426. }
  2427. }
  2428. }
  2429. return $ret;
  2430. }
  2431. public static function expire($uid, $days, $network = "", $force = false)
  2432. {
  2433. if (!$uid || ($days < 1)) {
  2434. return;
  2435. }
  2436. $condition = ["`uid` = ? AND NOT `deleted` AND `id` = `parent` AND `gravity` = ?",
  2437. $uid, GRAVITY_PARENT];
  2438. /*
  2439. * $expire_network_only = save your own wall posts
  2440. * and just expire conversations started by others
  2441. */
  2442. $expire_network_only = PConfig::get($uid, 'expire', 'network_only', false);
  2443. if ($expire_network_only) {
  2444. $condition[0] .= " AND NOT `wall`";
  2445. }
  2446. if ($network != "") {
  2447. $condition[0] .= " AND `network` = ?";
  2448. $condition[] = $network;
  2449. /*
  2450. * There is an index "uid_network_received" but not "uid_network_created"
  2451. * This avoids the creation of another index just for one purpose.
  2452. * And it doesn't really matter wether to look at "received" or "created"
  2453. */
  2454. $condition[0] .= " AND `received` < UTC_TIMESTAMP() - INTERVAL ? DAY";
  2455. $condition[] = $days;
  2456. } else {
  2457. $condition[0] .= " AND `created` < UTC_TIMESTAMP() - INTERVAL ? DAY";
  2458. $condition[] = $days;
  2459. }
  2460. $items = self::select(['file', 'resource-id', 'starred', 'type', 'id', 'post-type'], $condition);
  2461. if (!DBA::isResult($items)) {
  2462. return;
  2463. }
  2464. $expire_items = PConfig::get($uid, 'expire', 'items', true);
  2465. // Forcing expiring of items - but not notes and marked items
  2466. if ($force) {
  2467. $expire_items = true;
  2468. }
  2469. $expire_notes = PConfig::get($uid, 'expire', 'notes', true);
  2470. $expire_starred = PConfig::get($uid, 'expire', 'starred', true);
  2471. $expire_photos = PConfig::get($uid, 'expire', 'photos', false);
  2472. $expired = 0;
  2473. while ($item = Item::fetch($items)) {
  2474. // don't expire filed items
  2475. if (strpos($item['file'], '[') !== false) {
  2476. continue;
  2477. }
  2478. // Only expire posts, not photos and photo comments
  2479. if (!$expire_photos && strlen($item['resource-id'])) {
  2480. continue;
  2481. } elseif (!$expire_starred && intval($item['starred'])) {
  2482. continue;
  2483. } elseif (!$expire_notes && (($item['type'] == 'note') || ($item['post-type'] == Item::PT_PERSONAL_NOTE))) {
  2484. continue;
  2485. } elseif (!$expire_items && ($item['type'] != 'note') && ($item['post-type'] != Item::PT_PERSONAL_NOTE)) {
  2486. continue;
  2487. }
  2488. self::deleteById($item['id'], PRIORITY_LOW);
  2489. ++$expired;
  2490. }
  2491. DBA::close($items);
  2492. logger('User ' . $uid . ": expired $expired items; expire items: $expire_items, expire notes: $expire_notes, expire starred: $expire_starred, expire photos: $expire_photos");
  2493. }
  2494. public static function firstPostDate($uid, $wall = false)
  2495. {
  2496. $condition = ['uid' => $uid, 'wall' => $wall, 'deleted' => false, 'visible' => true, 'moderated' => false];
  2497. $params = ['order' => ['created' => false]];
  2498. $thread = DBA::selectFirst('thread', ['created'], $condition, $params);
  2499. if (DBA::isResult($thread)) {
  2500. return substr(DateTimeFormat::local($thread['created']), 0, 10);
  2501. }
  2502. return false;
  2503. }
  2504. /**
  2505. * @brief add/remove activity to an item
  2506. *
  2507. * Toggle activities as like,dislike,attend of an item
  2508. *
  2509. * @param string $item_id
  2510. * @param string $verb
  2511. * Activity verb. One of
  2512. * like, unlike, dislike, undislike, attendyes, unattendyes,
  2513. * attendno, unattendno, attendmaybe, unattendmaybe
  2514. * @hook 'post_local_end'
  2515. * array $arr
  2516. * 'post_id' => ID of posted item
  2517. */
  2518. public static function performLike($item_id, $verb)
  2519. {
  2520. if (!local_user() && !remote_user()) {
  2521. return false;
  2522. }
  2523. switch ($verb) {
  2524. case 'like':
  2525. case 'unlike':
  2526. $activity = ACTIVITY_LIKE;
  2527. break;
  2528. case 'dislike':
  2529. case 'undislike':
  2530. $activity = ACTIVITY_DISLIKE;
  2531. break;
  2532. case 'attendyes':
  2533. case 'unattendyes':
  2534. $activity = ACTIVITY_ATTEND;
  2535. break;
  2536. case 'attendno':
  2537. case 'unattendno':
  2538. $activity = ACTIVITY_ATTENDNO;
  2539. break;
  2540. case 'attendmaybe':
  2541. case 'unattendmaybe':
  2542. $activity = ACTIVITY_ATTENDMAYBE;
  2543. break;
  2544. default:
  2545. logger('like: unknown verb ' . $verb . ' for item ' . $item_id);
  2546. return false;
  2547. }
  2548. // Enable activity toggling instead of on/off
  2549. $event_verb_flag = $activity === ACTIVITY_ATTEND || $activity === ACTIVITY_ATTENDNO || $activity === ACTIVITY_ATTENDMAYBE;
  2550. logger('like: verb ' . $verb . ' item ' . $item_id);
  2551. $item = self::selectFirst(self::ITEM_FIELDLIST, ['`id` = ? OR `uri` = ?', $item_id, $item_id]);
  2552. if (!DBA::isResult($item)) {
  2553. logger('like: unknown item ' . $item_id);
  2554. return false;
  2555. }
  2556. $item_uri = $item['uri'];
  2557. $uid = $item['uid'];
  2558. if (($uid == 0) && local_user()) {
  2559. $uid = local_user();
  2560. }
  2561. if (!can_write_wall($uid)) {
  2562. logger('like: unable to write on wall ' . $uid);
  2563. return false;
  2564. }
  2565. // Retrieves the local post owner
  2566. $owner_self_contact = DBA::selectFirst('contact', [], ['uid' => $uid, 'self' => true]);
  2567. if (!DBA::isResult($owner_self_contact)) {
  2568. logger('like: unknown owner ' . $uid);
  2569. return false;
  2570. }
  2571. // Retrieve the current logged in user's public contact
  2572. $author_id = public_contact();
  2573. $author_contact = DBA::selectFirst('contact', ['url'], ['id' => $author_id]);
  2574. if (!DBA::isResult($author_contact)) {
  2575. logger('like: unknown author ' . $author_id);
  2576. return false;
  2577. }
  2578. // Contact-id is the uid-dependant author contact
  2579. if (local_user() == $uid) {
  2580. $item_contact_id = $owner_self_contact['id'];
  2581. $item_contact = $owner_self_contact;
  2582. } else {
  2583. $item_contact_id = Contact::getIdForURL($author_contact['url'], $uid, true);
  2584. $item_contact = DBA::selectFirst('contact', [], ['id' => $item_contact_id]);
  2585. if (!DBA::isResult($item_contact)) {
  2586. logger('like: unknown item contact ' . $item_contact_id);
  2587. return false;
  2588. }
  2589. }
  2590. // Look for an existing verb row
  2591. // event participation are essentially radio toggles. If you make a subsequent choice,
  2592. // we need to eradicate your first choice.
  2593. if ($event_verb_flag) {
  2594. $verbs = [ACTIVITY_ATTEND, ACTIVITY_ATTENDNO, ACTIVITY_ATTENDMAYBE];
  2595. // Translate to the index based activity index
  2596. $activities = [];
  2597. foreach ($verbs as $verb) {
  2598. $activities[] = self::activityToIndex($verb);
  2599. }
  2600. } else {
  2601. $activities = self::activityToIndex($activity);
  2602. }
  2603. $condition = ['activity' => $activities, 'deleted' => false, 'gravity' => GRAVITY_ACTIVITY,
  2604. 'author-id' => $author_id, 'uid' => $item['uid'], 'thr-parent' => $item_uri];
  2605. $like_item = self::selectFirst(['id', 'guid', 'verb'], $condition);
  2606. // If it exists, mark it as deleted
  2607. if (DBA::isResult($like_item)) {
  2608. self::deleteById($like_item['id']);
  2609. if (!$event_verb_flag || $like_item['verb'] == $activity) {
  2610. return true;
  2611. }
  2612. }
  2613. // Verb is "un-something", just trying to delete existing entries
  2614. if (strpos($verb, 'un') === 0) {
  2615. return true;
  2616. }
  2617. $objtype = $item['resource-id'] ? ACTIVITY_OBJ_IMAGE : ACTIVITY_OBJ_NOTE;
  2618. $new_item = [
  2619. 'guid' => System::createUUID(),
  2620. 'uri' => self::newURI($item['uid']),
  2621. 'uid' => $item['uid'],
  2622. 'contact-id' => $item_contact_id,
  2623. 'wall' => $item['wall'],
  2624. 'origin' => 1,
  2625. 'network' => Protocol::DFRN,
  2626. 'gravity' => GRAVITY_ACTIVITY,
  2627. 'parent' => $item['id'],
  2628. 'parent-uri' => $item['uri'],
  2629. 'thr-parent' => $item['uri'],
  2630. 'owner-id' => $author_id,
  2631. 'author-id' => $author_id,
  2632. 'body' => $activity,
  2633. 'verb' => $activity,
  2634. 'object-type' => $objtype,
  2635. 'allow_cid' => $item['allow_cid'],
  2636. 'allow_gid' => $item['allow_gid'],
  2637. 'deny_cid' => $item['deny_cid'],
  2638. 'deny_gid' => $item['deny_gid'],
  2639. 'visible' => 1,
  2640. 'unseen' => 1,
  2641. ];
  2642. $new_item_id = self::insert($new_item);
  2643. // If the parent item isn't visible then set it to visible
  2644. if (!$item['visible']) {
  2645. self::update(['visible' => true], ['id' => $item['id']]);
  2646. }
  2647. // Save the author information for the like in case we need to relay to Diaspora
  2648. Diaspora::storeLikeSignature($item_contact, $new_item_id);
  2649. $new_item['id'] = $new_item_id;
  2650. Addon::callHooks('post_local_end', $new_item);
  2651. Worker::add(PRIORITY_HIGH, "Notifier", "like", $new_item_id);
  2652. return true;
  2653. }
  2654. private static function addThread($itemid, $onlyshadow = false)
  2655. {
  2656. $fields = ['uid', 'created', 'edited', 'commented', 'received', 'changed', 'wall', 'private', 'pubmail',
  2657. 'moderated', 'visible', 'starred', 'contact-id', 'post-type',
  2658. 'deleted', 'origin', 'forum_mode', 'mention', 'network', 'author-id', 'owner-id'];
  2659. $condition = ["`id` = ? AND (`parent` = ? OR `parent` = 0)", $itemid, $itemid];
  2660. $item = self::selectFirst($fields, $condition);
  2661. if (!DBA::isResult($item)) {
  2662. return;
  2663. }
  2664. $item['iid'] = $itemid;
  2665. if (!$onlyshadow) {
  2666. $result = DBA::insert('thread', $item);
  2667. logger("Add thread for item ".$itemid." - ".print_r($result, true), LOGGER_DEBUG);
  2668. }
  2669. }
  2670. private static function updateThread($itemid, $setmention = false)
  2671. {
  2672. $fields = ['uid', 'guid', 'created', 'edited', 'commented', 'received', 'changed', 'post-type',
  2673. 'wall', 'private', 'pubmail', 'moderated', 'visible', 'starred', 'contact-id',
  2674. 'deleted', 'origin', 'forum_mode', 'network', 'author-id', 'owner-id'];
  2675. $condition = ["`id` = ? AND (`parent` = ? OR `parent` = 0)", $itemid, $itemid];
  2676. $item = self::selectFirst($fields, $condition);
  2677. if (!DBA::isResult($item)) {
  2678. return;
  2679. }
  2680. if ($setmention) {
  2681. $item["mention"] = 1;
  2682. }
  2683. $sql = "";
  2684. $fields = [];
  2685. foreach ($item as $field => $data) {
  2686. if (!in_array($field, ["guid"])) {
  2687. $fields[$field] = $data;
  2688. }
  2689. }
  2690. $result = DBA::update('thread', $fields, ['iid' => $itemid]);
  2691. logger("Update thread for item ".$itemid." - guid ".$item["guid"]." - ".(int)$result, LOGGER_DEBUG);
  2692. }
  2693. private static function deleteThread($itemid, $itemuri = "")
  2694. {
  2695. $item = DBA::selectFirst('thread', ['uid'], ['iid' => $itemid]);
  2696. if (!DBA::isResult($item)) {
  2697. logger('No thread found for id '.$itemid, LOGGER_DEBUG);
  2698. return;
  2699. }
  2700. $result = DBA::delete('thread', ['iid' => $itemid], ['cascade' => false]);
  2701. logger("deleteThread: Deleted thread for item ".$itemid." - ".print_r($result, true), LOGGER_DEBUG);
  2702. if ($itemuri != "") {
  2703. $condition = ["`uri` = ? AND NOT `deleted` AND NOT (`uid` IN (?, 0))", $itemuri, $item["uid"]];
  2704. if (!self::exists($condition)) {
  2705. DBA::delete('item', ['uri' => $itemuri, 'uid' => 0]);
  2706. logger("deleteThread: Deleted shadow for item ".$itemuri, LOGGER_DEBUG);
  2707. }
  2708. }
  2709. }
  2710. }