Friendica Communications Platform (please note that this is a clone of the repository at github, issues are handled there) https://friendi.ca
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 
 
 

1025 строки
29 KiB

  1. <?php
  2. /**
  3. * @file src/Object/Post.php
  4. */
  5. namespace Friendica\Object;
  6. use Friendica\Content\ContactSelector;
  7. use Friendica\Content\Feature;
  8. use Friendica\Core\Addon;
  9. use Friendica\Core\Config;
  10. use Friendica\Core\Hook;
  11. use Friendica\Core\L10n;
  12. use Friendica\Core\Logger;
  13. use Friendica\Core\Protocol;
  14. use Friendica\Core\Renderer;
  15. use Friendica\Core\Session;
  16. use Friendica\Database\DBA;
  17. use Friendica\DI;
  18. use Friendica\Model\Contact;
  19. use Friendica\Model\Item;
  20. use Friendica\Model\Term;
  21. use Friendica\Model\User;
  22. use Friendica\Protocol\Activity;
  23. use Friendica\Util\Crypto;
  24. use Friendica\Util\DateTimeFormat;
  25. use Friendica\Util\Proxy as ProxyUtils;
  26. use Friendica\Util\Strings;
  27. use Friendica\Util\Temporal;
  28. /**
  29. * An item
  30. */
  31. class Post
  32. {
  33. private $data = [];
  34. private $template = null;
  35. private $available_templates = [
  36. 'wall' => 'wall_thread.tpl',
  37. 'wall2wall' => 'wallwall_thread.tpl'
  38. ];
  39. private $comment_box_template = 'comment_item.tpl';
  40. private $toplevel = false;
  41. private $writable = false;
  42. /**
  43. * @var Post[]
  44. */
  45. private $children = [];
  46. private $parent = null;
  47. /**
  48. * @var Thread
  49. */
  50. private $thread = null;
  51. private $redirect_url = null;
  52. private $owner_url = '';
  53. private $owner_photo = '';
  54. private $owner_name = '';
  55. private $wall_to_wall = false;
  56. private $threaded = false;
  57. private $visiting = false;
  58. /**
  59. * Constructor
  60. *
  61. * @param array $data data array
  62. * @throws \Exception
  63. */
  64. public function __construct(array $data)
  65. {
  66. $this->data = $data;
  67. $this->setTemplate('wall');
  68. $this->toplevel = $this->getId() == $this->getDataValue('parent');
  69. if (!empty(Session::getUserIDForVisitorContactID($this->getDataValue('contact-id')))) {
  70. $this->visiting = true;
  71. }
  72. $this->writable = $this->getDataValue('writable') || $this->getDataValue('self');
  73. $author = ['uid' => 0, 'id' => $this->getDataValue('author-id'),
  74. 'network' => $this->getDataValue('author-network'),
  75. 'url' => $this->getDataValue('author-link')];
  76. $this->redirect_url = Contact::magicLinkByContact($author);
  77. if (!$this->isToplevel()) {
  78. $this->threaded = true;
  79. }
  80. // Prepare the children
  81. if (!empty($data['children'])) {
  82. foreach ($data['children'] as $item) {
  83. // Only add will be displayed
  84. if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) {
  85. continue;
  86. } elseif (!visible_activity($item)) {
  87. continue;
  88. }
  89. // You can always comment on Diaspora and OStatus items
  90. if (in_array($item['network'], [Protocol::OSTATUS, Protocol::DIASPORA]) && (local_user() == $item['uid'])) {
  91. $item['writable'] = true;
  92. }
  93. $item['pagedrop'] = $data['pagedrop'];
  94. $child = new Post($item);
  95. $this->addChild($child);
  96. }
  97. }
  98. }
  99. /**
  100. * Get data in a form usable by a conversation template
  101. *
  102. * @param array $conv_responses conversation responses
  103. * @param integer $thread_level default = 1
  104. *
  105. * @return mixed The data requested on success
  106. * false on failure
  107. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  108. * @throws \ImagickException
  109. */
  110. public function getTemplateData(array $conv_responses, $thread_level = 1)
  111. {
  112. $a = DI::app();
  113. $item = $this->getData();
  114. $edited = false;
  115. // If the time between "created" and "edited" differs we add
  116. // a notice that the post was edited.
  117. // Note: In some networks reshared items seem to have (sometimes) a difference
  118. // between creation time and edit time of a second. Thats why we add the notice
  119. // only if the difference is more than 1 second.
  120. if (strtotime($item['edited']) - strtotime($item['created']) > 1) {
  121. $edited = [
  122. 'label' => L10n::t('This entry was edited'),
  123. 'date' => DateTimeFormat::local($item['edited'], 'r'),
  124. 'relative' => Temporal::getRelativeDate($item['edited'])
  125. ];
  126. }
  127. $sparkle = '';
  128. $buttons = '';
  129. $dropping = false;
  130. $pinned = '';
  131. $pin = false;
  132. $star = false;
  133. $ignore = false;
  134. $ispinned = "unpinned";
  135. $isstarred = "unstarred";
  136. $indent = '';
  137. $shiny = '';
  138. $osparkle = '';
  139. $total_children = $this->countDescendants();
  140. $conv = $this->getThread();
  141. $lock = ((($item['private'] == 1) || (($item['uid'] == local_user()) && (strlen($item['allow_cid']) || strlen($item['allow_gid'])
  142. || strlen($item['deny_cid']) || strlen($item['deny_gid']))))
  143. ? L10n::t('Private Message')
  144. : false);
  145. $shareable = in_array($conv->getProfileOwner(), [0, local_user()]) && $item['private'] != 1;
  146. $edpost = false;
  147. if (local_user()) {
  148. if (Strings::compareLink($a->contact['url'], $item['author-link'])) {
  149. if ($item["event-id"] != 0) {
  150. $edpost = ["events/event/" . $item['event-id'], L10n::t("Edit")];
  151. } else {
  152. $edpost = ["editpost/" . $item['id'], L10n::t("Edit")];
  153. }
  154. }
  155. $dropping = in_array($item['uid'], [0, local_user()]);
  156. }
  157. // Editing on items of not subscribed users isn't currently possible
  158. // There are some issues on editing that prevent this.
  159. // But also it is an issue of the supported protocols that doesn't allow editing at all.
  160. if ($item['uid'] == 0) {
  161. $edpost = false;
  162. }
  163. if (($this->getDataValue('uid') == local_user()) || $this->isVisiting()) {
  164. $dropping = true;
  165. }
  166. $origin = $item['origin'];
  167. if (!$origin) {
  168. /// @todo This shouldn't be done as query here, but better during the data creation.
  169. // it is now done here, since during the RC phase we shouldn't make to intense changes.
  170. $parent = Item::selectFirst(['origin'], ['id' => $item['parent']]);
  171. if (DBA::isResult($parent)) {
  172. $origin = $parent['origin'];
  173. }
  174. } elseif ($item['pinned']) {
  175. $pinned = L10n::t('pinned item');
  176. }
  177. if ($origin && ($item['id'] != $item['parent']) && ($item['network'] == Protocol::ACTIVITYPUB)) {
  178. // ActivityPub doesn't allow removal of remote comments
  179. $delete = L10n::t('Delete locally');
  180. } else {
  181. // Showing the one or the other text, depending upon if we can only hide it or really delete it.
  182. $delete = $origin ? L10n::t('Delete globally') : L10n::t('Remove locally');
  183. }
  184. $drop = [
  185. 'dropping' => $dropping,
  186. 'pagedrop' => $item['pagedrop'],
  187. 'select' => L10n::t('Select'),
  188. 'delete' => $delete,
  189. ];
  190. if (!local_user()) {
  191. $drop = false;
  192. }
  193. $filer = (($conv->getProfileOwner() == local_user() && ($item['uid'] != 0)) ? L10n::t("save to folder") : false);
  194. $profile_name = $item['author-name'];
  195. if (!empty($item['author-link']) && empty($item['author-name'])) {
  196. $profile_name = $item['author-link'];
  197. }
  198. $author = ['uid' => 0, 'id' => $item['author-id'],
  199. 'network' => $item['author-network'], 'url' => $item['author-link']];
  200. if (Session::isAuthenticated()) {
  201. $profile_link = Contact::magicLinkByContact($author);
  202. } else {
  203. $profile_link = $item['author-link'];
  204. }
  205. if (strpos($profile_link, 'redir/') === 0) {
  206. $sparkle = ' sparkle';
  207. }
  208. $locate = ['location' => $item['location'], 'coord' => $item['coord'], 'html' => ''];
  209. Hook::callAll('render_location', $locate);
  210. $location = ((strlen($locate['html'])) ? $locate['html'] : render_location_dummy($locate));
  211. // process action responses - e.g. like/dislike/attend/agree/whatever
  212. $response_verbs = ['like', 'dislike', 'announce'];
  213. $isevent = false;
  214. $attend = [];
  215. if ($item['object-type'] === Activity\ObjectType::EVENT) {
  216. $response_verbs[] = 'attendyes';
  217. $response_verbs[] = 'attendno';
  218. $response_verbs[] = 'attendmaybe';
  219. if ($conv->isWritable()) {
  220. $isevent = true;
  221. $attend = [L10n::t('I will attend'), L10n::t('I will not attend'), L10n::t('I might attend')];
  222. }
  223. }
  224. $responses = get_responses($conv_responses, $response_verbs, $item, $this);
  225. foreach ($response_verbs as $value => $verbs) {
  226. $responses[$verbs]['output'] = !empty($conv_responses[$verbs][$item['uri']]) ? format_like($conv_responses[$verbs][$item['uri']], $conv_responses[$verbs][$item['uri'] . '-l'], $verbs, $item['uri']) : '';
  227. }
  228. /*
  229. * We should avoid doing this all the time, but it depends on the conversation mode
  230. * And the conv mode may change when we change the conv, or it changes its mode
  231. * Maybe we should establish a way to be notified about conversation changes
  232. */
  233. $this->checkWallToWall();
  234. if ($this->isWallToWall() && ($this->getOwnerUrl() == $this->getRedirectUrl())) {
  235. $osparkle = ' sparkle';
  236. }
  237. $tagger = '';
  238. if ($this->isToplevel()) {
  239. if(local_user()) {
  240. $thread = Item::selectFirstThreadForUser(local_user(), ['ignored'], ['iid' => $item['id']]);
  241. if (DBA::isResult($thread)) {
  242. $ignore = [
  243. 'do' => L10n::t("ignore thread"),
  244. 'undo' => L10n::t("unignore thread"),
  245. 'toggle' => L10n::t("toggle ignore status"),
  246. 'classdo' => $thread['ignored'] ? "hidden" : "",
  247. 'classundo' => $thread['ignored'] ? "" : "hidden",
  248. 'ignored' => L10n::t('ignored'),
  249. ];
  250. }
  251. if ($conv->getProfileOwner() == local_user() && ($item['uid'] != 0)) {
  252. if ($origin) {
  253. $ispinned = ($item['pinned'] ? 'pinned' : 'unpinned');
  254. $pin = [
  255. 'do' => L10n::t('pin'),
  256. 'undo' => L10n::t('unpin'),
  257. 'toggle' => L10n::t('toggle pin status'),
  258. 'classdo' => $item['pinned'] ? 'hidden' : '',
  259. 'classundo' => $item['pinned'] ? '' : 'hidden',
  260. 'pinned' => L10n::t('pinned'),
  261. ];
  262. }
  263. $isstarred = (($item['starred']) ? "starred" : "unstarred");
  264. $star = [
  265. 'do' => L10n::t("add star"),
  266. 'undo' => L10n::t("remove star"),
  267. 'toggle' => L10n::t("toggle star status"),
  268. 'classdo' => $item['starred'] ? "hidden" : "",
  269. 'classundo' => $item['starred'] ? "" : "hidden",
  270. 'starred' => L10n::t('starred'),
  271. ];
  272. $tagger = [
  273. 'add' => L10n::t("add tag"),
  274. 'class' => "",
  275. ];
  276. }
  277. }
  278. } else {
  279. $indent = 'comment';
  280. }
  281. if ($conv->isWritable()) {
  282. $buttons = [
  283. 'like' => [L10n::t("I like this \x28toggle\x29"), L10n::t("like")],
  284. 'dislike' => [L10n::t("I don't like this \x28toggle\x29"), L10n::t("dislike")],
  285. ];
  286. if ($shareable) {
  287. $buttons['share'] = [L10n::t('Share this'), L10n::t('share')];
  288. }
  289. }
  290. $comment = $this->getCommentBox($indent);
  291. if (strcmp(DateTimeFormat::utc($item['created']), DateTimeFormat::utc('now - 12 hours')) > 0) {
  292. $shiny = 'shiny';
  293. }
  294. localize_item($item);
  295. $body = Item::prepareBody($item, true);
  296. list($categories, $folders) = DI::contentItem()->determineCategoriesTerms($item);
  297. $body_e = $body;
  298. $text_e = strip_tags($body);
  299. $name_e = $profile_name;
  300. if (!empty($item['content-warning']) && DI::pConfig()->get(local_user(), 'system', 'disable_cw', false)) {
  301. $title_e = ucfirst($item['content-warning']);
  302. } else {
  303. $title_e = $item['title'];
  304. }
  305. $location_e = $location;
  306. $owner_name_e = $this->getOwnerName();
  307. // Disable features that aren't available in several networks
  308. if (!in_array($item["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA]) && isset($buttons["dislike"])) {
  309. unset($buttons["dislike"]);
  310. $isevent = false;
  311. $tagger = '';
  312. }
  313. if (($item["network"] == Protocol::FEED) && isset($buttons["like"])) {
  314. unset($buttons["like"]);
  315. }
  316. if (($item["network"] == Protocol::MAIL) && isset($buttons["like"])) {
  317. unset($buttons["like"]);
  318. }
  319. $tags = Term::populateTagsFromItem($item);
  320. $ago = Temporal::getRelativeDate($item['created']);
  321. $ago_received = Temporal::getRelativeDate($item['received']);
  322. if (Config::get('system', 'show_received') && (abs(strtotime($item['created']) - strtotime($item['received'])) > Config::get('system', 'show_received_seconds')) && ($ago != $ago_received)) {
  323. $ago = L10n::t('%s (Received %s)', $ago, $ago_received);
  324. }
  325. $tmp_item = [
  326. 'template' => $this->getTemplate(),
  327. 'type' => implode("", array_slice(explode("/", $item['verb']), -1)),
  328. 'suppress_tags' => Config::get('system', 'suppress_tags'),
  329. 'tags' => $tags['tags'],
  330. 'hashtags' => $tags['hashtags'],
  331. 'mentions' => $tags['mentions'],
  332. 'implicit_mentions' => $tags['implicit_mentions'],
  333. 'txt_cats' => L10n::t('Categories:'),
  334. 'txt_folders' => L10n::t('Filed under:'),
  335. 'has_cats' => ((count($categories)) ? 'true' : ''),
  336. 'has_folders' => ((count($folders)) ? 'true' : ''),
  337. 'categories' => $categories,
  338. 'folders' => $folders,
  339. 'body' => $body_e,
  340. 'text' => $text_e,
  341. 'id' => $this->getId(),
  342. 'guid' => urlencode($item['guid']),
  343. 'isevent' => $isevent,
  344. 'attend' => $attend,
  345. 'linktitle' => L10n::t('View %s\'s profile @ %s', $profile_name, $item['author-link']),
  346. 'olinktitle' => L10n::t('View %s\'s profile @ %s', $this->getOwnerName(), $item['owner-link']),
  347. 'to' => L10n::t('to'),
  348. 'via' => L10n::t('via'),
  349. 'wall' => L10n::t('Wall-to-Wall'),
  350. 'vwall' => L10n::t('via Wall-To-Wall:'),
  351. 'profile_url' => $profile_link,
  352. 'item_photo_menu' => item_photo_menu($item),
  353. 'name' => $name_e,
  354. 'thumb' => DI::baseUrl()->remove(ProxyUtils::proxifyUrl($item['author-avatar'], false, ProxyUtils::SIZE_THUMB)),
  355. 'osparkle' => $osparkle,
  356. 'sparkle' => $sparkle,
  357. 'title' => $title_e,
  358. 'localtime' => DateTimeFormat::local($item['created'], 'r'),
  359. 'ago' => $item['app'] ? L10n::t('%s from %s', $ago, $item['app']) : $ago,
  360. 'app' => $item['app'],
  361. 'created' => $ago,
  362. 'lock' => $lock,
  363. 'location' => $location_e,
  364. 'indent' => $indent,
  365. 'shiny' => $shiny,
  366. 'owner_self' => $item['author-link'] == Session::get('my_url'),
  367. 'owner_url' => $this->getOwnerUrl(),
  368. 'owner_photo' => DI::baseUrl()->remove(ProxyUtils::proxifyUrl($item['owner-avatar'], false, ProxyUtils::SIZE_THUMB)),
  369. 'owner_name' => $owner_name_e,
  370. 'plink' => Item::getPlink($item),
  371. 'edpost' => $edpost,
  372. 'ispinned' => $ispinned,
  373. 'pin' => $pin,
  374. 'pinned' => $pinned,
  375. 'isstarred' => $isstarred,
  376. 'star' => $star,
  377. 'ignore' => $ignore,
  378. 'tagger' => $tagger,
  379. 'filer' => $filer,
  380. 'drop' => $drop,
  381. 'vote' => $buttons,
  382. 'like' => $responses['like']['output'],
  383. 'dislike' => $responses['dislike']['output'],
  384. 'responses' => $responses,
  385. 'switchcomment' => L10n::t('Comment'),
  386. 'reply_label' => L10n::t('Reply to %s', $name_e),
  387. 'comment' => $comment,
  388. 'previewing' => $conv->isPreview() ? ' preview ' : '',
  389. 'wait' => L10n::t('Please wait'),
  390. 'thread_level' => $thread_level,
  391. 'edited' => $edited,
  392. 'network' => $item["network"],
  393. 'network_name' => ContactSelector::networkToName($item['author-network'], $item['author-link'], $item['network']),
  394. 'network_icon' => ContactSelector::networkToIcon($item['network'], $item['author-link']),
  395. 'received' => $item['received'],
  396. 'commented' => $item['commented'],
  397. 'created_date' => $item['created'],
  398. 'return' => (DI::args()->getCommand()) ? bin2hex(DI::args()->getCommand()) : '',
  399. 'delivery' => [
  400. 'queue_count' => $item['delivery_queue_count'],
  401. 'queue_done' => $item['delivery_queue_done'] + $item['delivery_queue_failed'], /// @todo Possibly display it separately in the future
  402. 'notifier_pending' => L10n::t('Notifier task is pending'),
  403. 'delivery_pending' => L10n::t('Delivery to remote servers is pending'),
  404. 'delivery_underway' => L10n::t('Delivery to remote servers is underway'),
  405. 'delivery_almost' => L10n::t('Delivery to remote servers is mostly done'),
  406. 'delivery_done' => L10n::t('Delivery to remote servers is done'),
  407. ],
  408. ];
  409. $arr = ['item' => $item, 'output' => $tmp_item];
  410. Hook::callAll('display_item', $arr);
  411. $result = $arr['output'];
  412. $result['children'] = [];
  413. $children = $this->getChildren();
  414. $nb_children = count($children);
  415. if ($nb_children > 0) {
  416. foreach ($children as $child) {
  417. $result['children'][] = $child->getTemplateData($conv_responses, $thread_level + 1);
  418. }
  419. // Collapse
  420. if (($nb_children > 2) || ($thread_level > 1)) {
  421. $result['children'][0]['comment_firstcollapsed'] = true;
  422. $result['children'][0]['num_comments'] = L10n::tt('%d comment', '%d comments', $total_children);
  423. $result['children'][0]['show_text'] = L10n::t('Show more');
  424. $result['children'][0]['hide_text'] = L10n::t('Show fewer');
  425. if ($thread_level > 1) {
  426. $result['children'][$nb_children - 1]['comment_lastcollapsed'] = true;
  427. } else {
  428. $result['children'][$nb_children - 3]['comment_lastcollapsed'] = true;
  429. }
  430. }
  431. }
  432. if ($this->isToplevel()) {
  433. $result['total_comments_num'] = "$total_children";
  434. $result['total_comments_text'] = L10n::tt('comment', 'comments', $total_children);
  435. }
  436. $result['private'] = $item['private'];
  437. $result['toplevel'] = ($this->isToplevel() ? 'toplevel_item' : '');
  438. if ($this->isThreaded()) {
  439. $result['flatten'] = false;
  440. $result['threaded'] = true;
  441. } else {
  442. $result['flatten'] = true;
  443. $result['threaded'] = false;
  444. }
  445. return $result;
  446. }
  447. /**
  448. * @return integer
  449. */
  450. public function getId()
  451. {
  452. return $this->getDataValue('id');
  453. }
  454. /**
  455. * @return boolean
  456. */
  457. public function isThreaded()
  458. {
  459. return $this->threaded;
  460. }
  461. /**
  462. * Add a child item
  463. *
  464. * @param Post $item The child item to add
  465. *
  466. * @return mixed
  467. * @throws \Exception
  468. */
  469. public function addChild(Post $item)
  470. {
  471. $item_id = $item->getId();
  472. if (!$item_id) {
  473. Logger::log('[ERROR] Post::addChild : Item has no ID!!', Logger::DEBUG);
  474. return false;
  475. } elseif ($this->getChild($item->getId())) {
  476. Logger::log('[WARN] Post::addChild : Item already exists (' . $item->getId() . ').', Logger::DEBUG);
  477. return false;
  478. }
  479. $activity = DI::activity();
  480. /*
  481. * Only add what will be displayed
  482. */
  483. if ($item->getDataValue('network') === Protocol::MAIL && local_user() != $item->getDataValue('uid')) {
  484. return false;
  485. } elseif ($activity->match($item->getDataValue('verb'), Activity::LIKE) ||
  486. $activity->match($item->getDataValue('verb'), Activity::DISLIKE)) {
  487. return false;
  488. }
  489. $item->setParent($this);
  490. $this->children[] = $item;
  491. return end($this->children);
  492. }
  493. /**
  494. * Get a child by its ID
  495. *
  496. * @param integer $id The child id
  497. *
  498. * @return mixed
  499. */
  500. public function getChild($id)
  501. {
  502. foreach ($this->getChildren() as $child) {
  503. if ($child->getId() == $id) {
  504. return $child;
  505. }
  506. }
  507. return null;
  508. }
  509. /**
  510. * Get all our children
  511. *
  512. * @return Post[]
  513. */
  514. public function getChildren()
  515. {
  516. return $this->children;
  517. }
  518. /**
  519. * Set our parent
  520. *
  521. * @param Post $item The item to set as parent
  522. *
  523. * @return void
  524. */
  525. protected function setParent(Post $item)
  526. {
  527. $parent = $this->getParent();
  528. if ($parent) {
  529. $parent->removeChild($this);
  530. }
  531. $this->parent = $item;
  532. $this->setThread($item->getThread());
  533. }
  534. /**
  535. * Remove our parent
  536. *
  537. * @return void
  538. */
  539. protected function removeParent()
  540. {
  541. $this->parent = null;
  542. $this->thread = null;
  543. }
  544. /**
  545. * Remove a child
  546. *
  547. * @param Post $item The child to be removed
  548. *
  549. * @return boolean Success or failure
  550. * @throws \Exception
  551. */
  552. public function removeChild(Post $item)
  553. {
  554. $id = $item->getId();
  555. foreach ($this->getChildren() as $key => $child) {
  556. if ($child->getId() == $id) {
  557. $child->removeParent();
  558. unset($this->children[$key]);
  559. // Reindex the array, in order to make sure there won't be any trouble on loops using count()
  560. $this->children = array_values($this->children);
  561. return true;
  562. }
  563. }
  564. Logger::log('[WARN] Item::removeChild : Item is not a child (' . $id . ').', Logger::DEBUG);
  565. return false;
  566. }
  567. /**
  568. * Get parent item
  569. *
  570. * @return object
  571. */
  572. protected function getParent()
  573. {
  574. return $this->parent;
  575. }
  576. /**
  577. * Set conversation thread
  578. *
  579. * @param Thread $thread
  580. *
  581. * @return void
  582. */
  583. public function setThread(Thread $thread = null)
  584. {
  585. $this->thread = $thread;
  586. // Set it on our children too
  587. foreach ($this->getChildren() as $child) {
  588. $child->setThread($thread);
  589. }
  590. }
  591. /**
  592. * Get conversation
  593. *
  594. * @return Thread
  595. */
  596. public function getThread()
  597. {
  598. return $this->thread;
  599. }
  600. /**
  601. * Get raw data
  602. *
  603. * We shouldn't need this
  604. *
  605. * @return array
  606. */
  607. public function getData()
  608. {
  609. return $this->data;
  610. }
  611. /**
  612. * Get a data value
  613. *
  614. * @param string $name key
  615. *
  616. * @return mixed value on success
  617. * false on failure
  618. */
  619. public function getDataValue($name)
  620. {
  621. if (!isset($this->data[$name])) {
  622. // Logger::log('[ERROR] Item::getDataValue : Item has no value name "'. $name .'".', Logger::DEBUG);
  623. return false;
  624. }
  625. return $this->data[$name];
  626. }
  627. /**
  628. * Set template
  629. *
  630. * @param string $name template name
  631. * @return bool
  632. * @throws \Exception
  633. */
  634. private function setTemplate($name)
  635. {
  636. if (empty($this->available_templates[$name])) {
  637. Logger::log('[ERROR] Item::setTemplate : Template not available ("' . $name . '").', Logger::DEBUG);
  638. return false;
  639. }
  640. $this->template = $this->available_templates[$name];
  641. return true;
  642. }
  643. /**
  644. * Get template
  645. *
  646. * @return object
  647. */
  648. private function getTemplate()
  649. {
  650. return $this->template;
  651. }
  652. /**
  653. * Check if this is a toplevel post
  654. *
  655. * @return boolean
  656. */
  657. private function isToplevel()
  658. {
  659. return $this->toplevel;
  660. }
  661. /**
  662. * Check if this is writable
  663. *
  664. * @return boolean
  665. */
  666. private function isWritable()
  667. {
  668. $conv = $this->getThread();
  669. if ($conv) {
  670. // This will allow us to comment on wall-to-wall items owned by our friends
  671. // and community forums even if somebody else wrote the post.
  672. // bug #517 - this fixes for conversation owner
  673. if ($conv->getMode() == 'profile' && $conv->getProfileOwner() == local_user()) {
  674. return true;
  675. }
  676. // this fixes for visitors
  677. return ($this->writable || ($this->isVisiting() && $conv->getMode() == 'profile'));
  678. }
  679. return $this->writable;
  680. }
  681. /**
  682. * Count the total of our descendants
  683. *
  684. * @return integer
  685. */
  686. private function countDescendants()
  687. {
  688. $children = $this->getChildren();
  689. $total = count($children);
  690. if ($total > 0) {
  691. foreach ($children as $child) {
  692. $total += $child->countDescendants();
  693. }
  694. }
  695. return $total;
  696. }
  697. /**
  698. * Get the template for the comment box
  699. *
  700. * @return string
  701. */
  702. private function getCommentBoxTemplate()
  703. {
  704. return $this->comment_box_template;
  705. }
  706. /**
  707. * Get default text for the comment box
  708. *
  709. * @return string
  710. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  711. */
  712. private function getDefaultText()
  713. {
  714. $a = DI::app();
  715. if (!local_user()) {
  716. return '';
  717. }
  718. $owner = User::getOwnerDataById($a->user['uid']);
  719. if (!Feature::isEnabled(local_user(), 'explicit_mentions')) {
  720. return '';
  721. }
  722. $item = Item::selectFirst(['author-addr'], ['id' => $this->getId()]);
  723. if (!DBA::isResult($item) || empty($item['author-addr'])) {
  724. // Should not happen
  725. return '';
  726. }
  727. if ($item['author-addr'] != $owner['addr']) {
  728. $text = '@' . $item['author-addr'] . ' ';
  729. } else {
  730. $text = '';
  731. }
  732. $terms = Term::tagArrayFromItemId($this->getId(), [Term::MENTION, Term::IMPLICIT_MENTION]);
  733. foreach ($terms as $term) {
  734. $profile = Contact::getDetailsByURL($term['url']);
  735. if (!empty($profile['addr']) && ((($profile['contact-type'] ?? '') ?: Contact::TYPE_UNKNOWN) != Contact::TYPE_COMMUNITY) &&
  736. ($profile['addr'] != $owner['addr']) && !strstr($text, $profile['addr'])) {
  737. $text .= '@' . $profile['addr'] . ' ';
  738. }
  739. }
  740. return $text;
  741. }
  742. /**
  743. * Get the comment box
  744. *
  745. * @param string $indent Indent value
  746. *
  747. * @return mixed The comment box string (empty if no comment box)
  748. * false on failure
  749. * @throws \Exception
  750. */
  751. private function getCommentBox($indent)
  752. {
  753. $a = DI::app();
  754. $comment_box = '';
  755. $conv = $this->getThread();
  756. $ww = '';
  757. if (($conv->getMode() === 'network') && $this->isWallToWall()) {
  758. $ww = 'ww';
  759. }
  760. if ($conv->isWritable() && $this->isWritable()) {
  761. $qcomment = null;
  762. /*
  763. * Hmmm, code depending on the presence of a particular addon?
  764. * This should be better if done by a hook
  765. */
  766. if (Addon::isEnabled('qcomment')) {
  767. $qc = ((local_user()) ? DI::pConfig()->get(local_user(), 'qcomment', 'words') : null);
  768. $qcomment = (($qc) ? explode("\n", $qc) : null);
  769. }
  770. // Fetch the user id from the parent when the owner user is empty
  771. $uid = $conv->getProfileOwner();
  772. $parent_uid = $this->getDataValue('uid');
  773. $default_text = $this->getDefaultText();
  774. if (!is_null($parent_uid) && ($uid != $parent_uid)) {
  775. $uid = $parent_uid;
  776. }
  777. $template = Renderer::getMarkupTemplate($this->getCommentBoxTemplate());
  778. $comment_box = Renderer::replaceMacros($template, [
  779. '$return_path' => DI::args()->getQueryString(),
  780. '$threaded' => $this->isThreaded(),
  781. '$jsreload' => '',
  782. '$wall' => ($conv->getMode() === 'profile'),
  783. '$id' => $this->getId(),
  784. '$parent' => $this->getId(),
  785. '$qcomment' => $qcomment,
  786. '$default' => $default_text,
  787. '$profile_uid' => $uid,
  788. '$mylink' => DI::baseUrl()->remove($a->contact['url']),
  789. '$mytitle' => L10n::t('This is you'),
  790. '$myphoto' => DI::baseUrl()->remove($a->contact['thumb']),
  791. '$comment' => L10n::t('Comment'),
  792. '$submit' => L10n::t('Submit'),
  793. '$edbold' => L10n::t('Bold'),
  794. '$editalic' => L10n::t('Italic'),
  795. '$eduline' => L10n::t('Underline'),
  796. '$edquote' => L10n::t('Quote'),
  797. '$edcode' => L10n::t('Code'),
  798. '$edimg' => L10n::t('Image'),
  799. '$edurl' => L10n::t('Link'),
  800. '$edattach' => L10n::t('Link or Media'),
  801. '$prompttext' => L10n::t('Please enter a image/video/audio/webpage URL:'),
  802. '$preview' => L10n::t('Preview'),
  803. '$indent' => $indent,
  804. '$sourceapp' => L10n::t($a->sourcename),
  805. '$ww' => $conv->getMode() === 'network' ? $ww : '',
  806. '$rand_num' => Crypto::randomDigits(12)
  807. ]);
  808. }
  809. return $comment_box;
  810. }
  811. /**
  812. * @return string
  813. */
  814. private function getRedirectUrl()
  815. {
  816. return $this->redirect_url;
  817. }
  818. /**
  819. * Check if we are a wall to wall item and set the relevant properties
  820. *
  821. * @return void
  822. * @throws \Exception
  823. */
  824. protected function checkWallToWall()
  825. {
  826. $a = DI::app();
  827. $conv = $this->getThread();
  828. $this->wall_to_wall = false;
  829. if ($this->isToplevel()) {
  830. if ($conv->getMode() !== 'profile') {
  831. if ($this->getDataValue('wall') && !$this->getDataValue('self')) {
  832. // On the network page, I am the owner. On the display page it will be the profile owner.
  833. // This will have been stored in $a->page_contact by our calling page.
  834. // Put this person as the wall owner of the wall-to-wall notice.
  835. $this->owner_url = Contact::magicLink($a->page_contact['url']);
  836. $this->owner_photo = $a->page_contact['thumb'];
  837. $this->owner_name = $a->page_contact['name'];
  838. $this->wall_to_wall = true;
  839. } elseif ($this->getDataValue('owner-link')) {
  840. $owner_linkmatch = (($this->getDataValue('owner-link')) && Strings::compareLink($this->getDataValue('owner-link'), $this->getDataValue('author-link')));
  841. $alias_linkmatch = (($this->getDataValue('alias')) && Strings::compareLink($this->getDataValue('alias'), $this->getDataValue('author-link')));
  842. $owner_namematch = (($this->getDataValue('owner-name')) && $this->getDataValue('owner-name') == $this->getDataValue('author-name'));
  843. if (!$owner_linkmatch && !$alias_linkmatch && !$owner_namematch) {
  844. // The author url doesn't match the owner (typically the contact)
  845. // and also doesn't match the contact alias.
  846. // The name match is a hack to catch several weird cases where URLs are
  847. // all over the park. It can be tricked, but this prevents you from
  848. // seeing "Bob Smith to Bob Smith via Wall-to-wall" and you know darn
  849. // well that it's the same Bob Smith.
  850. // But it could be somebody else with the same name. It just isn't highly likely.
  851. $this->owner_photo = $this->getDataValue('owner-avatar');
  852. $this->owner_name = $this->getDataValue('owner-name');
  853. $this->wall_to_wall = true;
  854. $owner = ['uid' => 0, 'id' => $this->getDataValue('owner-id'),
  855. 'network' => $this->getDataValue('owner-network'),
  856. 'url' => $this->getDataValue('owner-link')];
  857. $this->owner_url = Contact::magicLinkByContact($owner);
  858. }
  859. }
  860. }
  861. }
  862. if (!$this->wall_to_wall) {
  863. $this->setTemplate('wall');
  864. $this->owner_url = '';
  865. $this->owner_photo = '';
  866. $this->owner_name = '';
  867. }
  868. }
  869. /**
  870. * @return boolean
  871. */
  872. private function isWallToWall()
  873. {
  874. return $this->wall_to_wall;
  875. }
  876. /**
  877. * @return string
  878. */
  879. private function getOwnerUrl()
  880. {
  881. return $this->owner_url;
  882. }
  883. /**
  884. * @return string
  885. */
  886. private function getOwnerName()
  887. {
  888. return $this->owner_name;
  889. }
  890. /**
  891. * @return boolean
  892. */
  893. private function isVisiting()
  894. {
  895. return $this->visiting;
  896. }
  897. }