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