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