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