]> git.mxchange.org Git - friendica.git/blob - include/items.php
We now use memcache if configured and installed.
[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("START TRANSACTION;");
774
775         $r = dbq("INSERT INTO `item` (`"
776                         . implode("`, `", array_keys($arr))
777                         . "`) VALUES ('"
778                         . implode("', '", array_values($arr))
779                         . "')" );
780
781         // And restore it
782         $arr = $unescaped;
783
784         // find the item that we just created
785         $r = q("SELECT `id` FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` = '%s' ORDER BY `id` ASC",
786                 dbesc($arr['uri']),
787                 intval($arr['uid']),
788                 dbesc($arr['network'])
789         );
790
791         if (count($r) > 1) {
792                 // There are duplicates. Keep the oldest one, delete the others
793                 logger('item_store: duplicated post occurred. Removing newer duplicates. uri = '.$arr['uri'].' uid = '.$arr['uid']);
794                 q("DELETE FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` = '%s' AND `id` > %d",
795                         dbesc($arr['uri']),
796                         intval($arr['uid']),
797                         dbesc($arr['network']),
798                         intval($r[0]["id"])
799                 );
800                 q("COMMIT");
801                 return 0;
802         } elseif (count($r)) {
803
804                 $current_post = $r[0]['id'];
805                 logger('item_store: created item ' . $current_post);
806
807                 item_set_last_item($arr);
808         } else {
809                 logger('item_store: could not locate created item');
810                 q("COMMIT");
811                 return 0;
812         }
813
814         if (!$parent_id || ($arr['parent-uri'] === $arr['uri']))
815                 $parent_id = $current_post;
816
817         // Set parent id
818         $r = q("UPDATE `item` SET `parent` = %d WHERE `id` = %d",
819                 intval($parent_id),
820                 intval($current_post)
821         );
822
823         $arr['id'] = $current_post;
824         $arr['parent'] = $parent_id;
825
826         // update the commented timestamp on the parent
827         // Only update "commented" if it is really a comment
828         if (($arr['verb'] == ACTIVITY_POST) OR !get_config("system", "like_no_comment"))
829                 q("UPDATE `item` SET `commented` = '%s', `changed` = '%s' WHERE `id` = %d",
830                         dbesc(datetime_convert()),
831                         dbesc(datetime_convert()),
832                         intval($parent_id)
833                 );
834         else
835                 q("UPDATE `item` SET `changed` = '%s' WHERE `id` = %d",
836                         dbesc(datetime_convert()),
837                         intval($parent_id)
838                 );
839
840         if ($dsprsig) {
841
842                 // Friendica servers lower than 3.4.3-2 had double encoded the signature ...
843                 // We can check for this condition when we decode and encode the stuff again.
844                 if (base64_encode(base64_decode(base64_decode($dsprsig->signature))) == base64_decode($dsprsig->signature)) {
845                         $dsprsig->signature = base64_decode($dsprsig->signature);
846                         logger("Repaired double encoded signature from handle ".$dsprsig->signer, LOGGER_DEBUG);
847                 }
848
849                 q("insert into sign (`iid`,`signed_text`,`signature`,`signer`) values (%d,'%s','%s','%s') ",
850                         intval($current_post),
851                         dbesc($dsprsig->signed_text),
852                         dbesc($dsprsig->signature),
853                         dbesc($dsprsig->signer)
854                 );
855         }
856
857
858         /**
859          * If this is now the last-child, force all _other_ children of this parent to *not* be last-child
860          */
861
862         if ($arr['last-child']) {
863                 $r = q("UPDATE `item` SET `last-child` = 0 WHERE `parent-uri` = '%s' AND `uid` = %d AND `id` != %d",
864                         dbesc($arr['uri']),
865                         intval($arr['uid']),
866                         intval($current_post)
867                 );
868         }
869
870         $deleted = tag_deliver($arr['uid'],$current_post);
871
872         // current post can be deleted if is for a community page and no mention are
873         // in it.
874         if (!$deleted AND !$dontcache) {
875
876                 $r = q('SELECT * FROM `item` WHERE id = %d', intval($current_post));
877                 if (count($r) == 1) {
878                         if ($notify)
879                                 call_hooks('post_local_end', $r[0]);
880                         else
881                                 call_hooks('post_remote_end', $r[0]);
882                 } else
883                         logger('item_store: new item not found in DB, id ' . $current_post);
884         }
885
886         create_tags_from_item($current_post);
887         create_files_from_item($current_post);
888
889         // Only check for notifications on start posts
890         if ($arr['parent-uri'] === $arr['uri']) {
891                 add_thread($current_post);
892                 q("COMMIT");
893
894                 add_shadow_thread($current_post);
895         } else {
896                 update_thread($parent_id);
897                 q("COMMIT");
898
899                 add_shadow_entry($arr);
900         }
901
902         check_item_notification($current_post, $uid);
903
904         if ($notify)
905                 proc_run(PRIORITY_HIGH, "include/notifier.php", $notify_type, $current_post);
906
907         return $current_post;
908 }
909
910 /**
911  * @brief Set "success_update" and "last-item" to the date of the last time we heard from this contact
912  *
913  * This can be used to filter for inactive contacts.
914  * Only do this for public postings to avoid privacy problems, since poco data is public.
915  * Don't set this value if it isn't from the owner (could be an author that we don't know)
916  *
917  * @param array $arr Contains the just posted item record
918  */
919 function item_set_last_item($arr) {
920
921         $update = (!$arr['private'] AND (($arr["author-link"] === $arr["owner-link"]) OR ($arr["parent-uri"] === $arr["uri"])));
922
923         // Is it a forum? Then we don't care about the rules from above
924         if (!$update AND ($arr["network"] == NETWORK_DFRN) AND ($arr["parent-uri"] === $arr["uri"])) {
925                 $isforum = q("SELECT `forum` FROM `contact` WHERE `id` = %d AND `forum`",
926                                 intval($arr['contact-id']));
927                 if ($isforum) {
928                         $update = true;
929                 }
930         }
931
932         if ($update) {
933                 q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
934                         dbesc($arr['received']),
935                         dbesc($arr['received']),
936                         intval($arr['contact-id'])
937                 );
938         }
939         // Now do the same for the system wide contacts with uid=0
940         if (!$arr['private']) {
941                 q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
942                         dbesc($arr['received']),
943                         dbesc($arr['received']),
944                         intval($arr['owner-id'])
945                 );
946
947                 if ($arr['owner-id'] != $arr['author-id']) {
948                         q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
949                                 dbesc($arr['received']),
950                                 dbesc($arr['received']),
951                                 intval($arr['author-id'])
952                         );
953                 }
954         }
955 }
956
957 function item_body_set_hashtags(&$item) {
958
959         $tags = get_tags($item["body"]);
960
961         // No hashtags?
962         if (!count($tags))
963                 return(false);
964
965         // This sorting is important when there are hashtags that are part of other hashtags
966         // Otherwise there could be problems with hashtags like #test and #test2
967         rsort($tags);
968
969         $a = get_app();
970
971         $URLSearchString = "^\[\]";
972
973         // All hashtags should point to the home server
974         //$item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
975         //              "#[url=".$a->get_baseurl()."/search?tag=$2]$2[/url]", $item["body"]);
976
977         //$item["tag"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
978         //              "#[url=".$a->get_baseurl()."/search?tag=$2]$2[/url]", $item["tag"]);
979
980         // mask hashtags inside of url, bookmarks and attachments to avoid urls in urls
981         $item["body"] = preg_replace_callback("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
982                 function ($match){
983                         return("[url=".str_replace("#", "&num;", $match[1])."]".str_replace("#", "&num;", $match[2])."[/url]");
984                 },$item["body"]);
985
986         $item["body"] = preg_replace_callback("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism",
987                 function ($match){
988                         return("[bookmark=".str_replace("#", "&num;", $match[1])."]".str_replace("#", "&num;", $match[2])."[/bookmark]");
989                 },$item["body"]);
990
991         $item["body"] = preg_replace_callback("/\[attachment (.*)\](.*?)\[\/attachment\]/ism",
992                 function ($match){
993                         return("[attachment ".str_replace("#", "&num;", $match[1])."]".$match[2]."[/attachment]");
994                 },$item["body"]);
995
996         // Repair recursive urls
997         $item["body"] = preg_replace("/&num;\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
998                         "&num;$2", $item["body"]);
999
1000
1001         foreach($tags as $tag) {
1002                 if (strpos($tag,'#') !== 0)
1003                         continue;
1004
1005                 if (strpos($tag,'[url='))
1006                         continue;
1007
1008                 $basetag = str_replace('_',' ',substr($tag,1));
1009
1010                 $newtag = '#[url='.$a->get_baseurl().'/search?tag='.rawurlencode($basetag).']'.$basetag.'[/url]';
1011
1012                 $item["body"] = str_replace($tag, $newtag, $item["body"]);
1013
1014                 if (!stristr($item["tag"],"/search?tag=".$basetag."]".$basetag."[/url]")) {
1015                         if (strlen($item["tag"]))
1016                                 $item["tag"] = ','.$item["tag"];
1017                         $item["tag"] = $newtag.$item["tag"];
1018                 }
1019         }
1020
1021         // Convert back the masked hashtags
1022         $item["body"] = str_replace("&num;", "#", $item["body"]);
1023 }
1024
1025 function get_item_guid($id) {
1026         $r = q("SELECT `guid` FROM `item` WHERE `id` = %d LIMIT 1", intval($id));
1027         if (count($r))
1028                 return($r[0]["guid"]);
1029         else
1030                 return("");
1031 }
1032
1033 function get_item_id($guid, $uid = 0) {
1034
1035         $nick = "";
1036         $id = 0;
1037
1038         if ($uid == 0)
1039                 $uid == local_user();
1040
1041         // Does the given user have this item?
1042         if ($uid) {
1043                 $r = q("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid`
1044                         WHERE `item`.`visible` = 1 AND `item`.`deleted` = 0 and `item`.`moderated` = 0
1045                                 AND `item`.`guid` = '%s' AND `item`.`uid` = %d", dbesc($guid), intval($uid));
1046                 if (count($r)) {
1047                         $id = $r[0]["id"];
1048                         $nick = $r[0]["nickname"];
1049                 }
1050         }
1051
1052         // Or is it anywhere on the server?
1053         if ($nick == "") {
1054                 $r = q("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid`
1055                         WHERE `item`.`visible` = 1 AND `item`.`deleted` = 0 and `item`.`moderated` = 0
1056                                 AND `item`.`allow_cid` = ''  AND `item`.`allow_gid` = ''
1057                                 AND `item`.`deny_cid`  = '' AND `item`.`deny_gid`  = ''
1058                                 AND `item`.`private` = 0 AND `item`.`wall` = 1
1059                                 AND `item`.`guid` = '%s'", dbesc($guid));
1060                 if (count($r)) {
1061                         $id = $r[0]["id"];
1062                         $nick = $r[0]["nickname"];
1063                 }
1064         }
1065         return(array("nick" => $nick, "id" => $id));
1066 }
1067
1068 // return - test
1069 function get_item_contact($item,$contacts) {
1070         if (! count($contacts) || (! is_array($item)))
1071                 return false;
1072         foreach($contacts as $contact) {
1073                 if ($contact['id'] == $item['contact-id']) {
1074                         return $contact;
1075                         break; // NOTREACHED
1076                 }
1077         }
1078         return false;
1079 }
1080
1081 /**
1082  * look for mention tags and setup a second delivery chain for forum/community posts if appropriate
1083  * @param int $uid
1084  * @param int $item_id
1085  * @return bool true if item was deleted, else false
1086  */
1087 function tag_deliver($uid,$item_id) {
1088
1089         //
1090
1091         $a = get_app();
1092
1093         $mention = false;
1094
1095         $u = q("select * from user where uid = %d limit 1",
1096                 intval($uid)
1097         );
1098         if (! count($u))
1099                 return;
1100
1101         $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
1102         $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
1103
1104
1105         $i = q("select * from item where id = %d and uid = %d limit 1",
1106                 intval($item_id),
1107                 intval($uid)
1108         );
1109         if (! count($i))
1110                 return;
1111
1112         $item = $i[0];
1113
1114         $link = normalise_link($a->get_baseurl() . '/profile/' . $u[0]['nickname']);
1115
1116         // Diaspora uses their own hardwired link URL in @-tags
1117         // instead of the one we supply with webfinger
1118
1119         $dlink = normalise_link($a->get_baseurl() . '/u/' . $u[0]['nickname']);
1120
1121         $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism',$item['body'],$matches,PREG_SET_ORDER);
1122         if ($cnt) {
1123                 foreach($matches as $mtch) {
1124                         if (link_compare($link,$mtch[1]) || link_compare($dlink,$mtch[1])) {
1125                                 $mention = true;
1126                                 logger('tag_deliver: mention found: ' . $mtch[2]);
1127                         }
1128                 }
1129         }
1130
1131         if (! $mention){
1132                 if ( ($community_page || $prvgroup) &&
1133                           (!$item['wall']) && (!$item['origin']) && ($item['id'] == $item['parent'])){
1134                         // mmh.. no mention.. community page or private group... no wall.. no origin.. top-post (not a comment)
1135                         // delete it!
1136                         logger("tag_deliver: no-mention top-level post to communuty or private group. delete.");
1137                         q("DELETE FROM item WHERE id = %d and uid = %d",
1138                                 intval($item_id),
1139                                 intval($uid)
1140                         );
1141                         return true;
1142                 }
1143                 return;
1144         }
1145
1146         $arr = array('item' => $item, 'user' => $u[0], 'contact' => $r[0]);
1147
1148         call_hooks('tagged', $arr);
1149
1150         if ((! $community_page) && (! $prvgroup))
1151                 return;
1152
1153
1154         // tgroup delivery - setup a second delivery chain
1155         // prevent delivery looping - only proceed
1156         // if the message originated elsewhere and is a top-level post
1157
1158         if (($item['wall']) || ($item['origin']) || ($item['id'] != $item['parent']))
1159                 return;
1160
1161         // now change this copy of the post to a forum head message and deliver to all the tgroup members
1162
1163
1164         $c = q("select name, url, thumb from contact where self = 1 and uid = %d limit 1",
1165                 intval($u[0]['uid'])
1166         );
1167         if (! count($c))
1168                 return;
1169
1170         // also reset all the privacy bits to the forum default permissions
1171
1172         $private = ($u[0]['allow_cid'] || $u[0]['allow_gid'] || $u[0]['deny_cid'] || $u[0]['deny_gid']) ? 1 : 0;
1173
1174         $forum_mode = (($prvgroup) ? 2 : 1);
1175
1176         q("update item set wall = 1, origin = 1, forum_mode = %d, `owner-name` = '%s', `owner-link` = '%s', `owner-avatar` = '%s',
1177                 `private` = %d, `allow_cid` = '%s', `allow_gid` = '%s', `deny_cid` = '%s', `deny_gid` = '%s'  where id = %d",
1178                 intval($forum_mode),
1179                 dbesc($c[0]['name']),
1180                 dbesc($c[0]['url']),
1181                 dbesc($c[0]['thumb']),
1182                 intval($private),
1183                 dbesc($u[0]['allow_cid']),
1184                 dbesc($u[0]['allow_gid']),
1185                 dbesc($u[0]['deny_cid']),
1186                 dbesc($u[0]['deny_gid']),
1187                 intval($item_id)
1188         );
1189         update_thread($item_id);
1190
1191         proc_run(PRIORITY_HIGH,'include/notifier.php', 'tgroup', $item_id);
1192
1193 }
1194
1195
1196
1197 function tgroup_check($uid,$item) {
1198
1199         $a = get_app();
1200
1201         $mention = false;
1202
1203         // check that the message originated elsewhere and is a top-level post
1204
1205         if (($item['wall']) || ($item['origin']) || ($item['uri'] != $item['parent-uri']))
1206                 return false;
1207
1208
1209         $u = q("select * from user where uid = %d limit 1",
1210                 intval($uid)
1211         );
1212         if (! count($u))
1213                 return false;
1214
1215         $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
1216         $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
1217
1218
1219         $link = normalise_link($a->get_baseurl() . '/profile/' . $u[0]['nickname']);
1220
1221         // Diaspora uses their own hardwired link URL in @-tags
1222         // instead of the one we supply with webfinger
1223
1224         $dlink = normalise_link($a->get_baseurl() . '/u/' . $u[0]['nickname']);
1225
1226         $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism',$item['body'],$matches,PREG_SET_ORDER);
1227         if ($cnt) {
1228                 foreach($matches as $mtch) {
1229                         if (link_compare($link,$mtch[1]) || link_compare($dlink,$mtch[1])) {
1230                                 $mention = true;
1231                                 logger('tgroup_check: mention found: ' . $mtch[2]);
1232                         }
1233                 }
1234         }
1235
1236         if (! $mention)
1237                 return false;
1238
1239         if ((! $community_page) && (! $prvgroup))
1240                 return false;
1241
1242         return true;
1243 }
1244
1245 /*
1246   This function returns true if $update has an edited timestamp newer
1247   than $existing, i.e. $update contains new data which should override
1248   what's already there.  If there is no timestamp yet, the update is
1249   assumed to be newer.  If the update has no timestamp, the existing
1250   item is assumed to be up-to-date.  If the timestamps are equal it
1251   assumes the update has been seen before and should be ignored.
1252   */
1253 function edited_timestamp_is_newer($existing, $update) {
1254     if (!x($existing,'edited') || !$existing['edited']) {
1255         return true;
1256     }
1257     if (!x($update,'edited') || !$update['edited']) {
1258         return false;
1259     }
1260     $existing_edited = datetime_convert('UTC', 'UTC', $existing['edited']);
1261     $update_edited = datetime_convert('UTC', 'UTC', $update['edited']);
1262     return (strcmp($existing_edited, $update_edited) < 0);
1263 }
1264
1265 /**
1266  *
1267  * consume_feed - process atom feed and update anything/everything we might need to update
1268  *
1269  * $xml = the (atom) feed to consume - RSS isn't as fully supported but may work for simple feeds.
1270  *
1271  * $importer = the contact_record (joined to user_record) of the local user who owns this relationship.
1272  *             It is this person's stuff that is going to be updated.
1273  * $contact =  the person who is sending us stuff. If not set, we MAY be processing a "follow" activity
1274  *             from an external network and MAY create an appropriate contact record. Otherwise, we MUST
1275  *             have a contact record.
1276  * $hub = should we find a hub declation in the feed, pass it back to our calling process, who might (or
1277  *        might not) try and subscribe to it.
1278  * $datedir sorts in reverse order
1279  * $pass - by default ($pass = 0) we cannot guarantee that a parent item has been
1280  *      imported prior to its children being seen in the stream unless we are certain
1281  *      of how the feed is arranged/ordered.
1282  * With $pass = 1, we only pull parent items out of the stream.
1283  * With $pass = 2, we only pull children (comments/likes).
1284  *
1285  * So running this twice, first with pass 1 and then with pass 2 will do the right
1286  * thing regardless of feed ordering. This won't be adequate in a fully-threaded
1287  * model where comments can have sub-threads. That would require some massive sorting
1288  * to get all the feed items into a mostly linear ordering, and might still require
1289  * recursion.
1290  */
1291
1292 function consume_feed($xml,$importer,&$contact, &$hub, $datedir = 0, $pass = 0) {
1293         if ($contact['network'] === NETWORK_OSTATUS) {
1294                 if ($pass < 2) {
1295                         // Test - remove before flight
1296                         //$tempfile = tempnam(get_temppath(), "ostatus2");
1297                         //file_put_contents($tempfile, $xml);
1298                         logger("Consume OStatus messages ", LOGGER_DEBUG);
1299                         ostatus::import($xml,$importer,$contact, $hub);
1300                 }
1301                 return;
1302         }
1303
1304         if ($contact['network'] === NETWORK_FEED) {
1305                 if ($pass < 2) {
1306                         logger("Consume feeds", LOGGER_DEBUG);
1307                         feed_import($xml,$importer,$contact, $hub);
1308                 }
1309                 return;
1310         }
1311
1312         if ($contact['network'] === NETWORK_DFRN) {
1313                 logger("Consume DFRN messages", LOGGER_DEBUG);
1314
1315                 $r = q("SELECT  `contact`.*, `contact`.`uid` AS `importer_uid`,
1316                                         `contact`.`pubkey` AS `cpubkey`,
1317                                         `contact`.`prvkey` AS `cprvkey`,
1318                                         `contact`.`thumb` AS `thumb`,
1319                                         `contact`.`url` as `url`,
1320                                         `contact`.`name` as `senderName`,
1321                                         `user`.*
1322                         FROM `contact`
1323                         LEFT JOIN `user` ON `contact`.`uid` = `user`.`uid`
1324                         WHERE `contact`.`id` = %d AND `user`.`uid` = %d",
1325                         dbesc($contact["id"]), dbesc($importer["uid"])
1326                 );
1327                 if ($r) {
1328                         logger("Now import the DFRN feed");
1329                         dfrn::import($xml,$r[0], true);
1330                         return;
1331                 }
1332         }
1333 }
1334
1335 function item_is_remote_self($contact, &$datarray) {
1336         $a = get_app();
1337
1338         if (!$contact['remote_self'])
1339                 return false;
1340
1341         // Prevent the forwarding of posts that are forwarded
1342         if ($datarray["extid"] == NETWORK_DFRN)
1343                 return false;
1344
1345         // Prevent to forward already forwarded posts
1346         if ($datarray["app"] == $a->get_hostname())
1347                 return false;
1348
1349         // Only forward posts
1350         if ($datarray["verb"] != ACTIVITY_POST)
1351                 return false;
1352
1353         if (($contact['network'] != NETWORK_FEED) AND $datarray['private'])
1354                 return false;
1355
1356         $datarray2 = $datarray;
1357         logger('remote-self start - Contact '.$contact['url'].' - '.$contact['remote_self'].' Item '.print_r($datarray, true), LOGGER_DEBUG);
1358         if ($contact['remote_self'] == 2) {
1359                 $r = q("SELECT `id`,`url`,`name`,`thumb` FROM `contact` WHERE `uid` = %d AND `self`",
1360                         intval($contact['uid']));
1361                 if (count($r)) {
1362                         $datarray['contact-id'] = $r[0]["id"];
1363
1364                         $datarray['owner-name'] = $r[0]["name"];
1365                         $datarray['owner-link'] = $r[0]["url"];
1366                         $datarray['owner-avatar'] = $r[0]["thumb"];
1367
1368                         $datarray['author-name']   = $datarray['owner-name'];
1369                         $datarray['author-link']   = $datarray['owner-link'];
1370                         $datarray['author-avatar'] = $datarray['owner-avatar'];
1371                 }
1372
1373                 if ($contact['network'] != NETWORK_FEED) {
1374                         $datarray["guid"] = get_guid(32);
1375                         unset($datarray["plink"]);
1376                         $datarray["uri"] = item_new_uri($a->get_hostname(),$contact['uid'], $datarray["guid"]);
1377                         $datarray["parent-uri"] = $datarray["uri"];
1378                         $datarray["extid"] = $contact['network'];
1379                         $urlpart = parse_url($datarray2['author-link']);
1380                         $datarray["app"] = $urlpart["host"];
1381                 } else
1382                         $datarray['private'] = 0;
1383         }
1384
1385         if ($contact['network'] != NETWORK_FEED) {
1386                 // Store the original post
1387                 $r = item_store($datarray2, false, false);
1388                 logger('remote-self post original item - Contact '.$contact['url'].' return '.$r.' Item '.print_r($datarray2, true), LOGGER_DEBUG);
1389         } else
1390                 $datarray["app"] = "Feed";
1391
1392         return true;
1393 }
1394
1395 function new_follower($importer,$contact,$datarray,$item,$sharing = false) {
1396         $url = notags(trim($datarray['author-link']));
1397         $name = notags(trim($datarray['author-name']));
1398         $photo = notags(trim($datarray['author-avatar']));
1399
1400         if (is_object($item)) {
1401                 $rawtag = $item->get_item_tags(NAMESPACE_ACTIVITY,'actor');
1402                 if ($rawtag && $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data'])
1403                         $nick = $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data'];
1404         } else
1405                 $nick = $item;
1406
1407         if (is_array($contact)) {
1408                 if (($contact['network'] == NETWORK_OSTATUS && $contact['rel'] == CONTACT_IS_SHARING)
1409                         || ($sharing && $contact['rel'] == CONTACT_IS_FOLLOWER)) {
1410                         $r = q("UPDATE `contact` SET `rel` = %d, `writable` = 1 WHERE `id` = %d AND `uid` = %d",
1411                                 intval(CONTACT_IS_FRIEND),
1412                                 intval($contact['id']),
1413                                 intval($importer['uid'])
1414                         );
1415                 }
1416                 // send email notification to owner?
1417         } else {
1418
1419                 // create contact record
1420
1421                 $r = q("INSERT INTO `contact` (`uid`, `created`, `url`, `nurl`, `name`, `nick`, `photo`, `network`, `rel`,
1422                         `blocked`, `readonly`, `pending`, `writable`)
1423                         VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, 0, 0, 1, 1)",
1424                         intval($importer['uid']),
1425                         dbesc(datetime_convert()),
1426                         dbesc($url),
1427                         dbesc(normalise_link($url)),
1428                         dbesc($name),
1429                         dbesc($nick),
1430                         dbesc($photo),
1431                         dbesc(($sharing) ? NETWORK_ZOT : NETWORK_OSTATUS),
1432                         intval(($sharing) ? CONTACT_IS_SHARING : CONTACT_IS_FOLLOWER)
1433                 );
1434                 $r = q("SELECT `id`, `network` FROM `contact` WHERE `uid` = %d AND `url` = '%s' AND `pending` = 1 LIMIT 1",
1435                                 intval($importer['uid']),
1436                                 dbesc($url)
1437                 );
1438                 if (count($r)) {
1439                         $contact_record = $r[0];
1440                         update_contact_avatar($photo, $importer["uid"], $contact_record["id"], true);
1441                 }
1442
1443
1444                 $r = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1",
1445                         intval($importer['uid'])
1446                 );
1447                 $a = get_app();
1448                 if (count($r) AND !in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
1449
1450                         // create notification
1451                         $hash = random_string();
1452
1453                         if (is_array($contact_record)) {
1454                                 $ret = q("INSERT INTO `intro` ( `uid`, `contact-id`, `blocked`, `knowyou`, `hash`, `datetime`)
1455                                         VALUES ( %d, %d, 0, 0, '%s', '%s' )",
1456                                         intval($importer['uid']),
1457                                         intval($contact_record['id']),
1458                                         dbesc($hash),
1459                                         dbesc(datetime_convert())
1460                                 );
1461                         }
1462
1463                         $def_gid = get_default_group($importer['uid'], $contact_record["network"]);
1464
1465                         if (intval($def_gid))
1466                                 group_add_member($importer['uid'],'',$contact_record['id'],$def_gid);
1467
1468                         if (($r[0]['notify-flags'] & NOTIFY_INTRO) &&
1469                                 in_array($r[0]['page-flags'], array(PAGE_NORMAL))) {
1470
1471                                 notification(array(
1472                                         'type'         => NOTIFY_INTRO,
1473                                         'notify_flags' => $r[0]['notify-flags'],
1474                                         'language'     => $r[0]['language'],
1475                                         'to_name'      => $r[0]['username'],
1476                                         'to_email'     => $r[0]['email'],
1477                                         'uid'          => $r[0]['uid'],
1478                                         'link'             => $a->get_baseurl() . '/notifications/intro',
1479                                         'source_name'  => ((strlen(stripslashes($contact_record['name']))) ? stripslashes($contact_record['name']) : t('[Name Withheld]')),
1480                                         'source_link'  => $contact_record['url'],
1481                                         'source_photo' => $contact_record['photo'],
1482                                         'verb'         => ($sharing ? ACTIVITY_FRIEND : ACTIVITY_FOLLOW),
1483                                         'otype'        => 'intro'
1484                                 ));
1485
1486                         }
1487                 } elseif (count($r) AND in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
1488                         $r = q("UPDATE `contact` SET `pending` = 0 WHERE `uid` = %d AND `url` = '%s' AND `pending` LIMIT 1",
1489                                         intval($importer['uid']),
1490                                         dbesc($url)
1491                         );
1492                 }
1493
1494         }
1495 }
1496
1497 function lose_follower($importer,$contact,$datarray = array(),$item = "") {
1498
1499         if (($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_SHARING)) {
1500                 q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
1501                         intval(CONTACT_IS_SHARING),
1502                         intval($contact['id'])
1503                 );
1504         } else {
1505                 contact_remove($contact['id']);
1506         }
1507 }
1508
1509 function lose_sharer($importer,$contact,$datarray = array(),$item = "") {
1510
1511         if (($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_FOLLOWER)) {
1512                 q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
1513                         intval(CONTACT_IS_FOLLOWER),
1514                         intval($contact['id'])
1515                 );
1516         } else {
1517                 contact_remove($contact['id']);
1518         }
1519 }
1520
1521 function subscribe_to_hub($url,$importer,$contact,$hubmode = 'subscribe') {
1522
1523         $a = get_app();
1524
1525         if (is_array($importer)) {
1526                 $r = q("SELECT `nickname` FROM `user` WHERE `uid` = %d LIMIT 1",
1527                         intval($importer['uid'])
1528                 );
1529         }
1530
1531         // Diaspora has different message-ids in feeds than they do
1532         // through the direct Diaspora protocol. If we try and use
1533         // the feed, we'll get duplicates. So don't.
1534
1535         if ((! count($r)) || $contact['network'] === NETWORK_DIASPORA)
1536                 return;
1537
1538         $push_url = get_config('system','url') . '/pubsub/' . $r[0]['nickname'] . '/' . $contact['id'];
1539
1540         // Use a single verify token, even if multiple hubs
1541
1542         $verify_token = ((strlen($contact['hub-verify'])) ? $contact['hub-verify'] : random_string());
1543
1544         $params= 'hub.mode=' . $hubmode . '&hub.callback=' . urlencode($push_url) . '&hub.topic=' . urlencode($contact['poll']) . '&hub.verify=async&hub.verify_token=' . $verify_token;
1545
1546         logger('subscribe_to_hub: ' . $hubmode . ' ' . $contact['name'] . ' to hub ' . $url . ' endpoint: '  . $push_url . ' with verifier ' . $verify_token);
1547
1548         if (!strlen($contact['hub-verify']) OR ($contact['hub-verify'] != $verify_token)) {
1549                 $r = q("UPDATE `contact` SET `hub-verify` = '%s' WHERE `id` = %d",
1550                         dbesc($verify_token),
1551                         intval($contact['id'])
1552                 );
1553         }
1554
1555         post_url($url,$params);
1556
1557         logger('subscribe_to_hub: returns: ' . $a->get_curl_code(), LOGGER_DEBUG);
1558
1559         return;
1560
1561 }
1562
1563 function fix_private_photos($s, $uid, $item = null, $cid = 0) {
1564
1565         if (get_config('system','disable_embedded'))
1566                 return $s;
1567
1568         $a = get_app();
1569
1570         logger('fix_private_photos: check for photos', LOGGER_DEBUG);
1571         $site = substr($a->get_baseurl(),strpos($a->get_baseurl(),'://'));
1572
1573         $orig_body = $s;
1574         $new_body = '';
1575
1576         $img_start = strpos($orig_body, '[img');
1577         $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
1578         $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
1579         while( ($img_st_close !== false) && ($img_len !== false) ) {
1580
1581                 $img_st_close++; // make it point to AFTER the closing bracket
1582                 $image = substr($orig_body, $img_start + $img_st_close, $img_len);
1583
1584                 logger('fix_private_photos: found photo ' . $image, LOGGER_DEBUG);
1585
1586
1587                 if (stristr($image , $site . '/photo/')) {
1588                         // Only embed locally hosted photos
1589                         $replace = false;
1590                         $i = basename($image);
1591                         $i = str_replace(array('.jpg','.png','.gif'),array('','',''),$i);
1592                         $x = strpos($i,'-');
1593
1594                         if ($x) {
1595                                 $res = substr($i,$x+1);
1596                                 $i = substr($i,0,$x);
1597                                 $r = q("SELECT * FROM `photo` WHERE `resource-id` = '%s' AND `scale` = %d AND `uid` = %d",
1598                                         dbesc($i),
1599                                         intval($res),
1600                                         intval($uid)
1601                                 );
1602                                 if ($r) {
1603
1604                                         // Check to see if we should replace this photo link with an embedded image
1605                                         // 1. No need to do so if the photo is public
1606                                         // 2. If there's a contact-id provided, see if they're in the access list
1607                                         //    for the photo. If so, embed it.
1608                                         // 3. Otherwise, if we have an item, see if the item permissions match the photo
1609                                         //    permissions, regardless of order but first check to see if they're an exact
1610                                         //    match to save some processing overhead.
1611
1612                                         if (has_permissions($r[0])) {
1613                                                 if ($cid) {
1614                                                         $recips = enumerate_permissions($r[0]);
1615                                                         if (in_array($cid, $recips)) {
1616                                                                 $replace = true;
1617                                                         }
1618                                                 } elseif ($item) {
1619                                                         if (compare_permissions($item,$r[0]))
1620                                                                 $replace = true;
1621                                                 }
1622                                         }
1623                                         if ($replace) {
1624                                                 $data = $r[0]['data'];
1625                                                 $type = $r[0]['type'];
1626
1627                                                 // If a custom width and height were specified, apply before embedding
1628                                                 if (preg_match("/\[img\=([0-9]*)x([0-9]*)\]/is", substr($orig_body, $img_start, $img_st_close), $match)) {
1629                                                         logger('fix_private_photos: scaling photo', LOGGER_DEBUG);
1630
1631                                                         $width = intval($match[1]);
1632                                                         $height = intval($match[2]);
1633
1634                                                         $ph = new Photo($data, $type);
1635                                                         if ($ph->is_valid()) {
1636                                                                 $ph->scaleImage(max($width, $height));
1637                                                                 $data = $ph->imageString();
1638                                                                 $type = $ph->getType();
1639                                                         }
1640                                                 }
1641
1642                                                 logger('fix_private_photos: replacing photo', LOGGER_DEBUG);
1643                                                 $image = 'data:' . $type . ';base64,' . base64_encode($data);
1644                                                 logger('fix_private_photos: replaced: ' . $image, LOGGER_DATA);
1645                                         }
1646                                 }
1647                         }
1648                 }
1649
1650                 $new_body = $new_body . substr($orig_body, 0, $img_start + $img_st_close) . $image . '[/img]';
1651                 $orig_body = substr($orig_body, $img_start + $img_st_close + $img_len + strlen('[/img]'));
1652                 if ($orig_body === false)
1653                         $orig_body = '';
1654
1655                 $img_start = strpos($orig_body, '[img');
1656                 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
1657                 $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
1658         }
1659
1660         $new_body = $new_body . $orig_body;
1661
1662         return($new_body);
1663 }
1664
1665 function has_permissions($obj) {
1666         if (($obj['allow_cid'] != '') || ($obj['allow_gid'] != '') || ($obj['deny_cid'] != '') || ($obj['deny_gid'] != ''))
1667                 return true;
1668         return false;
1669 }
1670
1671 function compare_permissions($obj1,$obj2) {
1672         // first part is easy. Check that these are exactly the same.
1673         if (($obj1['allow_cid'] == $obj2['allow_cid'])
1674                 && ($obj1['allow_gid'] == $obj2['allow_gid'])
1675                 && ($obj1['deny_cid'] == $obj2['deny_cid'])
1676                 && ($obj1['deny_gid'] == $obj2['deny_gid']))
1677                 return true;
1678
1679         // This is harder. Parse all the permissions and compare the resulting set.
1680
1681         $recipients1 = enumerate_permissions($obj1);
1682         $recipients2 = enumerate_permissions($obj2);
1683         sort($recipients1);
1684         sort($recipients2);
1685         if ($recipients1 == $recipients2)
1686                 return true;
1687         return false;
1688 }
1689
1690 // returns an array of contact-ids that are allowed to see this object
1691
1692 function enumerate_permissions($obj) {
1693         $allow_people = expand_acl($obj['allow_cid']);
1694         $allow_groups = expand_groups(expand_acl($obj['allow_gid']));
1695         $deny_people  = expand_acl($obj['deny_cid']);
1696         $deny_groups  = expand_groups(expand_acl($obj['deny_gid']));
1697         $recipients   = array_unique(array_merge($allow_people,$allow_groups));
1698         $deny         = array_unique(array_merge($deny_people,$deny_groups));
1699         $recipients   = array_diff($recipients,$deny);
1700         return $recipients;
1701 }
1702
1703 function item_getfeedtags($item) {
1704         $ret = array();
1705         $matches = false;
1706         $cnt = preg_match_all('|\#\[url\=(.*?)\](.*?)\[\/url\]|',$item['tag'],$matches);
1707         if ($cnt) {
1708                 for($x = 0; $x < $cnt; $x ++) {
1709                         if ($matches[1][$x])
1710                                 $ret[$matches[2][$x]] = array('#',$matches[1][$x], $matches[2][$x]);
1711                 }
1712         }
1713         $matches = false;
1714         $cnt = preg_match_all('|\@\[url\=(.*?)\](.*?)\[\/url\]|',$item['tag'],$matches);
1715         if ($cnt) {
1716                 for($x = 0; $x < $cnt; $x ++) {
1717                         if ($matches[1][$x])
1718                                 $ret[] = array('@',$matches[1][$x], $matches[2][$x]);
1719                 }
1720         }
1721         return $ret;
1722 }
1723
1724 function item_expire($uid, $days, $network = "", $force = false) {
1725
1726         if ((! $uid) || ($days < 1))
1727                 return;
1728
1729         // $expire_network_only = save your own wall posts
1730         // and just expire conversations started by others
1731
1732         $expire_network_only = get_pconfig($uid,'expire','network_only');
1733         $sql_extra = ((intval($expire_network_only)) ? " AND wall = 0 " : "");
1734
1735         if ($network != "") {
1736                 $sql_extra .= sprintf(" AND network = '%s' ", dbesc($network));
1737                 // There is an index "uid_network_received" but not "uid_network_created"
1738                 // This avoids the creation of another index just for one purpose.
1739                 // And it doesn't really matter wether to look at "received" or "created"
1740                 $range = "AND `received` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
1741         } else
1742                 $range = "AND `created` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
1743
1744         $r = q("SELECT `file`, `resource-id`, `starred`, `type`, `id` FROM `item`
1745                 WHERE `uid` = %d $range
1746                 AND `id` = `parent`
1747                 $sql_extra
1748                 AND `deleted` = 0",
1749                 intval($uid),
1750                 intval($days)
1751         );
1752
1753         if (! count($r))
1754                 return;
1755
1756         $expire_items = get_pconfig($uid, 'expire','items');
1757         $expire_items = (($expire_items===false)?1:intval($expire_items)); // default if not set: 1
1758
1759         // Forcing expiring of items - but not notes and marked items
1760         if ($force)
1761                 $expire_items = true;
1762
1763         $expire_notes = get_pconfig($uid, 'expire','notes');
1764         $expire_notes = (($expire_notes===false)?1:intval($expire_notes)); // default if not set: 1
1765
1766         $expire_starred = get_pconfig($uid, 'expire','starred');
1767         $expire_starred = (($expire_starred===false)?1:intval($expire_starred)); // default if not set: 1
1768
1769         $expire_photos = get_pconfig($uid, 'expire','photos');
1770         $expire_photos = (($expire_photos===false)?0:intval($expire_photos)); // default if not set: 0
1771
1772         logger('expire: # items=' . count($r). "; expire items: $expire_items, expire notes: $expire_notes, expire starred: $expire_starred, expire photos: $expire_photos");
1773
1774         foreach($r as $item) {
1775
1776                 // don't expire filed items
1777
1778                 if (strpos($item['file'],'[') !== false)
1779                         continue;
1780
1781                 // Only expire posts, not photos and photo comments
1782
1783                 if ($expire_photos==0 && strlen($item['resource-id']))
1784                         continue;
1785                 if ($expire_starred==0 && intval($item['starred']))
1786                         continue;
1787                 if ($expire_notes==0 && $item['type']=='note')
1788                         continue;
1789                 if ($expire_items==0 && $item['type']!='note')
1790                         continue;
1791
1792                 drop_item($item['id'],false);
1793         }
1794
1795         proc_run(PRIORITY_HIGH,"include/notifier.php", "expire", $uid);
1796
1797 }
1798
1799
1800 function drop_items($items) {
1801         $uid = 0;
1802
1803         if (! local_user() && ! remote_user())
1804                 return;
1805
1806         if (count($items)) {
1807                 foreach($items as $item) {
1808                         $owner = drop_item($item,false);
1809                         if ($owner && ! $uid)
1810                                 $uid = $owner;
1811                 }
1812         }
1813
1814         // multiple threads may have been deleted, send an expire notification
1815
1816         if ($uid)
1817                 proc_run(PRIORITY_HIGH,"include/notifier.php", "expire", $uid);
1818 }
1819
1820
1821 function drop_item($id,$interactive = true) {
1822
1823         $a = get_app();
1824
1825         // locate item to be deleted
1826
1827         $r = q("SELECT * FROM `item` WHERE `id` = %d LIMIT 1",
1828                 intval($id)
1829         );
1830
1831         if (! count($r)) {
1832                 if (! $interactive)
1833                         return 0;
1834                 notice( t('Item not found.') . EOL);
1835                 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
1836         }
1837
1838         $item = $r[0];
1839
1840         $owner = $item['uid'];
1841
1842         $cid = 0;
1843
1844         // check if logged in user is either the author or owner of this item
1845
1846         if (is_array($_SESSION['remote'])) {
1847                 foreach($_SESSION['remote'] as $visitor) {
1848                         if ($visitor['uid'] == $item['uid'] && $visitor['cid'] == $item['contact-id']) {
1849                                 $cid = $visitor['cid'];
1850                                 break;
1851                         }
1852                 }
1853         }
1854
1855
1856         if ((local_user() == $item['uid']) || ($cid) || (! $interactive)) {
1857
1858                 // Check if we should do HTML-based delete confirmation
1859                 if ($_REQUEST['confirm']) {
1860                         // <form> can't take arguments in its "action" parameter
1861                         // so add any arguments as hidden inputs
1862                         $query = explode_querystring($a->query_string);
1863                         $inputs = array();
1864                         foreach($query['args'] as $arg) {
1865                                 if (strpos($arg, 'confirm=') === false) {
1866                                         $arg_parts = explode('=', $arg);
1867                                         $inputs[] = array('name' => $arg_parts[0], 'value' => $arg_parts[1]);
1868                                 }
1869                         }
1870
1871                         return replace_macros(get_markup_template('confirm.tpl'), array(
1872                                 '$method' => 'get',
1873                                 '$message' => t('Do you really want to delete this item?'),
1874                                 '$extra_inputs' => $inputs,
1875                                 '$confirm' => t('Yes'),
1876                                 '$confirm_url' => $query['base'],
1877                                 '$confirm_name' => 'confirmed',
1878                                 '$cancel' => t('Cancel'),
1879                         ));
1880                 }
1881                 // Now check how the user responded to the confirmation query
1882                 if ($_REQUEST['canceled']) {
1883                         goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
1884                 }
1885
1886                 logger('delete item: ' . $item['id'], LOGGER_DEBUG);
1887                 // delete the item
1888
1889                 $r = q("UPDATE `item` SET `deleted` = 1, `title` = '', `body` = '', `edited` = '%s', `changed` = '%s' WHERE `id` = %d",
1890                         dbesc(datetime_convert()),
1891                         dbesc(datetime_convert()),
1892                         intval($item['id'])
1893                 );
1894                 create_tags_from_item($item['id']);
1895                 create_files_from_item($item['id']);
1896                 delete_thread($item['id'], $item['parent-uri']);
1897
1898                 // clean up categories and tags so they don't end up as orphans
1899
1900                 $matches = false;
1901                 $cnt = preg_match_all('/<(.*?)>/',$item['file'],$matches,PREG_SET_ORDER);
1902                 if ($cnt) {
1903                         foreach($matches as $mtch) {
1904                                 file_tag_unsave_file($item['uid'],$item['id'],$mtch[1],true);
1905                         }
1906                 }
1907
1908                 $matches = false;
1909
1910                 $cnt = preg_match_all('/\[(.*?)\]/',$item['file'],$matches,PREG_SET_ORDER);
1911                 if ($cnt) {
1912                         foreach($matches as $mtch) {
1913                                 file_tag_unsave_file($item['uid'],$item['id'],$mtch[1],false);
1914                         }
1915                 }
1916
1917                 // If item is a link to a photo resource, nuke all the associated photos
1918                 // (visitors will not have photo resources)
1919                 // This only applies to photos uploaded from the photos page. Photos inserted into a post do not
1920                 // generate a resource-id and therefore aren't intimately linked to the item.
1921
1922                 if (strlen($item['resource-id'])) {
1923                         q("DELETE FROM `photo` WHERE `resource-id` = '%s' AND `uid` = %d ",
1924                                 dbesc($item['resource-id']),
1925                                 intval($item['uid'])
1926                         );
1927                         // ignore the result
1928                 }
1929
1930                 // If item is a link to an event, nuke the event record.
1931
1932                 if (intval($item['event-id'])) {
1933                         q("DELETE FROM `event` WHERE `id` = %d AND `uid` = %d",
1934                                 intval($item['event-id']),
1935                                 intval($item['uid'])
1936                         );
1937                         // ignore the result
1938                 }
1939
1940                 // If item has attachments, drop them
1941
1942                 foreach(explode(",",$item['attach']) as $attach){
1943                         preg_match("|attach/(\d+)|", $attach, $matches);
1944                         q("DELETE FROM `attach` WHERE `id` = %d AND `uid` = %d",
1945                                 intval($matches[1]),
1946                                 local_user()
1947                         );
1948                         // ignore the result
1949                 }
1950
1951
1952                 // clean up item_id and sign meta-data tables
1953
1954                 /*
1955                 // Old code - caused very long queries and warning entries in the mysql logfiles:
1956
1957                 $r = q("DELETE FROM item_id where iid in (select id from item where parent = %d and uid = %d)",
1958                         intval($item['id']),
1959                         intval($item['uid'])
1960                 );
1961
1962                 $r = q("DELETE FROM sign where iid in (select id from item where parent = %d and uid = %d)",
1963                         intval($item['id']),
1964                         intval($item['uid'])
1965                 );
1966                 */
1967
1968                 // The new code splits the queries since the mysql optimizer really has bad problems with subqueries
1969
1970                 // Creating list of parents
1971                 $r = q("select id from item where parent = %d and uid = %d",
1972                         intval($item['id']),
1973                         intval($item['uid'])
1974                 );
1975
1976                 $parentid = "";
1977
1978                 foreach ($r AS $row) {
1979                         if ($parentid != "")
1980                                 $parentid .= ", ";
1981
1982                         $parentid .= $row["id"];
1983                 }
1984
1985                 // Now delete them
1986                 if ($parentid != "") {
1987                         $r = q("DELETE FROM item_id where iid in (%s)", dbesc($parentid));
1988
1989                         $r = q("DELETE FROM sign where iid in (%s)", dbesc($parentid));
1990                 }
1991
1992                 // If it's the parent of a comment thread, kill all the kids
1993
1994                 if ($item['uri'] == $item['parent-uri']) {
1995                         $r = q("UPDATE `item` SET `deleted` = 1, `edited` = '%s', `changed` = '%s', `body` = '' , `title` = ''
1996                                 WHERE `parent-uri` = '%s' AND `uid` = %d ",
1997                                 dbesc(datetime_convert()),
1998                                 dbesc(datetime_convert()),
1999                                 dbesc($item['parent-uri']),
2000                                 intval($item['uid'])
2001                         );
2002                         create_tags_from_itemuri($item['parent-uri'], $item['uid']);
2003                         create_files_from_itemuri($item['parent-uri'], $item['uid']);
2004                         delete_thread_uri($item['parent-uri'], $item['uid']);
2005                         // ignore the result
2006                 } else {
2007                         // ensure that last-child is set in case the comment that had it just got wiped.
2008                         q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `parent-uri` = '%s' AND `uid` = %d ",
2009                                 dbesc(datetime_convert()),
2010                                 dbesc($item['parent-uri']),
2011                                 intval($item['uid'])
2012                         );
2013                         // who is the last child now?
2014                         $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",
2015                                 dbesc($item['parent-uri']),
2016                                 intval($item['uid'])
2017                         );
2018                         if (count($r)) {
2019                                 q("UPDATE `item` SET `last-child` = 1 WHERE `id` = %d",
2020                                         intval($r[0]['id'])
2021                                 );
2022                         }
2023                 }
2024
2025                 $drop_id = intval($item['id']);
2026
2027                 // send the notification upstream/downstream as the case may be
2028
2029                 proc_run(PRIORITY_HIGH,"include/notifier.php", "drop", $drop_id);
2030
2031                 if (! $interactive)
2032                         return $owner;
2033                 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
2034                 //NOTREACHED
2035         } else {
2036                 if (! $interactive)
2037                         return 0;
2038                 notice( t('Permission denied.') . EOL);
2039                 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
2040                 //NOTREACHED
2041         }
2042
2043 }
2044
2045
2046 function first_post_date($uid,$wall = false) {
2047         $r = q("select id, created from item
2048                 where uid = %d and wall = %d and deleted = 0 and visible = 1 AND moderated = 0
2049                 and id = parent
2050                 order by created asc limit 1",
2051                 intval($uid),
2052                 intval($wall ? 1 : 0)
2053         );
2054         if (count($r)) {
2055 //              logger('first_post_date: ' . $r[0]['id'] . ' ' . $r[0]['created'], LOGGER_DATA);
2056                 return substr(datetime_convert('',date_default_timezone_get(),$r[0]['created']),0,10);
2057         }
2058         return false;
2059 }
2060
2061 /* modified posted_dates() {below} to arrange the list in years */
2062 function list_post_dates($uid, $wall) {
2063         $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
2064
2065         $dthen = first_post_date($uid, $wall);
2066         if (! $dthen)
2067                 return array();
2068
2069         // Set the start and end date to the beginning of the month
2070         $dnow = substr($dnow,0,8).'01';
2071         $dthen = substr($dthen,0,8).'01';
2072
2073         $ret = array();
2074
2075         // Starting with the current month, get the first and last days of every
2076         // month down to and including the month of the first post
2077         while(substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
2078                 $dyear = intval(substr($dnow,0,4));
2079                 $dstart = substr($dnow,0,8) . '01';
2080                 $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
2081                 $start_month = datetime_convert('','',$dstart,'Y-m-d');
2082                 $end_month = datetime_convert('','',$dend,'Y-m-d');
2083                 $str = day_translate(datetime_convert('','',$dnow,'F'));
2084                 if (! $ret[$dyear])
2085                         $ret[$dyear] = array();
2086                 $ret[$dyear][] = array($str,$end_month,$start_month);
2087                 $dnow = datetime_convert('','',$dnow . ' -1 month', 'Y-m-d');
2088         }
2089         return $ret;
2090 }
2091
2092 function posted_dates($uid,$wall) {
2093         $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
2094
2095         $dthen = first_post_date($uid,$wall);
2096         if (! $dthen)
2097                 return array();
2098
2099         // Set the start and end date to the beginning of the month
2100         $dnow = substr($dnow,0,8).'01';
2101         $dthen = substr($dthen,0,8).'01';
2102
2103         $ret = array();
2104         // Starting with the current month, get the first and last days of every
2105         // month down to and including the month of the first post
2106         while(substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
2107                 $dstart = substr($dnow,0,8) . '01';
2108                 $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
2109                 $start_month = datetime_convert('','',$dstart,'Y-m-d');
2110                 $end_month = datetime_convert('','',$dend,'Y-m-d');
2111                 $str = day_translate(datetime_convert('','',$dnow,'F Y'));
2112                 $ret[] = array($str,$end_month,$start_month);
2113                 $dnow = datetime_convert('','',$dnow . ' -1 month', 'Y-m-d');
2114         }
2115         return $ret;
2116 }
2117
2118
2119 function posted_date_widget($url,$uid,$wall) {
2120         $o = '';
2121
2122         if (! feature_enabled($uid,'archives'))
2123                 return $o;
2124
2125         // For former Facebook folks that left because of "timeline"
2126
2127 /*      if ($wall && intval(get_pconfig($uid,'system','no_wall_archive_widget')))
2128                 return $o;*/
2129
2130         $visible_years = get_pconfig($uid,'system','archive_visible_years');
2131         if (! $visible_years)
2132                 $visible_years = 5;
2133
2134         $ret = list_post_dates($uid,$wall);
2135
2136         if (! count($ret))
2137                 return $o;
2138
2139         $cutoff_year = intval(datetime_convert('',date_default_timezone_get(),'now','Y')) - $visible_years;
2140         $cutoff = ((array_key_exists($cutoff_year,$ret))? true : false);
2141
2142         $o = replace_macros(get_markup_template('posted_date_widget.tpl'),array(
2143                 '$title' => t('Archives'),
2144                 '$size' => $visible_years,
2145                 '$cutoff_year' => $cutoff_year,
2146                 '$cutoff' => $cutoff,
2147                 '$url' => $url,
2148                 '$dates' => $ret,
2149                 '$showmore' => t('show more')
2150
2151         ));
2152         return $o;
2153 }