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