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