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