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