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