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