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