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