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