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