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