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