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