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