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