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