]> git.mxchange.org Git - friendica.git/blob - include/items.php
added curly braces
[friendica.git] / include / items.php
1 <?php
2
3 /**
4  * @file include/items.php
5  */
6
7 use \Friendica\ParseUrl;
8
9 require_once 'include/bbcode.php';
10 require_once 'include/oembed.php';
11 require_once 'include/salmon.php';
12 require_once 'include/crypto.php';
13 require_once 'include/Photo.php';
14 require_once 'include/tags.php';
15 require_once 'include/files.php';
16 require_once 'include/text.php';
17 require_once 'include/email.php';
18 require_once 'include/threads.php';
19 require_once 'include/socgraph.php';
20 require_once 'include/plaintext.php';
21 require_once 'include/ostatus.php';
22 require_once 'include/feed.php';
23 require_once 'include/Contact.php';
24 require_once 'mod/share.php';
25 require_once 'include/enotify.php';
26 require_once 'include/dfrn.php';
27 require_once 'include/group.php';
28
29 /// @TODO one day with composer autploader no more needed
30 require_once 'library/defuse/php-encryption-1.2.1/Crypto.php';
31
32 function construct_verb($item) {
33         if ($item['verb']) {
34                 return $item['verb'];
35         }
36         return ACTIVITY_POST;
37 }
38
39 /* limit_body_size()
40  *
41  *              The purpose of this function is to apply system message length limits to
42  *              imported messages without including any embedded photos in the length
43  */
44 if (! function_exists('limit_body_size')) {
45 function limit_body_size($body) {
46
47 //      logger('limit_body_size: start', LOGGER_DEBUG);
48
49         $maxlen = get_max_import_size();
50
51         // If the length of the body, including the embedded images, is smaller
52         // than the maximum, then don't waste time looking for the images
53         if ($maxlen && (strlen($body) > $maxlen)) {
54
55                 logger('limit_body_size: the total body length exceeds the limit', LOGGER_DEBUG);
56
57                 $orig_body = $body;
58                 $new_body = '';
59                 $textlen = 0;
60                 $max_found = false;
61
62                 $img_start = strpos($orig_body, '[img');
63                 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
64                 $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
65                 while (($img_st_close !== false) && ($img_end !== false)) {
66
67                         $img_st_close++; // make it point to AFTER the closing bracket
68                         $img_end += $img_start;
69                         $img_end += strlen('[/img]');
70
71                         if (! strcmp(substr($orig_body, $img_start + $img_st_close, 5), 'data:')) {
72                                 // This is an embedded image
73
74                                 if (($textlen + $img_start) > $maxlen ) {
75                                         if ($textlen < $maxlen) {
76                                                 logger('limit_body_size: the limit happens before an embedded image', LOGGER_DEBUG);
77                                                 $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
78                                                 $textlen = $maxlen;
79                                         }
80                                 } else {
81                                         $new_body = $new_body . substr($orig_body, 0, $img_start);
82                                         $textlen += $img_start;
83                                 }
84
85                                 $new_body = $new_body . substr($orig_body, $img_start, $img_end - $img_start);
86                         } else {
87
88                                 if (($textlen + $img_end) > $maxlen ) {
89                                         if ($textlen < $maxlen) {
90                                                 logger('limit_body_size: the limit happens before the end of a non-embedded image', LOGGER_DEBUG);
91                                                 $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
92                                                 $textlen = $maxlen;
93                                         }
94                                 } else {
95                                         $new_body = $new_body . substr($orig_body, 0, $img_end);
96                                         $textlen += $img_end;
97                                 }
98                         }
99                         $orig_body = substr($orig_body, $img_end);
100
101                         if ($orig_body === false) {
102                                 // in case the body ends on a closing image tag
103                                 $orig_body = '';
104                         }
105
106                         $img_start = strpos($orig_body, '[img');
107                         $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
108                         $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
109                 }
110
111                 if (($textlen + strlen($orig_body)) > $maxlen) {
112                         if ($textlen < $maxlen) {
113                                 logger('limit_body_size: the limit happens after the end of the last image', LOGGER_DEBUG);
114                                 $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
115                                 $textlen = $maxlen;
116                         }
117                 } else {
118                         logger('limit_body_size: the text size with embedded images extracted did not violate the limit', LOGGER_DEBUG);
119                         $new_body = $new_body . $orig_body;
120                         $textlen += strlen($orig_body);
121                 }
122
123                 return $new_body;
124         } else {
125                 return $body;
126         }
127 }}
128
129 function title_is_body($title, $body) {
130
131         $title = strip_tags($title);
132         $title = trim($title);
133         $title = html_entity_decode($title, ENT_QUOTES, 'UTF-8');
134         $title = str_replace(array("\n", "\r", "\t", " "), array("", "", "", ""), $title);
135
136         $body = strip_tags($body);
137         $body = trim($body);
138         $body = html_entity_decode($body, ENT_QUOTES, 'UTF-8');
139         $body = str_replace(array("\n", "\r", "\t", " "), array("", "", "", ""), $body);
140
141         if (strlen($title) < strlen($body))
142                 $body = substr($body, 0, strlen($title));
143
144         if (($title != $body) and (substr($title, -3) == "...")) {
145                 $pos = strrpos($title, "...");
146                 if ($pos > 0) {
147                         $title = substr($title, 0, $pos);
148                         $body = substr($body, 0, $pos);
149                 }
150         }
151
152         return($title == $body);
153 }
154
155 function add_page_info_data($data) {
156         call_hooks('page_info_data', $data);
157
158         // It maybe is a rich content, but if it does have everything that a link has,
159         // then treat it that way
160         if (($data["type"] == "rich") AND is_string($data["title"]) AND
161                 is_string($data["text"]) AND (sizeof($data["images"]) > 0)) {
162                 $data["type"] = "link";
163         }
164
165         if ((($data["type"] != "link") AND ($data["type"] != "video") AND ($data["type"] != "photo")) OR ($data["title"] == $data["url"])) {
166                 return "";
167         }
168
169         if ($no_photos AND ($data["type"] == "photo")) {
170                 return "";
171         }
172
173         if (sizeof($data["images"]) > 0) {
174                 $preview = $data["images"][0];
175         } else {
176                 $preview = "";
177         }
178
179         // Escape some bad characters
180         $data["url"] = str_replace(array("[", "]"), array("&#91;", "&#93;"), htmlentities($data["url"], ENT_QUOTES, 'UTF-8', false));
181         $data["title"] = str_replace(array("[", "]"), array("&#91;", "&#93;"), htmlentities($data["title"], ENT_QUOTES, 'UTF-8', false));
182
183         $text = "[attachment type='".$data["type"]."'";
184
185         if ($data["text"] == "") {
186                 $data["text"] = $data["title"];
187         }
188
189         if ($data["text"] == "") {
190                 $data["text"] = $data["url"];
191         }
192
193         if ($data["url"] != "") {
194                 $text .= " url='".$data["url"]."'";
195         }
196
197         if ($data["title"] != "") {
198                 $text .= " title='".$data["title"]."'";
199         }
200
201         if (sizeof($data["images"]) > 0) {
202                 $preview = str_replace(array("[", "]"), array("&#91;", "&#93;"), htmlentities($data["images"][0]["src"], ENT_QUOTES, 'UTF-8', false));
203                 // if the preview picture is larger than 500 pixels then show it in a larger mode
204                 // But only, if the picture isn't higher than large (To prevent huge posts)
205                 if (($data["images"][0]["width"] >= 500) AND ($data["images"][0]["width"] >= $data["images"][0]["height"])) {
206                         $text .= " image='".$preview."'";
207                 } else {
208                         $text .= " preview='".$preview."'";
209                 }
210         }
211
212         $text .= "]".$data["text"]."[/attachment]";
213
214         $hashtags = "";
215         if (isset($data["keywords"]) AND count($data["keywords"])) {
216                 $hashtags = "\n";
217                 foreach ($data["keywords"] AS $keyword) {
218                         /// @todo make a positive list of allowed characters
219                         $hashtag = str_replace(array(" ", "+", "/", ".", "#", "'", "’", "`", "(", ")", "„", "“"),
220                                                 array("", "", "", "", "", "", "", "", "", "", "", ""), $keyword);
221                         $hashtags .= "#[url=".App::get_baseurl()."/search?tag=".rawurlencode($hashtag)."]".$hashtag."[/url] ";
222                 }
223         }
224
225         return "\n".$text.$hashtags;
226 }
227
228 function query_page_info($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
229
230         $data = ParseUrl::getSiteinfoCached($url, true);
231
232         if ($photo != "") {
233                 $data["images"][0]["src"] = $photo;
234         }
235
236         logger('fetch page info for '.$url.' '.print_r($data, true), LOGGER_DEBUG);
237
238         if (!$keywords AND isset($data["keywords"])) {
239                 unset($data["keywords"]);
240         }
241
242         if (($keyword_blacklist != "") AND isset($data["keywords"])) {
243                 $list = explode(", ", $keyword_blacklist);
244                 foreach ($list AS $keyword) {
245                         $keyword = trim($keyword);
246                         $index = array_search($keyword, $data["keywords"]);
247                         if ($index !== false) {
248                                 unset($data["keywords"][$index]);
249                         }
250                 }
251         }
252
253         return($data);
254 }
255
256 function add_page_keywords($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
257         $data = query_page_info($url, $no_photos, $photo, $keywords, $keyword_blacklist);
258
259         $tags = "";
260         if (isset($data["keywords"]) AND count($data["keywords"])) {
261                 foreach ($data["keywords"] AS $keyword) {
262                         $hashtag = str_replace(array(" ", "+", "/", ".", "#", "'"),
263                                 array("", "", "", "", "", ""), $keyword);
264
265                         if ($tags != "") {
266                                 $tags .= ", ";
267                         }
268
269                         $tags .= "#[url=" . App::get_baseurl() . "/search?tag=" . rawurlencode($hashtag) . "]" . $hashtag . "[/url]";
270                 }
271         }
272
273         return($tags);
274 }
275
276 function add_page_info($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
277         $data = query_page_info($url, $no_photos, $photo, $keywords, $keyword_blacklist);
278
279         $text = add_page_info_data($data);
280
281         return($text);
282 }
283
284 function add_page_info_to_body($body, $texturl = false, $no_photos = false) {
285
286         logger('add_page_info_to_body: fetch page info for body '.$body, LOGGER_DEBUG);
287
288         $URLSearchString = "^\[\]";
289
290         // Fix for Mastodon where the mentions are in a different format
291         $body = preg_replace("/\[url\=([$URLSearchString]*)\]([#!@])(.*?)\[\/url\]/ism",
292                                 '$2[url=$1]$3[/url]', $body);
293
294         // Adding these spaces is a quick hack due to my problems with regular expressions :)
295         preg_match("/[^!#@]\[url\]([$URLSearchString]*)\[\/url\]/ism", " ".$body, $matches);
296
297         if (!$matches)
298                 preg_match("/[^!#@]\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", " ".$body, $matches);
299
300         // Convert urls without bbcode elements
301         if (!$matches AND $texturl) {
302                 preg_match("/([^\]\='".'"'."]|^)(https?\:\/\/[a-zA-Z0-9\:\/\-\?\&\;\.\=\_\~\#\%\$\!\+\,]+)/ism", " ".$body, $matches);
303
304                 // Yeah, a hack. I really hate regular expressions :)
305                 if ($matches)
306                         $matches[1] = $matches[2];
307         }
308
309         if ($matches)
310                 $footer = add_page_info($matches[1], $no_photos);
311
312         // Remove the link from the body if the link is attached at the end of the post
313         if (isset($footer) AND (trim($footer) != "") AND (strpos($footer, $matches[1]))) {
314                 $removedlink = trim(str_replace($matches[1], "", $body));
315                 if (($removedlink == "") OR strstr($body, $removedlink))
316                         $body = $removedlink;
317
318                 $url = str_replace(array('/', '.'), array('\/', '\.'), $matches[1]);
319                 $removedlink = preg_replace("/\[url\=".$url."\](.*?)\[\/url\]/ism", '', $body);
320                 if (($removedlink == "") OR strstr($body, $removedlink))
321                         $body = $removedlink;
322         }
323
324         // Add the page information to the bottom
325         if (isset($footer) AND (trim($footer) != ""))
326                 $body .= $footer;
327
328         return $body;
329 }
330
331 /**
332  * Adds a "lang" specification in a "postopts" element of given $arr,
333  * if possible and not already present.
334  * Expects "body" element to exist in $arr.
335  * 
336  * @todo Add a parameter to request forcing override
337  */
338 function item_add_language_opt(&$arr) {
339
340         if (version_compare(PHP_VERSION, '5.3.0', '<')) return; // LanguageDetect.php not available ?
341
342         if (x($arr, 'postopts') )
343         {
344                 if (strstr($arr['postopts'], 'lang=') )
345                 {
346                         // do not override
347                         /// @TODO Add parameter to request overriding
348                         return;
349                 }
350                 $postopts = $arr['postopts'];
351         } else {
352                 $postopts = "";
353         }
354
355         require_once('library/langdet/Text/LanguageDetect.php');
356         $naked_body = preg_replace('/\[(.+?)\]/','', $arr['body']);
357         $l = new Text_LanguageDetect;
358         //$lng = $l->detectConfidence($naked_body);
359         //$arr['postopts'] = (($lng['language']) ? 'lang=' . $lng['language'] . ';' . $lng['confidence'] : '');
360         $lng = $l->detect($naked_body, 3);
361
362         if (sizeof($lng) > 0) {
363                 if ($postopts != "") $postopts .= '&'; // arbitrary separator, to be reviewed
364                 $postopts .= 'lang=';
365                 $sep = "";
366                 foreach ($lng as $language => $score) {
367                         $postopts .= $sep . $language.";".$score;
368                         $sep = ':';
369                 }
370                 $arr['postopts'] = $postopts;
371         }
372 }
373
374 /**
375  * @brief Creates an unique guid out of a given uri
376  *
377  * @param string $uri uri of an item entry
378  * @param string $host (Optional) hostname for the GUID prefix
379  * @return string unique guid
380  */
381 function uri_to_guid($uri, $host = "") {
382
383         // Our regular guid routine is using this kind of prefix as well
384         // We have to avoid that different routines could accidentally create the same value
385         $parsed = parse_url($uri);
386
387         if ($host == "") {
388                 $host = $parsed["host"];
389         }
390
391         $guid_prefix = hash("crc32", $host);
392
393         // Remove the scheme to make sure that "https" and "http" doesn't make a difference
394         unset($parsed["scheme"]);
395
396         $host_id = implode("/", $parsed);
397
398         // We could use any hash algorithm since it isn't a security issue
399         $host_hash = hash("ripemd128", $host_id);
400
401         return $guid_prefix.$host_hash;
402 }
403
404 function item_store($arr, $force_parent = false, $notify = false, $dontcache = false) {
405
406         $a = get_app();
407
408         // If it is a posting where users should get notifications, then define it as wall posting
409         if ($notify) {
410                 $arr['wall'] = 1;
411                 $arr['type'] = 'wall';
412                 $arr['origin'] = 1;
413                 $arr['last-child'] = 1;
414                 $arr['network'] = NETWORK_DFRN;
415
416                 // We have to avoid duplicates. So we create the GUID in form of a hash of the plink or uri.
417                 // In difference to the call to "uri_to_guid" several lines below we add the hash of our own host.
418                 // This is done because our host is the original creator of the post.
419                 if (!isset($arr['guid'])) {
420                         if (isset($arr['plink'])) {
421                                 $arr['guid'] = uri_to_guid($arr['plink'], $a->get_hostname());
422                         } elseif (isset($arr['uri'])) {
423                                 $arr['guid'] = uri_to_guid($arr['uri'], $a->get_hostname());
424                         }
425                 }
426         }
427
428         // If a Diaspora signature structure was passed in, pull it out of the
429         // item array and set it aside for later storage.
430
431         $dsprsig = null;
432         if (x($arr,'dsprsig')) {
433                 $encoded_signature = $arr['dsprsig'];
434                 $dsprsig = json_decode(base64_decode($arr['dsprsig']));
435                 unset($arr['dsprsig']);
436         }
437
438         // Converting the plink
439         if ($arr['network'] == NETWORK_OSTATUS) {
440                 if (isset($arr['plink']))
441                         $arr['plink'] = ostatus::convert_href($arr['plink']);
442                 elseif (isset($arr['uri']))
443                         $arr['plink'] = ostatus::convert_href($arr['uri']);
444         }
445
446         if (x($arr, 'gravity'))
447                 $arr['gravity'] = intval($arr['gravity']);
448         elseif ($arr['parent-uri'] === $arr['uri'])
449                 $arr['gravity'] = 0;
450         elseif (activity_match($arr['verb'],ACTIVITY_POST))
451                 $arr['gravity'] = 6;
452         else
453                 $arr['gravity'] = 6;   // extensible catchall
454
455         if (! x($arr,'type'))
456                 $arr['type']      = 'remote';
457
458
459
460         /* check for create  date and expire time */
461         $uid = intval($arr['uid']);
462         $r = q("SELECT expire FROM user WHERE uid = %d", intval($uid));
463         if (dbm::is_result($r)) {
464                 $expire_interval = $r[0]['expire'];
465                 if ($expire_interval>0) {
466                         $expire_date =  new DateTime( '- '.$expire_interval.' days', new DateTimeZone('UTC'));
467                         $created_date = new DateTime($arr['created'], new DateTimeZone('UTC'));
468                         if ($created_date < $expire_date) {
469                                 logger('item-store: item created ('.$arr['created'].') before expiration time ('.$expire_date->format(DateTime::W3C).'). ignored. ' . print_r($arr,true), LOGGER_DEBUG);
470                                 return 0;
471                         }
472                 }
473         }
474
475         // Do we already have this item?
476         // We have to check several networks since Friendica posts could be repeated via OStatus (maybe Diasporsa as well)
477         if (in_array(trim($arr['network']), array(NETWORK_DIASPORA, NETWORK_DFRN, NETWORK_OSTATUS, ""))) {
478                 $r = q("SELECT `id`, `network` FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` IN ('%s', '%s', '%s')  LIMIT 1",
479                                 dbesc(trim($arr['uri'])),
480                                 intval($uid),
481                                 dbesc(NETWORK_DIASPORA),
482                                 dbesc(NETWORK_DFRN),
483                                 dbesc(NETWORK_OSTATUS)
484                         );
485                 if ($r) {
486                         // We only log the entries with a different user id than 0. Otherwise we would have too many false positives
487                         if ($uid != 0)
488                                 logger("Item with uri ".$arr['uri']." already existed for user ".$uid." with id ".$r[0]["id"]." target network ".$r[0]["network"]." - new network: ".$arr['network']);
489                         return($r[0]["id"]);
490                 }
491         }
492
493         // Shouldn't happen but we want to make absolutely sure it doesn't leak from a plugin.
494         // Deactivated, since the bbcode parser can handle with it - and it destroys posts with some smileys that contain "<"
495         //if ((strpos($arr['body'],'<') !== false) || (strpos($arr['body'],'>') !== false))
496         //      $arr['body'] = strip_tags($arr['body']);
497
498         item_add_language_opt($arr);
499
500         if ($notify)
501                 $guid_prefix = "";
502         elseif ((trim($arr['guid']) == "") AND (trim($arr['plink']) != ""))
503                 $arr['guid'] = uri_to_guid($arr['plink']);
504         elseif ((trim($arr['guid']) == "") AND (trim($arr['uri']) != ""))
505                 $arr['guid'] = uri_to_guid($arr['uri']);
506         else {
507                 $parsed = parse_url($arr["author-link"]);
508                 $guid_prefix = hash("crc32", $parsed["host"]);
509         }
510
511         $arr['wall']          = ((x($arr,'wall'))          ? intval($arr['wall'])                : 0);
512         $arr['guid']          = ((x($arr,'guid'))          ? notags(trim($arr['guid']))          : get_guid(32, $guid_prefix));
513         $arr['uri']           = ((x($arr,'uri'))           ? notags(trim($arr['uri']))           : item_new_uri($a->get_hostname(), $uid, $arr['guid']));
514         $arr['extid']         = ((x($arr,'extid'))         ? notags(trim($arr['extid']))         : '');
515         $arr['author-name']   = ((x($arr,'author-name'))   ? trim($arr['author-name'])   : '');
516         $arr['author-link']   = ((x($arr,'author-link'))   ? notags(trim($arr['author-link']))   : '');
517         $arr['author-avatar'] = ((x($arr,'author-avatar')) ? notags(trim($arr['author-avatar'])) : '');
518         $arr['owner-name']    = ((x($arr,'owner-name'))    ? trim($arr['owner-name'])    : '');
519         $arr['owner-link']    = ((x($arr,'owner-link'))    ? notags(trim($arr['owner-link']))    : '');
520         $arr['owner-avatar']  = ((x($arr,'owner-avatar'))  ? notags(trim($arr['owner-avatar']))  : '');
521         $arr['created']       = ((x($arr,'created') !== false) ? datetime_convert('UTC','UTC', $arr['created']) : datetime_convert());
522         $arr['edited']        = ((x($arr,'edited')  !== false) ? datetime_convert('UTC','UTC', $arr['edited'])  : datetime_convert());
523         $arr['commented']     = ((x($arr,'commented')  !== false) ? datetime_convert('UTC','UTC', $arr['commented'])  : datetime_convert());
524         $arr['received']      = ((x($arr,'received')  !== false) ? datetime_convert('UTC','UTC', $arr['received'])  : datetime_convert());
525         $arr['changed']       = ((x($arr,'changed')  !== false) ? datetime_convert('UTC','UTC', $arr['changed'])  : datetime_convert());
526         $arr['title']         = ((x($arr,'title'))         ? trim($arr['title'])         : '');
527         $arr['location']      = ((x($arr,'location'))      ? trim($arr['location'])      : '');
528         $arr['coord']         = ((x($arr,'coord'))         ? notags(trim($arr['coord']))         : '');
529         $arr['last-child']    = ((x($arr,'last-child'))    ? intval($arr['last-child'])          : 0 );
530         $arr['visible']       = ((x($arr,'visible') !== false) ? intval($arr['visible'])         : 1 );
531         $arr['deleted']       = 0;
532         $arr['parent-uri']    = ((x($arr,'parent-uri'))    ? notags(trim($arr['parent-uri']))    : $arr['uri']);
533         $arr['verb']          = ((x($arr,'verb'))          ? notags(trim($arr['verb']))          : '');
534         $arr['object-type']   = ((x($arr,'object-type'))   ? notags(trim($arr['object-type']))   : '');
535         $arr['object']        = ((x($arr,'object'))        ? trim($arr['object'])                : '');
536         $arr['target-type']   = ((x($arr,'target-type'))   ? notags(trim($arr['target-type']))   : '');
537         $arr['target']        = ((x($arr,'target'))        ? trim($arr['target'])                : '');
538         $arr['plink']         = ((x($arr,'plink'))         ? notags(trim($arr['plink']))         : '');
539         $arr['allow_cid']     = ((x($arr,'allow_cid'))     ? trim($arr['allow_cid'])             : '');
540         $arr['allow_gid']     = ((x($arr,'allow_gid'))     ? trim($arr['allow_gid'])             : '');
541         $arr['deny_cid']      = ((x($arr,'deny_cid'))      ? trim($arr['deny_cid'])              : '');
542         $arr['deny_gid']      = ((x($arr,'deny_gid'))      ? trim($arr['deny_gid'])              : '');
543         $arr['private']       = ((x($arr,'private'))       ? intval($arr['private'])             : 0 );
544         $arr['bookmark']      = ((x($arr,'bookmark'))      ? intval($arr['bookmark'])            : 0 );
545         $arr['body']          = ((x($arr,'body'))          ? trim($arr['body'])                  : '');
546         $arr['tag']           = ((x($arr,'tag'))           ? notags(trim($arr['tag']))           : '');
547         $arr['attach']        = ((x($arr,'attach'))        ? notags(trim($arr['attach']))        : '');
548         $arr['app']           = ((x($arr,'app'))           ? notags(trim($arr['app']))           : '');
549         $arr['origin']        = ((x($arr,'origin'))        ? intval($arr['origin'])              : 0 );
550         $arr['network']       = ((x($arr,'network'))       ? trim($arr['network'])               : '');
551         $arr['postopts']      = ((x($arr,'postopts'))      ? trim($arr['postopts'])              : '');
552         $arr['resource-id']   = ((x($arr,'resource-id'))   ? trim($arr['resource-id'])           : '');
553         $arr['event-id']      = ((x($arr,'event-id'))      ? intval($arr['event-id'])            : 0 );
554         $arr['inform']        = ((x($arr,'inform'))        ? trim($arr['inform'])                : '');
555         $arr['file']          = ((x($arr,'file'))          ? trim($arr['file'])                  : '');
556
557         // Items cannot be stored before they happen ...
558         if ($arr['created'] > datetime_convert())
559                 $arr['created'] = datetime_convert();
560
561         // We haven't invented time travel by now.
562         if ($arr['edited'] > datetime_convert())
563                 $arr['edited'] = datetime_convert();
564
565         if (($arr['author-link'] == "") AND ($arr['owner-link'] == ""))
566                 logger("Both author-link and owner-link are empty. Called by: ".App::callstack(), LOGGER_DEBUG);
567
568         if ($arr['plink'] == "") {
569                 $arr['plink'] = App::get_baseurl().'/display/'.urlencode($arr['guid']);
570         }
571
572         if ($arr['network'] == "") {
573                 $r = q("SELECT `network` FROM `contact` WHERE `network` IN ('%s', '%s', '%s') AND `nurl` = '%s' AND `uid` = %d LIMIT 1",
574                         dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS),
575                         dbesc(normalise_link($arr['author-link'])),
576                         intval($arr['uid'])
577                 );
578
579                 if (!dbm::is_result($r))
580                         $r = q("SELECT `network` FROM `gcontact` WHERE `network` IN ('%s', '%s', '%s') AND `nurl` = '%s' LIMIT 1",
581                                 dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS),
582                                 dbesc(normalise_link($arr['author-link']))
583                         );
584
585                 if (!dbm::is_result($r))
586                         $r = q("SELECT `network` FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
587                                 intval($arr['contact-id']),
588                                 intval($arr['uid'])
589                         );
590
591                 if (dbm::is_result($r))
592                         $arr['network'] = $r[0]["network"];
593
594                 // Fallback to friendica (why is it empty in some cases?)
595                 if ($arr['network'] == "")
596                         $arr['network'] = NETWORK_DFRN;
597
598                 logger("item_store: Set network to ".$arr["network"]." for ".$arr["uri"], LOGGER_DEBUG);
599         }
600
601         // The contact-id should be set before "item_store" was called - but there seems to be some issues
602         if ($arr["contact-id"] == 0) {
603                 // First we are looking for a suitable contact that matches with the author of the post
604                 // This is done only for comments (See below explanation at "gcontact-id")
605                 if ($arr['parent-uri'] != $arr['uri'])
606                         $arr["contact-id"] = get_contact($arr['author-link'], $uid);
607
608                 // If not present then maybe the owner was found
609                 if ($arr["contact-id"] == 0)
610                         $arr["contact-id"] = get_contact($arr['owner-link'], $uid);
611
612                 // Still missing? Then use the "self" contact of the current user
613                 if ($arr["contact-id"] == 0) {
614                         $r = q("SELECT `id` FROM `contact` WHERE `self` AND `uid` = %d", intval($uid));
615                         if ($r)
616                                 $arr["contact-id"] = $r[0]["id"];
617                 }
618                 logger("Contact-id was missing for post ".$arr["guid"]." from user id ".$uid." - now set to ".$arr["contact-id"], LOGGER_DEBUG);
619         }
620
621         if ($arr["gcontact-id"] == 0) {
622                 // The gcontact should mostly behave like the contact. But is is supposed to be global for the system.
623                 // This means that wall posts, repeated posts, etc. should have the gcontact id of the owner.
624                 // On comments the author is the better choice.
625                 if ($arr['parent-uri'] === $arr['uri'])
626                         $arr["gcontact-id"] = get_gcontact_id(array("url" => $arr['owner-link'], "network" => $arr['network'],
627                                                                  "photo" => $arr['owner-avatar'], "name" => $arr['owner-name']));
628                 else
629                         $arr["gcontact-id"] = get_gcontact_id(array("url" => $arr['author-link'], "network" => $arr['network'],
630                                                                  "photo" => $arr['author-avatar'], "name" => $arr['author-name']));
631         }
632
633         if ($arr["author-id"] == 0)
634                 $arr["author-id"] = get_contact($arr["author-link"], 0);
635
636         if ($arr["owner-id"] == 0)
637                 $arr["owner-id"] = get_contact($arr["owner-link"], 0);
638
639         if ($arr['guid'] != "") {
640                 // Checking if there is already an item with the same guid
641                 logger('checking for an item for user '.$arr['uid'].' on network '.$arr['network'].' with the guid '.$arr['guid'], LOGGER_DEBUG);
642                 $r = q("SELECT `guid` FROM `item` WHERE `guid` = '%s' AND `network` = '%s' AND `uid` = '%d' LIMIT 1",
643                         dbesc($arr['guid']), dbesc($arr['network']), intval($arr['uid']));
644
645                 if (dbm::is_result($r)) {
646                         logger('found item with guid '.$arr['guid'].' for user '.$arr['uid'].' on network '.$arr['network'], LOGGER_DEBUG);
647                         return 0;
648                 }
649         }
650
651         // Check for hashtags in the body and repair or add hashtag links
652         item_body_set_hashtags($arr);
653
654         $arr['thr-parent'] = $arr['parent-uri'];
655         if ($arr['parent-uri'] === $arr['uri']) {
656                 $parent_id = 0;
657                 $parent_deleted = 0;
658                 $allow_cid = $arr['allow_cid'];
659                 $allow_gid = $arr['allow_gid'];
660                 $deny_cid  = $arr['deny_cid'];
661                 $deny_gid  = $arr['deny_gid'];
662                 $notify_type = 'wall-new';
663         } else {
664
665                 // find the parent and snarf the item id and ACLs
666                 // and anything else we need to inherit
667
668                 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d ORDER BY `id` ASC LIMIT 1",
669                         dbesc($arr['parent-uri']),
670                         intval($arr['uid'])
671                 );
672
673                 if (dbm::is_result($r)) {
674
675                         // is the new message multi-level threaded?
676                         // even though we don't support it now, preserve the info
677                         // and re-attach to the conversation parent.
678
679                         if ($r[0]['uri'] != $r[0]['parent-uri']) {
680                                 $arr['parent-uri'] = $r[0]['parent-uri'];
681                                 $z = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `parent-uri` = '%s' AND `uid` = %d
682                                         ORDER BY `id` ASC LIMIT 1",
683                                         dbesc($r[0]['parent-uri']),
684                                         dbesc($r[0]['parent-uri']),
685                                         intval($arr['uid'])
686                                 );
687                                 if ($z && count($z))
688                                         $r = $z;
689                         }
690
691                         $parent_id      = $r[0]['id'];
692                         $parent_deleted = $r[0]['deleted'];
693                         $allow_cid      = $r[0]['allow_cid'];
694                         $allow_gid      = $r[0]['allow_gid'];
695                         $deny_cid       = $r[0]['deny_cid'];
696                         $deny_gid       = $r[0]['deny_gid'];
697                         $arr['wall']    = $r[0]['wall'];
698                         $notify_type    = 'comment-new';
699
700                         // if the parent is private, force privacy for the entire conversation
701                         // This differs from the above settings as it subtly allows comments from
702                         // email correspondents to be private even if the overall thread is not.
703
704                         if ($r[0]['private'])
705                                 $arr['private'] = $r[0]['private'];
706
707                         // Edge case. We host a public forum that was originally posted to privately.
708                         // The original author commented, but as this is a comment, the permissions
709                         // weren't fixed up so it will still show the comment as private unless we fix it here.
710
711                         if ((intval($r[0]['forum_mode']) == 1) && (! $r[0]['private']))
712                                 $arr['private'] = 0;
713
714
715                         // If its a post from myself then tag the thread as "mention"
716                         logger("item_store: Checking if parent ".$parent_id." has to be tagged as mention for user ".$arr['uid'], LOGGER_DEBUG);
717                         $u = q("SELECT `nickname` FROM `user` WHERE `uid` = %d", intval($arr['uid']));
718                         if (dbm::is_result($u)) {
719                                 $a = get_app();
720                                 $self = normalise_link(App::get_baseurl() . '/profile/' . $u[0]['nickname']);
721                                 logger("item_store: 'myself' is ".$self." for parent ".$parent_id." checking against ".$arr['author-link']." and ".$arr['owner-link'], LOGGER_DEBUG);
722                                 if ((normalise_link($arr['author-link']) == $self) OR (normalise_link($arr['owner-link']) == $self)) {
723                                         q("UPDATE `thread` SET `mention` = 1 WHERE `iid` = %d", intval($parent_id));
724                                         logger("item_store: tagged thread ".$parent_id." as mention for user ".$self, LOGGER_DEBUG);
725                                 }
726                         }
727                 } else {
728
729                         // Allow one to see reply tweets from status.net even when
730                         // we don't have or can't see the original post.
731
732                         if ($force_parent) {
733                                 logger('item_store: $force_parent=true, reply converted to top-level post.');
734                                 $parent_id = 0;
735                                 $arr['parent-uri'] = $arr['uri'];
736                                 $arr['gravity'] = 0;
737                         } else {
738                                 logger('item_store: item parent '.$arr['parent-uri'].' for '.$arr['uid'].' was not found - ignoring item');
739                                 return 0;
740                         }
741
742                         $parent_deleted = 0;
743                 }
744         }
745
746         $r = q("SELECT `id` FROM `item` WHERE `uri` = '%s' AND `network` IN ('%s', '%s') AND `uid` = %d LIMIT 1",
747                 dbesc($arr['uri']),
748                 dbesc($arr['network']),
749                 dbesc(NETWORK_DFRN),
750                 intval($arr['uid'])
751         );
752         if (dbm::is_result($r)) {
753                 logger('duplicated item with the same uri found. '.print_r($arr,true));
754                 return 0;
755         }
756
757         // On Friendica and Diaspora the GUID is unique
758         if (in_array($arr['network'], array(NETWORK_DFRN, NETWORK_DIASPORA))) {
759                 $r = q("SELECT `id` FROM `item` WHERE `guid` = '%s' AND `uid` = %d LIMIT 1",
760                         dbesc($arr['guid']),
761                         intval($arr['uid'])
762                 );
763                 if (dbm::is_result($r)) {
764                         logger('duplicated item with the same guid found. '.print_r($arr,true));
765                         return 0;
766                 }
767         } else {
768                 // Check for an existing post with the same content. There seems to be a problem with OStatus.
769                 $r = q("SELECT `id` FROM `item` WHERE `body` = '%s' AND `network` = '%s' AND `created` = '%s' AND `contact-id` = %d AND `uid` = %d LIMIT 1",
770                         dbesc($arr['body']),
771                         dbesc($arr['network']),
772                         dbesc($arr['created']),
773                         intval($arr['contact-id']),
774                         intval($arr['uid'])
775                 );
776                 if (dbm::is_result($r)) {
777                         logger('duplicated item with the same body found. '.print_r($arr,true));
778                         return 0;
779                 }
780         }
781
782         // Is this item available in the global items (with uid=0)?
783         if ($arr["uid"] == 0) {
784                 $arr["global"] = true;
785
786                 // Set the global flag on all items if this was a global item entry
787                 q("UPDATE `item` SET `global` = 1 WHERE `uri` = '%s'", dbesc($arr["uri"]));
788         } else {
789                 $isglobal = q("SELECT `global` FROM `item` WHERE `uid` = 0 AND `uri` = '%s'", dbesc($arr["uri"]));
790
791                 $arr["global"] = (count($isglobal) > 0);
792         }
793
794         // ACL settings
795         if (strlen($allow_cid) || strlen($allow_gid) || strlen($deny_cid) || strlen($deny_gid))
796                 $private = 1;
797         else
798                 $private = $arr['private'];
799
800         $arr["allow_cid"] = $allow_cid;
801         $arr["allow_gid"] = $allow_gid;
802         $arr["deny_cid"] = $deny_cid;
803         $arr["deny_gid"] = $deny_gid;
804         $arr["private"] = $private;
805         $arr["deleted"] = $parent_deleted;
806
807         // Fill the cache field
808         put_item_in_cache($arr);
809
810         if ($notify)
811                 call_hooks('post_local', $arr);
812         else
813                 call_hooks('post_remote', $arr);
814
815         if (x($arr,'cancel')) {
816                 logger('item_store: post cancelled by plugin.');
817                 return 0;
818         }
819
820         // Check for already added items.
821         // There is a timing issue here that sometimes creates double postings.
822         // An unique index would help - but the limitations of MySQL (maximum size of index values) prevent this.
823         if ($arr["uid"] == 0) {
824                 $r = qu("SELECT `id` FROM `item` WHERE `uri` = '%s' AND `uid` = 0 LIMIT 1", dbesc(trim($arr['uri'])));
825                 if (dbm::is_result($r)) {
826                         logger('Global item already stored. URI: '.$arr['uri'].' on network '.$arr['network'], LOGGER_DEBUG);
827                         return 0;
828                 }
829         }
830
831         // Store the unescaped version
832         $unescaped = $arr;
833
834         dbm::esc_array($arr, true);
835
836         logger('item_store: ' . print_r($arr,true), LOGGER_DATA);
837
838         q("COMMIT");
839         q("START TRANSACTION;");
840
841         $r = dbq("INSERT INTO `item` (`"
842                         . implode("`, `", array_keys($arr))
843                         . "`) VALUES ("
844                         . implode(", ", array_values($arr))
845                         . ")");
846
847         // And restore it
848         $arr = $unescaped;
849
850         // When the item was successfully stored we fetch the ID of the item.
851         if (dbm::is_result($r)) {
852                 $r = q("SELECT LAST_INSERT_ID() AS `item-id`");
853                 if (dbm::is_result($r)) {
854                         $current_post = $r[0]['item-id'];
855                 } else {
856                         // This shouldn't happen
857                         $current_post = 0;
858                 }
859         } else {
860                 // This can happen - for example - if there are locking timeouts.
861                 q("ROLLBACK");
862
863                 // Store the data into a spool file so that we can try again later.
864
865                 // At first we restore the Diaspora signature that we removed above.
866                 if (isset($encoded_signature)) {
867                         $arr['dsprsig'] = $encoded_signature;
868                 }
869
870                 // Now we store the data in the spool directory
871                 // We use "microtime" to keep the arrival order and "mt_rand" to avoid duplicates
872                 $file = 'item-'.round(microtime(true) * 10000).'-'.mt_rand().'.msg';
873
874                 $spoolpath = get_spoolpath();
875                 if ($spoolpath != "") {
876                         $spool = $spoolpath.'/'.$file;
877                         file_put_contents($spool, json_encode($arr));
878                         logger("Item wasn't stored - Item was spooled into file ".$file, LOGGER_DEBUG);
879                 }
880                 return 0;
881         }
882
883         if ($current_post == 0) {
884                 // This is one of these error messages that never should occur.
885                 logger("couldn't find created item - we better quit now.");
886                 q("ROLLBACK");
887                 return 0;
888         }
889
890         // How much entries have we created?
891         // We wouldn't need this query when we could use an unique index - but MySQL has length problems with them.
892         $r = q("SELECT COUNT(*) AS `entries` FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` = '%s'",
893                 dbesc($arr['uri']),
894                 intval($arr['uid']),
895                 dbesc($arr['network'])
896         );
897
898         if (!dbm::is_result($r)) {
899                 // This shouldn't happen, since COUNT always works when the database connection is there.
900                 logger("We couldn't count the stored entries. Very strange ...");
901                 q("ROLLBACK");
902                 return 0;
903         }
904
905         if ($r[0]["entries"] > 1) {
906                 // There are duplicates. We delete our just created entry.
907                 logger('Duplicated post occurred. uri = '.$arr['uri'].' uid = '.$arr['uid']);
908
909                 // Yes, we could do a rollback here - but we are having many users with MyISAM.
910                 q("DELETE FROM `item` WHERE `id` = %d", intval($current_post));
911                 q("COMMIT");
912                 return 0;
913         } elseif ($r[0]["entries"] == 0) {
914                 // This really should never happen since we quit earlier if there were problems.
915                 logger("Something is terribly wrong. We haven't found our created entry.");
916                 q("ROLLBACK");
917                 return 0;
918         }
919
920         logger('item_store: created item '.$current_post);
921         item_set_last_item($arr);
922
923         if (!$parent_id || ($arr['parent-uri'] === $arr['uri']))
924                 $parent_id = $current_post;
925
926         // Set parent id
927         $r = q("UPDATE `item` SET `parent` = %d WHERE `id` = %d",
928                 intval($parent_id),
929                 intval($current_post)
930         );
931
932         $arr['id'] = $current_post;
933         $arr['parent'] = $parent_id;
934
935         // update the commented timestamp on the parent
936         // Only update "commented" if it is really a comment
937         if (($arr['verb'] == ACTIVITY_POST) OR !get_config("system", "like_no_comment"))
938                 q("UPDATE `item` SET `commented` = '%s', `changed` = '%s' WHERE `id` = %d",
939                         dbesc(datetime_convert()),
940                         dbesc(datetime_convert()),
941                         intval($parent_id)
942                 );
943         else
944                 q("UPDATE `item` SET `changed` = '%s' WHERE `id` = %d",
945                         dbesc(datetime_convert()),
946                         intval($parent_id)
947                 );
948
949         if ($dsprsig) {
950
951                 // Friendica servers lower than 3.4.3-2 had double encoded the signature ...
952                 // We can check for this condition when we decode and encode the stuff again.
953                 if (base64_encode(base64_decode(base64_decode($dsprsig->signature))) == base64_decode($dsprsig->signature)) {
954                         $dsprsig->signature = base64_decode($dsprsig->signature);
955                         logger("Repaired double encoded signature from handle ".$dsprsig->signer, LOGGER_DEBUG);
956                 }
957
958                 q("insert into sign (`iid`,`signed_text`,`signature`,`signer`) values (%d,'%s','%s','%s') ",
959                         intval($current_post),
960                         dbesc($dsprsig->signed_text),
961                         dbesc($dsprsig->signature),
962                         dbesc($dsprsig->signer)
963                 );
964         }
965
966         $deleted = tag_deliver($arr['uid'], $current_post);
967
968         // current post can be deleted if is for a community page and no mention are
969         // in it.
970         if (!$deleted AND !$dontcache) {
971
972                 $r = q('SELECT * FROM `item` WHERE `id` = %d', intval($current_post));
973                 if ((dbm::is_result($r)) && (count($r) == 1)) {
974                         if ($notify) {
975                                 call_hooks('post_local_end', $r[0]);
976                         } else {
977                                 call_hooks('post_remote_end', $r[0]);
978                         }
979                 } else {
980                         logger('item_store: new item not found in DB, id ' . $current_post);
981                 }
982         }
983
984         if ($arr['parent-uri'] === $arr['uri']) {
985                 add_thread($current_post);
986         } else {
987                 update_thread($parent_id);
988         }
989
990         q("COMMIT");
991
992         // Due to deadlock issues with the "term" table we are doing these steps after the commit.
993         // This is not perfect - but a workable solution until we found the reason for the problem.
994         create_tags_from_item($current_post);
995         create_files_from_item($current_post);
996
997         // If this is now the last-child, force all _other_ children of this parent to *not* be last-child
998         // It is done after the transaction to avoid dead locks.
999         if ($arr['last-child']) {
1000                 $r = q("UPDATE `item` SET `last-child` = 0 WHERE `parent-uri` = '%s' AND `uid` = %d AND `id` != %d",
1001                         dbesc($arr['uri']),
1002                         intval($arr['uid']),
1003                         intval($current_post)
1004                 );
1005         }
1006
1007         if ($arr['parent-uri'] === $arr['uri']) {
1008                 add_shadow_thread($current_post);
1009         } else {
1010                 add_shadow_entry($current_post);
1011         }
1012
1013         check_item_notification($current_post, $uid);
1014
1015         if ($notify) {
1016                 proc_run(PRIORITY_HIGH, "include/notifier.php", $notify_type, $current_post);
1017         }
1018
1019         return $current_post;
1020 }
1021
1022 /**
1023  * @brief Set "success_update" and "last-item" to the date of the last time we heard from this contact
1024  *
1025  * This can be used to filter for inactive contacts.
1026  * Only do this for public postings to avoid privacy problems, since poco data is public.
1027  * Don't set this value if it isn't from the owner (could be an author that we don't know)
1028  *
1029  * @param array $arr Contains the just posted item record
1030  */
1031 function item_set_last_item($arr) {
1032
1033         $update = (!$arr['private'] AND (($arr["author-link"] === $arr["owner-link"]) OR ($arr["parent-uri"] === $arr["uri"])));
1034
1035         // Is it a forum? Then we don't care about the rules from above
1036         if (!$update AND ($arr["network"] == NETWORK_DFRN) AND ($arr["parent-uri"] === $arr["uri"])) {
1037                 $isforum = q("SELECT `forum` FROM `contact` WHERE `id` = %d AND `forum`",
1038                                 intval($arr['contact-id']));
1039                 if ($isforum) {
1040                         $update = true;
1041                 }
1042         }
1043
1044         if ($update) {
1045                 q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
1046                         dbesc($arr['received']),
1047                         dbesc($arr['received']),
1048                         intval($arr['contact-id'])
1049                 );
1050         }
1051         // Now do the same for the system wide contacts with uid=0
1052         if (!$arr['private']) {
1053                 q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
1054                         dbesc($arr['received']),
1055                         dbesc($arr['received']),
1056                         intval($arr['owner-id'])
1057                 );
1058
1059                 if ($arr['owner-id'] != $arr['author-id']) {
1060                         q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
1061                                 dbesc($arr['received']),
1062                                 dbesc($arr['received']),
1063                                 intval($arr['author-id'])
1064                         );
1065                 }
1066         }
1067 }
1068
1069 function item_body_set_hashtags(&$item) {
1070
1071         $tags = get_tags($item["body"]);
1072
1073         // No hashtags?
1074         if (!count($tags))
1075                 return(false);
1076
1077         // This sorting is important when there are hashtags that are part of other hashtags
1078         // Otherwise there could be problems with hashtags like #test and #test2
1079         rsort($tags);
1080
1081         $a = get_app();
1082
1083         $URLSearchString = "^\[\]";
1084
1085         // All hashtags should point to the home server
1086         //$item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1087         //              "#[url=".App::get_baseurl()."/search?tag=$2]$2[/url]", $item["body"]);
1088
1089         //$item["tag"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1090         //              "#[url=".App::get_baseurl()."/search?tag=$2]$2[/url]", $item["tag"]);
1091
1092         // mask hashtags inside of url, bookmarks and attachments to avoid urls in urls
1093         $item["body"] = preg_replace_callback("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1094                 function ($match) {
1095                         return("[url=".str_replace("#", "&num;", $match[1])."]".str_replace("#", "&num;", $match[2])."[/url]");
1096                 }, $item["body"]);
1097
1098         $item["body"] = preg_replace_callback("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism",
1099                 function ($match) {
1100                         return("[bookmark=".str_replace("#", "&num;", $match[1])."]".str_replace("#", "&num;", $match[2])."[/bookmark]");
1101                 }, $item["body"]);
1102
1103         $item["body"] = preg_replace_callback("/\[attachment (.*)\](.*?)\[\/attachment\]/ism",
1104                 function ($match) {
1105                         return("[attachment ".str_replace("#", "&num;", $match[1])."]".$match[2]."[/attachment]");
1106                 }, $item["body"]);
1107
1108         // Repair recursive urls
1109         $item["body"] = preg_replace("/&num;\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1110                         "&num;$2", $item["body"]);
1111
1112
1113         foreach($tags as $tag) {
1114                 if (strpos($tag,'#') !== 0)
1115                         continue;
1116
1117                 if (strpos($tag,'[url='))
1118                         continue;
1119
1120                 $basetag = str_replace('_',' ',substr($tag,1));
1121
1122                 $newtag = '#[url='.App::get_baseurl().'/search?tag='.rawurlencode($basetag).']'.$basetag.'[/url]';
1123
1124                 $item["body"] = str_replace($tag, $newtag, $item["body"]);
1125
1126                 if (!stristr($item["tag"], "/search?tag=".$basetag."]".$basetag."[/url]")) {
1127                         if (strlen($item["tag"]))
1128                                 $item["tag"] = ','.$item["tag"];
1129                         $item["tag"] = $newtag.$item["tag"];
1130                 }
1131         }
1132
1133         // Convert back the masked hashtags
1134         $item["body"] = str_replace("&num;", "#", $item["body"]);
1135 }
1136
1137 function get_item_guid($id) {
1138         $r = q("SELECT `guid` FROM `item` WHERE `id` = %d LIMIT 1", intval($id));
1139         if (dbm::is_result($r))
1140                 return($r[0]["guid"]);
1141         else
1142                 return("");
1143 }
1144
1145 function get_item_id($guid, $uid = 0) {
1146
1147         $nick = "";
1148         $id = 0;
1149
1150         if ($uid == 0)
1151                 $uid == local_user();
1152
1153         // Does the given user have this item?
1154         if ($uid) {
1155                 $r = q("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid`
1156                         WHERE `item`.`visible` = 1 AND `item`.`deleted` = 0 and `item`.`moderated` = 0
1157                                 AND `item`.`guid` = '%s' AND `item`.`uid` = %d", dbesc($guid), intval($uid));
1158                 if (dbm::is_result($r)) {
1159                         $id = $r[0]["id"];
1160                         $nick = $r[0]["nickname"];
1161                 }
1162         }
1163
1164         // Or is it anywhere on the server?
1165         if ($nick == "") {
1166                 $r = q("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid`
1167                         WHERE `item`.`visible` = 1 AND `item`.`deleted` = 0 and `item`.`moderated` = 0
1168                                 AND `item`.`allow_cid` = ''  AND `item`.`allow_gid` = ''
1169                                 AND `item`.`deny_cid`  = '' AND `item`.`deny_gid`  = ''
1170                                 AND `item`.`private` = 0 AND `item`.`wall` = 1
1171                                 AND `item`.`guid` = '%s'", dbesc($guid));
1172                 if (dbm::is_result($r)) {
1173                         $id = $r[0]["id"];
1174                         $nick = $r[0]["nickname"];
1175                 }
1176         }
1177         return(array("nick" => $nick, "id" => $id));
1178 }
1179
1180 // return - test
1181 function get_item_contact($item, $contacts) {
1182         if (! count($contacts) || (! is_array($item)))
1183                 return false;
1184         foreach($contacts as $contact) {
1185                 if ($contact['id'] == $item['contact-id']) {
1186                         return $contact;
1187                         break; // NOTREACHED
1188                 }
1189         }
1190         return false;
1191 }
1192
1193 /**
1194  * look for mention tags and setup a second delivery chain for forum/community posts if appropriate
1195  * @param int $uid
1196  * @param int $item_id
1197  * @return bool true if item was deleted, else false
1198  */
1199 function tag_deliver($uid, $item_id) {
1200
1201         //
1202
1203         $a = get_app();
1204
1205         $mention = false;
1206
1207         $u = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1",
1208                 intval($uid)
1209         );
1210
1211         if (! dbm::is_result($u)) {
1212                 return;
1213         }
1214
1215         $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
1216         $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
1217
1218
1219         $i = q("SELECT * FROM `item` WHERE `id` = %d AND `uid` = %d LIMIT 1",
1220                 intval($item_id),
1221                 intval($uid)
1222         );
1223         if (! dbm::is_result($i)) {
1224                 return;
1225         }
1226
1227         $item = $i[0];
1228
1229         $link = normalise_link(App::get_baseurl() . '/profile/' . $u[0]['nickname']);
1230
1231         // Diaspora uses their own hardwired link URL in @-tags
1232         // instead of the one we supply with webfinger
1233
1234         $dlink = normalise_link(App::get_baseurl() . '/u/' . $u[0]['nickname']);
1235
1236         $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism', $item['body'], $matches,PREG_SET_ORDER);
1237         if ($cnt) {
1238                 foreach($matches as $mtch) {
1239                         if (link_compare($link, $mtch[1]) || link_compare($dlink, $mtch[1])) {
1240                                 $mention = true;
1241                                 logger('tag_deliver: mention found: ' . $mtch[2]);
1242                         }
1243                 }
1244         }
1245
1246         if (! $mention) {
1247                 if (($community_page || $prvgroup) &&
1248                           (!$item['wall']) && (!$item['origin']) && ($item['id'] == $item['parent'])) {
1249                         // mmh.. no mention.. community page or private group... no wall.. no origin.. top-post (not a comment)
1250                         // delete it!
1251                         logger("tag_deliver: no-mention top-level post to communuty or private group. delete.");
1252                         q("DELETE FROM item WHERE id = %d and uid = %d",
1253                                 intval($item_id),
1254                                 intval($uid)
1255                         );
1256                         return true;
1257                 }
1258                 return;
1259         }
1260
1261         $arr = array('item' => $item, 'user' => $u[0], 'contact' => $r[0]);
1262
1263         call_hooks('tagged', $arr);
1264
1265         if ((! $community_page) && (! $prvgroup))
1266                 return;
1267
1268
1269         // tgroup delivery - setup a second delivery chain
1270         // prevent delivery looping - only proceed
1271         // if the message originated elsewhere and is a top-level post
1272
1273         if (($item['wall']) || ($item['origin']) || ($item['id'] != $item['parent'])) {
1274                 return;
1275         }
1276
1277         // now change this copy of the post to a forum head message and deliver to all the tgroup members
1278
1279
1280         $c = q("select name, url, thumb from contact where self = 1 and uid = %d limit 1",
1281                 intval($u[0]['uid'])
1282         );
1283         if (! dbm::is_result($c)) {
1284                 return;
1285         }
1286
1287         // also reset all the privacy bits to the forum default permissions
1288
1289         $private = ($u[0]['allow_cid'] || $u[0]['allow_gid'] || $u[0]['deny_cid'] || $u[0]['deny_gid']) ? 1 : 0;
1290
1291         $forum_mode = (($prvgroup) ? 2 : 1);
1292
1293         q("UPDATE `item` SET `wall` = 1, `origin` = 1, `forum_mode` = %d, `owner-name` = '%s', `owner-link` = '%s', `owner-avatar` = '%s',
1294                 `private` = %d, `allow_cid` = '%s', `allow_gid` = '%s', `deny_cid` = '%s', `deny_gid` = '%s'  WHERE `id` = %d",
1295                 intval($forum_mode),
1296                 dbesc($c[0]['name']),
1297                 dbesc($c[0]['url']),
1298                 dbesc($c[0]['thumb']),
1299                 intval($private),
1300                 dbesc($u[0]['allow_cid']),
1301                 dbesc($u[0]['allow_gid']),
1302                 dbesc($u[0]['deny_cid']),
1303                 dbesc($u[0]['deny_gid']),
1304                 intval($item_id)
1305         );
1306         update_thread($item_id);
1307
1308         proc_run(PRIORITY_HIGH,'include/notifier.php', 'tgroup', $item_id);
1309
1310 }
1311
1312
1313
1314 function tgroup_check($uid, $item) {
1315
1316         $mention = false;
1317
1318         // check that the message originated elsewhere and is a top-level post
1319
1320         if (($item['wall']) || ($item['origin']) || ($item['uri'] != $item['parent-uri'])) {
1321                 return false;
1322         }
1323
1324         /// @TODO Encapsulate this or find it encapsulated and replace all occurrances
1325         $u = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1",
1326                 intval($uid)
1327         );
1328         if (! dbm::is_result($u)) {
1329                 return false;
1330         }
1331
1332         $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
1333         $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
1334
1335
1336         $link = normalise_link(App::get_baseurl() . '/profile/' . $u[0]['nickname']);
1337
1338         // Diaspora uses their own hardwired link URL in @-tags
1339         // instead of the one we supply with webfinger
1340
1341         $dlink = normalise_link(App::get_baseurl() . '/u/' . $u[0]['nickname']);
1342
1343         $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism', $item['body'], $matches,PREG_SET_ORDER);
1344         if ($cnt) {
1345                 foreach ($matches as $mtch) {
1346                         if (link_compare($link, $mtch[1]) || link_compare($dlink, $mtch[1])) {
1347                                 $mention = true;
1348                                 logger('tgroup_check: mention found: ' . $mtch[2]);
1349                         }
1350                 }
1351         }
1352
1353         if (! $mention) {
1354                 return false;
1355         }
1356
1357         /// @TODO Combines both return statements into one
1358         return (($community_page) || ($prvgroup));
1359 }
1360
1361 /*
1362   This function returns true if $update has an edited timestamp newer
1363   than $existing, i.e. $update contains new data which should override
1364   what's already there.  If there is no timestamp yet, the update is
1365   assumed to be newer.  If the update has no timestamp, the existing
1366   item is assumed to be up-to-date.  If the timestamps are equal it
1367   assumes the update has been seen before and should be ignored.
1368   */
1369 function edited_timestamp_is_newer($existing, $update) {
1370         if (!x($existing,'edited') || !$existing['edited']) {
1371                 return true;
1372         }
1373         if (!x($update,'edited') || !$update['edited']) {
1374                 return false;
1375         }
1376
1377         $existing_edited = datetime_convert('UTC', 'UTC', $existing['edited']);
1378         $update_edited = datetime_convert('UTC', 'UTC', $update['edited']);
1379         return (strcmp($existing_edited, $update_edited) < 0);
1380 }
1381
1382 /**
1383  *
1384  * consume_feed - process atom feed and update anything/everything we might need to update
1385  *
1386  * $xml = the (atom) feed to consume - RSS isn't as fully supported but may work for simple feeds.
1387  *
1388  * $importer = the contact_record (joined to user_record) of the local user who owns this relationship.
1389  *             It is this person's stuff that is going to be updated.
1390  * $contact =  the person who is sending us stuff. If not set, we MAY be processing a "follow" activity
1391  *             from an external network and MAY create an appropriate contact record. Otherwise, we MUST
1392  *             have a contact record.
1393  * $hub = should we find a hub declation in the feed, pass it back to our calling process, who might (or
1394  *        might not) try and subscribe to it.
1395  * $datedir sorts in reverse order
1396  * $pass - by default ($pass = 0) we cannot guarantee that a parent item has been
1397  *      imported prior to its children being seen in the stream unless we are certain
1398  *      of how the feed is arranged/ordered.
1399  * With $pass = 1, we only pull parent items out of the stream.
1400  * With $pass = 2, we only pull children (comments/likes).
1401  *
1402  * So running this twice, first with pass 1 and then with pass 2 will do the right
1403  * thing regardless of feed ordering. This won't be adequate in a fully-threaded
1404  * model where comments can have sub-threads. That would require some massive sorting
1405  * to get all the feed items into a mostly linear ordering, and might still require
1406  * recursion.
1407  */
1408
1409 function consume_feed($xml, $importer,&$contact, &$hub, $datedir = 0, $pass = 0) {
1410         if ($contact['network'] === NETWORK_OSTATUS) {
1411                 if ($pass < 2) {
1412                         // Test - remove before flight
1413                         //$tempfile = tempnam(get_temppath(), "ostatus2");
1414                         //file_put_contents($tempfile, $xml);
1415                         logger("Consume OStatus messages ", LOGGER_DEBUG);
1416                         ostatus::import($xml, $importer, $contact, $hub);
1417                 }
1418                 return;
1419         }
1420
1421         if ($contact['network'] === NETWORK_FEED) {
1422                 if ($pass < 2) {
1423                         logger("Consume feeds", LOGGER_DEBUG);
1424                         feed_import($xml, $importer, $contact, $hub);
1425                 }
1426                 return;
1427         }
1428
1429         if ($contact['network'] === NETWORK_DFRN) {
1430                 logger("Consume DFRN messages", LOGGER_DEBUG);
1431
1432                 $r = q("SELECT  `contact`.*, `contact`.`uid` AS `importer_uid`,
1433                                         `contact`.`pubkey` AS `cpubkey`,
1434                                         `contact`.`prvkey` AS `cprvkey`,
1435                                         `contact`.`thumb` AS `thumb`,
1436                                         `contact`.`url` as `url`,
1437                                         `contact`.`name` as `senderName`,
1438                                         `user`.*
1439                         FROM `contact`
1440                         LEFT JOIN `user` ON `contact`.`uid` = `user`.`uid`
1441                         WHERE `contact`.`id` = %d AND `user`.`uid` = %d",
1442                         dbesc($contact["id"]), dbesc($importer["uid"])
1443                 );
1444                 if ($r) {
1445                         logger("Now import the DFRN feed");
1446                         dfrn::import($xml, $r[0], true);
1447                         return;
1448                 }
1449         }
1450 }
1451
1452 function item_is_remote_self($contact, &$datarray) {
1453         $a = get_app();
1454
1455         if (!$contact['remote_self'])
1456                 return false;
1457
1458         // Prevent the forwarding of posts that are forwarded
1459         if ($datarray["extid"] == NETWORK_DFRN)
1460                 return false;
1461
1462         // Prevent to forward already forwarded posts
1463         if ($datarray["app"] == $a->get_hostname())
1464                 return false;
1465
1466         // Only forward posts
1467         if ($datarray["verb"] != ACTIVITY_POST)
1468                 return false;
1469
1470         if (($contact['network'] != NETWORK_FEED) AND $datarray['private'])
1471                 return false;
1472
1473         $datarray2 = $datarray;
1474         logger('remote-self start - Contact '.$contact['url'].' - '.$contact['remote_self'].' Item '.print_r($datarray, true), LOGGER_DEBUG);
1475         if ($contact['remote_self'] == 2) {
1476                 $r = q("SELECT `id`,`url`,`name`,`thumb` FROM `contact` WHERE `uid` = %d AND `self`",
1477                         intval($contact['uid']));
1478                 if (dbm::is_result($r)) {
1479                         $datarray['contact-id'] = $r[0]["id"];
1480
1481                         $datarray['owner-name'] = $r[0]["name"];
1482                         $datarray['owner-link'] = $r[0]["url"];
1483                         $datarray['owner-avatar'] = $r[0]["thumb"];
1484
1485                         $datarray['author-name']   = $datarray['owner-name'];
1486                         $datarray['author-link']   = $datarray['owner-link'];
1487                         $datarray['author-avatar'] = $datarray['owner-avatar'];
1488                 }
1489
1490                 if ($contact['network'] != NETWORK_FEED) {
1491                         $datarray["guid"] = get_guid(32);
1492                         unset($datarray["plink"]);
1493                         $datarray["uri"] = item_new_uri($a->get_hostname(), $contact['uid'], $datarray["guid"]);
1494                         $datarray["parent-uri"] = $datarray["uri"];
1495                         $datarray["extid"] = $contact['network'];
1496                         $urlpart = parse_url($datarray2['author-link']);
1497                         $datarray["app"] = $urlpart["host"];
1498                 } else
1499                         $datarray['private'] = 0;
1500         }
1501
1502         if ($contact['network'] != NETWORK_FEED) {
1503                 // Store the original post
1504                 $r = item_store($datarray2, false, false);
1505                 logger('remote-self post original item - Contact '.$contact['url'].' return '.$r.' Item '.print_r($datarray2, true), LOGGER_DEBUG);
1506         } else
1507                 $datarray["app"] = "Feed";
1508
1509         return true;
1510 }
1511
1512 function new_follower($importer, $contact, $datarray, $item, $sharing = false) {
1513         $url = notags(trim($datarray['author-link']));
1514         $name = notags(trim($datarray['author-name']));
1515         $photo = notags(trim($datarray['author-avatar']));
1516
1517         if (is_object($item)) {
1518                 $rawtag = $item->get_item_tags(NAMESPACE_ACTIVITY,'actor');
1519                 if ($rawtag && $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data']) {
1520                         $nick = $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data'];
1521                 }
1522         } else {
1523                 $nick = $item;
1524         }
1525
1526         if (is_array($contact)) {
1527                 if (($contact['network'] == NETWORK_OSTATUS && $contact['rel'] == CONTACT_IS_SHARING)
1528                         || ($sharing && $contact['rel'] == CONTACT_IS_FOLLOWER)) {
1529                         $r = q("UPDATE `contact` SET `rel` = %d, `writable` = 1 WHERE `id` = %d AND `uid` = %d",
1530                                 intval(CONTACT_IS_FRIEND),
1531                                 intval($contact['id']),
1532                                 intval($importer['uid'])
1533                         );
1534                 }
1535                 // send email notification to owner?
1536         } else {
1537
1538                 // create contact record
1539
1540                 $r = q("INSERT INTO `contact` (`uid`, `created`, `url`, `nurl`, `name`, `nick`, `photo`, `network`, `rel`,
1541                         `blocked`, `readonly`, `pending`, `writable`)
1542                         VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, 0, 0, 1, 1)",
1543                         intval($importer['uid']),
1544                         dbesc(datetime_convert()),
1545                         dbesc($url),
1546                         dbesc(normalise_link($url)),
1547                         dbesc($name),
1548                         dbesc($nick),
1549                         dbesc($photo),
1550                         dbesc(($sharing) ? NETWORK_ZOT : NETWORK_OSTATUS),
1551                         intval(($sharing) ? CONTACT_IS_SHARING : CONTACT_IS_FOLLOWER)
1552                 );
1553                 $r = q("SELECT `id`, `network` FROM `contact` WHERE `uid` = %d AND `url` = '%s' AND `pending` = 1 LIMIT 1",
1554                                 intval($importer['uid']),
1555                                 dbesc($url)
1556                 );
1557                 if (dbm::is_result($r)) {
1558                         $contact_record = $r[0];
1559                         update_contact_avatar($photo, $importer["uid"], $contact_record["id"], true);
1560                 }
1561
1562                 $r = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1",
1563                         intval($importer['uid'])
1564                 );
1565
1566                 if (dbm::is_result($r) AND !in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
1567
1568                         // create notification
1569                         $hash = random_string();
1570
1571                         if (is_array($contact_record)) {
1572                                 $ret = q("INSERT INTO `intro` ( `uid`, `contact-id`, `blocked`, `knowyou`, `hash`, `datetime`)
1573                                         VALUES ( %d, %d, 0, 0, '%s', '%s' )",
1574                                         intval($importer['uid']),
1575                                         intval($contact_record['id']),
1576                                         dbesc($hash),
1577                                         dbesc(datetime_convert())
1578                                 );
1579                         }
1580
1581                         $def_gid = get_default_group($importer['uid'], $contact_record["network"]);
1582
1583                         if (intval($def_gid)) {
1584                                 group_add_member($importer['uid'], '', $contact_record['id'], $def_gid);
1585                         }
1586
1587                         if (($r[0]['notify-flags'] & NOTIFY_INTRO) &&
1588                                 in_array($r[0]['page-flags'], array(PAGE_NORMAL))) {
1589
1590                                 notification(array(
1591                                         'type'         => NOTIFY_INTRO,
1592                                         'notify_flags' => $r[0]['notify-flags'],
1593                                         'language'     => $r[0]['language'],
1594                                         'to_name'      => $r[0]['username'],
1595                                         'to_email'     => $r[0]['email'],
1596                                         'uid'          => $r[0]['uid'],
1597                                         'link'             => App::get_baseurl() . '/notifications/intro',
1598                                         'source_name'  => ((strlen(stripslashes($contact_record['name']))) ? stripslashes($contact_record['name']) : t('[Name Withheld]')),
1599                                         'source_link'  => $contact_record['url'],
1600                                         'source_photo' => $contact_record['photo'],
1601                                         'verb'         => ($sharing ? ACTIVITY_FRIEND : ACTIVITY_FOLLOW),
1602                                         'otype'        => 'intro'
1603                                 ));
1604
1605                         }
1606                 } elseif (dbm::is_result($r) AND in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
1607                         $r = q("UPDATE `contact` SET `pending` = 0 WHERE `uid` = %d AND `url` = '%s' AND `pending` LIMIT 1",
1608                                         intval($importer['uid']),
1609                                         dbesc($url)
1610                         );
1611                 }
1612
1613         }
1614 }
1615
1616 function lose_follower($importer, $contact, array $datarray = array(), $item = "") {
1617
1618         if (($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_SHARING)) {
1619                 q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
1620                         intval(CONTACT_IS_SHARING),
1621                         intval($contact['id'])
1622                 );
1623         } else {
1624                 contact_remove($contact['id']);
1625         }
1626 }
1627
1628 function lose_sharer($importer, $contact, array $datarray = array(), $item = "") {
1629
1630         if (($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_FOLLOWER)) {
1631                 q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
1632                         intval(CONTACT_IS_FOLLOWER),
1633                         intval($contact['id'])
1634                 );
1635         } else {
1636                 contact_remove($contact['id']);
1637         }
1638 }
1639
1640 function subscribe_to_hub($url, $importer, $contact, $hubmode = 'subscribe') {
1641
1642         $a = get_app();
1643
1644         if (is_array($importer)) {
1645                 $r = q("SELECT `nickname` FROM `user` WHERE `uid` = %d LIMIT 1",
1646                         intval($importer['uid'])
1647                 );
1648         }
1649
1650         // Diaspora has different message-ids in feeds than they do
1651         // through the direct Diaspora protocol. If we try and use
1652         // the feed, we'll get duplicates. So don't.
1653
1654         if ((! dbm::is_result($r)) || $contact['network'] === NETWORK_DIASPORA)
1655                 return;
1656
1657         $push_url = get_config('system','url') . '/pubsub/' . $r[0]['nickname'] . '/' . $contact['id'];
1658
1659         // Use a single verify token, even if multiple hubs
1660
1661         $verify_token = ((strlen($contact['hub-verify'])) ? $contact['hub-verify'] : random_string());
1662
1663         $params= 'hub.mode=' . $hubmode . '&hub.callback=' . urlencode($push_url) . '&hub.topic=' . urlencode($contact['poll']) . '&hub.verify=async&hub.verify_token=' . $verify_token;
1664
1665         logger('subscribe_to_hub: ' . $hubmode . ' ' . $contact['name'] . ' to hub ' . $url . ' endpoint: '  . $push_url . ' with verifier ' . $verify_token);
1666
1667         if (!strlen($contact['hub-verify']) OR ($contact['hub-verify'] != $verify_token)) {
1668                 $r = q("UPDATE `contact` SET `hub-verify` = '%s' WHERE `id` = %d",
1669                         dbesc($verify_token),
1670                         intval($contact['id'])
1671                 );
1672         }
1673
1674         post_url($url, $params);
1675
1676         logger('subscribe_to_hub: returns: ' . $a->get_curl_code(), LOGGER_DEBUG);
1677
1678         return;
1679
1680 }
1681
1682 function fix_private_photos($s, $uid, $item = null, $cid = 0) {
1683
1684         if (get_config('system','disable_embedded'))
1685                 return $s;
1686
1687         $a = get_app();
1688
1689         logger('fix_private_photos: check for photos', LOGGER_DEBUG);
1690         $site = substr(App::get_baseurl(),strpos(App::get_baseurl(),'://'));
1691
1692         $orig_body = $s;
1693         $new_body = '';
1694
1695         $img_start = strpos($orig_body, '[img');
1696         $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
1697         $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
1698         while ( ($img_st_close !== false) && ($img_len !== false) ) {
1699
1700                 $img_st_close++; // make it point to AFTER the closing bracket
1701                 $image = substr($orig_body, $img_start + $img_st_close, $img_len);
1702
1703                 logger('fix_private_photos: found photo ' . $image, LOGGER_DEBUG);
1704
1705
1706                 if (stristr($image , $site . '/photo/')) {
1707                         // Only embed locally hosted photos
1708                         $replace = false;
1709                         $i = basename($image);
1710                         $i = str_replace(array('.jpg','.png','.gif'),array('', '',''), $i);
1711                         $x = strpos($i,'-');
1712
1713                         if ($x) {
1714                                 $res = substr($i, $x+1);
1715                                 $i = substr($i,0, $x);
1716                                 $r = q("SELECT * FROM `photo` WHERE `resource-id` = '%s' AND `scale` = %d AND `uid` = %d",
1717                                         dbesc($i),
1718                                         intval($res),
1719                                         intval($uid)
1720                                 );
1721                                 if ($r) {
1722
1723                                         // Check to see if we should replace this photo link with an embedded image
1724                                         // 1. No need to do so if the photo is public
1725                                         // 2. If there's a contact-id provided, see if they're in the access list
1726                                         //    for the photo. If so, embed it.
1727                                         // 3. Otherwise, if we have an item, see if the item permissions match the photo
1728                                         //    permissions, regardless of order but first check to see if they're an exact
1729                                         //    match to save some processing overhead.
1730
1731                                         if (has_permissions($r[0])) {
1732                                                 if ($cid) {
1733                                                         $recips = enumerate_permissions($r[0]);
1734                                                         if (in_array($cid, $recips)) {
1735                                                                 $replace = true;
1736                                                         }
1737                                                 } elseif ($item) {
1738                                                         if (compare_permissions($item, $r[0]))
1739                                                                 $replace = true;
1740                                                 }
1741                                         }
1742                                         if ($replace) {
1743                                                 $data = $r[0]['data'];
1744                                                 $type = $r[0]['type'];
1745
1746                                                 // If a custom width and height were specified, apply before embedding
1747                                                 if (preg_match("/\[img\=([0-9]*)x([0-9]*)\]/is", substr($orig_body, $img_start, $img_st_close), $match)) {
1748                                                         logger('fix_private_photos: scaling photo', LOGGER_DEBUG);
1749
1750                                                         $width = intval($match[1]);
1751                                                         $height = intval($match[2]);
1752
1753                                                         $ph = new Photo($data, $type);
1754                                                         if ($ph->is_valid()) {
1755                                                                 $ph->scaleImage(max($width, $height));
1756                                                                 $data = $ph->imageString();
1757                                                                 $type = $ph->getType();
1758                                                         }
1759                                                 }
1760
1761                                                 logger('fix_private_photos: replacing photo', LOGGER_DEBUG);
1762                                                 $image = 'data:' . $type . ';base64,' . base64_encode($data);
1763                                                 logger('fix_private_photos: replaced: ' . $image, LOGGER_DATA);
1764                                         }
1765                                 }
1766                         }
1767                 }
1768
1769                 $new_body = $new_body . substr($orig_body, 0, $img_start + $img_st_close) . $image . '[/img]';
1770                 $orig_body = substr($orig_body, $img_start + $img_st_close + $img_len + strlen('[/img]'));
1771                 if ($orig_body === false) {
1772                         $orig_body = '';
1773                 }
1774
1775                 $img_start = strpos($orig_body, '[img');
1776                 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
1777                 $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
1778         }
1779
1780         $new_body = $new_body . $orig_body;
1781
1782         return($new_body);
1783 }
1784
1785 function has_permissions($obj) {
1786         if (($obj['allow_cid'] != '') || ($obj['allow_gid'] != '') || ($obj['deny_cid'] != '') || ($obj['deny_gid'] != ''))
1787                 return true;
1788         return false;
1789 }
1790
1791 function compare_permissions($obj1, $obj2) {
1792         // first part is easy. Check that these are exactly the same.
1793         if (($obj1['allow_cid'] == $obj2['allow_cid'])
1794                 && ($obj1['allow_gid'] == $obj2['allow_gid'])
1795                 && ($obj1['deny_cid'] == $obj2['deny_cid'])
1796                 && ($obj1['deny_gid'] == $obj2['deny_gid']))
1797                 return true;
1798
1799         // This is harder. Parse all the permissions and compare the resulting set.
1800
1801         $recipients1 = enumerate_permissions($obj1);
1802         $recipients2 = enumerate_permissions($obj2);
1803         sort($recipients1);
1804         sort($recipients2);
1805         if ($recipients1 == $recipients2)
1806                 return true;
1807         return false;
1808 }
1809
1810 // returns an array of contact-ids that are allowed to see this object
1811
1812 function enumerate_permissions($obj) {
1813         $allow_people = expand_acl($obj['allow_cid']);
1814         $allow_groups = expand_groups(expand_acl($obj['allow_gid']));
1815         $deny_people  = expand_acl($obj['deny_cid']);
1816         $deny_groups  = expand_groups(expand_acl($obj['deny_gid']));
1817         $recipients   = array_unique(array_merge($allow_people, $allow_groups));
1818         $deny         = array_unique(array_merge($deny_people, $deny_groups));
1819         $recipients   = array_diff($recipients, $deny);
1820         return $recipients;
1821 }
1822
1823 function item_getfeedtags($item) {
1824         $ret = array();
1825         $matches = false;
1826         $cnt = preg_match_all('|\#\[url\=(.*?)\](.*?)\[\/url\]|', $item['tag'], $matches);
1827         if ($cnt) {
1828                 for($x = 0; $x < $cnt; $x ++) {
1829                         if ($matches[1][$x])
1830                                 $ret[$matches[2][$x]] = array('#', $matches[1][$x], $matches[2][$x]);
1831                 }
1832         }
1833         $matches = false;
1834         $cnt = preg_match_all('|\@\[url\=(.*?)\](.*?)\[\/url\]|', $item['tag'], $matches);
1835         if ($cnt) {
1836                 for($x = 0; $x < $cnt; $x ++) {
1837                         if ($matches[1][$x])
1838                                 $ret[] = array('@', $matches[1][$x], $matches[2][$x]);
1839                 }
1840         }
1841         return $ret;
1842 }
1843
1844 function item_expire($uid, $days, $network = "", $force = false) {
1845
1846         if ((! $uid) || ($days < 1))
1847                 return;
1848
1849         // $expire_network_only = save your own wall posts
1850         // and just expire conversations started by others
1851
1852         $expire_network_only = get_pconfig($uid,'expire','network_only');
1853         $sql_extra = ((intval($expire_network_only)) ? " AND wall = 0 " : "");
1854
1855         if ($network != "") {
1856                 $sql_extra .= sprintf(" AND network = '%s' ", dbesc($network));
1857                 // There is an index "uid_network_received" but not "uid_network_created"
1858                 // This avoids the creation of another index just for one purpose.
1859                 // And it doesn't really matter wether to look at "received" or "created"
1860                 $range = "AND `received` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
1861         } else
1862                 $range = "AND `created` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
1863
1864         $r = q("SELECT `file`, `resource-id`, `starred`, `type`, `id` FROM `item`
1865                 WHERE `uid` = %d $range
1866                 AND `id` = `parent`
1867                 $sql_extra
1868                 AND `deleted` = 0",
1869                 intval($uid),
1870                 intval($days)
1871         );
1872
1873         if (! dbm::is_result($r))
1874                 return;
1875
1876         $expire_items = get_pconfig($uid, 'expire','items');
1877         $expire_items = (($expire_items===false)?1:intval($expire_items)); // default if not set: 1
1878
1879         // Forcing expiring of items - but not notes and marked items
1880         if ($force)
1881                 $expire_items = true;
1882
1883         $expire_notes = get_pconfig($uid, 'expire','notes');
1884         $expire_notes = (($expire_notes===false)?1:intval($expire_notes)); // default if not set: 1
1885
1886         $expire_starred = get_pconfig($uid, 'expire','starred');
1887         $expire_starred = (($expire_starred===false)?1:intval($expire_starred)); // default if not set: 1
1888
1889         $expire_photos = get_pconfig($uid, 'expire','photos');
1890         $expire_photos = (($expire_photos===false)?0:intval($expire_photos)); // default if not set: 0
1891
1892         logger('expire: # items=' . count($r). "; expire items: $expire_items, expire notes: $expire_notes, expire starred: $expire_starred, expire photos: $expire_photos");
1893
1894         foreach($r as $item) {
1895
1896                 // don't expire filed items
1897
1898                 if (strpos($item['file'],'[') !== false)
1899                         continue;
1900
1901                 // Only expire posts, not photos and photo comments
1902
1903                 if ($expire_photos==0 && strlen($item['resource-id']))
1904                         continue;
1905                 if ($expire_starred==0 && intval($item['starred']))
1906                         continue;
1907                 if ($expire_notes==0 && $item['type']=='note')
1908                         continue;
1909                 if ($expire_items==0 && $item['type']!='note')
1910                         continue;
1911
1912                 drop_item($item['id'],false);
1913         }
1914
1915         proc_run(PRIORITY_HIGH, "include/notifier.php", "expire", $uid);
1916
1917 }
1918
1919
1920 function drop_items($items) {
1921         $uid = 0;
1922
1923         if (! local_user() && ! remote_user())
1924                 return;
1925
1926         if (count($items)) {
1927                 foreach($items as $item) {
1928                         $owner = drop_item($item,false);
1929                         if ($owner && ! $uid)
1930                                 $uid = $owner;
1931                 }
1932         }
1933
1934         // multiple threads may have been deleted, send an expire notification
1935
1936         if ($uid) {
1937                 proc_run(PRIORITY_HIGH, "include/notifier.php", "expire", $uid);
1938         }
1939 }
1940
1941
1942 function drop_item($id, $interactive = true) {
1943
1944         $a = get_app();
1945
1946         // locate item to be deleted
1947
1948         $r = q("SELECT * FROM `item` WHERE `id` = %d LIMIT 1",
1949                 intval($id)
1950         );
1951
1952         if (! dbm::is_result($r)) {
1953                 if (! $interactive) {
1954                         return 0;
1955                 }
1956                 notice( t('Item not found.') . EOL);
1957                 goaway(App::get_baseurl() . '/' . $_SESSION['return_url']);
1958         }
1959
1960         $item = $r[0];
1961
1962         $owner = $item['uid'];
1963
1964         $contact_id = 0;
1965
1966         // check if logged in user is either the author or owner of this item
1967
1968         if (is_array($_SESSION['remote'])) {
1969                 foreach($_SESSION['remote'] as $visitor) {
1970                         if ($visitor['uid'] == $item['uid'] && $visitor['cid'] == $item['contact-id']) {
1971                                 $contact_id = $visitor['cid'];
1972                                 break;
1973                         }
1974                 }
1975         }
1976
1977
1978         if ((local_user() == $item['uid']) || ($contact_id) || (! $interactive)) {
1979
1980                 // Check if we should do HTML-based delete confirmation
1981                 if ($_REQUEST['confirm']) {
1982                         // <form> can't take arguments in its "action" parameter
1983                         // so add any arguments as hidden inputs
1984                         $query = explode_querystring($a->query_string);
1985                         $inputs = array();
1986                         foreach($query['args'] as $arg) {
1987                                 if (strpos($arg, 'confirm=') === false) {
1988                                         $arg_parts = explode('=', $arg);
1989                                         $inputs[] = array('name' => $arg_parts[0], 'value' => $arg_parts[1]);
1990                                 }
1991                         }
1992
1993                         return replace_macros(get_markup_template('confirm.tpl'), array(
1994                                 '$method' => 'get',
1995                                 '$message' => t('Do you really want to delete this item?'),
1996                                 '$extra_inputs' => $inputs,
1997                                 '$confirm' => t('Yes'),
1998                                 '$confirm_url' => $query['base'],
1999                                 '$confirm_name' => 'confirmed',
2000                                 '$cancel' => t('Cancel'),
2001                         ));
2002                 }
2003                 // Now check how the user responded to the confirmation query
2004                 if ($_REQUEST['canceled']) {
2005                         goaway(App::get_baseurl() . '/' . $_SESSION['return_url']);
2006                 }
2007
2008                 logger('delete item: ' . $item['id'], LOGGER_DEBUG);
2009                 // delete the item
2010
2011                 $r = q("UPDATE `item` SET `deleted` = 1, `title` = '', `body` = '', `edited` = '%s', `changed` = '%s' WHERE `id` = %d",
2012                         dbesc(datetime_convert()),
2013                         dbesc(datetime_convert()),
2014                         intval($item['id'])
2015                 );
2016                 create_tags_from_item($item['id']);
2017                 create_files_from_item($item['id']);
2018                 delete_thread($item['id'], $item['parent-uri']);
2019
2020                 // clean up categories and tags so they don't end up as orphans
2021
2022                 $matches = false;
2023                 $cnt = preg_match_all('/<(.*?)>/', $item['file'], $matches,PREG_SET_ORDER);
2024                 if ($cnt) {
2025                         foreach($matches as $mtch) {
2026                                 file_tag_unsave_file($item['uid'], $item['id'], $mtch[1],true);
2027                         }
2028                 }
2029
2030                 $matches = false;
2031
2032                 $cnt = preg_match_all('/\[(.*?)\]/', $item['file'], $matches,PREG_SET_ORDER);
2033                 if ($cnt) {
2034                         foreach($matches as $mtch) {
2035                                 file_tag_unsave_file($item['uid'], $item['id'], $mtch[1],false);
2036                         }
2037                 }
2038
2039                 // If item is a link to a photo resource, nuke all the associated photos
2040                 // (visitors will not have photo resources)
2041                 // This only applies to photos uploaded from the photos page. Photos inserted into a post do not
2042                 // generate a resource-id and therefore aren't intimately linked to the item.
2043
2044                 if (strlen($item['resource-id'])) {
2045                         q("DELETE FROM `photo` WHERE `resource-id` = '%s' AND `uid` = %d ",
2046                                 dbesc($item['resource-id']),
2047                                 intval($item['uid'])
2048                         );
2049                         // ignore the result
2050                 }
2051
2052                 // If item is a link to an event, nuke the event record.
2053
2054                 if (intval($item['event-id'])) {
2055                         q("DELETE FROM `event` WHERE `id` = %d AND `uid` = %d",
2056                                 intval($item['event-id']),
2057                                 intval($item['uid'])
2058                         );
2059                         // ignore the result
2060                 }
2061
2062                 // If item has attachments, drop them
2063
2064                 foreach(explode(", ", $item['attach']) as $attach) {
2065                         preg_match("|attach/(\d+)|", $attach, $matches);
2066                         q("DELETE FROM `attach` WHERE `id` = %d AND `uid` = %d",
2067                                 intval($matches[1]),
2068                                 local_user()
2069                         );
2070                         // ignore the result
2071                 }
2072
2073
2074                 // clean up item_id and sign meta-data tables
2075
2076                 /*
2077                 // Old code - caused very long queries and warning entries in the mysql logfiles:
2078
2079                 $r = q("DELETE FROM item_id where iid in (select id from item where parent = %d and uid = %d)",
2080                         intval($item['id']),
2081                         intval($item['uid'])
2082                 );
2083
2084                 $r = q("DELETE FROM sign where iid in (select id from item where parent = %d and uid = %d)",
2085                         intval($item['id']),
2086                         intval($item['uid'])
2087                 );
2088                 */
2089
2090                 // The new code splits the queries since the mysql optimizer really has bad problems with subqueries
2091
2092                 // Creating list of parents
2093                 $r = q("SELECT `id` FROM `item` WHERE `parent` = %d AND `uid` = %d",
2094                         intval($item['id']),
2095                         intval($item['uid'])
2096                 );
2097
2098                 $parentid = "";
2099
2100                 foreach ($r AS $row) {
2101                         if ($parentid != "")
2102                                 $parentid .= ", ";
2103
2104                         $parentid .= $row["id"];
2105                 }
2106
2107                 // Now delete them
2108                 if ($parentid != "") {
2109                         $r = q("DELETE FROM `item_id` WHERE `iid` IN (%s)", dbesc($parentid));
2110                         $r = q("DELETE FROM `sign` WHERE `iid` IN (%s)", dbesc($parentid));
2111                 }
2112
2113                 // If it's the parent of a comment thread, kill all the kids
2114                 if ($item['uri'] == $item['parent-uri']) {
2115                         $r = q("UPDATE `item` SET `deleted` = 1, `edited` = '%s', `changed` = '%s', `body` = '' , `title` = ''
2116                                 WHERE `parent-uri` = '%s' AND `uid` = %d ",
2117                                 dbesc(datetime_convert()),
2118                                 dbesc(datetime_convert()),
2119                                 dbesc($item['parent-uri']),
2120                                 intval($item['uid'])
2121                         );
2122                         create_tags_from_itemuri($item['parent-uri'], $item['uid']);
2123                         create_files_from_itemuri($item['parent-uri'], $item['uid']);
2124                         delete_thread_uri($item['parent-uri'], $item['uid']);
2125                         // ignore the result
2126                 } else {
2127                         // ensure that last-child is set in case the comment that had it just got wiped.
2128                         q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `parent-uri` = '%s' AND `uid` = %d ",
2129                                 dbesc(datetime_convert()),
2130                                 dbesc($item['parent-uri']),
2131                                 intval($item['uid'])
2132                         );
2133                         // who is the last child now?
2134                         $r = q("SELECT `id` FROM `item` WHERE `parent-uri` = '%s' AND `type` != 'activity' AND `deleted` = 0 AND `uid` = %d ORDER BY `edited` DESC LIMIT 1",
2135                                 dbesc($item['parent-uri']),
2136                                 intval($item['uid'])
2137                         );
2138                         if (dbm::is_result($r)) {
2139                                 q("UPDATE `item` SET `last-child` = 1 WHERE `id` = %d",
2140                                         intval($r[0]['id'])
2141                                 );
2142                         }
2143                 }
2144
2145                 $drop_id = intval($item['id']);
2146
2147                 // send the notification upstream/downstream as the case may be
2148
2149                 proc_run(PRIORITY_HIGH, "include/notifier.php", "drop", $drop_id);
2150
2151                 if (! $interactive) {
2152                         return $owner;
2153                 }
2154                 goaway(App::get_baseurl() . '/' . $_SESSION['return_url']);
2155                 //NOTREACHED
2156         } else {
2157                 if (! $interactive) {
2158                         return 0;
2159                 }
2160                 notice( t('Permission denied.') . EOL);
2161                 goaway(App::get_baseurl() . '/' . $_SESSION['return_url']);
2162                 //NOTREACHED
2163         }
2164
2165 }
2166
2167
2168 function first_post_date($uid, $wall = false) {
2169         $r = q("SELECT `id`, `created` FROM `item`
2170                 WHERE `uid` = %d AND `wall` = %d AND `deleted` = 0 AND `visible` = 1 AND `moderated` = 0
2171                 AND `id` = `parent`
2172                 ORDER BY `created` ASC LIMIT 1",
2173                 intval($uid),
2174                 intval($wall ? 1 : 0)
2175         );
2176         if (dbm::is_result($r)) {
2177 //              logger('first_post_date: ' . $r[0]['id'] . ' ' . $r[0]['created'], LOGGER_DATA);
2178                 return substr(datetime_convert('',date_default_timezone_get(), $r[0]['created']),0,10);
2179         }
2180         return false;
2181 }
2182
2183 /* modified posted_dates() {below} to arrange the list in years */
2184 function list_post_dates($uid, $wall) {
2185         $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
2186
2187         $dthen = first_post_date($uid, $wall);
2188         if (! $dthen) {
2189                 return array();
2190         }
2191
2192         // Set the start and end date to the beginning of the month
2193         $dnow = substr($dnow,0,8).'01';
2194         $dthen = substr($dthen,0,8).'01';
2195
2196         $ret = array();
2197
2198         /*
2199          * Starting with the current month, get the first and last days of every
2200          * month down to and including the month of the first post
2201          */
2202         while (substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
2203                 $dyear = intval(substr($dnow,0,4));
2204                 $dstart = substr($dnow,0,8) . '01';
2205                 $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
2206                 $start_month = datetime_convert('', '', $dstart,'Y-m-d');
2207                 $end_month = datetime_convert('', '', $dend,'Y-m-d');
2208                 $str = day_translate(datetime_convert('', '', $dnow,'F'));
2209                 if (!$ret[$dyear]) {
2210                         $ret[$dyear] = array();
2211                 }
2212                 $ret[$dyear][] = array($str, $end_month, $start_month);
2213                 $dnow = datetime_convert('', '', $dnow . ' -1 month', 'Y-m-d');
2214         }
2215         return $ret;
2216 }
2217
2218 function posted_dates($uid, $wall) {
2219         $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
2220
2221         $dthen = first_post_date($uid, $wall);
2222         if (! $dthen) {
2223                 return array();
2224         }
2225
2226         // Set the start and end date to the beginning of the month
2227         $dnow = substr($dnow,0,8).'01';
2228         $dthen = substr($dthen,0,8).'01';
2229
2230         $ret = array();
2231         // Starting with the current month, get the first and last days of every
2232         // month down to and including the month of the first post
2233         while (substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
2234                 $dstart = substr($dnow,0,8) . '01';
2235                 $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
2236                 $start_month = datetime_convert('', '', $dstart,'Y-m-d');
2237                 $end_month = datetime_convert('', '', $dend,'Y-m-d');
2238                 $str = day_translate(datetime_convert('', '', $dnow,'F Y'));
2239                 $ret[] = array($str, $end_month, $start_month);
2240                 $dnow = datetime_convert('', '', $dnow . ' -1 month', 'Y-m-d');
2241         }
2242         return $ret;
2243 }
2244
2245
2246 function posted_date_widget($url, $uid, $wall) {
2247         $o = '';
2248
2249         if (! feature_enabled($uid, 'archives')) {
2250                 return $o;
2251         }
2252
2253         // For former Facebook folks that left because of "timeline"
2254
2255 /*      if ($wall && intval(get_pconfig($uid,'system','no_wall_archive_widget')))
2256                 return $o;*/
2257
2258         $visible_years = get_pconfig($uid,'system','archive_visible_years');
2259         if (! $visible_years) {
2260                 $visible_years = 5;
2261         }
2262
2263         $ret = list_post_dates($uid, $wall);
2264
2265         if (! dbm::is_result($ret)) {
2266                 return $o;
2267         }
2268
2269         $cutoff_year = intval(datetime_convert('',date_default_timezone_get(),'now','Y')) - $visible_years;
2270         $cutoff = ((array_key_exists($cutoff_year, $ret))? true : false);
2271
2272         $o = replace_macros(get_markup_template('posted_date_widget.tpl'),array(
2273                 '$title' => t('Archives'),
2274                 '$size' => $visible_years,
2275                 '$cutoff_year' => $cutoff_year,
2276                 '$cutoff' => $cutoff,
2277                 '$url' => $url,
2278                 '$dates' => $ret,
2279                 '$showmore' => t('show more')
2280
2281         ));
2282         return $o;
2283 }