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