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