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