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