]> git.mxchange.org Git - friendica.git/blob - include/items.php
327096341c942cc4a355fac84691f6bdec39859c
[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         dbesc_array($arr);
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                 $file = 'item-'.round(microtime(true) * 10000).".msg";
861                 $spool = get_spoolpath().'/'.$file;
862                 file_put_contents($spool, json_encode($arr));
863                 logger("Item wasn't stored - Item was spooled into file ".$file, LOGGER_DEBUG);
864                 return 0;
865         }
866
867         if ($current_post == 0) {
868                 // This is one of these error messages that never should occur.
869                 logger("couldn't find created item - we better quit now.");
870                 q("ROLLBACK");
871                 return 0;
872         }
873
874         // How much entries have we created?
875         // We wouldn't need this query when we could use an unique index - but MySQL has length problems with them.
876         $r = q("SELECT COUNT(*) AS `entries` FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` = '%s'",
877                 dbesc($arr['uri']),
878                 intval($arr['uid']),
879                 dbesc($arr['network'])
880         );
881
882         if (!dbm::is_result($r)) {
883                 // This shouldn't happen, since COUNT always works when the database connection is there.
884                 logger("We couldn't count the stored entries. Very strange ...");
885                 q("ROLLBACK");
886                 return 0;
887         }
888
889         if ($r[0]["entries"] > 1) {
890                 // There are duplicates. We delete our just created entry.
891                 logger('Duplicated post occurred. uri = '.$arr['uri'].' uid = '.$arr['uid']);
892
893                 // Yes, we could do a rollback here - but we are having many users with MyISAM.
894                 q("DELETE FROM `item` WHERE `id` = %d", intval($current_post));
895                 q("COMMIT");
896                 return 0;
897         } elseif ($r[0]["entries"] == 0) {
898                 // This really should never happen since we quit earlier if there were problems.
899                 logger("Something is terribly wrong. We haven't found our created entry.");
900                 q("ROLLBACK");
901                 return 0;
902         }
903
904         logger('item_store: created item '.$current_post);
905         item_set_last_item($arr);
906
907         if (!$parent_id || ($arr['parent-uri'] === $arr['uri']))
908                 $parent_id = $current_post;
909
910         // Set parent id
911         $r = q("UPDATE `item` SET `parent` = %d WHERE `id` = %d",
912                 intval($parent_id),
913                 intval($current_post)
914         );
915
916         $arr['id'] = $current_post;
917         $arr['parent'] = $parent_id;
918
919         // update the commented timestamp on the parent
920         // Only update "commented" if it is really a comment
921         if (($arr['verb'] == ACTIVITY_POST) OR !get_config("system", "like_no_comment"))
922                 q("UPDATE `item` SET `commented` = '%s', `changed` = '%s' WHERE `id` = %d",
923                         dbesc(datetime_convert()),
924                         dbesc(datetime_convert()),
925                         intval($parent_id)
926                 );
927         else
928                 q("UPDATE `item` SET `changed` = '%s' WHERE `id` = %d",
929                         dbesc(datetime_convert()),
930                         intval($parent_id)
931                 );
932
933         if ($dsprsig) {
934
935                 // Friendica servers lower than 3.4.3-2 had double encoded the signature ...
936                 // We can check for this condition when we decode and encode the stuff again.
937                 if (base64_encode(base64_decode(base64_decode($dsprsig->signature))) == base64_decode($dsprsig->signature)) {
938                         $dsprsig->signature = base64_decode($dsprsig->signature);
939                         logger("Repaired double encoded signature from handle ".$dsprsig->signer, LOGGER_DEBUG);
940                 }
941
942                 q("insert into sign (`iid`,`signed_text`,`signature`,`signer`) values (%d,'%s','%s','%s') ",
943                         intval($current_post),
944                         dbesc($dsprsig->signed_text),
945                         dbesc($dsprsig->signature),
946                         dbesc($dsprsig->signer)
947                 );
948         }
949
950         $deleted = tag_deliver($arr['uid'],$current_post);
951
952         // current post can be deleted if is for a community page and no mention are
953         // in it.
954         if (!$deleted AND !$dontcache) {
955
956                 $r = q('SELECT * FROM `item` WHERE `id` = %d', intval($current_post));
957                 if ((dbm::is_result($r)) && (count($r) == 1)) {
958                         if ($notify)
959                                 call_hooks('post_local_end', $r[0]);
960                         else
961                                 call_hooks('post_remote_end', $r[0]);
962                 } else
963                         logger('item_store: new item not found in DB, id ' . $current_post);
964         }
965
966         if ($arr['parent-uri'] === $arr['uri']) {
967                 add_thread($current_post);
968         } else {
969                 update_thread($parent_id);
970         }
971
972         q("COMMIT");
973
974         // Due to deadlock issues with the "term" table we are doing these steps after the commit.
975         // This is not perfect - but a workable solution until we found the reason for the problem.
976         create_tags_from_item($current_post);
977         create_files_from_item($current_post);
978
979         // If this is now the last-child, force all _other_ children of this parent to *not* be last-child
980         // It is done after the transaction to avoid dead locks.
981         if ($arr['last-child']) {
982                 $r = q("UPDATE `item` SET `last-child` = 0 WHERE `parent-uri` = '%s' AND `uid` = %d AND `id` != %d",
983                         dbesc($arr['uri']),
984                         intval($arr['uid']),
985                         intval($current_post)
986                 );
987         }
988
989         if ($arr['parent-uri'] === $arr['uri']) {
990                 add_shadow_thread($current_post);
991         } else {
992                 add_shadow_entry($current_post);
993         }
994
995         check_item_notification($current_post, $uid);
996
997         if ($notify)
998                 proc_run(PRIORITY_HIGH, "include/notifier.php", $notify_type, $current_post);
999
1000         return $current_post;
1001 }
1002
1003 /**
1004  * @brief Set "success_update" and "last-item" to the date of the last time we heard from this contact
1005  *
1006  * This can be used to filter for inactive contacts.
1007  * Only do this for public postings to avoid privacy problems, since poco data is public.
1008  * Don't set this value if it isn't from the owner (could be an author that we don't know)
1009  *
1010  * @param array $arr Contains the just posted item record
1011  */
1012 function item_set_last_item($arr) {
1013
1014         $update = (!$arr['private'] AND (($arr["author-link"] === $arr["owner-link"]) OR ($arr["parent-uri"] === $arr["uri"])));
1015
1016         // Is it a forum? Then we don't care about the rules from above
1017         if (!$update AND ($arr["network"] == NETWORK_DFRN) AND ($arr["parent-uri"] === $arr["uri"])) {
1018                 $isforum = q("SELECT `forum` FROM `contact` WHERE `id` = %d AND `forum`",
1019                                 intval($arr['contact-id']));
1020                 if ($isforum) {
1021                         $update = true;
1022                 }
1023         }
1024
1025         if ($update) {
1026                 q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
1027                         dbesc($arr['received']),
1028                         dbesc($arr['received']),
1029                         intval($arr['contact-id'])
1030                 );
1031         }
1032         // Now do the same for the system wide contacts with uid=0
1033         if (!$arr['private']) {
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['owner-id'])
1038                 );
1039
1040                 if ($arr['owner-id'] != $arr['author-id']) {
1041                         q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
1042                                 dbesc($arr['received']),
1043                                 dbesc($arr['received']),
1044                                 intval($arr['author-id'])
1045                         );
1046                 }
1047         }
1048 }
1049
1050 function item_body_set_hashtags(&$item) {
1051
1052         $tags = get_tags($item["body"]);
1053
1054         // No hashtags?
1055         if (!count($tags))
1056                 return(false);
1057
1058         // This sorting is important when there are hashtags that are part of other hashtags
1059         // Otherwise there could be problems with hashtags like #test and #test2
1060         rsort($tags);
1061
1062         $a = get_app();
1063
1064         $URLSearchString = "^\[\]";
1065
1066         // All hashtags should point to the home server
1067         //$item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1068         //              "#[url=".App::get_baseurl()."/search?tag=$2]$2[/url]", $item["body"]);
1069
1070         //$item["tag"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1071         //              "#[url=".App::get_baseurl()."/search?tag=$2]$2[/url]", $item["tag"]);
1072
1073         // mask hashtags inside of url, bookmarks and attachments to avoid urls in urls
1074         $item["body"] = preg_replace_callback("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1075                 function ($match){
1076                         return("[url=".str_replace("#", "&num;", $match[1])."]".str_replace("#", "&num;", $match[2])."[/url]");
1077                 },$item["body"]);
1078
1079         $item["body"] = preg_replace_callback("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism",
1080                 function ($match){
1081                         return("[bookmark=".str_replace("#", "&num;", $match[1])."]".str_replace("#", "&num;", $match[2])."[/bookmark]");
1082                 },$item["body"]);
1083
1084         $item["body"] = preg_replace_callback("/\[attachment (.*)\](.*?)\[\/attachment\]/ism",
1085                 function ($match){
1086                         return("[attachment ".str_replace("#", "&num;", $match[1])."]".$match[2]."[/attachment]");
1087                 },$item["body"]);
1088
1089         // Repair recursive urls
1090         $item["body"] = preg_replace("/&num;\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1091                         "&num;$2", $item["body"]);
1092
1093
1094         foreach($tags as $tag) {
1095                 if (strpos($tag,'#') !== 0)
1096                         continue;
1097
1098                 if (strpos($tag,'[url='))
1099                         continue;
1100
1101                 $basetag = str_replace('_',' ',substr($tag,1));
1102
1103                 $newtag = '#[url='.App::get_baseurl().'/search?tag='.rawurlencode($basetag).']'.$basetag.'[/url]';
1104
1105                 $item["body"] = str_replace($tag, $newtag, $item["body"]);
1106
1107                 if (!stristr($item["tag"],"/search?tag=".$basetag."]".$basetag."[/url]")) {
1108                         if (strlen($item["tag"]))
1109                                 $item["tag"] = ','.$item["tag"];
1110                         $item["tag"] = $newtag.$item["tag"];
1111                 }
1112         }
1113
1114         // Convert back the masked hashtags
1115         $item["body"] = str_replace("&num;", "#", $item["body"]);
1116 }
1117
1118 function get_item_guid($id) {
1119         $r = q("SELECT `guid` FROM `item` WHERE `id` = %d LIMIT 1", intval($id));
1120         if (dbm::is_result($r))
1121                 return($r[0]["guid"]);
1122         else
1123                 return("");
1124 }
1125
1126 function get_item_id($guid, $uid = 0) {
1127
1128         $nick = "";
1129         $id = 0;
1130
1131         if ($uid == 0)
1132                 $uid == local_user();
1133
1134         // Does the given user have this item?
1135         if ($uid) {
1136                 $r = q("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid`
1137                         WHERE `item`.`visible` = 1 AND `item`.`deleted` = 0 and `item`.`moderated` = 0
1138                                 AND `item`.`guid` = '%s' AND `item`.`uid` = %d", dbesc($guid), intval($uid));
1139                 if (dbm::is_result($r)) {
1140                         $id = $r[0]["id"];
1141                         $nick = $r[0]["nickname"];
1142                 }
1143         }
1144
1145         // Or is it anywhere on the server?
1146         if ($nick == "") {
1147                 $r = q("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid`
1148                         WHERE `item`.`visible` = 1 AND `item`.`deleted` = 0 and `item`.`moderated` = 0
1149                                 AND `item`.`allow_cid` = ''  AND `item`.`allow_gid` = ''
1150                                 AND `item`.`deny_cid`  = '' AND `item`.`deny_gid`  = ''
1151                                 AND `item`.`private` = 0 AND `item`.`wall` = 1
1152                                 AND `item`.`guid` = '%s'", dbesc($guid));
1153                 if (dbm::is_result($r)) {
1154                         $id = $r[0]["id"];
1155                         $nick = $r[0]["nickname"];
1156                 }
1157         }
1158         return(array("nick" => $nick, "id" => $id));
1159 }
1160
1161 // return - test
1162 function get_item_contact($item,$contacts) {
1163         if (! count($contacts) || (! is_array($item)))
1164                 return false;
1165         foreach($contacts as $contact) {
1166                 if ($contact['id'] == $item['contact-id']) {
1167                         return $contact;
1168                         break; // NOTREACHED
1169                 }
1170         }
1171         return false;
1172 }
1173
1174 /**
1175  * look for mention tags and setup a second delivery chain for forum/community posts if appropriate
1176  * @param int $uid
1177  * @param int $item_id
1178  * @return bool true if item was deleted, else false
1179  */
1180 function tag_deliver($uid,$item_id) {
1181
1182         //
1183
1184         $a = get_app();
1185
1186         $mention = false;
1187
1188         $u = q("select * from user where uid = %d limit 1",
1189                 intval($uid)
1190         );
1191
1192         if (! dbm::is_result($u)) {
1193                 return;
1194         }
1195
1196         $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
1197         $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
1198
1199
1200         $i = q("SELECT * FROM `item` WHERE `id` = %d AND `uid` = %d LIMIT 1",
1201                 intval($item_id),
1202                 intval($uid)
1203         );
1204         if (! dbm::is_result($i)) {
1205                 return;
1206         }
1207
1208         $item = $i[0];
1209
1210         $link = normalise_link(App::get_baseurl() . '/profile/' . $u[0]['nickname']);
1211
1212         // Diaspora uses their own hardwired link URL in @-tags
1213         // instead of the one we supply with webfinger
1214
1215         $dlink = normalise_link(App::get_baseurl() . '/u/' . $u[0]['nickname']);
1216
1217         $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism',$item['body'],$matches,PREG_SET_ORDER);
1218         if ($cnt) {
1219                 foreach($matches as $mtch) {
1220                         if (link_compare($link,$mtch[1]) || link_compare($dlink,$mtch[1])) {
1221                                 $mention = true;
1222                                 logger('tag_deliver: mention found: ' . $mtch[2]);
1223                         }
1224                 }
1225         }
1226
1227         if (! $mention){
1228                 if ( ($community_page || $prvgroup) &&
1229                           (!$item['wall']) && (!$item['origin']) && ($item['id'] == $item['parent'])){
1230                         // mmh.. no mention.. community page or private group... no wall.. no origin.. top-post (not a comment)
1231                         // delete it!
1232                         logger("tag_deliver: no-mention top-level post to communuty or private group. delete.");
1233                         q("DELETE FROM item WHERE id = %d and uid = %d",
1234                                 intval($item_id),
1235                                 intval($uid)
1236                         );
1237                         return true;
1238                 }
1239                 return;
1240         }
1241
1242         $arr = array('item' => $item, 'user' => $u[0], 'contact' => $r[0]);
1243
1244         call_hooks('tagged', $arr);
1245
1246         if ((! $community_page) && (! $prvgroup))
1247                 return;
1248
1249
1250         // tgroup delivery - setup a second delivery chain
1251         // prevent delivery looping - only proceed
1252         // if the message originated elsewhere and is a top-level post
1253
1254         if (($item['wall']) || ($item['origin']) || ($item['id'] != $item['parent']))
1255                 return;
1256
1257         // now change this copy of the post to a forum head message and deliver to all the tgroup members
1258
1259
1260         $c = q("select name, url, thumb from contact where self = 1 and uid = %d limit 1",
1261                 intval($u[0]['uid'])
1262         );
1263         if (! count($c)) {
1264                 return;
1265         }
1266
1267         // also reset all the privacy bits to the forum default permissions
1268
1269         $private = ($u[0]['allow_cid'] || $u[0]['allow_gid'] || $u[0]['deny_cid'] || $u[0]['deny_gid']) ? 1 : 0;
1270
1271         $forum_mode = (($prvgroup) ? 2 : 1);
1272
1273         q("UPDATE `item` SET `wall` = 1, `origin` = 1, `forum_mode` = %d, `owner-name` = '%s', `owner-link` = '%s', `owner-avatar` = '%s',
1274                 `private` = %d, `allow_cid` = '%s', `allow_gid` = '%s', `deny_cid` = '%s', `deny_gid` = '%s'  WHERE `id` = %d",
1275                 intval($forum_mode),
1276                 dbesc($c[0]['name']),
1277                 dbesc($c[0]['url']),
1278                 dbesc($c[0]['thumb']),
1279                 intval($private),
1280                 dbesc($u[0]['allow_cid']),
1281                 dbesc($u[0]['allow_gid']),
1282                 dbesc($u[0]['deny_cid']),
1283                 dbesc($u[0]['deny_gid']),
1284                 intval($item_id)
1285         );
1286         update_thread($item_id);
1287
1288         proc_run(PRIORITY_HIGH,'include/notifier.php', 'tgroup', $item_id);
1289
1290 }
1291
1292
1293
1294 function tgroup_check($uid,$item) {
1295
1296         $a = get_app();
1297
1298         $mention = false;
1299
1300         // check that the message originated elsewhere and is a top-level post
1301
1302         if (($item['wall']) || ($item['origin']) || ($item['uri'] != $item['parent-uri']))
1303                 return false;
1304
1305         /// @TODO Encapsulate this or find it encapsulated and replace all occurrances
1306         $u = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1",
1307                 intval($uid)
1308         );
1309         if (! dbm::is_result($u)) {
1310                 return false;
1311         }
1312
1313         $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
1314         $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
1315
1316
1317         $link = normalise_link(App::get_baseurl() . '/profile/' . $u[0]['nickname']);
1318
1319         // Diaspora uses their own hardwired link URL in @-tags
1320         // instead of the one we supply with webfinger
1321
1322         $dlink = normalise_link(App::get_baseurl() . '/u/' . $u[0]['nickname']);
1323
1324         $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism',$item['body'],$matches,PREG_SET_ORDER);
1325         if ($cnt) {
1326                 foreach ($matches as $mtch) {
1327                         if (link_compare($link,$mtch[1]) || link_compare($dlink,$mtch[1])) {
1328                                 $mention = true;
1329                                 logger('tgroup_check: mention found: ' . $mtch[2]);
1330                         }
1331                 }
1332         }
1333
1334         if (! $mention) {
1335                 return false;
1336         }
1337
1338         /// @TODO Combines both return statements into one
1339         return (($community_page) || ($prvgroup));
1340 }
1341
1342 /*
1343   This function returns true if $update has an edited timestamp newer
1344   than $existing, i.e. $update contains new data which should override
1345   what's already there.  If there is no timestamp yet, the update is
1346   assumed to be newer.  If the update has no timestamp, the existing
1347   item is assumed to be up-to-date.  If the timestamps are equal it
1348   assumes the update has been seen before and should be ignored.
1349   */
1350 function edited_timestamp_is_newer($existing, $update) {
1351         if (!x($existing,'edited') || !$existing['edited']) {
1352                 return true;
1353         }
1354         if (!x($update,'edited') || !$update['edited']) {
1355                 return false;
1356         }
1357
1358         $existing_edited = datetime_convert('UTC', 'UTC', $existing['edited']);
1359         $update_edited = datetime_convert('UTC', 'UTC', $update['edited']);
1360         return (strcmp($existing_edited, $update_edited) < 0);
1361 }
1362
1363 /**
1364  *
1365  * consume_feed - process atom feed and update anything/everything we might need to update
1366  *
1367  * $xml = the (atom) feed to consume - RSS isn't as fully supported but may work for simple feeds.
1368  *
1369  * $importer = the contact_record (joined to user_record) of the local user who owns this relationship.
1370  *             It is this person's stuff that is going to be updated.
1371  * $contact =  the person who is sending us stuff. If not set, we MAY be processing a "follow" activity
1372  *             from an external network and MAY create an appropriate contact record. Otherwise, we MUST
1373  *             have a contact record.
1374  * $hub = should we find a hub declation in the feed, pass it back to our calling process, who might (or
1375  *        might not) try and subscribe to it.
1376  * $datedir sorts in reverse order
1377  * $pass - by default ($pass = 0) we cannot guarantee that a parent item has been
1378  *      imported prior to its children being seen in the stream unless we are certain
1379  *      of how the feed is arranged/ordered.
1380  * With $pass = 1, we only pull parent items out of the stream.
1381  * With $pass = 2, we only pull children (comments/likes).
1382  *
1383  * So running this twice, first with pass 1 and then with pass 2 will do the right
1384  * thing regardless of feed ordering. This won't be adequate in a fully-threaded
1385  * model where comments can have sub-threads. That would require some massive sorting
1386  * to get all the feed items into a mostly linear ordering, and might still require
1387  * recursion.
1388  */
1389
1390 function consume_feed($xml,$importer,&$contact, &$hub, $datedir = 0, $pass = 0) {
1391         if ($contact['network'] === NETWORK_OSTATUS) {
1392                 if ($pass < 2) {
1393                         // Test - remove before flight
1394                         //$tempfile = tempnam(get_temppath(), "ostatus2");
1395                         //file_put_contents($tempfile, $xml);
1396                         logger("Consume OStatus messages ", LOGGER_DEBUG);
1397                         ostatus::import($xml,$importer,$contact, $hub);
1398                 }
1399                 return;
1400         }
1401
1402         if ($contact['network'] === NETWORK_FEED) {
1403                 if ($pass < 2) {
1404                         logger("Consume feeds", LOGGER_DEBUG);
1405                         feed_import($xml,$importer,$contact, $hub);
1406                 }
1407                 return;
1408         }
1409
1410         if ($contact['network'] === NETWORK_DFRN) {
1411                 logger("Consume DFRN messages", LOGGER_DEBUG);
1412
1413                 $r = q("SELECT  `contact`.*, `contact`.`uid` AS `importer_uid`,
1414                                         `contact`.`pubkey` AS `cpubkey`,
1415                                         `contact`.`prvkey` AS `cprvkey`,
1416                                         `contact`.`thumb` AS `thumb`,
1417                                         `contact`.`url` as `url`,
1418                                         `contact`.`name` as `senderName`,
1419                                         `user`.*
1420                         FROM `contact`
1421                         LEFT JOIN `user` ON `contact`.`uid` = `user`.`uid`
1422                         WHERE `contact`.`id` = %d AND `user`.`uid` = %d",
1423                         dbesc($contact["id"]), dbesc($importer["uid"])
1424                 );
1425                 if ($r) {
1426                         logger("Now import the DFRN feed");
1427                         dfrn::import($xml,$r[0], true);
1428                         return;
1429                 }
1430         }
1431 }
1432
1433 function item_is_remote_self($contact, &$datarray) {
1434         $a = get_app();
1435
1436         if (!$contact['remote_self'])
1437                 return false;
1438
1439         // Prevent the forwarding of posts that are forwarded
1440         if ($datarray["extid"] == NETWORK_DFRN)
1441                 return false;
1442
1443         // Prevent to forward already forwarded posts
1444         if ($datarray["app"] == $a->get_hostname())
1445                 return false;
1446
1447         // Only forward posts
1448         if ($datarray["verb"] != ACTIVITY_POST)
1449                 return false;
1450
1451         if (($contact['network'] != NETWORK_FEED) AND $datarray['private'])
1452                 return false;
1453
1454         $datarray2 = $datarray;
1455         logger('remote-self start - Contact '.$contact['url'].' - '.$contact['remote_self'].' Item '.print_r($datarray, true), LOGGER_DEBUG);
1456         if ($contact['remote_self'] == 2) {
1457                 $r = q("SELECT `id`,`url`,`name`,`thumb` FROM `contact` WHERE `uid` = %d AND `self`",
1458                         intval($contact['uid']));
1459                 if (dbm::is_result($r)) {
1460                         $datarray['contact-id'] = $r[0]["id"];
1461
1462                         $datarray['owner-name'] = $r[0]["name"];
1463                         $datarray['owner-link'] = $r[0]["url"];
1464                         $datarray['owner-avatar'] = $r[0]["thumb"];
1465
1466                         $datarray['author-name']   = $datarray['owner-name'];
1467                         $datarray['author-link']   = $datarray['owner-link'];
1468                         $datarray['author-avatar'] = $datarray['owner-avatar'];
1469                 }
1470
1471                 if ($contact['network'] != NETWORK_FEED) {
1472                         $datarray["guid"] = get_guid(32);
1473                         unset($datarray["plink"]);
1474                         $datarray["uri"] = item_new_uri($a->get_hostname(),$contact['uid'], $datarray["guid"]);
1475                         $datarray["parent-uri"] = $datarray["uri"];
1476                         $datarray["extid"] = $contact['network'];
1477                         $urlpart = parse_url($datarray2['author-link']);
1478                         $datarray["app"] = $urlpart["host"];
1479                 } else
1480                         $datarray['private'] = 0;
1481         }
1482
1483         if ($contact['network'] != NETWORK_FEED) {
1484                 // Store the original post
1485                 $r = item_store($datarray2, false, false);
1486                 logger('remote-self post original item - Contact '.$contact['url'].' return '.$r.' Item '.print_r($datarray2, true), LOGGER_DEBUG);
1487         } else
1488                 $datarray["app"] = "Feed";
1489
1490         return true;
1491 }
1492
1493 function new_follower($importer,$contact,$datarray,$item,$sharing = false) {
1494         $url = notags(trim($datarray['author-link']));
1495         $name = notags(trim($datarray['author-name']));
1496         $photo = notags(trim($datarray['author-avatar']));
1497
1498         if (is_object($item)) {
1499                 $rawtag = $item->get_item_tags(NAMESPACE_ACTIVITY,'actor');
1500                 if ($rawtag && $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data'])
1501                         $nick = $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data'];
1502         } else
1503                 $nick = $item;
1504
1505         if (is_array($contact)) {
1506                 if (($contact['network'] == NETWORK_OSTATUS && $contact['rel'] == CONTACT_IS_SHARING)
1507                         || ($sharing && $contact['rel'] == CONTACT_IS_FOLLOWER)) {
1508                         $r = q("UPDATE `contact` SET `rel` = %d, `writable` = 1 WHERE `id` = %d AND `uid` = %d",
1509                                 intval(CONTACT_IS_FRIEND),
1510                                 intval($contact['id']),
1511                                 intval($importer['uid'])
1512                         );
1513                 }
1514                 // send email notification to owner?
1515         } else {
1516
1517                 // create contact record
1518
1519                 $r = q("INSERT INTO `contact` (`uid`, `created`, `url`, `nurl`, `name`, `nick`, `photo`, `network`, `rel`,
1520                         `blocked`, `readonly`, `pending`, `writable`)
1521                         VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, 0, 0, 1, 1)",
1522                         intval($importer['uid']),
1523                         dbesc(datetime_convert()),
1524                         dbesc($url),
1525                         dbesc(normalise_link($url)),
1526                         dbesc($name),
1527                         dbesc($nick),
1528                         dbesc($photo),
1529                         dbesc(($sharing) ? NETWORK_ZOT : NETWORK_OSTATUS),
1530                         intval(($sharing) ? CONTACT_IS_SHARING : CONTACT_IS_FOLLOWER)
1531                 );
1532                 $r = q("SELECT `id`, `network` FROM `contact` WHERE `uid` = %d AND `url` = '%s' AND `pending` = 1 LIMIT 1",
1533                                 intval($importer['uid']),
1534                                 dbesc($url)
1535                 );
1536                 if (dbm::is_result($r)) {
1537                         $contact_record = $r[0];
1538                         update_contact_avatar($photo, $importer["uid"], $contact_record["id"], true);
1539                 }
1540
1541
1542                 $r = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1",
1543                         intval($importer['uid'])
1544                 );
1545                 $a = get_app();
1546
1547                 if (dbm::is_result($r) AND !in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
1548
1549                         // create notification
1550                         $hash = random_string();
1551
1552                         if (is_array($contact_record)) {
1553                                 $ret = q("INSERT INTO `intro` ( `uid`, `contact-id`, `blocked`, `knowyou`, `hash`, `datetime`)
1554                                         VALUES ( %d, %d, 0, 0, '%s', '%s' )",
1555                                         intval($importer['uid']),
1556                                         intval($contact_record['id']),
1557                                         dbesc($hash),
1558                                         dbesc(datetime_convert())
1559                                 );
1560                         }
1561
1562                         $def_gid = get_default_group($importer['uid'], $contact_record["network"]);
1563
1564                         if (intval($def_gid))
1565                                 group_add_member($importer['uid'],'',$contact_record['id'],$def_gid);
1566
1567                         if (($r[0]['notify-flags'] & NOTIFY_INTRO) &&
1568                                 in_array($r[0]['page-flags'], array(PAGE_NORMAL))) {
1569
1570                                 notification(array(
1571                                         'type'         => NOTIFY_INTRO,
1572                                         'notify_flags' => $r[0]['notify-flags'],
1573                                         'language'     => $r[0]['language'],
1574                                         'to_name'      => $r[0]['username'],
1575                                         'to_email'     => $r[0]['email'],
1576                                         'uid'          => $r[0]['uid'],
1577                                         'link'             => App::get_baseurl() . '/notifications/intro',
1578                                         'source_name'  => ((strlen(stripslashes($contact_record['name']))) ? stripslashes($contact_record['name']) : t('[Name Withheld]')),
1579                                         'source_link'  => $contact_record['url'],
1580                                         'source_photo' => $contact_record['photo'],
1581                                         'verb'         => ($sharing ? ACTIVITY_FRIEND : ACTIVITY_FOLLOW),
1582                                         'otype'        => 'intro'
1583                                 ));
1584
1585                         }
1586                 } elseif (dbm::is_result($r) AND in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
1587                         $r = q("UPDATE `contact` SET `pending` = 0 WHERE `uid` = %d AND `url` = '%s' AND `pending` LIMIT 1",
1588                                         intval($importer['uid']),
1589                                         dbesc($url)
1590                         );
1591                 }
1592
1593         }
1594 }
1595
1596 function lose_follower($importer,$contact,$datarray = array(),$item = "") {
1597
1598         if (($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_SHARING)) {
1599                 q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
1600                         intval(CONTACT_IS_SHARING),
1601                         intval($contact['id'])
1602                 );
1603         } else {
1604                 contact_remove($contact['id']);
1605         }
1606 }
1607
1608 function lose_sharer($importer,$contact,$datarray = array(),$item = "") {
1609
1610         if (($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_FOLLOWER)) {
1611                 q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
1612                         intval(CONTACT_IS_FOLLOWER),
1613                         intval($contact['id'])
1614                 );
1615         } else {
1616                 contact_remove($contact['id']);
1617         }
1618 }
1619
1620 function subscribe_to_hub($url,$importer,$contact,$hubmode = 'subscribe') {
1621
1622         $a = get_app();
1623
1624         if (is_array($importer)) {
1625                 $r = q("SELECT `nickname` FROM `user` WHERE `uid` = %d LIMIT 1",
1626                         intval($importer['uid'])
1627                 );
1628         }
1629
1630         // Diaspora has different message-ids in feeds than they do
1631         // through the direct Diaspora protocol. If we try and use
1632         // the feed, we'll get duplicates. So don't.
1633
1634         if ((! dbm::is_result($r)) || $contact['network'] === NETWORK_DIASPORA)
1635                 return;
1636
1637         $push_url = get_config('system','url') . '/pubsub/' . $r[0]['nickname'] . '/' . $contact['id'];
1638
1639         // Use a single verify token, even if multiple hubs
1640
1641         $verify_token = ((strlen($contact['hub-verify'])) ? $contact['hub-verify'] : random_string());
1642
1643         $params= 'hub.mode=' . $hubmode . '&hub.callback=' . urlencode($push_url) . '&hub.topic=' . urlencode($contact['poll']) . '&hub.verify=async&hub.verify_token=' . $verify_token;
1644
1645         logger('subscribe_to_hub: ' . $hubmode . ' ' . $contact['name'] . ' to hub ' . $url . ' endpoint: '  . $push_url . ' with verifier ' . $verify_token);
1646
1647         if (!strlen($contact['hub-verify']) OR ($contact['hub-verify'] != $verify_token)) {
1648                 $r = q("UPDATE `contact` SET `hub-verify` = '%s' WHERE `id` = %d",
1649                         dbesc($verify_token),
1650                         intval($contact['id'])
1651                 );
1652         }
1653
1654         post_url($url,$params);
1655
1656         logger('subscribe_to_hub: returns: ' . $a->get_curl_code(), LOGGER_DEBUG);
1657
1658         return;
1659
1660 }
1661
1662 function fix_private_photos($s, $uid, $item = null, $cid = 0) {
1663
1664         if (get_config('system','disable_embedded'))
1665                 return $s;
1666
1667         $a = get_app();
1668
1669         logger('fix_private_photos: check for photos', LOGGER_DEBUG);
1670         $site = substr(App::get_baseurl(),strpos(App::get_baseurl(),'://'));
1671
1672         $orig_body = $s;
1673         $new_body = '';
1674
1675         $img_start = strpos($orig_body, '[img');
1676         $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
1677         $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
1678         while( ($img_st_close !== false) && ($img_len !== false) ) {
1679
1680                 $img_st_close++; // make it point to AFTER the closing bracket
1681                 $image = substr($orig_body, $img_start + $img_st_close, $img_len);
1682
1683                 logger('fix_private_photos: found photo ' . $image, LOGGER_DEBUG);
1684
1685
1686                 if (stristr($image , $site . '/photo/')) {
1687                         // Only embed locally hosted photos
1688                         $replace = false;
1689                         $i = basename($image);
1690                         $i = str_replace(array('.jpg','.png','.gif'),array('','',''),$i);
1691                         $x = strpos($i,'-');
1692
1693                         if ($x) {
1694                                 $res = substr($i,$x+1);
1695                                 $i = substr($i,0,$x);
1696                                 $r = q("SELECT * FROM `photo` WHERE `resource-id` = '%s' AND `scale` = %d AND `uid` = %d",
1697                                         dbesc($i),
1698                                         intval($res),
1699                                         intval($uid)
1700                                 );
1701                                 if ($r) {
1702
1703                                         // Check to see if we should replace this photo link with an embedded image
1704                                         // 1. No need to do so if the photo is public
1705                                         // 2. If there's a contact-id provided, see if they're in the access list
1706                                         //    for the photo. If so, embed it.
1707                                         // 3. Otherwise, if we have an item, see if the item permissions match the photo
1708                                         //    permissions, regardless of order but first check to see if they're an exact
1709                                         //    match to save some processing overhead.
1710
1711                                         if (has_permissions($r[0])) {
1712                                                 if ($cid) {
1713                                                         $recips = enumerate_permissions($r[0]);
1714                                                         if (in_array($cid, $recips)) {
1715                                                                 $replace = true;
1716                                                         }
1717                                                 } elseif ($item) {
1718                                                         if (compare_permissions($item,$r[0]))
1719                                                                 $replace = true;
1720                                                 }
1721                                         }
1722                                         if ($replace) {
1723                                                 $data = $r[0]['data'];
1724                                                 $type = $r[0]['type'];
1725
1726                                                 // If a custom width and height were specified, apply before embedding
1727                                                 if (preg_match("/\[img\=([0-9]*)x([0-9]*)\]/is", substr($orig_body, $img_start, $img_st_close), $match)) {
1728                                                         logger('fix_private_photos: scaling photo', LOGGER_DEBUG);
1729
1730                                                         $width = intval($match[1]);
1731                                                         $height = intval($match[2]);
1732
1733                                                         $ph = new Photo($data, $type);
1734                                                         if ($ph->is_valid()) {
1735                                                                 $ph->scaleImage(max($width, $height));
1736                                                                 $data = $ph->imageString();
1737                                                                 $type = $ph->getType();
1738                                                         }
1739                                                 }
1740
1741                                                 logger('fix_private_photos: replacing photo', LOGGER_DEBUG);
1742                                                 $image = 'data:' . $type . ';base64,' . base64_encode($data);
1743                                                 logger('fix_private_photos: replaced: ' . $image, LOGGER_DATA);
1744                                         }
1745                                 }
1746                         }
1747                 }
1748
1749                 $new_body = $new_body . substr($orig_body, 0, $img_start + $img_st_close) . $image . '[/img]';
1750                 $orig_body = substr($orig_body, $img_start + $img_st_close + $img_len + strlen('[/img]'));
1751                 if ($orig_body === false)
1752                         $orig_body = '';
1753
1754                 $img_start = strpos($orig_body, '[img');
1755                 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
1756                 $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
1757         }
1758
1759         $new_body = $new_body . $orig_body;
1760
1761         return($new_body);
1762 }
1763
1764 function has_permissions($obj) {
1765         if (($obj['allow_cid'] != '') || ($obj['allow_gid'] != '') || ($obj['deny_cid'] != '') || ($obj['deny_gid'] != ''))
1766                 return true;
1767         return false;
1768 }
1769
1770 function compare_permissions($obj1,$obj2) {
1771         // first part is easy. Check that these are exactly the same.
1772         if (($obj1['allow_cid'] == $obj2['allow_cid'])
1773                 && ($obj1['allow_gid'] == $obj2['allow_gid'])
1774                 && ($obj1['deny_cid'] == $obj2['deny_cid'])
1775                 && ($obj1['deny_gid'] == $obj2['deny_gid']))
1776                 return true;
1777
1778         // This is harder. Parse all the permissions and compare the resulting set.
1779
1780         $recipients1 = enumerate_permissions($obj1);
1781         $recipients2 = enumerate_permissions($obj2);
1782         sort($recipients1);
1783         sort($recipients2);
1784         if ($recipients1 == $recipients2)
1785                 return true;
1786         return false;
1787 }
1788
1789 // returns an array of contact-ids that are allowed to see this object
1790
1791 function enumerate_permissions($obj) {
1792         $allow_people = expand_acl($obj['allow_cid']);
1793         $allow_groups = expand_groups(expand_acl($obj['allow_gid']));
1794         $deny_people  = expand_acl($obj['deny_cid']);
1795         $deny_groups  = expand_groups(expand_acl($obj['deny_gid']));
1796         $recipients   = array_unique(array_merge($allow_people,$allow_groups));
1797         $deny         = array_unique(array_merge($deny_people,$deny_groups));
1798         $recipients   = array_diff($recipients,$deny);
1799         return $recipients;
1800 }
1801
1802 function item_getfeedtags($item) {
1803         $ret = array();
1804         $matches = false;
1805         $cnt = preg_match_all('|\#\[url\=(.*?)\](.*?)\[\/url\]|',$item['tag'],$matches);
1806         if ($cnt) {
1807                 for($x = 0; $x < $cnt; $x ++) {
1808                         if ($matches[1][$x])
1809                                 $ret[$matches[2][$x]] = array('#',$matches[1][$x], $matches[2][$x]);
1810                 }
1811         }
1812         $matches = false;
1813         $cnt = preg_match_all('|\@\[url\=(.*?)\](.*?)\[\/url\]|',$item['tag'],$matches);
1814         if ($cnt) {
1815                 for($x = 0; $x < $cnt; $x ++) {
1816                         if ($matches[1][$x])
1817                                 $ret[] = array('@',$matches[1][$x], $matches[2][$x]);
1818                 }
1819         }
1820         return $ret;
1821 }
1822
1823 function item_expire($uid, $days, $network = "", $force = false) {
1824
1825         if ((! $uid) || ($days < 1))
1826                 return;
1827
1828         // $expire_network_only = save your own wall posts
1829         // and just expire conversations started by others
1830
1831         $expire_network_only = get_pconfig($uid,'expire','network_only');
1832         $sql_extra = ((intval($expire_network_only)) ? " AND wall = 0 " : "");
1833
1834         if ($network != "") {
1835                 $sql_extra .= sprintf(" AND network = '%s' ", dbesc($network));
1836                 // There is an index "uid_network_received" but not "uid_network_created"
1837                 // This avoids the creation of another index just for one purpose.
1838                 // And it doesn't really matter wether to look at "received" or "created"
1839                 $range = "AND `received` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
1840         } else
1841                 $range = "AND `created` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
1842
1843         $r = q("SELECT `file`, `resource-id`, `starred`, `type`, `id` FROM `item`
1844                 WHERE `uid` = %d $range
1845                 AND `id` = `parent`
1846                 $sql_extra
1847                 AND `deleted` = 0",
1848                 intval($uid),
1849                 intval($days)
1850         );
1851
1852         if (! dbm::is_result($r))
1853                 return;
1854
1855         $expire_items = get_pconfig($uid, 'expire','items');
1856         $expire_items = (($expire_items===false)?1:intval($expire_items)); // default if not set: 1
1857
1858         // Forcing expiring of items - but not notes and marked items
1859         if ($force)
1860                 $expire_items = true;
1861
1862         $expire_notes = get_pconfig($uid, 'expire','notes');
1863         $expire_notes = (($expire_notes===false)?1:intval($expire_notes)); // default if not set: 1
1864
1865         $expire_starred = get_pconfig($uid, 'expire','starred');
1866         $expire_starred = (($expire_starred===false)?1:intval($expire_starred)); // default if not set: 1
1867
1868         $expire_photos = get_pconfig($uid, 'expire','photos');
1869         $expire_photos = (($expire_photos===false)?0:intval($expire_photos)); // default if not set: 0
1870
1871         logger('expire: # items=' . count($r). "; expire items: $expire_items, expire notes: $expire_notes, expire starred: $expire_starred, expire photos: $expire_photos");
1872
1873         foreach($r as $item) {
1874
1875                 // don't expire filed items
1876
1877                 if (strpos($item['file'],'[') !== false)
1878                         continue;
1879
1880                 // Only expire posts, not photos and photo comments
1881
1882                 if ($expire_photos==0 && strlen($item['resource-id']))
1883                         continue;
1884                 if ($expire_starred==0 && intval($item['starred']))
1885                         continue;
1886                 if ($expire_notes==0 && $item['type']=='note')
1887                         continue;
1888                 if ($expire_items==0 && $item['type']!='note')
1889                         continue;
1890
1891                 drop_item($item['id'],false);
1892         }
1893
1894         proc_run(PRIORITY_HIGH,"include/notifier.php", "expire", $uid);
1895
1896 }
1897
1898
1899 function drop_items($items) {
1900         $uid = 0;
1901
1902         if (! local_user() && ! remote_user())
1903                 return;
1904
1905         if (count($items)) {
1906                 foreach($items as $item) {
1907                         $owner = drop_item($item,false);
1908                         if ($owner && ! $uid)
1909                                 $uid = $owner;
1910                 }
1911         }
1912
1913         // multiple threads may have been deleted, send an expire notification
1914
1915         if ($uid)
1916                 proc_run(PRIORITY_HIGH,"include/notifier.php", "expire", $uid);
1917 }
1918
1919
1920 function drop_item($id,$interactive = true) {
1921
1922         $a = get_app();
1923
1924         // locate item to be deleted
1925
1926         $r = q("SELECT * FROM `item` WHERE `id` = %d LIMIT 1",
1927                 intval($id)
1928         );
1929
1930         if (! dbm::is_result($r)) {
1931                 if (! $interactive)
1932                         return 0;
1933                 notice( t('Item not found.') . EOL);
1934                 goaway(App::get_baseurl() . '/' . $_SESSION['return_url']);
1935         }
1936
1937         $item = $r[0];
1938
1939         $owner = $item['uid'];
1940
1941         $contact_id = 0;
1942
1943         // check if logged in user is either the author or owner of this item
1944
1945         if (is_array($_SESSION['remote'])) {
1946                 foreach($_SESSION['remote'] as $visitor) {
1947                         if ($visitor['uid'] == $item['uid'] && $visitor['cid'] == $item['contact-id']) {
1948                                 $contact_id = $visitor['cid'];
1949                                 break;
1950                         }
1951                 }
1952         }
1953
1954
1955         if ((local_user() == $item['uid']) || ($contact_id) || (! $interactive)) {
1956
1957                 // Check if we should do HTML-based delete confirmation
1958                 if ($_REQUEST['confirm']) {
1959                         // <form> can't take arguments in its "action" parameter
1960                         // so add any arguments as hidden inputs
1961                         $query = explode_querystring($a->query_string);
1962                         $inputs = array();
1963                         foreach($query['args'] as $arg) {
1964                                 if (strpos($arg, 'confirm=') === false) {
1965                                         $arg_parts = explode('=', $arg);
1966                                         $inputs[] = array('name' => $arg_parts[0], 'value' => $arg_parts[1]);
1967                                 }
1968                         }
1969
1970                         return replace_macros(get_markup_template('confirm.tpl'), array(
1971                                 '$method' => 'get',
1972                                 '$message' => t('Do you really want to delete this item?'),
1973                                 '$extra_inputs' => $inputs,
1974                                 '$confirm' => t('Yes'),
1975                                 '$confirm_url' => $query['base'],
1976                                 '$confirm_name' => 'confirmed',
1977                                 '$cancel' => t('Cancel'),
1978                         ));
1979                 }
1980                 // Now check how the user responded to the confirmation query
1981                 if ($_REQUEST['canceled']) {
1982                         goaway(App::get_baseurl() . '/' . $_SESSION['return_url']);
1983                 }
1984
1985                 logger('delete item: ' . $item['id'], LOGGER_DEBUG);
1986                 // delete the item
1987
1988                 $r = q("UPDATE `item` SET `deleted` = 1, `title` = '', `body` = '', `edited` = '%s', `changed` = '%s' WHERE `id` = %d",
1989                         dbesc(datetime_convert()),
1990                         dbesc(datetime_convert()),
1991                         intval($item['id'])
1992                 );
1993                 create_tags_from_item($item['id']);
1994                 create_files_from_item($item['id']);
1995                 delete_thread($item['id'], $item['parent-uri']);
1996
1997                 // clean up categories and tags so they don't end up as orphans
1998
1999                 $matches = false;
2000                 $cnt = preg_match_all('/<(.*?)>/',$item['file'],$matches,PREG_SET_ORDER);
2001                 if ($cnt) {
2002                         foreach($matches as $mtch) {
2003                                 file_tag_unsave_file($item['uid'],$item['id'],$mtch[1],true);
2004                         }
2005                 }
2006
2007                 $matches = false;
2008
2009                 $cnt = preg_match_all('/\[(.*?)\]/',$item['file'],$matches,PREG_SET_ORDER);
2010                 if ($cnt) {
2011                         foreach($matches as $mtch) {
2012                                 file_tag_unsave_file($item['uid'],$item['id'],$mtch[1],false);
2013                         }
2014                 }
2015
2016                 // If item is a link to a photo resource, nuke all the associated photos
2017                 // (visitors will not have photo resources)
2018                 // This only applies to photos uploaded from the photos page. Photos inserted into a post do not
2019                 // generate a resource-id and therefore aren't intimately linked to the item.
2020
2021                 if (strlen($item['resource-id'])) {
2022                         q("DELETE FROM `photo` WHERE `resource-id` = '%s' AND `uid` = %d ",
2023                                 dbesc($item['resource-id']),
2024                                 intval($item['uid'])
2025                         );
2026                         // ignore the result
2027                 }
2028
2029                 // If item is a link to an event, nuke the event record.
2030
2031                 if (intval($item['event-id'])) {
2032                         q("DELETE FROM `event` WHERE `id` = %d AND `uid` = %d",
2033                                 intval($item['event-id']),
2034                                 intval($item['uid'])
2035                         );
2036                         // ignore the result
2037                 }
2038
2039                 // If item has attachments, drop them
2040
2041                 foreach(explode(",",$item['attach']) as $attach){
2042                         preg_match("|attach/(\d+)|", $attach, $matches);
2043                         q("DELETE FROM `attach` WHERE `id` = %d AND `uid` = %d",
2044                                 intval($matches[1]),
2045                                 local_user()
2046                         );
2047                         // ignore the result
2048                 }
2049
2050
2051                 // clean up item_id and sign meta-data tables
2052
2053                 /*
2054                 // Old code - caused very long queries and warning entries in the mysql logfiles:
2055
2056                 $r = q("DELETE FROM item_id where iid in (select id from item where parent = %d and uid = %d)",
2057                         intval($item['id']),
2058                         intval($item['uid'])
2059                 );
2060
2061                 $r = q("DELETE FROM sign where iid in (select id from item where parent = %d and uid = %d)",
2062                         intval($item['id']),
2063                         intval($item['uid'])
2064                 );
2065                 */
2066
2067                 // The new code splits the queries since the mysql optimizer really has bad problems with subqueries
2068
2069                 // Creating list of parents
2070                 $r = q("select id from item where parent = %d and uid = %d",
2071                         intval($item['id']),
2072                         intval($item['uid'])
2073                 );
2074
2075                 $parentid = "";
2076
2077                 foreach ($r AS $row) {
2078                         if ($parentid != "")
2079                                 $parentid .= ", ";
2080
2081                         $parentid .= $row["id"];
2082                 }
2083
2084                 // Now delete them
2085                 if ($parentid != "") {
2086                         $r = q("DELETE FROM item_id where iid in (%s)", dbesc($parentid));
2087
2088                         $r = q("DELETE FROM sign where iid in (%s)", dbesc($parentid));
2089                 }
2090
2091                 // If it's the parent of a comment thread, kill all the kids
2092
2093                 if ($item['uri'] == $item['parent-uri']) {
2094                         $r = q("UPDATE `item` SET `deleted` = 1, `edited` = '%s', `changed` = '%s', `body` = '' , `title` = ''
2095                                 WHERE `parent-uri` = '%s' AND `uid` = %d ",
2096                                 dbesc(datetime_convert()),
2097                                 dbesc(datetime_convert()),
2098                                 dbesc($item['parent-uri']),
2099                                 intval($item['uid'])
2100                         );
2101                         create_tags_from_itemuri($item['parent-uri'], $item['uid']);
2102                         create_files_from_itemuri($item['parent-uri'], $item['uid']);
2103                         delete_thread_uri($item['parent-uri'], $item['uid']);
2104                         // ignore the result
2105                 } else {
2106                         // ensure that last-child is set in case the comment that had it just got wiped.
2107                         q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `parent-uri` = '%s' AND `uid` = %d ",
2108                                 dbesc(datetime_convert()),
2109                                 dbesc($item['parent-uri']),
2110                                 intval($item['uid'])
2111                         );
2112                         // who is the last child now?
2113                         $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",
2114                                 dbesc($item['parent-uri']),
2115                                 intval($item['uid'])
2116                         );
2117                         if (dbm::is_result($r)) {
2118                                 q("UPDATE `item` SET `last-child` = 1 WHERE `id` = %d",
2119                                         intval($r[0]['id'])
2120                                 );
2121                         }
2122                 }
2123
2124                 $drop_id = intval($item['id']);
2125
2126                 // send the notification upstream/downstream as the case may be
2127
2128                 proc_run(PRIORITY_HIGH,"include/notifier.php", "drop", $drop_id);
2129
2130                 if (! $interactive)
2131                         return $owner;
2132                 goaway(App::get_baseurl() . '/' . $_SESSION['return_url']);
2133                 //NOTREACHED
2134         } else {
2135                 if (! $interactive)
2136                         return 0;
2137                 notice( t('Permission denied.') . EOL);
2138                 goaway(App::get_baseurl() . '/' . $_SESSION['return_url']);
2139                 //NOTREACHED
2140         }
2141
2142 }
2143
2144
2145 function first_post_date($uid,$wall = false) {
2146         $r = q("select id, created from item
2147                 where uid = %d and wall = %d and deleted = 0 and visible = 1 AND moderated = 0
2148                 and id = parent
2149                 order by created asc limit 1",
2150                 intval($uid),
2151                 intval($wall ? 1 : 0)
2152         );
2153         if (dbm::is_result($r)) {
2154 //              logger('first_post_date: ' . $r[0]['id'] . ' ' . $r[0]['created'], LOGGER_DATA);
2155                 return substr(datetime_convert('',date_default_timezone_get(),$r[0]['created']),0,10);
2156         }
2157         return false;
2158 }
2159
2160 /* modified posted_dates() {below} to arrange the list in years */
2161 function list_post_dates($uid, $wall) {
2162         $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
2163
2164         $dthen = first_post_date($uid, $wall);
2165         if (! $dthen)
2166                 return array();
2167
2168         // Set the start and end date to the beginning of the month
2169         $dnow = substr($dnow,0,8).'01';
2170         $dthen = substr($dthen,0,8).'01';
2171
2172         $ret = array();
2173
2174         // Starting with the current month, get the first and last days of every
2175         // month down to and including the month of the first post
2176         while(substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
2177                 $dyear = intval(substr($dnow,0,4));
2178                 $dstart = substr($dnow,0,8) . '01';
2179                 $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
2180                 $start_month = datetime_convert('','',$dstart,'Y-m-d');
2181                 $end_month = datetime_convert('','',$dend,'Y-m-d');
2182                 $str = day_translate(datetime_convert('','',$dnow,'F'));
2183                 if (! $ret[$dyear])
2184                         $ret[$dyear] = array();
2185                 $ret[$dyear][] = array($str,$end_month,$start_month);
2186                 $dnow = datetime_convert('','',$dnow . ' -1 month', 'Y-m-d');
2187         }
2188         return $ret;
2189 }
2190
2191 function posted_dates($uid,$wall) {
2192         $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
2193
2194         $dthen = first_post_date($uid,$wall);
2195         if (! $dthen)
2196                 return array();
2197
2198         // Set the start and end date to the beginning of the month
2199         $dnow = substr($dnow,0,8).'01';
2200         $dthen = substr($dthen,0,8).'01';
2201
2202         $ret = array();
2203         // Starting with the current month, get the first and last days of every
2204         // month down to and including the month of the first post
2205         while(substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
2206                 $dstart = substr($dnow,0,8) . '01';
2207                 $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
2208                 $start_month = datetime_convert('','',$dstart,'Y-m-d');
2209                 $end_month = datetime_convert('','',$dend,'Y-m-d');
2210                 $str = day_translate(datetime_convert('','',$dnow,'F Y'));
2211                 $ret[] = array($str,$end_month,$start_month);
2212                 $dnow = datetime_convert('','',$dnow . ' -1 month', 'Y-m-d');
2213         }
2214         return $ret;
2215 }
2216
2217
2218 function posted_date_widget($url,$uid,$wall) {
2219         $o = '';
2220
2221         if (! feature_enabled($uid,'archives'))
2222                 return $o;
2223
2224         // For former Facebook folks that left because of "timeline"
2225
2226 /*      if ($wall && intval(get_pconfig($uid,'system','no_wall_archive_widget')))
2227                 return $o;*/
2228
2229         $visible_years = get_pconfig($uid,'system','archive_visible_years');
2230         if (! $visible_years)
2231                 $visible_years = 5;
2232
2233         $ret = list_post_dates($uid,$wall);
2234
2235         if (! dbm::is_result($ret))
2236                 return $o;
2237
2238         $cutoff_year = intval(datetime_convert('',date_default_timezone_get(),'now','Y')) - $visible_years;
2239         $cutoff = ((array_key_exists($cutoff_year,$ret))? true : false);
2240
2241         $o = replace_macros(get_markup_template('posted_date_widget.tpl'),array(
2242                 '$title' => t('Archives'),
2243                 '$size' => $visible_years,
2244                 '$cutoff_year' => $cutoff_year,
2245                 '$cutoff' => $cutoff,
2246                 '$url' => $url,
2247                 '$dates' => $ret,
2248                 '$showmore' => t('show more')
2249
2250         ));
2251         return $o;
2252 }