]> git.mxchange.org Git - friendica.git/blob - include/conversation.php
Merge pull request #8075 from annando/html-escaping
[friendica.git] / include / conversation.php
1 <?php
2 /**
3  * @file include/conversation.php
4  */
5
6 use Friendica\App;
7 use Friendica\Content\ContactSelector;
8 use Friendica\Content\Feature;
9 use Friendica\Content\Pager;
10 use Friendica\Content\Text\BBCode;
11 use Friendica\Core\Config;
12 use Friendica\Core\Hook;
13 use Friendica\Core\L10n;
14 use Friendica\Core\Logger;
15 use Friendica\Core\PConfig;
16 use Friendica\Core\Protocol;
17 use Friendica\Core\Renderer;
18 use Friendica\Core\Session;
19 use Friendica\Database\DBA;
20 use Friendica\DI;
21 use Friendica\Model\Contact;
22 use Friendica\Model\Item;
23 use Friendica\Model\Profile;
24 use Friendica\Model\Term;
25 use Friendica\Object\Post;
26 use Friendica\Object\Thread;
27 use Friendica\Protocol\Activity;
28 use Friendica\Util\Crypto;
29 use Friendica\Util\DateTimeFormat;
30 use Friendica\Util\Proxy as ProxyUtils;
31 use Friendica\Util\Strings;
32 use Friendica\Util\Temporal;
33 use Friendica\Util\XML;
34
35 function item_extract_images($body) {
36
37         $saved_image = [];
38         $orig_body = $body;
39         $new_body = '';
40
41         $cnt = 0;
42         $img_start = strpos($orig_body, '[img');
43         $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
44         $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
45         while (($img_st_close !== false) && ($img_end !== false)) {
46
47                 $img_st_close++; // make it point to AFTER the closing bracket
48                 $img_end += $img_start;
49
50                 if (!strcmp(substr($orig_body, $img_start + $img_st_close, 5), 'data:')) {
51                         // This is an embedded image
52
53                         $saved_image[$cnt] = substr($orig_body, $img_start + $img_st_close, $img_end - ($img_start + $img_st_close));
54                         $new_body = $new_body . substr($orig_body, 0, $img_start) . '[!#saved_image' . $cnt . '#!]';
55
56                         $cnt++;
57                 } else {
58                         $new_body = $new_body . substr($orig_body, 0, $img_end + strlen('[/img]'));
59                 }
60
61                 $orig_body = substr($orig_body, $img_end + strlen('[/img]'));
62
63                 if ($orig_body === false) {
64                         // in case the body ends on a closing image tag
65                         $orig_body = '';
66                 }
67
68                 $img_start = strpos($orig_body, '[img');
69                 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
70                 $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
71         }
72
73         $new_body = $new_body . $orig_body;
74
75         return ['body' => $new_body, 'images' => $saved_image];
76 }
77
78 function item_redir_and_replace_images($body, $images, $cid) {
79
80         $origbody = $body;
81         $newbody = '';
82
83         $cnt = 1;
84         $pos = BBCode::getTagPosition($origbody, 'url', 0);
85         while ($pos !== false && $cnt < 1000) {
86
87                 $search = '/\[url\=(.*?)\]\[!#saved_image([0-9]*)#!\]\[\/url\]' . '/is';
88                 $replace = '[url=' . DI::baseUrl() . '/redir/' . $cid
89                                    . '?f=1&url=' . '$1' . '][!#saved_image' . '$2' .'#!][/url]';
90
91                 $newbody .= substr($origbody, 0, $pos['start']['open']);
92                 $subject = substr($origbody, $pos['start']['open'], $pos['end']['close'] - $pos['start']['open']);
93                 $origbody = substr($origbody, $pos['end']['close']);
94                 if ($origbody === false) {
95                         $origbody = '';
96                 }
97
98                 $subject = preg_replace($search, $replace, $subject);
99                 $newbody .= $subject;
100
101                 $cnt++;
102                 // Isn't this supposed to use $cnt value for $occurrences? - @MrPetovan
103                 $pos = BBCode::getTagPosition($origbody, 'url', 0);
104         }
105         $newbody .= $origbody;
106
107         $cnt = 0;
108         foreach ($images as $image) {
109                 /*
110                  * We're depending on the property of 'foreach' (specified on the PHP website) that
111                  * it loops over the array starting from the first element and going sequentially
112                  * to the last element.
113                  */
114                 $newbody = str_replace('[!#saved_image' . $cnt . '#!]', '[img]' . $image . '[/img]', $newbody);
115                 $cnt++;
116         }
117         return $newbody;
118 }
119
120 /**
121  * Render actions localized
122  *
123  * @param $item
124  * @throws ImagickException
125  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
126  */
127 function localize_item(&$item)
128 {
129         $extracted = item_extract_images($item['body']);
130         if ($extracted['images']) {
131                 $item['body'] = item_redir_and_replace_images($extracted['body'], $extracted['images'], $item['contact-id']);
132         }
133
134         /*
135         heluecht 2018-06-19: from my point of view this whole code part is useless.
136         It just renders the body message of technical posts (Like, dislike, ...).
137         But: The body isn't visible at all. So we do this stuff just because we can.
138         Even if these messages were visible, this would only mean that something went wrong.
139         During the further steps of the database restructuring I would like to address this issue.
140         */
141
142         $activity = DI::activity();
143
144         $xmlhead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">";
145         if ($activity->match($item['verb'], Activity::LIKE)
146                 || $activity->match($item['verb'], Activity::DISLIKE)
147                 || $activity->match($item['verb'], Activity::ATTEND)
148                 || $activity->match($item['verb'], Activity::ATTENDNO)
149                 || $activity->match($item['verb'], Activity::ATTENDMAYBE)) {
150
151                 $fields = ['author-link', 'author-name', 'verb', 'object-type', 'resource-id', 'body', 'plink'];
152                 $obj = Item::selectFirst($fields, ['uri' => $item['parent-uri']]);
153                 if (!DBA::isResult($obj)) {
154                         return;
155                 }
156
157                 $author  = '[url=' . $item['author-link'] . ']' . $item['author-name'] . '[/url]';
158                 $objauthor =  '[url=' . $obj['author-link'] . ']' . $obj['author-name'] . '[/url]';
159
160                 switch ($obj['verb']) {
161                         case Activity::POST:
162                                 switch ($obj['object-type']) {
163                                         case Activity\ObjectType::EVENT:
164                                                 $post_type = L10n::t('event');
165                                                 break;
166                                         default:
167                                                 $post_type = L10n::t('status');
168                                 }
169                                 break;
170                         default:
171                                 if ($obj['resource-id']) {
172                                         $post_type = L10n::t('photo');
173                                         $m = [];
174                                         preg_match("/\[url=([^]]*)\]/", $obj['body'], $m);
175                                         $rr['plink'] = $m[1];
176                                 } else {
177                                         $post_type = L10n::t('status');
178                                 }
179                 }
180
181                 $plink = '[url=' . $obj['plink'] . ']' . $post_type . '[/url]';
182
183                 $bodyverb = '';
184                 if ($activity->match($item['verb'], Activity::LIKE)) {
185                         $bodyverb = L10n::t('%1$s likes %2$s\'s %3$s');
186                 } elseif ($activity->match($item['verb'], Activity::DISLIKE)) {
187                         $bodyverb = L10n::t('%1$s doesn\'t like %2$s\'s %3$s');
188                 } elseif ($activity->match($item['verb'], Activity::ATTEND)) {
189                         $bodyverb = L10n::t('%1$s attends %2$s\'s %3$s');
190                 } elseif ($activity->match($item['verb'], Activity::ATTENDNO)) {
191                         $bodyverb = L10n::t('%1$s doesn\'t attend %2$s\'s %3$s');
192                 } elseif ($activity->match($item['verb'], Activity::ATTENDMAYBE)) {
193                         $bodyverb = L10n::t('%1$s attends maybe %2$s\'s %3$s');
194                 }
195
196                 $item['body'] = sprintf($bodyverb, $author, $objauthor, $plink);
197         }
198
199         if ($activity->match($item['verb'], Activity::FRIEND)) {
200
201                 if ($item['object-type']=="" || $item['object-type']!== Activity\ObjectType::PERSON) return;
202
203                 $Aname = $item['author-name'];
204                 $Alink = $item['author-link'];
205
206                 $xmlhead="<"."?xml version='1.0' encoding='UTF-8' ?".">";
207
208                 $obj = XML::parseString($xmlhead.$item['object']);
209                 $links = XML::parseString($xmlhead."<links>".XML::unescape($obj->link)."</links>");
210
211                 $Bname = $obj->title;
212                 $Blink = "";
213                 $Bphoto = "";
214                 foreach ($links->link as $l) {
215                         $atts = $l->attributes();
216                         switch ($atts['rel']) {
217                                 case "alternate": $Blink = $atts['href']; break;
218                                 case "photo": $Bphoto = $atts['href']; break;
219                         }
220                 }
221
222                 $A = '[url=' . Contact::magicLink($Alink) . ']' . $Aname . '[/url]';
223                 $B = '[url=' . Contact::magicLink($Blink) . ']' . $Bname . '[/url]';
224                 if ($Bphoto != "") {
225                         $Bphoto = '[url=' . Contact::magicLink($Blink) . '][img]' . $Bphoto . '[/img][/url]';
226                 }
227
228                 $item['body'] = L10n::t('%1$s is now friends with %2$s', $A, $B)."\n\n\n".$Bphoto;
229
230         }
231         if (stristr($item['verb'], Activity::POKE)) {
232                 $verb = urldecode(substr($item['verb'],strpos($item['verb'],'#')+1));
233                 if (!$verb) {
234                         return;
235                 }
236                 if ($item['object-type']=="" || $item['object-type']!== Activity\ObjectType::PERSON) {
237                         return;
238                 }
239
240                 $Aname = $item['author-name'];
241                 $Alink = $item['author-link'];
242
243                 $xmlhead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">";
244
245                 $obj = XML::parseString($xmlhead.$item['object']);
246
247                 $Bname = $obj->title;
248                 $Blink = $obj->id;
249                 $Bphoto = "";
250
251                 foreach ($obj->link as $l) {
252                         $atts = $l->attributes();
253                         switch ($atts['rel']) {
254                                 case "alternate": $Blink = $atts['href'];
255                                 case "photo": $Bphoto = $atts['href'];
256                         }
257                 }
258
259                 $A = '[url=' . Contact::magicLink($Alink) . ']' . $Aname . '[/url]';
260                 $B = '[url=' . Contact::magicLink($Blink) . ']' . $Bname . '[/url]';
261                 if ($Bphoto != "") {
262                         $Bphoto = '[url=' . Contact::magicLink($Blink) . '][img=80x80]' . $Bphoto . '[/img][/url]';
263                 }
264
265                 /*
266                  * we can't have a translation string with three positions but no distinguishable text
267                  * So here is the translate string.
268                  */
269                 $txt = L10n::t('%1$s poked %2$s');
270
271                 // now translate the verb
272                 $poked_t = trim(sprintf($txt, "", ""));
273                 $txt = str_replace($poked_t, L10n::t($verb), $txt);
274
275                 // then do the sprintf on the translation string
276
277                 $item['body'] = sprintf($txt, $A, $B). "\n\n\n" . $Bphoto;
278
279         }
280
281         if ($activity->match($item['verb'],  Activity::TAG)) {
282                 $fields = ['author-id', 'author-link', 'author-name', 'author-network',
283                         'verb', 'object-type', 'resource-id', 'body', 'plink'];
284                 $obj = Item::selectFirst($fields, ['uri' => $item['parent-uri']]);
285                 if (!DBA::isResult($obj)) {
286                         return;
287                 }
288
289                 $author_arr = ['uid' => 0, 'id' => $item['author-id'],
290                         'network' => $item['author-network'], 'url' => $item['author-link']];
291                 $author  = '[url=' . Contact::magicLinkByContact($author_arr) . ']' . $item['author-name'] . '[/url]';
292
293                 $author_arr = ['uid' => 0, 'id' => $obj['author-id'],
294                         'network' => $obj['author-network'], 'url' => $obj['author-link']];
295                 $objauthor  = '[url=' . Contact::magicLinkByContact($author_arr) . ']' . $obj['author-name'] . '[/url]';
296
297                 switch ($obj['verb']) {
298                         case Activity::POST:
299                                 switch ($obj['object-type']) {
300                                         case Activity\ObjectType::EVENT:
301                                                 $post_type = L10n::t('event');
302                                                 break;
303                                         default:
304                                                 $post_type = L10n::t('status');
305                                 }
306                                 break;
307                         default:
308                                 if ($obj['resource-id']) {
309                                         $post_type = L10n::t('photo');
310                                         $m=[]; preg_match("/\[url=([^]]*)\]/", $obj['body'], $m);
311                                         $rr['plink'] = $m[1];
312                                 } else {
313                                         $post_type = L10n::t('status');
314                                 }
315                                 // Let's break everthing ... ;-)
316                                 break;
317                 }
318                 $plink = '[url=' . $obj['plink'] . ']' . $post_type . '[/url]';
319
320                 $parsedobj = XML::parseString($xmlhead.$item['object']);
321
322                 $tag = sprintf('#[url=%s]%s[/url]', $parsedobj->id, $parsedobj->content);
323                 $item['body'] = L10n::t('%1$s tagged %2$s\'s %3$s with %4$s', $author, $objauthor, $plink, $tag);
324         }
325
326         if ($activity->match($item['verb'], Activity::FAVORITE)) {
327                 if ($item['object-type'] == "") {
328                         return;
329                 }
330
331                 $Aname = $item['author-name'];
332                 $Alink = $item['author-link'];
333
334                 $xmlhead = "<" . "?xml version='1.0' encoding='UTF-8' ?" . ">";
335
336                 $obj = XML::parseString($xmlhead.$item['object']);
337                 if (strlen($obj->id)) {
338                         $fields = ['author-link', 'author-name', 'plink'];
339                         $target = Item::selectFirst($fields, ['uri' => $obj->id, 'uid' => $item['uid']]);
340                         if (DBA::isResult($target) && $target['plink']) {
341                                 $Bname = $target['author-name'];
342                                 $Blink = $target['author-link'];
343                                 $A = '[url=' . Contact::magicLink($Alink) . ']' . $Aname . '[/url]';
344                                 $B = '[url=' . Contact::magicLink($Blink) . ']' . $Bname . '[/url]';
345                                 $P = '[url=' . $target['plink'] . ']' . L10n::t('post/item') . '[/url]';
346                                 $item['body'] = L10n::t('%1$s marked %2$s\'s %3$s as favorite', $A, $B, $P)."\n";
347                         }
348                 }
349         }
350         $matches = null;
351         if (preg_match_all('/@\[url=(.*?)\]/is', $item['body'], $matches, PREG_SET_ORDER)) {
352                 foreach ($matches as $mtch) {
353                         if (!strpos($mtch[1], 'zrl=')) {
354                                 $item['body'] = str_replace($mtch[0], '@[url=' . Contact::magicLink($mtch[1]) . ']', $item['body']);
355                         }
356                 }
357         }
358
359         // add zrl's to public images
360         $photo_pattern = "/\[url=(.*?)\/photos\/(.*?)\/image\/(.*?)\]\[img(.*?)\]h(.*?)\[\/img\]\[\/url\]/is";
361         if (preg_match($photo_pattern, $item['body'])) {
362                 $photo_replace = '[url=' . Profile::zrl('$1' . '/photos/' . '$2' . '/image/' . '$3' ,true) . '][img' . '$4' . ']h' . '$5'  . '[/img][/url]';
363                 $item['body'] = BBCode::pregReplaceInTag($photo_pattern, $photo_replace, 'url', $item['body']);
364         }
365
366         // add sparkle links to appropriate permalinks
367         $author = ['uid' => 0, 'id' => $item['author-id'],
368                 'network' => $item['author-network'], 'url' => $item['author-link']];
369
370         // Only create a redirection to a magic link when logged in
371         if (!empty($item['plink']) && Session::isAuthenticated()) {
372                 $item['plink'] = Contact::magicLinkByContact($author, $item['plink']);
373         }
374 }
375
376 /**
377  * Count the total of comments on this item and its desendants
378  * @TODO proper type-hint + doc-tag
379  * @param $item
380  * @return int
381  */
382 function count_descendants($item) {
383         $total = count($item['children']);
384
385         if ($total > 0) {
386                 foreach ($item['children'] as $child) {
387                         if (!visible_activity($child)) {
388                                 $total --;
389                         }
390                         $total += count_descendants($child);
391                 }
392         }
393
394         return $total;
395 }
396
397 function visible_activity($item) {
398
399         $activity = DI::activity();
400
401         if (empty($item['verb']) || $activity->isHidden($item['verb'])) {
402                 return false;
403         }
404
405         // @TODO below if() block can be rewritten to a single line: $isVisible = allConditionsHere;
406         if ($activity->match($item['verb'], Activity::FOLLOW) &&
407             $item['object-type'] === Activity\ObjectType::NOTE &&
408             empty($item['self']) &&
409             $item['uid'] == local_user()) {
410                 return false;
411         }
412
413         return true;
414 }
415
416 function conv_get_blocklist()
417 {
418         if (!local_user()) {
419                 return [];
420         }
421
422         $str_blocked = PConfig::get(local_user(), 'system', 'blocked');
423         if (empty($str_blocked)) {
424                 return [];
425         }
426
427         $blocklist = [];
428
429         foreach (explode(',', $str_blocked) as $entry) {
430                 // The 4th parameter guarantees that there always will be a public contact entry
431                 $cid = Contact::getIdForURL(trim($entry), 0, true, ['url' => trim($entry)]);
432                 if (!empty($cid)) {
433                         $blocklist[] = $cid;
434                 }
435         }
436
437         return $blocklist;
438 }
439
440 /**
441  * "Render" a conversation or list of items for HTML display.
442  * There are two major forms of display:
443  *      - Sequential or unthreaded ("New Item View" or search results)
444  *      - conversation view
445  * The $mode parameter decides between the various renderings and also
446  * figures out how to determine page owner and other contextual items
447  * that are based on unique features of the calling module.
448  * @param App    $a
449  * @param array  $items
450  * @param Pager  $pager
451  * @param        $mode
452  * @param        $update
453  * @param bool   $preview
454  * @param string $order
455  * @param int    $uid
456  * @return string
457  * @throws ImagickException
458  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
459  */
460 function conversation(App $a, array $items, Pager $pager, $mode, $update, $preview = false, $order = 'commented', $uid = 0)
461 {
462         $ssl_state = (local_user() ? true : false);
463
464         $profile_owner = 0;
465         $live_update_div = '';
466
467         $blocklist = conv_get_blocklist();
468
469         $previewing = (($preview) ? ' preview ' : '');
470
471         if ($mode === 'network') {
472                 $items = conversation_add_children($items, false, $order, $uid);
473                 $profile_owner = local_user();
474                 if (!$update) {
475                         /*
476                          * The special div is needed for liveUpdate to kick in for this page.
477                          * We only launch liveUpdate if you aren't filtering in some incompatible
478                          * way and also you aren't writing a comment (discovered in javascript).
479                          */
480                         $live_update_div = '<div id="live-network"></div>' . "\r\n"
481                                 . "<script> var profile_uid = " . $_SESSION['uid']
482                                 . "; var netargs = '" . substr(DI::args()->getCommand(), 8)
483                                 . '?f='
484                                 . (!empty($_GET['cid'])    ? '&cid='    . rawurlencode($_GET['cid'])    : '')
485                                 . (!empty($_GET['search']) ? '&search=' . rawurlencode($_GET['search']) : '')
486                                 . (!empty($_GET['star'])   ? '&star='   . rawurlencode($_GET['star'])   : '')
487                                 . (!empty($_GET['order'])  ? '&order='  . rawurlencode($_GET['order'])  : '')
488                                 . (!empty($_GET['bmark'])  ? '&bmark='  . rawurlencode($_GET['bmark'])  : '')
489                                 . (!empty($_GET['liked'])  ? '&liked='  . rawurlencode($_GET['liked'])  : '')
490                                 . (!empty($_GET['conv'])   ? '&conv='   . rawurlencode($_GET['conv'])   : '')
491                                 . (!empty($_GET['nets'])   ? '&nets='   . rawurlencode($_GET['nets'])   : '')
492                                 . (!empty($_GET['cmin'])   ? '&cmin='   . rawurlencode($_GET['cmin'])   : '')
493                                 . (!empty($_GET['cmax'])   ? '&cmax='   . rawurlencode($_GET['cmax'])   : '')
494                                 . (!empty($_GET['file'])   ? '&file='   . rawurlencode($_GET['file'])   : '')
495
496                                 . "'; var profile_page = " . $pager->getPage() . "; </script>\r\n";
497                 }
498         } elseif ($mode === 'profile') {
499                 $items = conversation_add_children($items, false, $order, $uid);
500                 $profile_owner = $a->profile['profile_uid'];
501
502                 if (!$update) {
503                         $tab = 'posts';
504                         if (!empty($_GET['tab'])) {
505                                 $tab = Strings::escapeTags(trim($_GET['tab']));
506                         }
507                         if ($tab === 'posts') {
508                                 /*
509                                  * This is ugly, but we can't pass the profile_uid through the session to the ajax updater,
510                                  * because browser prefetching might change it on us. We have to deliver it with the page.
511                                  */
512
513                                 $live_update_div = '<div id="live-profile"></div>' . "\r\n"
514                                         . "<script> var profile_uid = " . $a->profile['profile_uid']
515                                         . "; var netargs = '?f='; var profile_page = " . $pager->getPage() . "; </script>\r\n";
516                         }
517                 }
518         } elseif ($mode === 'notes') {
519                 $items = conversation_add_children($items, false, $order, local_user());
520                 $profile_owner = local_user();
521
522                 if (!$update) {
523                         $live_update_div = '<div id="live-notes"></div>' . "\r\n"
524                                 . "<script> var profile_uid = " . local_user()
525                                 . "; var netargs = '/?f='; var profile_page = " . $pager->getPage() . "; </script>\r\n";
526                 }
527         } elseif ($mode === 'display') {
528                 $items = conversation_add_children($items, false, $order, $uid);
529                 $profile_owner = $a->profile['uid'];
530
531                 if (!$update) {
532                         $live_update_div = '<div id="live-display"></div>' . "\r\n"
533                                 . "<script> var profile_uid = " . Session::get('uid', 0) . ";"
534                                 . " var profile_page = 1; </script>";
535                 }
536         } elseif ($mode === 'community') {
537                 $items = conversation_add_children($items, true, $order, $uid);
538                 $profile_owner = 0;
539
540                 if (!$update) {
541                         $live_update_div = '<div id="live-community"></div>' . "\r\n"
542                                 . "<script> var profile_uid = -1; var netargs = '" . substr(DI::args()->getCommand(), 10)
543                                 ."/?f='; var profile_page = " . $pager->getPage() . "; </script>\r\n";
544                 }
545         } elseif ($mode === 'contacts') {
546                 $items = conversation_add_children($items, false, $order, $uid);
547                 $profile_owner = 0;
548
549                 if (!$update) {
550                         $live_update_div = '<div id="live-contacts"></div>' . "\r\n"
551                                 . "<script> var profile_uid = -1; var netargs = '" . substr(DI::args()->getCommand(), 9)
552                                 ."/?f='; var profile_page = " . $pager->getPage() . "; </script>\r\n";
553                 }
554         } elseif ($mode === 'search') {
555                 $live_update_div = '<div id="live-search"></div>' . "\r\n";
556         }
557
558         $page_dropping = ((local_user() && local_user() == $profile_owner) ? true : false);
559
560         if (!$update) {
561                 $_SESSION['return_path'] = DI::args()->getQueryString();
562         }
563
564         $cb = ['items' => $items, 'mode' => $mode, 'update' => $update, 'preview' => $preview];
565         Hook::callAll('conversation_start',$cb);
566
567         $items = $cb['items'];
568
569         $conv_responses = [
570                 'like' => ['title' => L10n::t('Likes','title')],
571                 'dislike' => ['title' => L10n::t('Dislikes','title')],
572                 'attendyes' => ['title' => L10n::t('Attending','title')],
573                 'attendno' => ['title' => L10n::t('Not attending','title')],
574                 'attendmaybe' => ['title' => L10n::t('Might attend','title')],
575                 'announce' => ['title' => L10n::t('Reshares','title')]
576         ];
577
578         // array with html for each thread (parent+comments)
579         $threads = [];
580         $threadsid = -1;
581
582         $page_template = Renderer::getMarkupTemplate("conversation.tpl");
583
584         if (!empty($items)) {
585                 if (in_array($mode, ['community', 'contacts'])) {
586                         $writable = true;
587                 } else {
588                         $writable = ($items[0]['uid'] == 0) && in_array($items[0]['network'], Protocol::FEDERATED);
589                 }
590
591                 if (!local_user()) {
592                         $writable = false;
593                 }
594
595                 if (in_array($mode, ['network-new', 'search', 'contact-posts'])) {
596
597                         /*
598                          * "New Item View" on network page or search page results
599                          * - just loop through the items and format them minimally for display
600                          */
601
602                         $tpl = 'search_item.tpl';
603
604                         foreach ($items as $item) {
605
606                                 if (!visible_activity($item)) {
607                                         continue;
608                                 }
609
610                                 if (in_array($item['author-id'], $blocklist)) {
611                                         continue;
612                                 }
613
614                                 $threadsid++;
615
616                                 $owner_url   = '';
617                                 $owner_name  = '';
618                                 $sparkle     = '';
619
620                                 // prevent private email from leaking.
621                                 if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) {
622                                         continue;
623                                 }
624
625                                 $profile_name = $item['author-name'];
626                                 if (!empty($item['author-link']) && empty($item['author-name'])) {
627                                         $profile_name = $item['author-link'];
628                                 }
629
630                                 $tags = Term::populateTagsFromItem($item);
631
632                                 $author = ['uid' => 0, 'id' => $item['author-id'],
633                                         'network' => $item['author-network'], 'url' => $item['author-link']];
634                                 $profile_link = Contact::magicLinkByContact($author);
635
636                                 if (strpos($profile_link, 'redir/') === 0) {
637                                         $sparkle = ' sparkle';
638                                 }
639
640                                 $locate = ['location' => $item['location'], 'coord' => $item['coord'], 'html' => ''];
641                                 Hook::callAll('render_location',$locate);
642
643                                 $location = ((strlen($locate['html'])) ? $locate['html'] : render_location_dummy($locate));
644
645                                 localize_item($item);
646                                 if ($mode === 'network-new') {
647                                         $dropping = true;
648                                 } else {
649                                         $dropping = false;
650                                 }
651
652                                 $drop = [
653                                         'dropping' => $dropping,
654                                         'pagedrop' => $page_dropping,
655                                         'select' => L10n::t('Select'),
656                                         'delete' => L10n::t('Delete'),
657                                 ];
658
659                                 $star = false;
660                                 $isstarred = "unstarred";
661
662                                 $lock = false;
663                                 $likebuttons = false;
664
665                                 $body = Item::prepareBody($item, true, $preview);
666
667                                 list($categories, $folders) = DI::contentItem()->determineCategoriesTerms($item);
668
669                                 if (!empty($item['content-warning']) && PConfig::get(local_user(), 'system', 'disable_cw', false)) {
670                                         $title = ucfirst($item['content-warning']);
671                                 } else {
672                                         $title = $item['title'];
673                                 }
674
675                                 $tmp_item = [
676                                         'template' => $tpl,
677                                         'id' => ($preview ? 'P0' : $item['id']),
678                                         'guid' => ($preview ? 'Q0' : $item['guid']),
679                                         'network' => $item['network'],
680                                         'network_name' => ContactSelector::networkToName($item['author-network'], $item['author-link'], $item['network']),
681                                         'network_icon' => ContactSelector::networkToIcon($item['network'], $item['author-link']),
682                                         'linktitle' => L10n::t('View %s\'s profile @ %s', $profile_name, $item['author-link']),
683                                         'profile_url' => $profile_link,
684                                         'item_photo_menu' => item_photo_menu($item),
685                                         'name' => $profile_name,
686                                         'sparkle' => $sparkle,
687                                         'lock' => $lock,
688                                         'thumb' => DI::baseUrl()->remove(ProxyUtils::proxifyUrl($item['author-avatar'], false, ProxyUtils::SIZE_THUMB)),
689                                         'title' => $title,
690                                         'body' => $body,
691                                         'tags' => $tags['tags'],
692                                         'hashtags' => $tags['hashtags'],
693                                         'mentions' => $tags['mentions'],
694                                         'implicit_mentions' => $tags['implicit_mentions'],
695                                         'txt_cats' => L10n::t('Categories:'),
696                                         'txt_folders' => L10n::t('Filed under:'),
697                                         'has_cats' => ((count($categories)) ? 'true' : ''),
698                                         'has_folders' => ((count($folders)) ? 'true' : ''),
699                                         'categories' => $categories,
700                                         'folders' => $folders,
701                                         'text' => strip_tags($body),
702                                         'localtime' => DateTimeFormat::local($item['created'], 'r'),
703                                         'ago' => (($item['app']) ? L10n::t('%s from %s', Temporal::getRelativeDate($item['created']),$item['app']) : Temporal::getRelativeDate($item['created'])),
704                                         'location' => $location,
705                                         'indent' => '',
706                                         'owner_name' => $owner_name,
707                                         'owner_url' => $owner_url,
708                                         'owner_photo' => DI::baseUrl()->remove(ProxyUtils::proxifyUrl($item['owner-avatar'], false, ProxyUtils::SIZE_THUMB)),
709                                         'plink' => Item::getPlink($item),
710                                         'edpost' => false,
711                                         'isstarred' => $isstarred,
712                                         'star' => $star,
713                                         'drop' => $drop,
714                                         'vote' => $likebuttons,
715                                         'like' => '',
716                                         'dislike' => '',
717                                         'comment' => '',
718                                         'conv' => (($preview) ? '' : ['href'=> 'display/'.$item['guid'], 'title'=> L10n::t('View in context')]),
719                                         'previewing' => $previewing,
720                                         'wait' => L10n::t('Please wait'),
721                                         'thread_level' => 1,
722                                 ];
723
724                                 $arr = ['item' => $item, 'output' => $tmp_item];
725                                 Hook::callAll('display_item', $arr);
726
727                                 $threads[$threadsid]['id'] = $item['id'];
728                                 $threads[$threadsid]['network'] = $item['network'];
729                                 $threads[$threadsid]['items'] = [$arr['output']];
730
731                         }
732                 } else {
733                         // Normal View
734                         $page_template = Renderer::getMarkupTemplate("threaded_conversation.tpl");
735
736                         $conv = new Thread($mode, $preview, $writable);
737
738                         /*
739                          * get all the topmost parents
740                          * this shouldn't be needed, as we should have only them in our array
741                          * But for now, this array respects the old style, just in case
742                          */
743                         foreach ($items as $item) {
744                                 if (in_array($item['author-id'], $blocklist)) {
745                                         continue;
746                                 }
747
748                                 // Can we put this after the visibility check?
749                                 builtin_activity_puller($item, $conv_responses);
750
751                                 // Only add what is visible
752                                 if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) {
753                                         continue;
754                                 }
755
756                                 if (!visible_activity($item)) {
757                                         continue;
758                                 }
759
760                                 /// @todo Check if this call is needed or not
761                                 $arr = ['item' => $item];
762                                 Hook::callAll('display_item', $arr);
763
764                                 $item['pagedrop'] = $page_dropping;
765
766                                 if ($item['id'] == $item['parent']) {
767                                         $item_object = new Post($item);
768                                         $conv->addParent($item_object);
769                                 }
770                         }
771
772                         $threads = $conv->getTemplateData($conv_responses);
773                         if (!$threads) {
774                                 Logger::log('[ERROR] conversation : Failed to get template data.', Logger::DEBUG);
775                                 $threads = [];
776                         }
777                 }
778         }
779
780         $o = Renderer::replaceMacros($page_template, [
781                 '$baseurl' => DI::baseUrl()->get($ssl_state),
782                 '$return_path' => DI::args()->getQueryString(),
783                 '$live_update' => $live_update_div,
784                 '$remove' => L10n::t('remove'),
785                 '$mode' => $mode,
786                 '$user' => $a->user,
787                 '$threads' => $threads,
788                 '$dropping' => ($page_dropping ? L10n::t('Delete Selected Items') : False),
789         ]);
790
791         return $o;
792 }
793
794 /**
795  * Fetch all comments from a query. Additionally set the newest resharer as thread owner.
796  *
797  * @param array   $thread_items Database statement with thread posts
798  * @param boolean $pinned       Is the item pinned?
799  *
800  * @return array items with parents and comments
801  */
802 function conversation_fetch_comments($thread_items, $pinned) {
803         $comments = [];
804         $parentlines = [];
805         $lineno = 0;
806         $actor = [];
807         $received = '';
808
809         while ($row = Item::fetch($thread_items)) {
810                 if (($row['verb'] == Activity::ANNOUNCE) && !empty($row['contact-uid']) && ($row['received'] > $received) && ($row['thr-parent'] == $row['parent-uri'])) {
811                         $actor = ['link' => $row['author-link'], 'avatar' => $row['author-avatar'], 'name' => $row['author-name']];
812                         $received = $row['received'];
813                 }
814
815                 if ((($row['gravity'] == GRAVITY_PARENT) && !$row['origin'] && !in_array($row['network'], [Protocol::DIASPORA])) &&
816                         (empty($row['contact-uid']) || !in_array($row['network'], Protocol::NATIVE_SUPPORT))) {
817                         $parentlines[] = $lineno;
818                 }
819
820                 if ($row['gravity'] == GRAVITY_PARENT) {
821                         $row['pinned'] = $pinned;
822                 }
823
824                 $comments[] = $row;
825                 $lineno++;
826         }
827
828         DBA::close($thread_items);
829
830         if (!empty($actor)) {
831                 foreach ($parentlines as $line) {
832                         $comments[$line]['owner-link'] = $actor['link'];
833                         $comments[$line]['owner-avatar'] = $actor['avatar'];
834                         $comments[$line]['owner-name'] = $actor['name'];
835                 }
836         }
837         return $comments;
838 }
839
840 /**
841  * @brief Add comments to top level entries that had been fetched before
842  *
843  * The system will fetch the comments for the local user whenever possible.
844  * This behaviour is currently needed to allow commenting on Friendica posts.
845  *
846  * @param array $parents Parent items
847  *
848  * @param       $block_authors
849  * @param       $order
850  * @param       $uid
851  * @return array items with parents and comments
852  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
853  */
854 function conversation_add_children(array $parents, $block_authors, $order, $uid) {
855         $max_comments = Config::get('system', 'max_comments', 100);
856
857         $params = ['order' => ['uid', 'commented' => true]];
858
859         if ($max_comments > 0) {
860                 $params['limit'] = $max_comments;
861         }
862
863         $items = [];
864
865         foreach ($parents AS $parent) {
866                 $condition = ["`item`.`parent-uri` = ? AND `item`.`uid` IN (0, ?) ",
867                         $parent['uri'], $uid];
868                 if ($block_authors) {
869                         $condition[0] .= "AND NOT `author`.`hidden`";
870                 }
871
872                 $thread_items = Item::selectForUser(local_user(), array_merge(Item::DISPLAY_FIELDLIST, ['contact-uid', 'gravity']), $condition, $params);
873
874                 $comments = conversation_fetch_comments($thread_items, $parent['pinned'] ?? false);
875
876                 if (count($comments) != 0) {
877                         $items = array_merge($items, $comments);
878                 }
879         }
880
881         foreach ($items as $index => $item) {
882                 if ($item['uid'] == 0) {
883                         $items[$index]['writable'] = in_array($item['network'], Protocol::FEDERATED);
884                 }
885         }
886
887         $items = conv_sort($items, $order);
888
889         return $items;
890 }
891
892 function item_photo_menu($item) {
893         $sub_link = '';
894         $poke_link = '';
895         $contact_url = '';
896         $pm_url = '';
897         $status_link = '';
898         $photos_link = '';
899         $posts_link = '';
900         $block_link = '';
901         $ignore_link = '';
902
903         if (local_user() && local_user() == $item['uid'] && $item['parent'] == $item['id'] && !$item['self']) {
904                 $sub_link = 'javascript:dosubthread(' . $item['id'] . '); return false;';
905         }
906
907         $author = ['uid' => 0, 'id' => $item['author-id'],
908                 'network' => $item['author-network'], 'url' => $item['author-link']];
909         $profile_link = Contact::magicLinkByContact($author, $item['author-link']);
910         $sparkle = (strpos($profile_link, 'redir/') === 0);
911
912         $cid = 0;
913         $pcid = Contact::getIdForURL($item['author-link'], 0, true);
914         $network = '';
915         $rel = 0;
916         $condition = ['uid' => local_user(), 'nurl' => Strings::normaliseLink($item['author-link'])];
917         $contact = DBA::selectFirst('contact', ['id', 'network', 'rel'], $condition);
918         if (DBA::isResult($contact)) {
919                 $cid = $contact['id'];
920                 $network = $contact['network'];
921                 $rel = $contact['rel'];
922         }
923
924         if ($sparkle) {
925                 $status_link = $profile_link . '?tab=status';
926                 $photos_link = str_replace('/profile/', '/photos/', $profile_link);
927                 $profile_link = $profile_link . '?=profile';
928         }
929
930         if (!empty($pcid)) {
931                 $contact_url = 'contact/' . $pcid;
932                 $posts_link = 'contact/' . $pcid . '/posts';
933                 $block_link = 'contact/' . $pcid . '/block';
934                 $ignore_link = 'contact/' . $pcid . '/ignore';
935         }
936
937         if ($cid && !$item['self']) {
938                 $poke_link = 'poke/?f=&c=' . $cid;
939                 $contact_url = 'contact/' . $cid;
940                 $posts_link = 'contact/' . $cid . '/posts';
941
942                 if (in_array($network, [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA])) {
943                         $pm_url = 'message/new/' . $cid;
944                 }
945         }
946
947         if (local_user()) {
948                 $menu = [
949                         L10n::t('Follow Thread') => $sub_link,
950                         L10n::t('View Status') => $status_link,
951                         L10n::t('View Profile') => $profile_link,
952                         L10n::t('View Photos') => $photos_link,
953                         L10n::t('Network Posts') => $posts_link,
954                         L10n::t('View Contact') => $contact_url,
955                         L10n::t('Send PM') => $pm_url,
956                         L10n::t('Block') => $block_link,
957                         L10n::t('Ignore') => $ignore_link
958                 ];
959
960                 if ($network == Protocol::DFRN) {
961                         $menu[L10n::t("Poke")] = $poke_link;
962                 }
963
964                 if ((($cid == 0) || ($rel == Contact::FOLLOWER)) &&
965                         in_array($item['network'], Protocol::FEDERATED)) {
966                         $menu[L10n::t('Connect/Follow')] = 'follow?url=' . urlencode($item['author-link']);
967                 }
968         } else {
969                 $menu = [L10n::t('View Profile') => $item['author-link']];
970         }
971
972         $args = ['item' => $item, 'menu' => $menu];
973
974         Hook::callAll('item_photo_menu', $args);
975
976         $menu = $args['menu'];
977
978         $o = '';
979         foreach ($menu as $k => $v) {
980                 if (strpos($v, 'javascript:') === 0) {
981                         $v = substr($v, 11);
982                         $o .= '<li role="menuitem"><a onclick="' . $v . '">' . $k . '</a></li>' . PHP_EOL;
983                 } elseif ($v!='') {
984                         $o .= '<li role="menuitem"><a href="' . $v . '">' . $k . '</a></li>' . PHP_EOL;
985                 }
986         }
987         return $o;
988 }
989
990 /**
991  * @brief Checks item to see if it is one of the builtin activities (like/dislike, event attendance, consensus items, etc.)
992  * Increments the count of each matching activity and adds a link to the author as needed.
993  *
994  * @param array  $item
995  * @param array &$conv_responses (already created with builtin activity structure)
996  * @return void
997  * @throws ImagickException
998  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
999  */
1000 function builtin_activity_puller($item, &$conv_responses) {
1001         foreach ($conv_responses as $mode => $v) {
1002                 $sparkle = '';
1003
1004                 switch ($mode) {
1005                         case 'like':
1006                                 $verb = Activity::LIKE;
1007                                 break;
1008                         case 'dislike':
1009                                 $verb = Activity::DISLIKE;
1010                                 break;
1011                         case 'attendyes':
1012                                 $verb = Activity::ATTEND;
1013                                 break;
1014                         case 'attendno':
1015                                 $verb = Activity::ATTENDNO;
1016                                 break;
1017                         case 'attendmaybe':
1018                                 $verb = Activity::ATTENDMAYBE;
1019                                 break;
1020                         case 'announce':
1021                                 $verb = Activity::ANNOUNCE;
1022                                 break;
1023                         default:
1024                                 return;
1025                 }
1026
1027                 if (!empty($item['verb']) && DI::activity()->match($item['verb'], $verb) && ($item['id'] != $item['parent'])) {
1028                         $author = ['uid' => 0, 'id' => $item['author-id'],
1029                                 'network' => $item['author-network'], 'url' => $item['author-link']];
1030                         $url = Contact::magicLinkByContact($author);
1031                         if (strpos($url, 'redir/') === 0) {
1032                                 $sparkle = ' class="sparkle" ';
1033                         }
1034
1035                         $url = '<a href="'. $url . '"'. $sparkle .'>' . htmlentities($item['author-name']) . '</a>';
1036
1037                         if (empty($item['thr-parent'])) {
1038                                 $item['thr-parent'] = $item['parent-uri'];
1039                         }
1040
1041                         if (!(isset($conv_responses[$mode][$item['thr-parent'] . '-l'])
1042                                 && is_array($conv_responses[$mode][$item['thr-parent'] . '-l']))) {
1043                                 $conv_responses[$mode][$item['thr-parent'] . '-l'] = [];
1044                         }
1045
1046                         // only list each unique author once
1047                         if (in_array($url,$conv_responses[$mode][$item['thr-parent'] . '-l'])) {
1048                                 continue;
1049                         }
1050
1051                         if (!isset($conv_responses[$mode][$item['thr-parent']])) {
1052                                 $conv_responses[$mode][$item['thr-parent']] = 1;
1053                         } else {
1054                                 $conv_responses[$mode][$item['thr-parent']] ++;
1055                         }
1056
1057                         if (public_contact() == $item['author-id']) {
1058                                 $conv_responses[$mode][$item['thr-parent'] . '-self'] = 1;
1059                         }
1060
1061                         $conv_responses[$mode][$item['thr-parent'] . '-l'][] = $url;
1062
1063                         // there can only be one activity verb per item so if we found anything, we can stop looking
1064                         return;
1065                 }
1066         }
1067 }
1068
1069 /**
1070  * Format the vote text for a profile item
1071  *
1072  * @param int    $cnt  = number of people who vote the item
1073  * @param array  $arr  = array of pre-linked names of likers/dislikers
1074  * @param string $type = one of 'like, 'dislike', 'attendyes', 'attendno', 'attendmaybe'
1075  * @param int    $id   = item id
1076  * @return string formatted text
1077  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1078  */
1079 function format_like($cnt, array $arr, $type, $id) {
1080         $o = '';
1081         $expanded = '';
1082         $phrase = '';
1083
1084         if ($cnt == 1) {
1085                 $likers = $arr[0];
1086
1087                 // Phrase if there is only one liker. In other cases it will be uses for the expanded
1088                 // list which show all likers
1089                 switch ($type) {
1090                         case 'like' :
1091                                 $phrase = L10n::t('%s likes this.', $likers);
1092                                 break;
1093                         case 'dislike' :
1094                                 $phrase = L10n::t('%s doesn\'t like this.', $likers);
1095                                 break;
1096                         case 'attendyes' :
1097                                 $phrase = L10n::t('%s attends.', $likers);
1098                                 break;
1099                         case 'attendno' :
1100                                 $phrase = L10n::t('%s doesn\'t attend.', $likers);
1101                                 break;
1102                         case 'attendmaybe' :
1103                                 $phrase = L10n::t('%s attends maybe.', $likers);
1104                                 break;
1105                         case 'announce' :
1106                                 $phrase = L10n::t('%s reshared this.', $likers);
1107                                 break;
1108                 }
1109         }
1110
1111         if ($cnt > 1) {
1112                 $total = count($arr);
1113                 if ($total < MAX_LIKERS) {
1114                         $last = L10n::t('and') . ' ' . $arr[count($arr)-1];
1115                         $arr2 = array_slice($arr, 0, -1);
1116                         $likers = implode(', ', $arr2) . ' ' . $last;
1117                 } else  {
1118                         $arr = array_slice($arr, 0, MAX_LIKERS - 1);
1119                         $likers = implode(', ', $arr);
1120                         $likers .= L10n::t('and %d other people', $total - MAX_LIKERS);
1121                 }
1122
1123                 $spanatts = "class=\"fakelink\" onclick=\"openClose('{$type}list-$id');\"";
1124
1125                 $explikers = '';
1126                 switch ($type) {
1127                         case 'like':
1128                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> like this', $spanatts, $cnt);
1129                                 $explikers = L10n::t('%s like this.', $likers);
1130                                 break;
1131                         case 'dislike':
1132                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> don\'t like this', $spanatts, $cnt);
1133                                 $explikers = L10n::t('%s don\'t like this.', $likers);
1134                                 break;
1135                         case 'attendyes':
1136                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> attend', $spanatts, $cnt);
1137                                 $explikers = L10n::t('%s attend.', $likers);
1138                                 break;
1139                         case 'attendno':
1140                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> don\'t attend', $spanatts, $cnt);
1141                                 $explikers = L10n::t('%s don\'t attend.', $likers);
1142                                 break;
1143                         case 'attendmaybe':
1144                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> attend maybe', $spanatts, $cnt);
1145                                 $explikers = L10n::t('%s attend maybe.', $likers);
1146                                 break;
1147                         case 'announce':
1148                                 $phrase = L10n::t('<span  %1$s>%2$d people</span> reshared this', $spanatts, $cnt);
1149                                 $explikers = L10n::t('%s reshared this.', $likers);
1150                                 break;
1151                 }
1152
1153                 $expanded .= "\t" . '<p class="wall-item-' . $type . '-expanded" id="' . $type . 'list-' . $id . '" style="display: none;" >' . $explikers . EOL . '</p>';
1154         }
1155
1156         $o .= Renderer::replaceMacros(Renderer::getMarkupTemplate('voting_fakelink.tpl'), [
1157                 '$phrase' => $phrase,
1158                 '$type' => $type,
1159                 '$id' => $id
1160         ]);
1161         $o .= $expanded;
1162
1163         return $o;
1164 }
1165
1166 function status_editor(App $a, $x, $notes_cid = 0, $popup = false)
1167 {
1168         $o = '';
1169
1170         $geotag = !empty($x['allow_location']) ? Renderer::replaceMacros(Renderer::getMarkupTemplate('jot_geotag.tpl'), []) : '';
1171
1172         $tpl = Renderer::getMarkupTemplate('jot-header.tpl');
1173         DI::page()['htmlhead'] .= Renderer::replaceMacros($tpl, [
1174                 '$newpost'   => 'true',
1175                 '$baseurl'   => DI::baseUrl()->get(true),
1176                 '$geotag'    => $geotag,
1177                 '$nickname'  => $x['nickname'],
1178                 '$ispublic'  => L10n::t('Visible to <strong>everybody</strong>'),
1179                 '$linkurl'   => L10n::t('Please enter a image/video/audio/webpage URL:'),
1180                 '$term'      => L10n::t('Tag term:'),
1181                 '$fileas'    => L10n::t('Save to Folder:'),
1182                 '$whereareu' => L10n::t('Where are you right now?'),
1183                 '$delitems'  => L10n::t("Delete item\x28s\x29?")
1184         ]);
1185
1186         $jotplugins = '';
1187         Hook::callAll('jot_tool', $jotplugins);
1188
1189         // Private/public post links for the non-JS ACL form
1190         $private_post = 1;
1191         if (!empty($_REQUEST['public'])) {
1192                 $private_post = 0;
1193         }
1194
1195         $query_str = DI::args()->getQueryString();
1196         if (strpos($query_str, 'public=1') !== false) {
1197                 $query_str = str_replace(['?public=1', '&public=1'], ['', ''], $query_str);
1198         }
1199
1200         /*
1201          * I think $a->query_string may never have ? in it, but I could be wrong
1202          * It looks like it's from the index.php?q=[etc] rewrite that the web
1203          * server does, which converts any ? to &, e.g. suggest&ignore=61 for suggest?ignore=61
1204          */
1205         if (strpos($query_str, '?') === false) {
1206                 $public_post_link = '?public=1';
1207         } else {
1208                 $public_post_link = '&public=1';
1209         }
1210
1211         // $tpl = Renderer::replaceMacros($tpl,array('$jotplugins' => $jotplugins));
1212         $tpl = Renderer::getMarkupTemplate("jot.tpl");
1213
1214         $o .= Renderer::replaceMacros($tpl,[
1215                 '$new_post' => L10n::t('New Post'),
1216                 '$return_path'  => $query_str,
1217                 '$action'       => 'item',
1218                 '$share'        => ($x['button'] ?? '') ?: L10n::t('Share'),
1219                 '$upload'       => L10n::t('Upload photo'),
1220                 '$shortupload'  => L10n::t('upload photo'),
1221                 '$attach'       => L10n::t('Attach file'),
1222                 '$shortattach'  => L10n::t('attach file'),
1223                 '$edbold'       => L10n::t('Bold'),
1224                 '$editalic'     => L10n::t('Italic'),
1225                 '$eduline'      => L10n::t('Underline'),
1226                 '$edquote'      => L10n::t('Quote'),
1227                 '$edcode'       => L10n::t('Code'),
1228                 '$edimg'        => L10n::t('Image'),
1229                 '$edurl'        => L10n::t('Link'),
1230                 '$edattach'     => L10n::t('Link or Media'),
1231                 '$setloc'       => L10n::t('Set your location'),
1232                 '$shortsetloc'  => L10n::t('set location'),
1233                 '$noloc'        => L10n::t('Clear browser location'),
1234                 '$shortnoloc'   => L10n::t('clear location'),
1235                 '$title'        => $x['title'] ?? '',
1236                 '$placeholdertitle' => L10n::t('Set title'),
1237                 '$category'     => $x['category'] ?? '',
1238                 '$placeholdercategory' => Feature::isEnabled(local_user(), 'categories') ? L10n::t("Categories \x28comma-separated list\x29") : '',
1239                 '$wait'         => L10n::t('Please wait'),
1240                 '$permset'      => L10n::t('Permission settings'),
1241                 '$shortpermset' => L10n::t('permissions'),
1242                 '$wall'         => $notes_cid ? 0 : 1,
1243                 '$posttype'     => $notes_cid ? Item::PT_PERSONAL_NOTE : Item::PT_ARTICLE,
1244                 '$content'      => $x['content'] ?? '',
1245                 '$post_id'      => $x['post_id'] ?? '',
1246                 '$baseurl'      => DI::baseUrl()->get(true),
1247                 '$defloc'       => $x['default_location'],
1248                 '$visitor'      => $x['visitor'],
1249                 '$pvisit'       => $notes_cid ? 'none' : $x['visitor'],
1250                 '$public'       => L10n::t('Public post'),
1251                 '$lockstate'    => $x['lockstate'],
1252                 '$bang'         => $x['bang'],
1253                 '$profile_uid'  => $x['profile_uid'],
1254                 '$preview'      => L10n::t('Preview'),
1255                 '$jotplugins'   => $jotplugins,
1256                 '$notes_cid'    => $notes_cid,
1257                 '$sourceapp'    => L10n::t($a->sourcename),
1258                 '$cancel'       => L10n::t('Cancel'),
1259                 '$rand_num'     => Crypto::randomDigits(12),
1260
1261                 // ACL permissions box
1262                 '$acl'           => $x['acl'],
1263                 '$group_perms'   => L10n::t('Post to Groups'),
1264                 '$contact_perms' => L10n::t('Post to Contacts'),
1265                 '$private'       => L10n::t('Private post'),
1266                 '$is_private'    => $private_post,
1267                 '$public_link'   => $public_post_link,
1268
1269                 //jot nav tab (used in some themes)
1270                 '$message' => L10n::t('Message'),
1271                 '$browser' => L10n::t('Browser'),
1272         ]);
1273
1274
1275         if ($popup == true) {
1276                 $o = '<div id="jot-popup" style="display: none;">' . $o . '</div>';
1277         }
1278
1279         return $o;
1280 }
1281
1282 /**
1283  * Plucks the children of the given parent from a given item list.
1284  *
1285  * @brief Plucks all the children in the given item list of the given parent
1286  *
1287  * @param array $item_list
1288  * @param array $parent
1289  * @param bool  $recursive
1290  * @return array
1291  */
1292 function get_item_children(array &$item_list, array $parent, $recursive = true)
1293 {
1294         $children = [];
1295         foreach ($item_list as $i => $item) {
1296                 if ($item['id'] != $item['parent']) {
1297                         if ($recursive) {
1298                                 // Fallback to parent-uri if thr-parent is not set
1299                                 $thr_parent = $item['thr-parent'];
1300                                 if ($thr_parent == '') {
1301                                         $thr_parent = $item['parent-uri'];
1302                                 }
1303
1304                                 if ($thr_parent == $parent['uri']) {
1305                                         $item['children'] = get_item_children($item_list, $item);
1306                                         $children[] = $item;
1307                                         unset($item_list[$i]);
1308                                 }
1309                         } elseif ($item['parent'] == $parent['id']) {
1310                                 $children[] = $item;
1311                                 unset($item_list[$i]);
1312                         }
1313                 }
1314         }
1315         return $children;
1316 }
1317
1318 /**
1319  * @brief Recursively sorts a tree-like item array
1320  *
1321  * @param array $items
1322  * @return array
1323  */
1324 function sort_item_children(array $items)
1325 {
1326         $result = $items;
1327         usort($result, 'sort_thr_received_rev');
1328         foreach ($result as $k => $i) {
1329                 if (isset($result[$k]['children'])) {
1330                         $result[$k]['children'] = sort_item_children($result[$k]['children']);
1331                 }
1332         }
1333         return $result;
1334 }
1335
1336 /**
1337  * @brief Recursively add all children items at the top level of a list
1338  *
1339  * @param array $children List of items to append
1340  * @param array $item_list
1341  */
1342 function add_children_to_list(array $children, array &$item_list)
1343 {
1344         foreach ($children as $child) {
1345                 $item_list[] = $child;
1346                 if (isset($child['children'])) {
1347                         add_children_to_list($child['children'], $item_list);
1348                 }
1349         }
1350 }
1351
1352 /**
1353  * This recursive function takes the item tree structure created by conv_sort() and
1354  * flatten the extraneous depth levels when people reply sequentially, removing the
1355  * stairs effect in threaded conversations limiting the available content width.
1356  *
1357  * The basic principle is the following: if a post item has only one reply and is
1358  * the last reply of its parent, then the reply is moved to the parent.
1359  *
1360  * This process is rendered somewhat more complicated because items can be either
1361  * replies or likes, and these don't factor at all in the reply count/last reply.
1362  *
1363  * @brief Selectively flattens a tree-like item structure to prevent threading stairs
1364  *
1365  * @param array $parent A tree-like array of items
1366  * @return array
1367  */
1368 function smart_flatten_conversation(array $parent)
1369 {
1370         if (!isset($parent['children']) || count($parent['children']) == 0) {
1371                 return $parent;
1372         }
1373
1374         // We use a for loop to ensure we process the newly-moved items
1375         for ($i = 0; $i < count($parent['children']); $i++) {
1376                 $child = $parent['children'][$i];
1377
1378                 if (isset($child['children']) && count($child['children'])) {
1379                         // This helps counting only the regular posts
1380                         $count_post_closure = function($var) {
1381                                 return $var['verb'] === Activity::POST;
1382                         };
1383
1384                         $child_post_count = count(array_filter($child['children'], $count_post_closure));
1385
1386                         $remaining_post_count = count(array_filter(array_slice($parent['children'], $i), $count_post_closure));
1387
1388                         // If there's only one child's children post and this is the last child post
1389                         if ($child_post_count == 1 && $remaining_post_count == 1) {
1390
1391                                 // Searches the post item in the children
1392                                 $j = 0;
1393                                 while($child['children'][$j]['verb'] !== Activity::POST && $j < count($child['children'])) {
1394                                         $j ++;
1395                                 }
1396
1397                                 $moved_item = $child['children'][$j];
1398                                 unset($parent['children'][$i]['children'][$j]);
1399                                 $parent['children'][] = $moved_item;
1400                         } else {
1401                                 $parent['children'][$i] = smart_flatten_conversation($child);
1402                         }
1403                 }
1404         }
1405
1406         return $parent;
1407 }
1408
1409
1410 /**
1411  * Expands a flat list of items into corresponding tree-like conversation structures,
1412  * sort the top-level posts either on "received" or "commented", and finally
1413  * append all the items at the top level (???)
1414  *
1415  * @brief Expands a flat item list into a conversation array for display
1416  *
1417  * @param array  $item_list A list of items belonging to one or more conversations
1418  * @param string $order     Either on "received" or "commented"
1419  * @return array
1420  * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1421  */
1422 function conv_sort(array $item_list, $order)
1423 {
1424         $parents = [];
1425
1426         if (!(is_array($item_list) && count($item_list))) {
1427                 return $parents;
1428         }
1429
1430         $blocklist = conv_get_blocklist();
1431
1432         $item_array = [];
1433
1434         // Dedupes the item list on the uri to prevent infinite loops
1435         foreach ($item_list as $item) {
1436                 if (in_array($item['author-id'], $blocklist)) {
1437                         continue;
1438                 }
1439
1440                 $item_array[$item['uri']] = $item;
1441         }
1442
1443         // Extract the top level items
1444         foreach ($item_array as $item) {
1445                 if ($item['id'] == $item['parent']) {
1446                         $parents[] = $item;
1447                 }
1448         }
1449
1450         if (stristr($order, 'pinned_received')) {
1451                 usort($parents, 'sort_thr_pinned_received');
1452         } elseif (stristr($order, 'received')) {
1453                 usort($parents, 'sort_thr_received');
1454         } elseif (stristr($order, 'commented')) {
1455                 usort($parents, 'sort_thr_commented');
1456         }
1457
1458         /*
1459          * Plucks children from the item_array, second pass collects eventual orphan
1460          * items and add them as children of their top-level post.
1461          */
1462         foreach ($parents as $i => $parent) {
1463                 $parents[$i]['children'] =
1464                         array_merge(get_item_children($item_array, $parent, true),
1465                                 get_item_children($item_array, $parent, false));
1466         }
1467
1468         foreach ($parents as $i => $parent) {
1469                 $parents[$i]['children'] = sort_item_children($parents[$i]['children']);
1470         }
1471
1472         if (!PConfig::get(local_user(), 'system', 'no_smart_threading', 0)) {
1473                 foreach ($parents as $i => $parent) {
1474                         $parents[$i] = smart_flatten_conversation($parent);
1475                 }
1476         }
1477
1478         /// @TODO: Stop recusrsively adding all children back to the top level (!!!)
1479         /// However, this apparently ensures responses (likes, attendance) display (?!)
1480         foreach ($parents as $parent) {
1481                 if (count($parent['children'])) {
1482                         add_children_to_list($parent['children'], $parents);
1483                 }
1484         }
1485
1486         return $parents;
1487 }
1488
1489 /**
1490  * @brief usort() callback to sort item arrays by pinned and the received key
1491  *
1492  * @param array $a
1493  * @param array $b
1494  * @return int
1495  */
1496 function sort_thr_pinned_received(array $a, array $b)
1497 {
1498         if ($b['pinned'] && !$a['pinned']) {
1499                 return 1;
1500         } elseif (!$b['pinned'] && $a['pinned']) {
1501                 return -1;
1502         }
1503
1504         return strcmp($b['received'], $a['received']);
1505 }
1506
1507 /**
1508  * @brief usort() callback to sort item arrays by the received key
1509  *
1510  * @param array $a
1511  * @param array $b
1512  * @return int
1513  */
1514 function sort_thr_received(array $a, array $b)
1515 {
1516         return strcmp($b['received'], $a['received']);
1517 }
1518
1519 /**
1520  * @brief usort() callback to reverse sort item arrays by the received key
1521  *
1522  * @param array $a
1523  * @param array $b
1524  * @return int
1525  */
1526 function sort_thr_received_rev(array $a, array $b)
1527 {
1528         return strcmp($a['received'], $b['received']);
1529 }
1530
1531 /**
1532  * @brief usort() callback to sort item arrays by the commented key
1533  *
1534  * @param array $a
1535  * @param array $b
1536  * @return int
1537  */
1538 function sort_thr_commented(array $a, array $b)
1539 {
1540         return strcmp($b['commented'], $a['commented']);
1541 }
1542
1543 function render_location_dummy(array $item) {
1544         if (!empty($item['location']) && !empty($item['location'])) {
1545                 return $item['location'];
1546         }
1547
1548         if (!empty($item['coord']) && !empty($item['coord'])) {
1549                 return $item['coord'];
1550         }
1551 }
1552
1553 function get_responses(array $conv_responses, array $response_verbs, array $item, Post $ob = null) {
1554         $ret = [];
1555         foreach ($response_verbs as $v) {
1556                 $ret[$v] = [];
1557                 $ret[$v]['count'] = $conv_responses[$v][$item['uri']] ?? 0;
1558                 $ret[$v]['list']  = $conv_responses[$v][$item['uri'] . '-l'] ?? [];
1559                 $ret[$v]['self']  = $conv_responses[$v][$item['uri'] . '-self'] ?? '0';
1560                 if (count($ret[$v]['list']) > MAX_LIKERS) {
1561                         $ret[$v]['list_part'] = array_slice($ret[$v]['list'], 0, MAX_LIKERS);
1562                         array_push($ret[$v]['list_part'], '<a href="#" data-toggle="modal" data-target="#' . $v . 'Modal-'
1563                                 . (($ob) ? $ob->getId() : $item['id']) . '"><b>' . L10n::t('View all') . '</b></a>');
1564                 } else {
1565                         $ret[$v]['list_part'] = '';
1566                 }
1567                 $ret[$v]['button'] = get_response_button_text($v, $ret[$v]['count']);
1568                 $ret[$v]['title'] = $conv_responses[$v]['title'];
1569         }
1570
1571         $count = 0;
1572         foreach ($ret as $key) {
1573                 if ($key['count'] == true) {
1574                         $count++;
1575                 }
1576         }
1577         $ret['count'] = $count;
1578
1579         return $ret;
1580 }
1581
1582 function get_response_button_text($v, $count)
1583 {
1584         $return = '';
1585         switch ($v) {
1586                 case 'like':
1587                         $return = L10n::tt('Like', 'Likes', $count);
1588                         break;
1589                 case 'dislike':
1590                         $return = L10n::tt('Dislike', 'Dislikes', $count);
1591                         break;
1592                 case 'attendyes':
1593                         $return = L10n::tt('Attending', 'Attending', $count);
1594                         break;
1595                 case 'attendno':
1596                         $return = L10n::tt('Not Attending', 'Not Attending', $count);
1597                         break;
1598                 case 'attendmaybe':
1599                         $return = L10n::tt('Undecided', 'Undecided', $count);
1600                         break;
1601         }
1602
1603         return $return;
1604 }