]> git.mxchange.org Git - friendica.git/blob - include/items.php
Shadow entry mechanism improved
[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         /**
926          * If this is now the last-child, force all _other_ children of this parent to *not* be last-child
927          * It is done after the transaction to avoid dead locks.
928          */
929
930         if ($arr['last-child']) {
931                 $r = q("UPDATE `item` SET `last-child` = 0 WHERE `parent-uri` = '%s' AND `uid` = %d AND `id` != %d",
932                         dbesc($arr['uri']),
933                         intval($arr['uid']),
934                         intval($current_post)
935                 );
936         }
937
938         if ($arr['parent-uri'] === $arr['uri']) {
939                 add_shadow_thread($current_post);
940         } else {
941                 add_shadow_entry($current_post);
942         }
943
944         check_item_notification($current_post, $uid);
945
946         if ($notify)
947                 proc_run(PRIORITY_HIGH, "include/notifier.php", $notify_type, $current_post);
948
949         return $current_post;
950 }
951
952 /**
953  * @brief Set "success_update" and "last-item" to the date of the last time we heard from this contact
954  *
955  * This can be used to filter for inactive contacts.
956  * Only do this for public postings to avoid privacy problems, since poco data is public.
957  * Don't set this value if it isn't from the owner (could be an author that we don't know)
958  *
959  * @param array $arr Contains the just posted item record
960  */
961 function item_set_last_item($arr) {
962
963         $update = (!$arr['private'] AND (($arr["author-link"] === $arr["owner-link"]) OR ($arr["parent-uri"] === $arr["uri"])));
964
965         // Is it a forum? Then we don't care about the rules from above
966         if (!$update AND ($arr["network"] == NETWORK_DFRN) AND ($arr["parent-uri"] === $arr["uri"])) {
967                 $isforum = q("SELECT `forum` FROM `contact` WHERE `id` = %d AND `forum`",
968                                 intval($arr['contact-id']));
969                 if ($isforum) {
970                         $update = true;
971                 }
972         }
973
974         if ($update) {
975                 q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
976                         dbesc($arr['received']),
977                         dbesc($arr['received']),
978                         intval($arr['contact-id'])
979                 );
980         }
981         // Now do the same for the system wide contacts with uid=0
982         if (!$arr['private']) {
983                 q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
984                         dbesc($arr['received']),
985                         dbesc($arr['received']),
986                         intval($arr['owner-id'])
987                 );
988
989                 if ($arr['owner-id'] != $arr['author-id']) {
990                         q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
991                                 dbesc($arr['received']),
992                                 dbesc($arr['received']),
993                                 intval($arr['author-id'])
994                         );
995                 }
996         }
997 }
998
999 function item_body_set_hashtags(&$item) {
1000
1001         $tags = get_tags($item["body"]);
1002
1003         // No hashtags?
1004         if (!count($tags))
1005                 return(false);
1006
1007         // This sorting is important when there are hashtags that are part of other hashtags
1008         // Otherwise there could be problems with hashtags like #test and #test2
1009         rsort($tags);
1010
1011         $a = get_app();
1012
1013         $URLSearchString = "^\[\]";
1014
1015         // All hashtags should point to the home server
1016         //$item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1017         //              "#[url=".$a->get_baseurl()."/search?tag=$2]$2[/url]", $item["body"]);
1018
1019         //$item["tag"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1020         //              "#[url=".$a->get_baseurl()."/search?tag=$2]$2[/url]", $item["tag"]);
1021
1022         // mask hashtags inside of url, bookmarks and attachments to avoid urls in urls
1023         $item["body"] = preg_replace_callback("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1024                 function ($match){
1025                         return("[url=".str_replace("#", "&num;", $match[1])."]".str_replace("#", "&num;", $match[2])."[/url]");
1026                 },$item["body"]);
1027
1028         $item["body"] = preg_replace_callback("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism",
1029                 function ($match){
1030                         return("[bookmark=".str_replace("#", "&num;", $match[1])."]".str_replace("#", "&num;", $match[2])."[/bookmark]");
1031                 },$item["body"]);
1032
1033         $item["body"] = preg_replace_callback("/\[attachment (.*)\](.*?)\[\/attachment\]/ism",
1034                 function ($match){
1035                         return("[attachment ".str_replace("#", "&num;", $match[1])."]".$match[2]."[/attachment]");
1036                 },$item["body"]);
1037
1038         // Repair recursive urls
1039         $item["body"] = preg_replace("/&num;\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1040                         "&num;$2", $item["body"]);
1041
1042
1043         foreach($tags as $tag) {
1044                 if (strpos($tag,'#') !== 0)
1045                         continue;
1046
1047                 if (strpos($tag,'[url='))
1048                         continue;
1049
1050                 $basetag = str_replace('_',' ',substr($tag,1));
1051
1052                 $newtag = '#[url='.$a->get_baseurl().'/search?tag='.rawurlencode($basetag).']'.$basetag.'[/url]';
1053
1054                 $item["body"] = str_replace($tag, $newtag, $item["body"]);
1055
1056                 if (!stristr($item["tag"],"/search?tag=".$basetag."]".$basetag."[/url]")) {
1057                         if (strlen($item["tag"]))
1058                                 $item["tag"] = ','.$item["tag"];
1059                         $item["tag"] = $newtag.$item["tag"];
1060                 }
1061         }
1062
1063         // Convert back the masked hashtags
1064         $item["body"] = str_replace("&num;", "#", $item["body"]);
1065 }
1066
1067 function get_item_guid($id) {
1068         $r = q("SELECT `guid` FROM `item` WHERE `id` = %d LIMIT 1", intval($id));
1069         if (count($r))
1070                 return($r[0]["guid"]);
1071         else
1072                 return("");
1073 }
1074
1075 function get_item_id($guid, $uid = 0) {
1076
1077         $nick = "";
1078         $id = 0;
1079
1080         if ($uid == 0)
1081                 $uid == local_user();
1082
1083         // Does the given user have this item?
1084         if ($uid) {
1085                 $r = q("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid`
1086                         WHERE `item`.`visible` = 1 AND `item`.`deleted` = 0 and `item`.`moderated` = 0
1087                                 AND `item`.`guid` = '%s' AND `item`.`uid` = %d", dbesc($guid), intval($uid));
1088                 if (count($r)) {
1089                         $id = $r[0]["id"];
1090                         $nick = $r[0]["nickname"];
1091                 }
1092         }
1093
1094         // Or is it anywhere on the server?
1095         if ($nick == "") {
1096                 $r = q("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid`
1097                         WHERE `item`.`visible` = 1 AND `item`.`deleted` = 0 and `item`.`moderated` = 0
1098                                 AND `item`.`allow_cid` = ''  AND `item`.`allow_gid` = ''
1099                                 AND `item`.`deny_cid`  = '' AND `item`.`deny_gid`  = ''
1100                                 AND `item`.`private` = 0 AND `item`.`wall` = 1
1101                                 AND `item`.`guid` = '%s'", dbesc($guid));
1102                 if (count($r)) {
1103                         $id = $r[0]["id"];
1104                         $nick = $r[0]["nickname"];
1105                 }
1106         }
1107         return(array("nick" => $nick, "id" => $id));
1108 }
1109
1110 // return - test
1111 function get_item_contact($item,$contacts) {
1112         if (! count($contacts) || (! is_array($item)))
1113                 return false;
1114         foreach($contacts as $contact) {
1115                 if ($contact['id'] == $item['contact-id']) {
1116                         return $contact;
1117                         break; // NOTREACHED
1118                 }
1119         }
1120         return false;
1121 }
1122
1123 /**
1124  * look for mention tags and setup a second delivery chain for forum/community posts if appropriate
1125  * @param int $uid
1126  * @param int $item_id
1127  * @return bool true if item was deleted, else false
1128  */
1129 function tag_deliver($uid,$item_id) {
1130
1131         //
1132
1133         $a = get_app();
1134
1135         $mention = false;
1136
1137         $u = q("select * from user where uid = %d limit 1",
1138                 intval($uid)
1139         );
1140         if (! count($u))
1141                 return;
1142
1143         $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
1144         $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
1145
1146
1147         $i = q("select * from item where id = %d and uid = %d limit 1",
1148                 intval($item_id),
1149                 intval($uid)
1150         );
1151         if (! count($i))
1152                 return;
1153
1154         $item = $i[0];
1155
1156         $link = normalise_link($a->get_baseurl() . '/profile/' . $u[0]['nickname']);
1157
1158         // Diaspora uses their own hardwired link URL in @-tags
1159         // instead of the one we supply with webfinger
1160
1161         $dlink = normalise_link($a->get_baseurl() . '/u/' . $u[0]['nickname']);
1162
1163         $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism',$item['body'],$matches,PREG_SET_ORDER);
1164         if ($cnt) {
1165                 foreach($matches as $mtch) {
1166                         if (link_compare($link,$mtch[1]) || link_compare($dlink,$mtch[1])) {
1167                                 $mention = true;
1168                                 logger('tag_deliver: mention found: ' . $mtch[2]);
1169                         }
1170                 }
1171         }
1172
1173         if (! $mention){
1174                 if ( ($community_page || $prvgroup) &&
1175                           (!$item['wall']) && (!$item['origin']) && ($item['id'] == $item['parent'])){
1176                         // mmh.. no mention.. community page or private group... no wall.. no origin.. top-post (not a comment)
1177                         // delete it!
1178                         logger("tag_deliver: no-mention top-level post to communuty or private group. delete.");
1179                         q("DELETE FROM item WHERE id = %d and uid = %d",
1180                                 intval($item_id),
1181                                 intval($uid)
1182                         );
1183                         return true;
1184                 }
1185                 return;
1186         }
1187
1188         $arr = array('item' => $item, 'user' => $u[0], 'contact' => $r[0]);
1189
1190         call_hooks('tagged', $arr);
1191
1192         if ((! $community_page) && (! $prvgroup))
1193                 return;
1194
1195
1196         // tgroup delivery - setup a second delivery chain
1197         // prevent delivery looping - only proceed
1198         // if the message originated elsewhere and is a top-level post
1199
1200         if (($item['wall']) || ($item['origin']) || ($item['id'] != $item['parent']))
1201                 return;
1202
1203         // now change this copy of the post to a forum head message and deliver to all the tgroup members
1204
1205
1206         $c = q("select name, url, thumb from contact where self = 1 and uid = %d limit 1",
1207                 intval($u[0]['uid'])
1208         );
1209         if (! count($c))
1210                 return;
1211
1212         // also reset all the privacy bits to the forum default permissions
1213
1214         $private = ($u[0]['allow_cid'] || $u[0]['allow_gid'] || $u[0]['deny_cid'] || $u[0]['deny_gid']) ? 1 : 0;
1215
1216         $forum_mode = (($prvgroup) ? 2 : 1);
1217
1218         q("update item set wall = 1, origin = 1, forum_mode = %d, `owner-name` = '%s', `owner-link` = '%s', `owner-avatar` = '%s',
1219                 `private` = %d, `allow_cid` = '%s', `allow_gid` = '%s', `deny_cid` = '%s', `deny_gid` = '%s'  where id = %d",
1220                 intval($forum_mode),
1221                 dbesc($c[0]['name']),
1222                 dbesc($c[0]['url']),
1223                 dbesc($c[0]['thumb']),
1224                 intval($private),
1225                 dbesc($u[0]['allow_cid']),
1226                 dbesc($u[0]['allow_gid']),
1227                 dbesc($u[0]['deny_cid']),
1228                 dbesc($u[0]['deny_gid']),
1229                 intval($item_id)
1230         );
1231         update_thread($item_id);
1232
1233         proc_run(PRIORITY_HIGH,'include/notifier.php', 'tgroup', $item_id);
1234
1235 }
1236
1237
1238
1239 function tgroup_check($uid,$item) {
1240
1241         $a = get_app();
1242
1243         $mention = false;
1244
1245         // check that the message originated elsewhere and is a top-level post
1246
1247         if (($item['wall']) || ($item['origin']) || ($item['uri'] != $item['parent-uri']))
1248                 return false;
1249
1250
1251         $u = q("select * from user where uid = %d limit 1",
1252                 intval($uid)
1253         );
1254         if (! count($u))
1255                 return false;
1256
1257         $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
1258         $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
1259
1260
1261         $link = normalise_link($a->get_baseurl() . '/profile/' . $u[0]['nickname']);
1262
1263         // Diaspora uses their own hardwired link URL in @-tags
1264         // instead of the one we supply with webfinger
1265
1266         $dlink = normalise_link($a->get_baseurl() . '/u/' . $u[0]['nickname']);
1267
1268         $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism',$item['body'],$matches,PREG_SET_ORDER);
1269         if ($cnt) {
1270                 foreach($matches as $mtch) {
1271                         if (link_compare($link,$mtch[1]) || link_compare($dlink,$mtch[1])) {
1272                                 $mention = true;
1273                                 logger('tgroup_check: mention found: ' . $mtch[2]);
1274                         }
1275                 }
1276         }
1277
1278         if (! $mention)
1279                 return false;
1280
1281         if ((! $community_page) && (! $prvgroup))
1282                 return false;
1283
1284         return true;
1285 }
1286
1287 /*
1288   This function returns true if $update has an edited timestamp newer
1289   than $existing, i.e. $update contains new data which should override
1290   what's already there.  If there is no timestamp yet, the update is
1291   assumed to be newer.  If the update has no timestamp, the existing
1292   item is assumed to be up-to-date.  If the timestamps are equal it
1293   assumes the update has been seen before and should be ignored.
1294   */
1295 function edited_timestamp_is_newer($existing, $update) {
1296     if (!x($existing,'edited') || !$existing['edited']) {
1297         return true;
1298     }
1299     if (!x($update,'edited') || !$update['edited']) {
1300         return false;
1301     }
1302     $existing_edited = datetime_convert('UTC', 'UTC', $existing['edited']);
1303     $update_edited = datetime_convert('UTC', 'UTC', $update['edited']);
1304     return (strcmp($existing_edited, $update_edited) < 0);
1305 }
1306
1307 /**
1308  *
1309  * consume_feed - process atom feed and update anything/everything we might need to update
1310  *
1311  * $xml = the (atom) feed to consume - RSS isn't as fully supported but may work for simple feeds.
1312  *
1313  * $importer = the contact_record (joined to user_record) of the local user who owns this relationship.
1314  *             It is this person's stuff that is going to be updated.
1315  * $contact =  the person who is sending us stuff. If not set, we MAY be processing a "follow" activity
1316  *             from an external network and MAY create an appropriate contact record. Otherwise, we MUST
1317  *             have a contact record.
1318  * $hub = should we find a hub declation in the feed, pass it back to our calling process, who might (or
1319  *        might not) try and subscribe to it.
1320  * $datedir sorts in reverse order
1321  * $pass - by default ($pass = 0) we cannot guarantee that a parent item has been
1322  *      imported prior to its children being seen in the stream unless we are certain
1323  *      of how the feed is arranged/ordered.
1324  * With $pass = 1, we only pull parent items out of the stream.
1325  * With $pass = 2, we only pull children (comments/likes).
1326  *
1327  * So running this twice, first with pass 1 and then with pass 2 will do the right
1328  * thing regardless of feed ordering. This won't be adequate in a fully-threaded
1329  * model where comments can have sub-threads. That would require some massive sorting
1330  * to get all the feed items into a mostly linear ordering, and might still require
1331  * recursion.
1332  */
1333
1334 function consume_feed($xml,$importer,&$contact, &$hub, $datedir = 0, $pass = 0) {
1335         if ($contact['network'] === NETWORK_OSTATUS) {
1336                 if ($pass < 2) {
1337                         // Test - remove before flight
1338                         //$tempfile = tempnam(get_temppath(), "ostatus2");
1339                         //file_put_contents($tempfile, $xml);
1340                         logger("Consume OStatus messages ", LOGGER_DEBUG);
1341                         ostatus::import($xml,$importer,$contact, $hub);
1342                 }
1343                 return;
1344         }
1345
1346         if ($contact['network'] === NETWORK_FEED) {
1347                 if ($pass < 2) {
1348                         logger("Consume feeds", LOGGER_DEBUG);
1349                         feed_import($xml,$importer,$contact, $hub);
1350                 }
1351                 return;
1352         }
1353
1354         if ($contact['network'] === NETWORK_DFRN) {
1355                 logger("Consume DFRN messages", LOGGER_DEBUG);
1356
1357                 $r = q("SELECT  `contact`.*, `contact`.`uid` AS `importer_uid`,
1358                                         `contact`.`pubkey` AS `cpubkey`,
1359                                         `contact`.`prvkey` AS `cprvkey`,
1360                                         `contact`.`thumb` AS `thumb`,
1361                                         `contact`.`url` as `url`,
1362                                         `contact`.`name` as `senderName`,
1363                                         `user`.*
1364                         FROM `contact`
1365                         LEFT JOIN `user` ON `contact`.`uid` = `user`.`uid`
1366                         WHERE `contact`.`id` = %d AND `user`.`uid` = %d",
1367                         dbesc($contact["id"]), dbesc($importer["uid"])
1368                 );
1369                 if ($r) {
1370                         logger("Now import the DFRN feed");
1371                         dfrn::import($xml,$r[0], true);
1372                         return;
1373                 }
1374         }
1375 }
1376
1377 function item_is_remote_self($contact, &$datarray) {
1378         $a = get_app();
1379
1380         if (!$contact['remote_self'])
1381                 return false;
1382
1383         // Prevent the forwarding of posts that are forwarded
1384         if ($datarray["extid"] == NETWORK_DFRN)
1385                 return false;
1386
1387         // Prevent to forward already forwarded posts
1388         if ($datarray["app"] == $a->get_hostname())
1389                 return false;
1390
1391         // Only forward posts
1392         if ($datarray["verb"] != ACTIVITY_POST)
1393                 return false;
1394
1395         if (($contact['network'] != NETWORK_FEED) AND $datarray['private'])
1396                 return false;
1397
1398         $datarray2 = $datarray;
1399         logger('remote-self start - Contact '.$contact['url'].' - '.$contact['remote_self'].' Item '.print_r($datarray, true), LOGGER_DEBUG);
1400         if ($contact['remote_self'] == 2) {
1401                 $r = q("SELECT `id`,`url`,`name`,`thumb` FROM `contact` WHERE `uid` = %d AND `self`",
1402                         intval($contact['uid']));
1403                 if (count($r)) {
1404                         $datarray['contact-id'] = $r[0]["id"];
1405
1406                         $datarray['owner-name'] = $r[0]["name"];
1407                         $datarray['owner-link'] = $r[0]["url"];
1408                         $datarray['owner-avatar'] = $r[0]["thumb"];
1409
1410                         $datarray['author-name']   = $datarray['owner-name'];
1411                         $datarray['author-link']   = $datarray['owner-link'];
1412                         $datarray['author-avatar'] = $datarray['owner-avatar'];
1413                 }
1414
1415                 if ($contact['network'] != NETWORK_FEED) {
1416                         $datarray["guid"] = get_guid(32);
1417                         unset($datarray["plink"]);
1418                         $datarray["uri"] = item_new_uri($a->get_hostname(),$contact['uid'], $datarray["guid"]);
1419                         $datarray["parent-uri"] = $datarray["uri"];
1420                         $datarray["extid"] = $contact['network'];
1421                         $urlpart = parse_url($datarray2['author-link']);
1422                         $datarray["app"] = $urlpart["host"];
1423                 } else
1424                         $datarray['private'] = 0;
1425         }
1426
1427         if ($contact['network'] != NETWORK_FEED) {
1428                 // Store the original post
1429                 $r = item_store($datarray2, false, false);
1430                 logger('remote-self post original item - Contact '.$contact['url'].' return '.$r.' Item '.print_r($datarray2, true), LOGGER_DEBUG);
1431         } else
1432                 $datarray["app"] = "Feed";
1433
1434         return true;
1435 }
1436
1437 function new_follower($importer,$contact,$datarray,$item,$sharing = false) {
1438         $url = notags(trim($datarray['author-link']));
1439         $name = notags(trim($datarray['author-name']));
1440         $photo = notags(trim($datarray['author-avatar']));
1441
1442         if (is_object($item)) {
1443                 $rawtag = $item->get_item_tags(NAMESPACE_ACTIVITY,'actor');
1444                 if ($rawtag && $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data'])
1445                         $nick = $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data'];
1446         } else
1447                 $nick = $item;
1448
1449         if (is_array($contact)) {
1450                 if (($contact['network'] == NETWORK_OSTATUS && $contact['rel'] == CONTACT_IS_SHARING)
1451                         || ($sharing && $contact['rel'] == CONTACT_IS_FOLLOWER)) {
1452                         $r = q("UPDATE `contact` SET `rel` = %d, `writable` = 1 WHERE `id` = %d AND `uid` = %d",
1453                                 intval(CONTACT_IS_FRIEND),
1454                                 intval($contact['id']),
1455                                 intval($importer['uid'])
1456                         );
1457                 }
1458                 // send email notification to owner?
1459         } else {
1460
1461                 // create contact record
1462
1463                 $r = q("INSERT INTO `contact` (`uid`, `created`, `url`, `nurl`, `name`, `nick`, `photo`, `network`, `rel`,
1464                         `blocked`, `readonly`, `pending`, `writable`)
1465                         VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, 0, 0, 1, 1)",
1466                         intval($importer['uid']),
1467                         dbesc(datetime_convert()),
1468                         dbesc($url),
1469                         dbesc(normalise_link($url)),
1470                         dbesc($name),
1471                         dbesc($nick),
1472                         dbesc($photo),
1473                         dbesc(($sharing) ? NETWORK_ZOT : NETWORK_OSTATUS),
1474                         intval(($sharing) ? CONTACT_IS_SHARING : CONTACT_IS_FOLLOWER)
1475                 );
1476                 $r = q("SELECT `id`, `network` FROM `contact` WHERE `uid` = %d AND `url` = '%s' AND `pending` = 1 LIMIT 1",
1477                                 intval($importer['uid']),
1478                                 dbesc($url)
1479                 );
1480                 if (count($r)) {
1481                         $contact_record = $r[0];
1482                         update_contact_avatar($photo, $importer["uid"], $contact_record["id"], true);
1483                 }
1484
1485
1486                 $r = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1",
1487                         intval($importer['uid'])
1488                 );
1489                 $a = get_app();
1490                 if (count($r) AND !in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
1491
1492                         // create notification
1493                         $hash = random_string();
1494
1495                         if (is_array($contact_record)) {
1496                                 $ret = q("INSERT INTO `intro` ( `uid`, `contact-id`, `blocked`, `knowyou`, `hash`, `datetime`)
1497                                         VALUES ( %d, %d, 0, 0, '%s', '%s' )",
1498                                         intval($importer['uid']),
1499                                         intval($contact_record['id']),
1500                                         dbesc($hash),
1501                                         dbesc(datetime_convert())
1502                                 );
1503                         }
1504
1505                         $def_gid = get_default_group($importer['uid'], $contact_record["network"]);
1506
1507                         if (intval($def_gid))
1508                                 group_add_member($importer['uid'],'',$contact_record['id'],$def_gid);
1509
1510                         if (($r[0]['notify-flags'] & NOTIFY_INTRO) &&
1511                                 in_array($r[0]['page-flags'], array(PAGE_NORMAL))) {
1512
1513                                 notification(array(
1514                                         'type'         => NOTIFY_INTRO,
1515                                         'notify_flags' => $r[0]['notify-flags'],
1516                                         'language'     => $r[0]['language'],
1517                                         'to_name'      => $r[0]['username'],
1518                                         'to_email'     => $r[0]['email'],
1519                                         'uid'          => $r[0]['uid'],
1520                                         'link'             => $a->get_baseurl() . '/notifications/intro',
1521                                         'source_name'  => ((strlen(stripslashes($contact_record['name']))) ? stripslashes($contact_record['name']) : t('[Name Withheld]')),
1522                                         'source_link'  => $contact_record['url'],
1523                                         'source_photo' => $contact_record['photo'],
1524                                         'verb'         => ($sharing ? ACTIVITY_FRIEND : ACTIVITY_FOLLOW),
1525                                         'otype'        => 'intro'
1526                                 ));
1527
1528                         }
1529                 } elseif (count($r) AND in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
1530                         $r = q("UPDATE `contact` SET `pending` = 0 WHERE `uid` = %d AND `url` = '%s' AND `pending` LIMIT 1",
1531                                         intval($importer['uid']),
1532                                         dbesc($url)
1533                         );
1534                 }
1535
1536         }
1537 }
1538
1539 function lose_follower($importer,$contact,$datarray = array(),$item = "") {
1540
1541         if (($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_SHARING)) {
1542                 q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
1543                         intval(CONTACT_IS_SHARING),
1544                         intval($contact['id'])
1545                 );
1546         } else {
1547                 contact_remove($contact['id']);
1548         }
1549 }
1550
1551 function lose_sharer($importer,$contact,$datarray = array(),$item = "") {
1552
1553         if (($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_FOLLOWER)) {
1554                 q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
1555                         intval(CONTACT_IS_FOLLOWER),
1556                         intval($contact['id'])
1557                 );
1558         } else {
1559                 contact_remove($contact['id']);
1560         }
1561 }
1562
1563 function subscribe_to_hub($url,$importer,$contact,$hubmode = 'subscribe') {
1564
1565         $a = get_app();
1566
1567         if (is_array($importer)) {
1568                 $r = q("SELECT `nickname` FROM `user` WHERE `uid` = %d LIMIT 1",
1569                         intval($importer['uid'])
1570                 );
1571         }
1572
1573         // Diaspora has different message-ids in feeds than they do
1574         // through the direct Diaspora protocol. If we try and use
1575         // the feed, we'll get duplicates. So don't.
1576
1577         if ((! count($r)) || $contact['network'] === NETWORK_DIASPORA)
1578                 return;
1579
1580         $push_url = get_config('system','url') . '/pubsub/' . $r[0]['nickname'] . '/' . $contact['id'];
1581
1582         // Use a single verify token, even if multiple hubs
1583
1584         $verify_token = ((strlen($contact['hub-verify'])) ? $contact['hub-verify'] : random_string());
1585
1586         $params= 'hub.mode=' . $hubmode . '&hub.callback=' . urlencode($push_url) . '&hub.topic=' . urlencode($contact['poll']) . '&hub.verify=async&hub.verify_token=' . $verify_token;
1587
1588         logger('subscribe_to_hub: ' . $hubmode . ' ' . $contact['name'] . ' to hub ' . $url . ' endpoint: '  . $push_url . ' with verifier ' . $verify_token);
1589
1590         if (!strlen($contact['hub-verify']) OR ($contact['hub-verify'] != $verify_token)) {
1591                 $r = q("UPDATE `contact` SET `hub-verify` = '%s' WHERE `id` = %d",
1592                         dbesc($verify_token),
1593                         intval($contact['id'])
1594                 );
1595         }
1596
1597         post_url($url,$params);
1598
1599         logger('subscribe_to_hub: returns: ' . $a->get_curl_code(), LOGGER_DEBUG);
1600
1601         return;
1602
1603 }
1604
1605 function fix_private_photos($s, $uid, $item = null, $cid = 0) {
1606
1607         if (get_config('system','disable_embedded'))
1608                 return $s;
1609
1610         $a = get_app();
1611
1612         logger('fix_private_photos: check for photos', LOGGER_DEBUG);
1613         $site = substr($a->get_baseurl(),strpos($a->get_baseurl(),'://'));
1614
1615         $orig_body = $s;
1616         $new_body = '';
1617
1618         $img_start = strpos($orig_body, '[img');
1619         $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
1620         $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
1621         while( ($img_st_close !== false) && ($img_len !== false) ) {
1622
1623                 $img_st_close++; // make it point to AFTER the closing bracket
1624                 $image = substr($orig_body, $img_start + $img_st_close, $img_len);
1625
1626                 logger('fix_private_photos: found photo ' . $image, LOGGER_DEBUG);
1627
1628
1629                 if (stristr($image , $site . '/photo/')) {
1630                         // Only embed locally hosted photos
1631                         $replace = false;
1632                         $i = basename($image);
1633                         $i = str_replace(array('.jpg','.png','.gif'),array('','',''),$i);
1634                         $x = strpos($i,'-');
1635
1636                         if ($x) {
1637                                 $res = substr($i,$x+1);
1638                                 $i = substr($i,0,$x);
1639                                 $r = q("SELECT * FROM `photo` WHERE `resource-id` = '%s' AND `scale` = %d AND `uid` = %d",
1640                                         dbesc($i),
1641                                         intval($res),
1642                                         intval($uid)
1643                                 );
1644                                 if ($r) {
1645
1646                                         // Check to see if we should replace this photo link with an embedded image
1647                                         // 1. No need to do so if the photo is public
1648                                         // 2. If there's a contact-id provided, see if they're in the access list
1649                                         //    for the photo. If so, embed it.
1650                                         // 3. Otherwise, if we have an item, see if the item permissions match the photo
1651                                         //    permissions, regardless of order but first check to see if they're an exact
1652                                         //    match to save some processing overhead.
1653
1654                                         if (has_permissions($r[0])) {
1655                                                 if ($cid) {
1656                                                         $recips = enumerate_permissions($r[0]);
1657                                                         if (in_array($cid, $recips)) {
1658                                                                 $replace = true;
1659                                                         }
1660                                                 } elseif ($item) {
1661                                                         if (compare_permissions($item,$r[0]))
1662                                                                 $replace = true;
1663                                                 }
1664                                         }
1665                                         if ($replace) {
1666                                                 $data = $r[0]['data'];
1667                                                 $type = $r[0]['type'];
1668
1669                                                 // If a custom width and height were specified, apply before embedding
1670                                                 if (preg_match("/\[img\=([0-9]*)x([0-9]*)\]/is", substr($orig_body, $img_start, $img_st_close), $match)) {
1671                                                         logger('fix_private_photos: scaling photo', LOGGER_DEBUG);
1672
1673                                                         $width = intval($match[1]);
1674                                                         $height = intval($match[2]);
1675
1676                                                         $ph = new Photo($data, $type);
1677                                                         if ($ph->is_valid()) {
1678                                                                 $ph->scaleImage(max($width, $height));
1679                                                                 $data = $ph->imageString();
1680                                                                 $type = $ph->getType();
1681                                                         }
1682                                                 }
1683
1684                                                 logger('fix_private_photos: replacing photo', LOGGER_DEBUG);
1685                                                 $image = 'data:' . $type . ';base64,' . base64_encode($data);
1686                                                 logger('fix_private_photos: replaced: ' . $image, LOGGER_DATA);
1687                                         }
1688                                 }
1689                         }
1690                 }
1691
1692                 $new_body = $new_body . substr($orig_body, 0, $img_start + $img_st_close) . $image . '[/img]';
1693                 $orig_body = substr($orig_body, $img_start + $img_st_close + $img_len + strlen('[/img]'));
1694                 if ($orig_body === false)
1695                         $orig_body = '';
1696
1697                 $img_start = strpos($orig_body, '[img');
1698                 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
1699                 $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
1700         }
1701
1702         $new_body = $new_body . $orig_body;
1703
1704         return($new_body);
1705 }
1706
1707 function has_permissions($obj) {
1708         if (($obj['allow_cid'] != '') || ($obj['allow_gid'] != '') || ($obj['deny_cid'] != '') || ($obj['deny_gid'] != ''))
1709                 return true;
1710         return false;
1711 }
1712
1713 function compare_permissions($obj1,$obj2) {
1714         // first part is easy. Check that these are exactly the same.
1715         if (($obj1['allow_cid'] == $obj2['allow_cid'])
1716                 && ($obj1['allow_gid'] == $obj2['allow_gid'])
1717                 && ($obj1['deny_cid'] == $obj2['deny_cid'])
1718                 && ($obj1['deny_gid'] == $obj2['deny_gid']))
1719                 return true;
1720
1721         // This is harder. Parse all the permissions and compare the resulting set.
1722
1723         $recipients1 = enumerate_permissions($obj1);
1724         $recipients2 = enumerate_permissions($obj2);
1725         sort($recipients1);
1726         sort($recipients2);
1727         if ($recipients1 == $recipients2)
1728                 return true;
1729         return false;
1730 }
1731
1732 // returns an array of contact-ids that are allowed to see this object
1733
1734 function enumerate_permissions($obj) {
1735         $allow_people = expand_acl($obj['allow_cid']);
1736         $allow_groups = expand_groups(expand_acl($obj['allow_gid']));
1737         $deny_people  = expand_acl($obj['deny_cid']);
1738         $deny_groups  = expand_groups(expand_acl($obj['deny_gid']));
1739         $recipients   = array_unique(array_merge($allow_people,$allow_groups));
1740         $deny         = array_unique(array_merge($deny_people,$deny_groups));
1741         $recipients   = array_diff($recipients,$deny);
1742         return $recipients;
1743 }
1744
1745 function item_getfeedtags($item) {
1746         $ret = array();
1747         $matches = false;
1748         $cnt = preg_match_all('|\#\[url\=(.*?)\](.*?)\[\/url\]|',$item['tag'],$matches);
1749         if ($cnt) {
1750                 for($x = 0; $x < $cnt; $x ++) {
1751                         if ($matches[1][$x])
1752                                 $ret[$matches[2][$x]] = array('#',$matches[1][$x], $matches[2][$x]);
1753                 }
1754         }
1755         $matches = false;
1756         $cnt = preg_match_all('|\@\[url\=(.*?)\](.*?)\[\/url\]|',$item['tag'],$matches);
1757         if ($cnt) {
1758                 for($x = 0; $x < $cnt; $x ++) {
1759                         if ($matches[1][$x])
1760                                 $ret[] = array('@',$matches[1][$x], $matches[2][$x]);
1761                 }
1762         }
1763         return $ret;
1764 }
1765
1766 function item_expire($uid, $days, $network = "", $force = false) {
1767
1768         if ((! $uid) || ($days < 1))
1769                 return;
1770
1771         // $expire_network_only = save your own wall posts
1772         // and just expire conversations started by others
1773
1774         $expire_network_only = get_pconfig($uid,'expire','network_only');
1775         $sql_extra = ((intval($expire_network_only)) ? " AND wall = 0 " : "");
1776
1777         if ($network != "") {
1778                 $sql_extra .= sprintf(" AND network = '%s' ", dbesc($network));
1779                 // There is an index "uid_network_received" but not "uid_network_created"
1780                 // This avoids the creation of another index just for one purpose.
1781                 // And it doesn't really matter wether to look at "received" or "created"
1782                 $range = "AND `received` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
1783         } else
1784                 $range = "AND `created` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
1785
1786         $r = q("SELECT `file`, `resource-id`, `starred`, `type`, `id` FROM `item`
1787                 WHERE `uid` = %d $range
1788                 AND `id` = `parent`
1789                 $sql_extra
1790                 AND `deleted` = 0",
1791                 intval($uid),
1792                 intval($days)
1793         );
1794
1795         if (! count($r))
1796                 return;
1797
1798         $expire_items = get_pconfig($uid, 'expire','items');
1799         $expire_items = (($expire_items===false)?1:intval($expire_items)); // default if not set: 1
1800
1801         // Forcing expiring of items - but not notes and marked items
1802         if ($force)
1803                 $expire_items = true;
1804
1805         $expire_notes = get_pconfig($uid, 'expire','notes');
1806         $expire_notes = (($expire_notes===false)?1:intval($expire_notes)); // default if not set: 1
1807
1808         $expire_starred = get_pconfig($uid, 'expire','starred');
1809         $expire_starred = (($expire_starred===false)?1:intval($expire_starred)); // default if not set: 1
1810
1811         $expire_photos = get_pconfig($uid, 'expire','photos');
1812         $expire_photos = (($expire_photos===false)?0:intval($expire_photos)); // default if not set: 0
1813
1814         logger('expire: # items=' . count($r). "; expire items: $expire_items, expire notes: $expire_notes, expire starred: $expire_starred, expire photos: $expire_photos");
1815
1816         foreach($r as $item) {
1817
1818                 // don't expire filed items
1819
1820                 if (strpos($item['file'],'[') !== false)
1821                         continue;
1822
1823                 // Only expire posts, not photos and photo comments
1824
1825                 if ($expire_photos==0 && strlen($item['resource-id']))
1826                         continue;
1827                 if ($expire_starred==0 && intval($item['starred']))
1828                         continue;
1829                 if ($expire_notes==0 && $item['type']=='note')
1830                         continue;
1831                 if ($expire_items==0 && $item['type']!='note')
1832                         continue;
1833
1834                 drop_item($item['id'],false);
1835         }
1836
1837         proc_run(PRIORITY_HIGH,"include/notifier.php", "expire", $uid);
1838
1839 }
1840
1841
1842 function drop_items($items) {
1843         $uid = 0;
1844
1845         if (! local_user() && ! remote_user())
1846                 return;
1847
1848         if (count($items)) {
1849                 foreach($items as $item) {
1850                         $owner = drop_item($item,false);
1851                         if ($owner && ! $uid)
1852                                 $uid = $owner;
1853                 }
1854         }
1855
1856         // multiple threads may have been deleted, send an expire notification
1857
1858         if ($uid)
1859                 proc_run(PRIORITY_HIGH,"include/notifier.php", "expire", $uid);
1860 }
1861
1862
1863 function drop_item($id,$interactive = true) {
1864
1865         $a = get_app();
1866
1867         // locate item to be deleted
1868
1869         $r = q("SELECT * FROM `item` WHERE `id` = %d LIMIT 1",
1870                 intval($id)
1871         );
1872
1873         if (! count($r)) {
1874                 if (! $interactive)
1875                         return 0;
1876                 notice( t('Item not found.') . EOL);
1877                 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
1878         }
1879
1880         $item = $r[0];
1881
1882         $owner = $item['uid'];
1883
1884         $cid = 0;
1885
1886         // check if logged in user is either the author or owner of this item
1887
1888         if (is_array($_SESSION['remote'])) {
1889                 foreach($_SESSION['remote'] as $visitor) {
1890                         if ($visitor['uid'] == $item['uid'] && $visitor['cid'] == $item['contact-id']) {
1891                                 $cid = $visitor['cid'];
1892                                 break;
1893                         }
1894                 }
1895         }
1896
1897
1898         if ((local_user() == $item['uid']) || ($cid) || (! $interactive)) {
1899
1900                 // Check if we should do HTML-based delete confirmation
1901                 if ($_REQUEST['confirm']) {
1902                         // <form> can't take arguments in its "action" parameter
1903                         // so add any arguments as hidden inputs
1904                         $query = explode_querystring($a->query_string);
1905                         $inputs = array();
1906                         foreach($query['args'] as $arg) {
1907                                 if (strpos($arg, 'confirm=') === false) {
1908                                         $arg_parts = explode('=', $arg);
1909                                         $inputs[] = array('name' => $arg_parts[0], 'value' => $arg_parts[1]);
1910                                 }
1911                         }
1912
1913                         return replace_macros(get_markup_template('confirm.tpl'), array(
1914                                 '$method' => 'get',
1915                                 '$message' => t('Do you really want to delete this item?'),
1916                                 '$extra_inputs' => $inputs,
1917                                 '$confirm' => t('Yes'),
1918                                 '$confirm_url' => $query['base'],
1919                                 '$confirm_name' => 'confirmed',
1920                                 '$cancel' => t('Cancel'),
1921                         ));
1922                 }
1923                 // Now check how the user responded to the confirmation query
1924                 if ($_REQUEST['canceled']) {
1925                         goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
1926                 }
1927
1928                 logger('delete item: ' . $item['id'], LOGGER_DEBUG);
1929                 // delete the item
1930
1931                 $r = q("UPDATE `item` SET `deleted` = 1, `title` = '', `body` = '', `edited` = '%s', `changed` = '%s' WHERE `id` = %d",
1932                         dbesc(datetime_convert()),
1933                         dbesc(datetime_convert()),
1934                         intval($item['id'])
1935                 );
1936                 create_tags_from_item($item['id']);
1937                 create_files_from_item($item['id']);
1938                 delete_thread($item['id'], $item['parent-uri']);
1939
1940                 // clean up categories and tags so they don't end up as orphans
1941
1942                 $matches = false;
1943                 $cnt = preg_match_all('/<(.*?)>/',$item['file'],$matches,PREG_SET_ORDER);
1944                 if ($cnt) {
1945                         foreach($matches as $mtch) {
1946                                 file_tag_unsave_file($item['uid'],$item['id'],$mtch[1],true);
1947                         }
1948                 }
1949
1950                 $matches = false;
1951
1952                 $cnt = preg_match_all('/\[(.*?)\]/',$item['file'],$matches,PREG_SET_ORDER);
1953                 if ($cnt) {
1954                         foreach($matches as $mtch) {
1955                                 file_tag_unsave_file($item['uid'],$item['id'],$mtch[1],false);
1956                         }
1957                 }
1958
1959                 // If item is a link to a photo resource, nuke all the associated photos
1960                 // (visitors will not have photo resources)
1961                 // This only applies to photos uploaded from the photos page. Photos inserted into a post do not
1962                 // generate a resource-id and therefore aren't intimately linked to the item.
1963
1964                 if (strlen($item['resource-id'])) {
1965                         q("DELETE FROM `photo` WHERE `resource-id` = '%s' AND `uid` = %d ",
1966                                 dbesc($item['resource-id']),
1967                                 intval($item['uid'])
1968                         );
1969                         // ignore the result
1970                 }
1971
1972                 // If item is a link to an event, nuke the event record.
1973
1974                 if (intval($item['event-id'])) {
1975                         q("DELETE FROM `event` WHERE `id` = %d AND `uid` = %d",
1976                                 intval($item['event-id']),
1977                                 intval($item['uid'])
1978                         );
1979                         // ignore the result
1980                 }
1981
1982                 // If item has attachments, drop them
1983
1984                 foreach(explode(",",$item['attach']) as $attach){
1985                         preg_match("|attach/(\d+)|", $attach, $matches);
1986                         q("DELETE FROM `attach` WHERE `id` = %d AND `uid` = %d",
1987                                 intval($matches[1]),
1988                                 local_user()
1989                         );
1990                         // ignore the result
1991                 }
1992
1993
1994                 // clean up item_id and sign meta-data tables
1995
1996                 /*
1997                 // Old code - caused very long queries and warning entries in the mysql logfiles:
1998
1999                 $r = q("DELETE FROM item_id where iid in (select id from item where parent = %d and uid = %d)",
2000                         intval($item['id']),
2001                         intval($item['uid'])
2002                 );
2003
2004                 $r = q("DELETE FROM sign where iid in (select id from item where parent = %d and uid = %d)",
2005                         intval($item['id']),
2006                         intval($item['uid'])
2007                 );
2008                 */
2009
2010                 // The new code splits the queries since the mysql optimizer really has bad problems with subqueries
2011
2012                 // Creating list of parents
2013                 $r = q("select id from item where parent = %d and uid = %d",
2014                         intval($item['id']),
2015                         intval($item['uid'])
2016                 );
2017
2018                 $parentid = "";
2019
2020                 foreach ($r AS $row) {
2021                         if ($parentid != "")
2022                                 $parentid .= ", ";
2023
2024                         $parentid .= $row["id"];
2025                 }
2026
2027                 // Now delete them
2028                 if ($parentid != "") {
2029                         $r = q("DELETE FROM item_id where iid in (%s)", dbesc($parentid));
2030
2031                         $r = q("DELETE FROM sign where iid in (%s)", dbesc($parentid));
2032                 }
2033
2034                 // If it's the parent of a comment thread, kill all the kids
2035
2036                 if ($item['uri'] == $item['parent-uri']) {
2037                         $r = q("UPDATE `item` SET `deleted` = 1, `edited` = '%s', `changed` = '%s', `body` = '' , `title` = ''
2038                                 WHERE `parent-uri` = '%s' AND `uid` = %d ",
2039                                 dbesc(datetime_convert()),
2040                                 dbesc(datetime_convert()),
2041                                 dbesc($item['parent-uri']),
2042                                 intval($item['uid'])
2043                         );
2044                         create_tags_from_itemuri($item['parent-uri'], $item['uid']);
2045                         create_files_from_itemuri($item['parent-uri'], $item['uid']);
2046                         delete_thread_uri($item['parent-uri'], $item['uid']);
2047                         // ignore the result
2048                 } else {
2049                         // ensure that last-child is set in case the comment that had it just got wiped.
2050                         q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `parent-uri` = '%s' AND `uid` = %d ",
2051                                 dbesc(datetime_convert()),
2052                                 dbesc($item['parent-uri']),
2053                                 intval($item['uid'])
2054                         );
2055                         // who is the last child now?
2056                         $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",
2057                                 dbesc($item['parent-uri']),
2058                                 intval($item['uid'])
2059                         );
2060                         if (count($r)) {
2061                                 q("UPDATE `item` SET `last-child` = 1 WHERE `id` = %d",
2062                                         intval($r[0]['id'])
2063                                 );
2064                         }
2065                 }
2066
2067                 $drop_id = intval($item['id']);
2068
2069                 // send the notification upstream/downstream as the case may be
2070
2071                 proc_run(PRIORITY_HIGH,"include/notifier.php", "drop", $drop_id);
2072
2073                 if (! $interactive)
2074                         return $owner;
2075                 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
2076                 //NOTREACHED
2077         } else {
2078                 if (! $interactive)
2079                         return 0;
2080                 notice( t('Permission denied.') . EOL);
2081                 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
2082                 //NOTREACHED
2083         }
2084
2085 }
2086
2087
2088 function first_post_date($uid,$wall = false) {
2089         $r = q("select id, created from item
2090                 where uid = %d and wall = %d and deleted = 0 and visible = 1 AND moderated = 0
2091                 and id = parent
2092                 order by created asc limit 1",
2093                 intval($uid),
2094                 intval($wall ? 1 : 0)
2095         );
2096         if (count($r)) {
2097 //              logger('first_post_date: ' . $r[0]['id'] . ' ' . $r[0]['created'], LOGGER_DATA);
2098                 return substr(datetime_convert('',date_default_timezone_get(),$r[0]['created']),0,10);
2099         }
2100         return false;
2101 }
2102
2103 /* modified posted_dates() {below} to arrange the list in years */
2104 function list_post_dates($uid, $wall) {
2105         $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
2106
2107         $dthen = first_post_date($uid, $wall);
2108         if (! $dthen)
2109                 return array();
2110
2111         // Set the start and end date to the beginning of the month
2112         $dnow = substr($dnow,0,8).'01';
2113         $dthen = substr($dthen,0,8).'01';
2114
2115         $ret = array();
2116
2117         // Starting with the current month, get the first and last days of every
2118         // month down to and including the month of the first post
2119         while(substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
2120                 $dyear = intval(substr($dnow,0,4));
2121                 $dstart = substr($dnow,0,8) . '01';
2122                 $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
2123                 $start_month = datetime_convert('','',$dstart,'Y-m-d');
2124                 $end_month = datetime_convert('','',$dend,'Y-m-d');
2125                 $str = day_translate(datetime_convert('','',$dnow,'F'));
2126                 if (! $ret[$dyear])
2127                         $ret[$dyear] = array();
2128                 $ret[$dyear][] = array($str,$end_month,$start_month);
2129                 $dnow = datetime_convert('','',$dnow . ' -1 month', 'Y-m-d');
2130         }
2131         return $ret;
2132 }
2133
2134 function posted_dates($uid,$wall) {
2135         $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
2136
2137         $dthen = first_post_date($uid,$wall);
2138         if (! $dthen)
2139                 return array();
2140
2141         // Set the start and end date to the beginning of the month
2142         $dnow = substr($dnow,0,8).'01';
2143         $dthen = substr($dthen,0,8).'01';
2144
2145         $ret = array();
2146         // Starting with the current month, get the first and last days of every
2147         // month down to and including the month of the first post
2148         while(substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
2149                 $dstart = substr($dnow,0,8) . '01';
2150                 $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
2151                 $start_month = datetime_convert('','',$dstart,'Y-m-d');
2152                 $end_month = datetime_convert('','',$dend,'Y-m-d');
2153                 $str = day_translate(datetime_convert('','',$dnow,'F Y'));
2154                 $ret[] = array($str,$end_month,$start_month);
2155                 $dnow = datetime_convert('','',$dnow . ' -1 month', 'Y-m-d');
2156         }
2157         return $ret;
2158 }
2159
2160
2161 function posted_date_widget($url,$uid,$wall) {
2162         $o = '';
2163
2164         if (! feature_enabled($uid,'archives'))
2165                 return $o;
2166
2167         // For former Facebook folks that left because of "timeline"
2168
2169 /*      if ($wall && intval(get_pconfig($uid,'system','no_wall_archive_widget')))
2170                 return $o;*/
2171
2172         $visible_years = get_pconfig($uid,'system','archive_visible_years');
2173         if (! $visible_years)
2174                 $visible_years = 5;
2175
2176         $ret = list_post_dates($uid,$wall);
2177
2178         if (! count($ret))
2179                 return $o;
2180
2181         $cutoff_year = intval(datetime_convert('',date_default_timezone_get(),'now','Y')) - $visible_years;
2182         $cutoff = ((array_key_exists($cutoff_year,$ret))? true : false);
2183
2184         $o = replace_macros(get_markup_template('posted_date_widget.tpl'),array(
2185                 '$title' => t('Archives'),
2186                 '$size' => $visible_years,
2187                 '$cutoff_year' => $cutoff_year,
2188                 '$cutoff' => $cutoff,
2189                 '$url' => $url,
2190                 '$dates' => $ret,
2191                 '$showmore' => t('show more')
2192
2193         ));
2194         return $o;
2195 }