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