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');
23 require_once('library/defuse/php-encryption-1.2.1/Crypto.php');
25 function construct_verb($item) {
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
36 if(! function_exists('limit_body_size')) {
37 function limit_body_size($body) {
39 // logger('limit_body_size: start', LOGGER_DEBUG);
41 $maxlen = get_max_import_size();
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)) {
47 logger('limit_body_size: the total body length exceeds the limit', LOGGER_DEBUG);
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)) {
59 $img_st_close++; // make it point to AFTER the closing bracket
60 $img_end += $img_start;
61 $img_end += strlen('[/img]');
63 if(! strcmp(substr($orig_body, $img_start + $img_st_close, 5), 'data:')) {
64 // This is an embedded image
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);
74 $new_body = $new_body . substr($orig_body, 0, $img_start);
75 $textlen += $img_start;
78 $new_body = $new_body . substr($orig_body, $img_start, $img_end - $img_start);
82 if( ($textlen + $img_end) > $maxlen ) {
83 if($textlen < $maxlen) {
84 logger('limit_body_size: the limit happens before the end of a non-embedded image', LOGGER_DEBUG);
85 $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
90 $new_body = $new_body . substr($orig_body, 0, $img_end);
94 $orig_body = substr($orig_body, $img_end);
96 if($orig_body === false) // in case the body ends on a closing image tag
99 $img_start = strpos($orig_body, '[img');
100 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
101 $img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
104 if( ($textlen + strlen($orig_body)) > $maxlen) {
105 if($textlen < $maxlen) {
106 logger('limit_body_size: the limit happens after the end of the last image', LOGGER_DEBUG);
107 $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
112 logger('limit_body_size: the text size with embedded images extracted did not violate the limit', LOGGER_DEBUG);
113 $new_body = $new_body . $orig_body;
114 $textlen += strlen($orig_body);
123 function title_is_body($title, $body) {
125 $title = strip_tags($title);
126 $title = trim($title);
127 $title = html_entity_decode($title, ENT_QUOTES, 'UTF-8');
128 $title = str_replace(array("\n", "\r", "\t", " "), array("","","",""), $title);
130 $body = strip_tags($body);
132 $body = html_entity_decode($body, ENT_QUOTES, 'UTF-8');
133 $body = str_replace(array("\n", "\r", "\t", " "), array("","","",""), $body);
135 if (strlen($title) < strlen($body))
136 $body = substr($body, 0, strlen($title));
138 if (($title != $body) and (substr($title, -3) == "...")) {
139 $pos = strrpos($title, "...");
141 $title = substr($title, 0, $pos);
142 $body = substr($body, 0, $pos);
146 return($title == $body);
149 function add_page_info_data($data) {
150 call_hooks('page_info_data', $data);
152 // It maybe is a rich content, but if it does have everything that a link has,
153 // then treat it that way
154 if (($data["type"] == "rich") AND is_string($data["title"]) AND
155 is_string($data["text"]) AND (sizeof($data["images"]) > 0))
156 $data["type"] = "link";
158 if ((($data["type"] != "link") AND ($data["type"] != "video") AND ($data["type"] != "photo")) OR ($data["title"] == $url))
161 if ($no_photos AND ($data["type"] == "photo"))
164 if (sizeof($data["images"]) > 0)
165 $preview = $data["images"][0];
169 // Escape some bad characters
170 $data["url"] = str_replace(array("[", "]"), array("[", "]"), htmlentities($data["url"], ENT_QUOTES, 'UTF-8', false));
171 $data["title"] = str_replace(array("[", "]"), array("[", "]"), htmlentities($data["title"], ENT_QUOTES, 'UTF-8', false));
173 $text = "[attachment type='".$data["type"]."'";
175 if ($data["url"] != "")
176 $text .= " url='".$data["url"]."'";
177 if ($data["title"] != "")
178 $text .= " title='".$data["title"]."'";
179 if (sizeof($data["images"]) > 0) {
180 $preview = str_replace(array("[", "]"), array("[", "]"), htmlentities($data["images"][0]["src"], ENT_QUOTES, 'UTF-8', false));
181 // if the preview picture is larger than 500 pixels then show it in a larger mode
182 // But only, if the picture isn't higher than large (To prevent huge posts)
183 if (($data["images"][0]["width"] >= 500) AND ($data["images"][0]["width"] >= $data["images"][0]["height"]))
184 $text .= " image='".$preview."'";
186 $text .= " preview='".$preview."'";
188 $text .= "]".$data["text"]."[/attachment]";
191 if (isset($data["keywords"]) AND count($data["keywords"])) {
194 foreach ($data["keywords"] AS $keyword) {
195 /// @todo make a positive list of allowed characters
196 $hashtag = str_replace(array(" ", "+", "/", ".", "#", "'", "’", "`", "(", ")", "„", "“"),
197 array("","", "", "", "", "", "", "", "", "", "", ""), $keyword);
198 $hashtags .= "#[url=".$a->get_baseurl()."/search?tag=".rawurlencode($hashtag)."]".$hashtag."[/url] ";
202 return "\n".$text.$hashtags;
205 function query_page_info($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
206 require_once("mod/parse_url.php");
208 $data = parseurl_getsiteinfo_cached($url, true);
211 $data["images"][0]["src"] = $photo;
213 logger('fetch page info for '.$url.' '.print_r($data, true), LOGGER_DEBUG);
215 if (!$keywords AND isset($data["keywords"]))
216 unset($data["keywords"]);
218 if (($keyword_blacklist != "") AND isset($data["keywords"])) {
219 $list = explode(",", $keyword_blacklist);
220 foreach ($list AS $keyword) {
221 $keyword = trim($keyword);
222 $index = array_search($keyword, $data["keywords"]);
223 if ($index !== false)
224 unset($data["keywords"][$index]);
231 function add_page_keywords($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
232 $data = query_page_info($url, $no_photos, $photo, $keywords, $keyword_blacklist);
235 if (isset($data["keywords"]) AND count($data["keywords"])) {
237 foreach ($data["keywords"] AS $keyword) {
238 $hashtag = str_replace(array(" ", "+", "/", ".", "#", "'"),
239 array("","", "", "", "", ""), $keyword);
244 $tags .= "#[url=".$a->get_baseurl()."/search?tag=".rawurlencode($hashtag)."]".$hashtag."[/url]";
251 function add_page_info($url, $no_photos = false, $photo = "", $keywords = false, $keyword_blacklist = "") {
252 $data = query_page_info($url, $no_photos, $photo, $keywords, $keyword_blacklist);
254 $text = add_page_info_data($data);
259 function add_page_info_to_body($body, $texturl = false, $no_photos = false) {
261 logger('add_page_info_to_body: fetch page info for body '.$body, LOGGER_DEBUG);
263 $URLSearchString = "^\[\]";
265 // Adding these spaces is a quick hack due to my problems with regular expressions :)
266 preg_match("/[^!#@]\[url\]([$URLSearchString]*)\[\/url\]/ism", " ".$body, $matches);
269 preg_match("/[^!#@]\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", " ".$body, $matches);
271 // Convert urls without bbcode elements
272 if (!$matches AND $texturl) {
273 preg_match("/([^\]\='".'"'."]|^)(https?\:\/\/[a-zA-Z0-9\:\/\-\?\&\;\.\=\_\~\#\%\$\!\+\,]+)/ism", " ".$body, $matches);
275 // Yeah, a hack. I really hate regular expressions :)
277 $matches[1] = $matches[2];
281 $footer = add_page_info($matches[1], $no_photos);
283 // Remove the link from the body if the link is attached at the end of the post
284 if (isset($footer) AND (trim($footer) != "") AND (strpos($footer, $matches[1]))) {
285 $removedlink = trim(str_replace($matches[1], "", $body));
286 if (($removedlink == "") OR strstr($body, $removedlink))
287 $body = $removedlink;
289 $url = str_replace(array('/', '.'), array('\/', '\.'), $matches[1]);
290 $removedlink = preg_replace("/\[url\=".$url."\](.*?)\[\/url\]/ism", '', $body);
291 if (($removedlink == "") OR strstr($body, $removedlink))
292 $body = $removedlink;
295 // Add the page information to the bottom
296 if (isset($footer) AND (trim($footer) != ""))
303 * Adds a "lang" specification in a "postopts" element of given $arr,
304 * if possible and not already present.
305 * Expects "body" element to exist in $arr.
307 * @todo Add a parameter to request forcing override
309 function item_add_language_opt(&$arr) {
311 if (version_compare(PHP_VERSION, '5.3.0', '<')) return; // LanguageDetect.php not available ?
313 if ( x($arr, 'postopts') )
315 if ( strstr($arr['postopts'], 'lang=') )
318 /// @TODO Add parameter to request overriding
321 $postopts = $arr['postopts'];
328 require_once('library/langdet/Text/LanguageDetect.php');
329 $naked_body = preg_replace('/\[(.+?)\]/','',$arr['body']);
330 $l = new Text_LanguageDetect;
331 //$lng = $l->detectConfidence($naked_body);
332 //$arr['postopts'] = (($lng['language']) ? 'lang=' . $lng['language'] . ';' . $lng['confidence'] : '');
333 $lng = $l->detect($naked_body, 3);
335 if (sizeof($lng) > 0) {
336 if ($postopts != "") $postopts .= '&'; // arbitrary separator, to be reviewed
337 $postopts .= 'lang=';
339 foreach ($lng as $language => $score) {
340 $postopts .= $sep . $language.";".$score;
343 $arr['postopts'] = $postopts;
348 * @brief Creates an unique guid out of a given uri
350 * @param string $uri uri of an item entry
351 * @return string unique guid
353 function uri_to_guid($uri) {
355 // Our regular guid routine is using this kind of prefix as well
356 // We have to avoid that different routines could accidentally create the same value
357 $parsed = parse_url($uri);
358 $guid_prefix = hash("crc32", $parsed["host"]);
360 // Remove the scheme to make sure that "https" and "http" doesn't make a difference
361 unset($parsed["scheme"]);
363 $host_id = implode("/", $parsed);
365 // We could use any hash algorithm since it isn't a security issue
366 $host_hash = hash("ripemd128", $host_id);
368 return $guid_prefix.$host_hash;
371 function item_store($arr,$force_parent = false, $notify = false, $dontcache = false) {
373 // If it is a posting where users should get notifications, then define it as wall posting
376 $arr['type'] = 'wall';
378 $arr['last-child'] = 1;
379 $arr['network'] = NETWORK_DFRN;
382 // If a Diaspora signature structure was passed in, pull it out of the
383 // item array and set it aside for later storage.
386 if(x($arr,'dsprsig')) {
387 $dsprsig = json_decode(base64_decode($arr['dsprsig']));
388 unset($arr['dsprsig']);
391 // Converting the plink
392 if ($arr['network'] == NETWORK_OSTATUS) {
393 if (isset($arr['plink']))
394 $arr['plink'] = ostatus::convert_href($arr['plink']);
395 elseif (isset($arr['uri']))
396 $arr['plink'] = ostatus::convert_href($arr['uri']);
399 if(x($arr, 'gravity'))
400 $arr['gravity'] = intval($arr['gravity']);
401 elseif($arr['parent-uri'] === $arr['uri'])
403 elseif(activity_match($arr['verb'],ACTIVITY_POST))
406 $arr['gravity'] = 6; // extensible catchall
409 $arr['type'] = 'remote';
413 /* check for create date and expire time */
414 $uid = intval($arr['uid']);
415 $r = q("SELECT expire FROM user WHERE uid = %d", intval($uid));
417 $expire_interval = $r[0]['expire'];
418 if ($expire_interval>0) {
419 $expire_date = new DateTime( '- '.$expire_interval.' days', new DateTimeZone('UTC'));
420 $created_date = new DateTime($arr['created'], new DateTimeZone('UTC'));
421 if ($created_date < $expire_date) {
422 logger('item-store: item created ('.$arr['created'].') before expiration time ('.$expire_date->format(DateTime::W3C).'). ignored. ' . print_r($arr,true), LOGGER_DEBUG);
428 // Do we already have this item?
429 // We have to check several networks since Friendica posts could be repeated via OStatus (maybe Diasporsa as well)
430 if (in_array(trim($arr['network']), array(NETWORK_DIASPORA, NETWORK_DFRN, NETWORK_OSTATUS, ""))) {
431 $r = q("SELECT `id`, `network` FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` IN ('%s', '%s', '%s') LIMIT 1",
432 dbesc(trim($arr['uri'])),
434 dbesc(NETWORK_DIASPORA),
436 dbesc(NETWORK_OSTATUS)
439 // We only log the entries with a different user id than 0. Otherwise we would have too many false positives
441 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']);
446 // Shouldn't happen but we want to make absolutely sure it doesn't leak from a plugin.
447 // Deactivated, since the bbcode parser can handle with it - and it destroys posts with some smileys that contain "<"
448 //if((strpos($arr['body'],'<') !== false) || (strpos($arr['body'],'>') !== false))
449 // $arr['body'] = strip_tags($arr['body']);
451 item_add_language_opt($arr);
455 elseif ((trim($arr['guid']) == "") AND (trim($arr['plink']) != ""))
456 $arr['guid'] = uri_to_guid($arr['plink']);
457 elseif ((trim($arr['guid']) == "") AND (trim($arr['uri']) != ""))
458 $arr['guid'] = uri_to_guid($arr['uri']);
460 $parsed = parse_url($arr["author-link"]);
461 $guid_prefix = hash("crc32", $parsed["host"]);
464 $arr['wall'] = ((x($arr,'wall')) ? intval($arr['wall']) : 0);
465 $arr['guid'] = ((x($arr,'guid')) ? notags(trim($arr['guid'])) : get_guid(32, $guid_prefix));
466 $arr['uri'] = ((x($arr,'uri')) ? notags(trim($arr['uri'])) : $arr['guid']);
467 $arr['extid'] = ((x($arr,'extid')) ? notags(trim($arr['extid'])) : '');
468 $arr['author-name'] = ((x($arr,'author-name')) ? trim($arr['author-name']) : '');
469 $arr['author-link'] = ((x($arr,'author-link')) ? notags(trim($arr['author-link'])) : '');
470 $arr['author-avatar'] = ((x($arr,'author-avatar')) ? notags(trim($arr['author-avatar'])) : '');
471 $arr['owner-name'] = ((x($arr,'owner-name')) ? trim($arr['owner-name']) : '');
472 $arr['owner-link'] = ((x($arr,'owner-link')) ? notags(trim($arr['owner-link'])) : '');
473 $arr['owner-avatar'] = ((x($arr,'owner-avatar')) ? notags(trim($arr['owner-avatar'])) : '');
474 $arr['created'] = ((x($arr,'created') !== false) ? datetime_convert('UTC','UTC',$arr['created']) : datetime_convert());
475 $arr['edited'] = ((x($arr,'edited') !== false) ? datetime_convert('UTC','UTC',$arr['edited']) : datetime_convert());
476 $arr['commented'] = ((x($arr,'commented') !== false) ? datetime_convert('UTC','UTC',$arr['commented']) : datetime_convert());
477 $arr['received'] = ((x($arr,'received') !== false) ? datetime_convert('UTC','UTC',$arr['received']) : datetime_convert());
478 $arr['changed'] = ((x($arr,'changed') !== false) ? datetime_convert('UTC','UTC',$arr['changed']) : datetime_convert());
479 $arr['title'] = ((x($arr,'title')) ? trim($arr['title']) : '');
480 $arr['location'] = ((x($arr,'location')) ? trim($arr['location']) : '');
481 $arr['coord'] = ((x($arr,'coord')) ? notags(trim($arr['coord'])) : '');
482 $arr['last-child'] = ((x($arr,'last-child')) ? intval($arr['last-child']) : 0 );
483 $arr['visible'] = ((x($arr,'visible') !== false) ? intval($arr['visible']) : 1 );
485 $arr['parent-uri'] = ((x($arr,'parent-uri')) ? notags(trim($arr['parent-uri'])) : '');
486 $arr['verb'] = ((x($arr,'verb')) ? notags(trim($arr['verb'])) : '');
487 $arr['object-type'] = ((x($arr,'object-type')) ? notags(trim($arr['object-type'])) : '');
488 $arr['object'] = ((x($arr,'object')) ? trim($arr['object']) : '');
489 $arr['target-type'] = ((x($arr,'target-type')) ? notags(trim($arr['target-type'])) : '');
490 $arr['target'] = ((x($arr,'target')) ? trim($arr['target']) : '');
491 $arr['plink'] = ((x($arr,'plink')) ? notags(trim($arr['plink'])) : '');
492 $arr['allow_cid'] = ((x($arr,'allow_cid')) ? trim($arr['allow_cid']) : '');
493 $arr['allow_gid'] = ((x($arr,'allow_gid')) ? trim($arr['allow_gid']) : '');
494 $arr['deny_cid'] = ((x($arr,'deny_cid')) ? trim($arr['deny_cid']) : '');
495 $arr['deny_gid'] = ((x($arr,'deny_gid')) ? trim($arr['deny_gid']) : '');
496 $arr['private'] = ((x($arr,'private')) ? intval($arr['private']) : 0 );
497 $arr['bookmark'] = ((x($arr,'bookmark')) ? intval($arr['bookmark']) : 0 );
498 $arr['body'] = ((x($arr,'body')) ? trim($arr['body']) : '');
499 $arr['tag'] = ((x($arr,'tag')) ? notags(trim($arr['tag'])) : '');
500 $arr['attach'] = ((x($arr,'attach')) ? notags(trim($arr['attach'])) : '');
501 $arr['app'] = ((x($arr,'app')) ? notags(trim($arr['app'])) : '');
502 $arr['origin'] = ((x($arr,'origin')) ? intval($arr['origin']) : 0 );
503 $arr['network'] = ((x($arr,'network')) ? trim($arr['network']) : '');
504 $arr['postopts'] = ((x($arr,'postopts')) ? trim($arr['postopts']) : '');
505 $arr['resource-id'] = ((x($arr,'resource-id')) ? trim($arr['resource-id']) : '');
506 $arr['event-id'] = ((x($arr,'event-id')) ? intval($arr['event-id']) : 0 );
507 $arr['inform'] = ((x($arr,'inform')) ? trim($arr['inform']) : '');
508 $arr['file'] = ((x($arr,'file')) ? trim($arr['file']) : '');
511 if (($arr['author-link'] == "") AND ($arr['owner-link'] == ""))
512 logger("Both author-link and owner-link are empty. Called by: ".App::callstack(), LOGGER_DEBUG);
514 if ($arr['plink'] == "") {
516 $arr['plink'] = $a->get_baseurl().'/display/'.urlencode($arr['guid']);
519 if ($arr['network'] == "") {
520 $r = q("SELECT `network` FROM `contact` WHERE `network` IN ('%s', '%s', '%s') AND `nurl` = '%s' AND `uid` = %d LIMIT 1",
521 dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS),
522 dbesc(normalise_link($arr['author-link'])),
527 $r = q("SELECT `network` FROM `gcontact` WHERE `network` IN ('%s', '%s', '%s') AND `nurl` = '%s' LIMIT 1",
528 dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS),
529 dbesc(normalise_link($arr['author-link']))
533 $r = q("SELECT `network` FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
534 intval($arr['contact-id']),
539 $arr['network'] = $r[0]["network"];
541 // Fallback to friendica (why is it empty in some cases?)
542 if ($arr['network'] == "")
543 $arr['network'] = NETWORK_DFRN;
545 logger("item_store: Set network to ".$arr["network"]." for ".$arr["uri"], LOGGER_DEBUG);
548 // The contact-id should be set before "item_store" was called - but there seems to be some issues
549 if ($arr["contact-id"] == 0) {
550 // First we are looking for a suitable contact that matches with the author of the post
551 // This is done only for comments (See below explanation at "gcontact-id")
552 if($arr['parent-uri'] != $arr['uri'])
553 $arr["contact-id"] = get_contact($arr['author-link'], $uid);
555 // If not present then maybe the owner was found
556 if ($arr["contact-id"] == 0)
557 $arr["contact-id"] = get_contact($arr['owner-link'], $uid);
559 // Still missing? Then use the "self" contact of the current user
560 if ($arr["contact-id"] == 0) {
561 $r = q("SELECT `id` FROM `contact` WHERE `self` AND `uid` = %d", intval($uid));
563 $arr["contact-id"] = $r[0]["id"];
565 logger("Contact-id was missing for post ".$arr["guid"]." from user id ".$uid." - now set to ".$arr["contact-id"], LOGGER_DEBUG);
568 if ($arr["gcontact-id"] == 0) {
569 // The gcontact should mostly behave like the contact. But is is supposed to be global for the system.
570 // This means that wall posts, repeated posts, etc. should have the gcontact id of the owner.
571 // On comments the author is the better choice.
572 if($arr['parent-uri'] === $arr['uri'])
573 $arr["gcontact-id"] = get_gcontact_id(array("url" => $arr['owner-link'], "network" => $arr['network'],
574 "photo" => $arr['owner-avatar'], "name" => $arr['owner-name']));
576 $arr["gcontact-id"] = get_gcontact_id(array("url" => $arr['author-link'], "network" => $arr['network'],
577 "photo" => $arr['author-avatar'], "name" => $arr['author-name']));
580 if ($arr['guid'] != "") {
581 // Checking if there is already an item with the same guid
582 logger('checking for an item for user '.$arr['uid'].' on network '.$arr['network'].' with the guid '.$arr['guid'], LOGGER_DEBUG);
583 $r = q("SELECT `guid` FROM `item` WHERE `guid` = '%s' AND `network` = '%s' AND `uid` = '%d' LIMIT 1",
584 dbesc($arr['guid']), dbesc($arr['network']), intval($arr['uid']));
587 logger('found item with guid '.$arr['guid'].' for user '.$arr['uid'].' on network '.$arr['network'], LOGGER_DEBUG);
592 // Check for hashtags in the body and repair or add hashtag links
593 item_body_set_hashtags($arr);
595 $arr['thr-parent'] = $arr['parent-uri'];
596 if($arr['parent-uri'] === $arr['uri']) {
599 $allow_cid = $arr['allow_cid'];
600 $allow_gid = $arr['allow_gid'];
601 $deny_cid = $arr['deny_cid'];
602 $deny_gid = $arr['deny_gid'];
603 $notify_type = 'wall-new';
607 // find the parent and snarf the item id and ACLs
608 // and anything else we need to inherit
610 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d ORDER BY `id` ASC LIMIT 1",
611 dbesc($arr['parent-uri']),
617 // is the new message multi-level threaded?
618 // even though we don't support it now, preserve the info
619 // and re-attach to the conversation parent.
621 if($r[0]['uri'] != $r[0]['parent-uri']) {
622 $arr['parent-uri'] = $r[0]['parent-uri'];
623 $z = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `parent-uri` = '%s' AND `uid` = %d
624 ORDER BY `id` ASC LIMIT 1",
625 dbesc($r[0]['parent-uri']),
626 dbesc($r[0]['parent-uri']),
633 $parent_id = $r[0]['id'];
634 $parent_deleted = $r[0]['deleted'];
635 $allow_cid = $r[0]['allow_cid'];
636 $allow_gid = $r[0]['allow_gid'];
637 $deny_cid = $r[0]['deny_cid'];
638 $deny_gid = $r[0]['deny_gid'];
639 $arr['wall'] = $r[0]['wall'];
640 $notify_type = 'comment-new';
642 // if the parent is private, force privacy for the entire conversation
643 // This differs from the above settings as it subtly allows comments from
644 // email correspondents to be private even if the overall thread is not.
647 $arr['private'] = $r[0]['private'];
649 // Edge case. We host a public forum that was originally posted to privately.
650 // The original author commented, but as this is a comment, the permissions
651 // weren't fixed up so it will still show the comment as private unless we fix it here.
653 if((intval($r[0]['forum_mode']) == 1) && (! $r[0]['private']))
657 // If its a post from myself then tag the thread as "mention"
658 logger("item_store: Checking if parent ".$parent_id." has to be tagged as mention for user ".$arr['uid'], LOGGER_DEBUG);
659 $u = q("select * from user where uid = %d limit 1", intval($arr['uid']));
662 $self = normalise_link($a->get_baseurl() . '/profile/' . $u[0]['nickname']);
663 logger("item_store: 'myself' is ".$self." for parent ".$parent_id." checking against ".$arr['author-link']." and ".$arr['owner-link'], LOGGER_DEBUG);
664 if ((normalise_link($arr['author-link']) == $self) OR (normalise_link($arr['owner-link']) == $self)) {
665 q("UPDATE `thread` SET `mention` = 1 WHERE `iid` = %d", intval($parent_id));
666 logger("item_store: tagged thread ".$parent_id." as mention for user ".$self, LOGGER_DEBUG);
672 // Allow one to see reply tweets from status.net even when
673 // we don't have or can't see the original post.
676 logger('item_store: $force_parent=true, reply converted to top-level post.');
678 $arr['parent-uri'] = $arr['uri'];
682 logger('item_store: item parent '.$arr['parent-uri'].' for '.$arr['uid'].' was not found - ignoring item');
690 $r = q("SELECT `id` FROM `item` WHERE `uri` = '%s' AND `network` IN ('%s', '%s') AND `uid` = %d LIMIT 1",
692 dbesc($arr['network']),
696 if($r && count($r)) {
697 logger('duplicated item with the same uri found. ' . print_r($arr,true));
701 // Check for an existing post with the same content. There seems to be a problem with OStatus.
702 $r = q("SELECT `id` FROM `item` WHERE `body` = '%s' AND `network` = '%s' AND `created` = '%s' AND `contact-id` = %d AND `uid` = %d LIMIT 1",
704 dbesc($arr['network']),
705 dbesc($arr['created']),
706 intval($arr['contact-id']),
709 if($r && count($r)) {
710 logger('duplicated item with the same body found. ' . print_r($arr,true));
714 // Is this item available in the global items (with uid=0)?
715 if ($arr["uid"] == 0) {
716 $arr["global"] = true;
718 q("UPDATE `item` SET `global` = 1 WHERE `uri` = '%s'", dbesc($arr["uri"]));
720 $isglobal = q("SELECT `global` FROM `item` WHERE `uid` = 0 AND `uri` = '%s'", dbesc($arr["uri"]));
722 $arr["global"] = (count($isglobal) > 0);
725 // Fill the cache field
726 put_item_in_cache($arr);
729 call_hooks('post_local',$arr);
731 call_hooks('post_remote',$arr);
733 if(x($arr,'cancel')) {
734 logger('item_store: post cancelled by plugin.');
738 // Store the unescaped version
743 logger('item_store: ' . print_r($arr,true), LOGGER_DATA);
745 $r = dbq("INSERT INTO `item` (`"
746 . implode("`, `", array_keys($arr))
748 . implode("', '", array_values($arr))
754 // find the item that we just created
755 $r = q("SELECT `id` FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` = '%s' ORDER BY `id` ASC",
758 dbesc($arr['network'])
762 // There are duplicates. Keep the oldest one, delete the others
763 logger('item_store: duplicated post occurred. Removing newer duplicates. uri = '.$arr['uri'].' uid = '.$arr['uid']);
764 q("DELETE FROM `item` WHERE `uri` = '%s' AND `uid` = %d AND `network` = '%s' AND `id` > %d",
767 dbesc($arr['network']),
771 } elseif(count($r)) {
773 $current_post = $r[0]['id'];
774 logger('item_store: created item ' . $current_post);
776 // Set "success_update" and "last-item" to the date of the last time we heard from this contact
777 // This can be used to filter for inactive contacts.
778 // Only do this for public postings to avoid privacy problems, since poco data is public.
779 // Don't set this value if it isn't from the owner (could be an author that we don't know)
781 $update = (!$arr['private'] AND (($arr["author-link"] === $arr["owner-link"]) OR ($arr["parent-uri"] === $arr["uri"])));
783 // Is it a forum? Then we don't care about the rules from above
784 if (!$update AND ($arr["network"] == NETWORK_DFRN) AND ($arr["parent-uri"] === $arr["uri"])) {
785 $isforum = q("SELECT `forum` FROM `contact` WHERE `id` = %d AND `forum`",
786 intval($arr['contact-id']));
792 q("UPDATE `contact` SET `success_update` = '%s', `last-item` = '%s' WHERE `id` = %d",
793 dbesc($arr['received']),
794 dbesc($arr['received']),
795 intval($arr['contact-id'])
798 logger('item_store: could not locate created item');
802 if((! $parent_id) || ($arr['parent-uri'] === $arr['uri']))
803 $parent_id = $current_post;
805 if(strlen($allow_cid) || strlen($allow_gid) || strlen($deny_cid) || strlen($deny_gid))
808 $private = $arr['private'];
810 // Set parent id - and also make sure to inherit the parent's ACLs.
812 $r = q("UPDATE `item` SET `parent` = %d, `allow_cid` = '%s', `allow_gid` = '%s',
813 `deny_cid` = '%s', `deny_gid` = '%s', `private` = %d, `deleted` = %d WHERE `id` = %d",
820 intval($parent_deleted),
821 intval($current_post)
824 $arr['id'] = $current_post;
825 $arr['parent'] = $parent_id;
826 $arr['allow_cid'] = $allow_cid;
827 $arr['allow_gid'] = $allow_gid;
828 $arr['deny_cid'] = $deny_cid;
829 $arr['deny_gid'] = $deny_gid;
830 $arr['private'] = $private;
831 $arr['deleted'] = $parent_deleted;
833 // update the commented timestamp on the parent
834 // Only update "commented" if it is really a comment
835 if (($arr['verb'] == ACTIVITY_POST) OR !get_config("system", "like_no_comment"))
836 q("UPDATE `item` SET `commented` = '%s', `changed` = '%s' WHERE `id` = %d",
837 dbesc(datetime_convert()),
838 dbesc(datetime_convert()),
842 q("UPDATE `item` SET `changed` = '%s' WHERE `id` = %d",
843 dbesc(datetime_convert()),
849 // Friendica servers lower than 3.4.3-2 had double encoded the signature ...
850 // We can check for this condition when we decode and encode the stuff again.
851 if (base64_encode(base64_decode(base64_decode($dsprsig->signature))) == base64_decode($dsprsig->signature)) {
852 $dsprsig->signature = base64_decode($dsprsig->signature);
853 logger("Repaired double encoded signature from handle ".$dsprsig->signer, LOGGER_DEBUG);
856 q("insert into sign (`iid`,`signed_text`,`signature`,`signer`) values (%d,'%s','%s','%s') ",
857 intval($current_post),
858 dbesc($dsprsig->signed_text),
859 dbesc($dsprsig->signature),
860 dbesc($dsprsig->signer)
866 * If this is now the last-child, force all _other_ children of this parent to *not* be last-child
869 if($arr['last-child']) {
870 $r = q("UPDATE `item` SET `last-child` = 0 WHERE `parent-uri` = '%s' AND `uid` = %d AND `id` != %d",
873 intval($current_post)
877 $deleted = tag_deliver($arr['uid'],$current_post);
879 // current post can be deleted if is for a community page and no mention are
881 if (!$deleted AND !$dontcache) {
883 $r = q('SELECT * FROM `item` WHERE id = %d', intval($current_post));
884 if (count($r) == 1) {
886 call_hooks('post_local_end', $r[0]);
888 call_hooks('post_remote_end', $r[0]);
890 logger('item_store: new item not found in DB, id ' . $current_post);
893 create_tags_from_item($current_post);
894 create_files_from_item($current_post);
896 // Only check for notifications on start posts
897 if ($arr['parent-uri'] === $arr['uri'])
898 add_thread($current_post);
900 update_thread($parent_id);
901 add_shadow_entry($arr);
904 check_item_notification($current_post, $uid);
907 proc_run('php', "include/notifier.php", $notify_type, $current_post);
909 return $current_post;
912 function item_body_set_hashtags(&$item) {
914 $tags = get_tags($item["body"]);
920 // This sorting is important when there are hashtags that are part of other hashtags
921 // Otherwise there could be problems with hashtags like #test and #test2
926 $URLSearchString = "^\[\]";
928 // All hashtags should point to the home server
929 //$item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
930 // "#[url=".$a->get_baseurl()."/search?tag=$2]$2[/url]", $item["body"]);
932 //$item["tag"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
933 // "#[url=".$a->get_baseurl()."/search?tag=$2]$2[/url]", $item["tag"]);
935 // mask hashtags inside of url, bookmarks and attachments to avoid urls in urls
936 $item["body"] = preg_replace_callback("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
938 return("[url=".str_replace("#", "#", $match[1])."]".str_replace("#", "#", $match[2])."[/url]");
941 $item["body"] = preg_replace_callback("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism",
943 return("[bookmark=".str_replace("#", "#", $match[1])."]".str_replace("#", "#", $match[2])."[/bookmark]");
946 $item["body"] = preg_replace_callback("/\[attachment (.*)\](.*?)\[\/attachment\]/ism",
948 return("[attachment ".str_replace("#", "#", $match[1])."]".$match[2]."[/attachment]");
951 // Repair recursive urls
952 $item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
953 "#$2", $item["body"]);
956 foreach($tags as $tag) {
957 if(strpos($tag,'#') !== 0)
960 if(strpos($tag,'[url='))
963 $basetag = str_replace('_',' ',substr($tag,1));
965 $newtag = '#[url='.$a->get_baseurl().'/search?tag='.rawurlencode($basetag).']'.$basetag.'[/url]';
967 $item["body"] = str_replace($tag, $newtag, $item["body"]);
969 if(!stristr($item["tag"],"/search?tag=".$basetag."]".$basetag."[/url]")) {
970 if(strlen($item["tag"]))
971 $item["tag"] = ','.$item["tag"];
972 $item["tag"] = $newtag.$item["tag"];
976 // Convert back the masked hashtags
977 $item["body"] = str_replace("#", "#", $item["body"]);
980 function get_item_guid($id) {
981 $r = q("SELECT `guid` FROM `item` WHERE `id` = %d LIMIT 1", intval($id));
983 return($r[0]["guid"]);
988 function get_item_id($guid, $uid = 0) {
994 $uid == local_user();
996 // Does the given user have this item?
998 $r = q("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid`
999 WHERE `item`.`visible` = 1 AND `item`.`deleted` = 0 and `item`.`moderated` = 0
1000 AND `item`.`guid` = '%s' AND `item`.`uid` = %d", dbesc($guid), intval($uid));
1003 $nick = $r[0]["nickname"];
1007 // Or is it anywhere on the server?
1009 $r = q("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid`
1010 WHERE `item`.`visible` = 1 AND `item`.`deleted` = 0 and `item`.`moderated` = 0
1011 AND `item`.`allow_cid` = '' AND `item`.`allow_gid` = ''
1012 AND `item`.`deny_cid` = '' AND `item`.`deny_gid` = ''
1013 AND `item`.`private` = 0 AND `item`.`wall` = 1
1014 AND `item`.`guid` = '%s'", dbesc($guid));
1017 $nick = $r[0]["nickname"];
1020 return(array("nick" => $nick, "id" => $id));
1024 function get_item_contact($item,$contacts) {
1025 if(! count($contacts) || (! is_array($item)))
1027 foreach($contacts as $contact) {
1028 if($contact['id'] == $item['contact-id']) {
1030 break; // NOTREACHED
1037 * look for mention tags and setup a second delivery chain for forum/community posts if appropriate
1039 * @param int $item_id
1040 * @return bool true if item was deleted, else false
1042 function tag_deliver($uid,$item_id) {
1050 $u = q("select * from user where uid = %d limit 1",
1056 $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
1057 $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
1060 $i = q("select * from item where id = %d and uid = %d limit 1",
1069 $link = normalise_link($a->get_baseurl() . '/profile/' . $u[0]['nickname']);
1071 // Diaspora uses their own hardwired link URL in @-tags
1072 // instead of the one we supply with webfinger
1074 $dlink = normalise_link($a->get_baseurl() . '/u/' . $u[0]['nickname']);
1076 $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism',$item['body'],$matches,PREG_SET_ORDER);
1078 foreach($matches as $mtch) {
1079 if(link_compare($link,$mtch[1]) || link_compare($dlink,$mtch[1])) {
1081 logger('tag_deliver: mention found: ' . $mtch[2]);
1087 if ( ($community_page || $prvgroup) &&
1088 (!$item['wall']) && (!$item['origin']) && ($item['id'] == $item['parent'])){
1089 // mmh.. no mention.. community page or private group... no wall.. no origin.. top-post (not a comment)
1091 logger("tag_deliver: no-mention top-level post to communuty or private group. delete.");
1092 q("DELETE FROM item WHERE id = %d and uid = %d",
1101 $arr = array('item' => $item, 'user' => $u[0], 'contact' => $r[0]);
1103 call_hooks('tagged', $arr);
1105 if((! $community_page) && (! $prvgroup))
1109 // tgroup delivery - setup a second delivery chain
1110 // prevent delivery looping - only proceed
1111 // if the message originated elsewhere and is a top-level post
1113 if(($item['wall']) || ($item['origin']) || ($item['id'] != $item['parent']))
1116 // now change this copy of the post to a forum head message and deliver to all the tgroup members
1119 $c = q("select name, url, thumb from contact where self = 1 and uid = %d limit 1",
1120 intval($u[0]['uid'])
1125 // also reset all the privacy bits to the forum default permissions
1127 $private = ($u[0]['allow_cid'] || $u[0]['allow_gid'] || $u[0]['deny_cid'] || $u[0]['deny_gid']) ? 1 : 0;
1129 $forum_mode = (($prvgroup) ? 2 : 1);
1131 q("update item set wall = 1, origin = 1, forum_mode = %d, `owner-name` = '%s', `owner-link` = '%s', `owner-avatar` = '%s',
1132 `private` = %d, `allow_cid` = '%s', `allow_gid` = '%s', `deny_cid` = '%s', `deny_gid` = '%s' where id = %d",
1133 intval($forum_mode),
1134 dbesc($c[0]['name']),
1135 dbesc($c[0]['url']),
1136 dbesc($c[0]['thumb']),
1138 dbesc($u[0]['allow_cid']),
1139 dbesc($u[0]['allow_gid']),
1140 dbesc($u[0]['deny_cid']),
1141 dbesc($u[0]['deny_gid']),
1144 update_thread($item_id);
1146 proc_run('php','include/notifier.php','tgroup',$item_id);
1152 function tgroup_check($uid,$item) {
1158 // check that the message originated elsewhere and is a top-level post
1160 if(($item['wall']) || ($item['origin']) || ($item['uri'] != $item['parent-uri']))
1164 $u = q("select * from user where uid = %d limit 1",
1170 $community_page = (($u[0]['page-flags'] == PAGE_COMMUNITY) ? true : false);
1171 $prvgroup = (($u[0]['page-flags'] == PAGE_PRVGROUP) ? true : false);
1174 $link = normalise_link($a->get_baseurl() . '/profile/' . $u[0]['nickname']);
1176 // Diaspora uses their own hardwired link URL in @-tags
1177 // instead of the one we supply with webfinger
1179 $dlink = normalise_link($a->get_baseurl() . '/u/' . $u[0]['nickname']);
1181 $cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism',$item['body'],$matches,PREG_SET_ORDER);
1183 foreach($matches as $mtch) {
1184 if(link_compare($link,$mtch[1]) || link_compare($dlink,$mtch[1])) {
1186 logger('tgroup_check: mention found: ' . $mtch[2]);
1194 if((! $community_page) && (! $prvgroup))
1201 This function returns true if $update has an edited timestamp newer
1202 than $existing, i.e. $update contains new data which should override
1203 what's already there. If there is no timestamp yet, the update is
1204 assumed to be newer. If the update has no timestamp, the existing
1205 item is assumed to be up-to-date. If the timestamps are equal it
1206 assumes the update has been seen before and should be ignored.
1208 function edited_timestamp_is_newer($existing, $update) {
1209 if (!x($existing,'edited') || !$existing['edited']) {
1212 if (!x($update,'edited') || !$update['edited']) {
1215 $existing_edited = datetime_convert('UTC', 'UTC', $existing['edited']);
1216 $update_edited = datetime_convert('UTC', 'UTC', $update['edited']);
1217 return (strcmp($existing_edited, $update_edited) < 0);
1222 * consume_feed - process atom feed and update anything/everything we might need to update
1224 * $xml = the (atom) feed to consume - RSS isn't as fully supported but may work for simple feeds.
1226 * $importer = the contact_record (joined to user_record) of the local user who owns this relationship.
1227 * It is this person's stuff that is going to be updated.
1228 * $contact = the person who is sending us stuff. If not set, we MAY be processing a "follow" activity
1229 * from an external network and MAY create an appropriate contact record. Otherwise, we MUST
1230 * have a contact record.
1231 * $hub = should we find a hub declation in the feed, pass it back to our calling process, who might (or
1232 * might not) try and subscribe to it.
1233 * $datedir sorts in reverse order
1234 * $pass - by default ($pass = 0) we cannot guarantee that a parent item has been
1235 * imported prior to its children being seen in the stream unless we are certain
1236 * of how the feed is arranged/ordered.
1237 * With $pass = 1, we only pull parent items out of the stream.
1238 * With $pass = 2, we only pull children (comments/likes).
1240 * So running this twice, first with pass 1 and then with pass 2 will do the right
1241 * thing regardless of feed ordering. This won't be adequate in a fully-threaded
1242 * model where comments can have sub-threads. That would require some massive sorting
1243 * to get all the feed items into a mostly linear ordering, and might still require
1247 function consume_feed($xml,$importer,&$contact, &$hub, $datedir = 0, $pass = 0) {
1248 if ($contact['network'] === NETWORK_OSTATUS) {
1250 // Test - remove before flight
1251 //$tempfile = tempnam(get_temppath(), "ostatus2");
1252 //file_put_contents($tempfile, $xml);
1253 logger("Consume OStatus messages ", LOGGER_DEBUG);
1254 ostatus::import($xml,$importer,$contact, $hub);
1259 if ($contact['network'] === NETWORK_FEED) {
1261 logger("Consume feeds", LOGGER_DEBUG);
1262 feed_import($xml,$importer,$contact, $hub);
1267 if ($contact['network'] === NETWORK_DFRN) {
1268 logger("Consume DFRN messages", LOGGER_DEBUG);
1270 $r = q("SELECT `contact`.*, `contact`.`uid` AS `importer_uid`,
1271 `contact`.`pubkey` AS `cpubkey`,
1272 `contact`.`prvkey` AS `cprvkey`,
1273 `contact`.`thumb` AS `thumb`,
1274 `contact`.`url` as `url`,
1275 `contact`.`name` as `senderName`,
1278 LEFT JOIN `user` ON `contact`.`uid` = `user`.`uid`
1279 WHERE `contact`.`id` = %d AND `user`.`uid` = %d",
1280 dbesc($contact["id"]), dbesc($importer["uid"])
1283 logger("Now import the DFRN feed");
1284 dfrn::import($xml,$r[0], true);
1290 function item_is_remote_self($contact, &$datarray) {
1293 if (!$contact['remote_self'])
1296 // Prevent the forwarding of posts that are forwarded
1297 if ($datarray["extid"] == NETWORK_DFRN)
1300 // Prevent to forward already forwarded posts
1301 if ($datarray["app"] == $a->get_hostname())
1304 // Only forward posts
1305 if ($datarray["verb"] != ACTIVITY_POST)
1308 if (($contact['network'] != NETWORK_FEED) AND $datarray['private'])
1311 $datarray2 = $datarray;
1312 logger('remote-self start - Contact '.$contact['url'].' - '.$contact['remote_self'].' Item '.print_r($datarray, true), LOGGER_DEBUG);
1313 if ($contact['remote_self'] == 2) {
1314 $r = q("SELECT `id`,`url`,`name`,`thumb` FROM `contact` WHERE `uid` = %d AND `self`",
1315 intval($contact['uid']));
1317 $datarray['contact-id'] = $r[0]["id"];
1319 $datarray['owner-name'] = $r[0]["name"];
1320 $datarray['owner-link'] = $r[0]["url"];
1321 $datarray['owner-avatar'] = $r[0]["thumb"];
1323 $datarray['author-name'] = $datarray['owner-name'];
1324 $datarray['author-link'] = $datarray['owner-link'];
1325 $datarray['author-avatar'] = $datarray['owner-avatar'];
1328 if ($contact['network'] != NETWORK_FEED) {
1329 $datarray["guid"] = get_guid(32);
1330 unset($datarray["plink"]);
1331 $datarray["uri"] = item_new_uri($a->get_hostname(),$contact['uid'], $datarray["guid"]);
1332 $datarray["parent-uri"] = $datarray["uri"];
1333 $datarray["extid"] = $contact['network'];
1334 $urlpart = parse_url($datarray2['author-link']);
1335 $datarray["app"] = $urlpart["host"];
1337 $datarray['private'] = 0;
1340 if ($contact['network'] != NETWORK_FEED) {
1341 // Store the original post
1342 $r = item_store($datarray2, false, false);
1343 logger('remote-self post original item - Contact '.$contact['url'].' return '.$r.' Item '.print_r($datarray2, true), LOGGER_DEBUG);
1345 $datarray["app"] = "Feed";
1350 function new_follower($importer,$contact,$datarray,$item,$sharing = false) {
1351 $url = notags(trim($datarray['author-link']));
1352 $name = notags(trim($datarray['author-name']));
1353 $photo = notags(trim($datarray['author-avatar']));
1355 if (is_object($item)) {
1356 $rawtag = $item->get_item_tags(NAMESPACE_ACTIVITY,'actor');
1357 if($rawtag && $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data'])
1358 $nick = $rawtag[0]['child'][NAMESPACE_POCO]['preferredUsername'][0]['data'];
1362 if(is_array($contact)) {
1363 if(($contact['network'] == NETWORK_OSTATUS && $contact['rel'] == CONTACT_IS_SHARING)
1364 || ($sharing && $contact['rel'] == CONTACT_IS_FOLLOWER)) {
1365 $r = q("UPDATE `contact` SET `rel` = %d, `writable` = 1 WHERE `id` = %d AND `uid` = %d",
1366 intval(CONTACT_IS_FRIEND),
1367 intval($contact['id']),
1368 intval($importer['uid'])
1371 // send email notification to owner?
1374 // create contact record
1376 $r = q("INSERT INTO `contact` (`uid`, `created`, `url`, `nurl`, `name`, `nick`, `photo`, `network`, `rel`,
1377 `blocked`, `readonly`, `pending`, `writable`)
1378 VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, 0, 0, 1, 1)",
1379 intval($importer['uid']),
1380 dbesc(datetime_convert()),
1382 dbesc(normalise_link($url)),
1386 dbesc(($sharing) ? NETWORK_ZOT : NETWORK_OSTATUS),
1387 intval(($sharing) ? CONTACT_IS_SHARING : CONTACT_IS_FOLLOWER)
1389 $r = q("SELECT `id`, `network` FROM `contact` WHERE `uid` = %d AND `url` = '%s' AND `pending` = 1 LIMIT 1",
1390 intval($importer['uid']),
1394 $contact_record = $r[0];
1396 $photos = import_profile_photo($photo,$importer["uid"],$contact_record["id"]);
1398 q("UPDATE `contact` SET `photo` = '%s', `thumb` = '%s', `micro` = '%s' WHERE `id` = %d",
1402 intval($contact_record["id"])
1407 $r = q("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1",
1408 intval($importer['uid'])
1411 if(count($r) AND !in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
1413 // create notification
1414 $hash = random_string();
1416 if(is_array($contact_record)) {
1417 $ret = q("INSERT INTO `intro` ( `uid`, `contact-id`, `blocked`, `knowyou`, `hash`, `datetime`)
1418 VALUES ( %d, %d, 0, 0, '%s', '%s' )",
1419 intval($importer['uid']),
1420 intval($contact_record['id']),
1422 dbesc(datetime_convert())
1426 $def_gid = get_default_group($importer['uid'], $contact_record["network"]);
1428 if(intval($def_gid))
1429 group_add_member($importer['uid'],'',$contact_record['id'],$def_gid);
1431 if(($r[0]['notify-flags'] & NOTIFY_INTRO) &&
1432 in_array($r[0]['page-flags'], array(PAGE_NORMAL))) {
1435 'type' => NOTIFY_INTRO,
1436 'notify_flags' => $r[0]['notify-flags'],
1437 'language' => $r[0]['language'],
1438 'to_name' => $r[0]['username'],
1439 'to_email' => $r[0]['email'],
1440 'uid' => $r[0]['uid'],
1441 'link' => $a->get_baseurl() . '/notifications/intro',
1442 'source_name' => ((strlen(stripslashes($contact_record['name']))) ? stripslashes($contact_record['name']) : t('[Name Withheld]')),
1443 'source_link' => $contact_record['url'],
1444 'source_photo' => $contact_record['photo'],
1445 'verb' => ($sharing ? ACTIVITY_FRIEND : ACTIVITY_FOLLOW),
1450 } elseif (count($r) AND in_array($r[0]['page-flags'], array(PAGE_SOAPBOX, PAGE_FREELOVE))) {
1451 $r = q("UPDATE `contact` SET `pending` = 0 WHERE `uid` = %d AND `url` = '%s' AND `pending` LIMIT 1",
1452 intval($importer['uid']),
1460 function lose_follower($importer,$contact,$datarray = array(),$item = "") {
1462 if(($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_SHARING)) {
1463 q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
1464 intval(CONTACT_IS_SHARING),
1465 intval($contact['id'])
1469 contact_remove($contact['id']);
1473 function lose_sharer($importer,$contact,$datarray = array(),$item = "") {
1475 if(($contact['rel'] == CONTACT_IS_FRIEND) || ($contact['rel'] == CONTACT_IS_FOLLOWER)) {
1476 q("UPDATE `contact` SET `rel` = %d WHERE `id` = %d",
1477 intval(CONTACT_IS_FOLLOWER),
1478 intval($contact['id'])
1482 contact_remove($contact['id']);
1486 function subscribe_to_hub($url,$importer,$contact,$hubmode = 'subscribe') {
1490 if(is_array($importer)) {
1491 $r = q("SELECT `nickname` FROM `user` WHERE `uid` = %d LIMIT 1",
1492 intval($importer['uid'])
1496 // Diaspora has different message-ids in feeds than they do
1497 // through the direct Diaspora protocol. If we try and use
1498 // the feed, we'll get duplicates. So don't.
1500 if((! count($r)) || $contact['network'] === NETWORK_DIASPORA)
1503 $push_url = get_config('system','url') . '/pubsub/' . $r[0]['nickname'] . '/' . $contact['id'];
1505 // Use a single verify token, even if multiple hubs
1507 $verify_token = ((strlen($contact['hub-verify'])) ? $contact['hub-verify'] : random_string());
1509 $params= 'hub.mode=' . $hubmode . '&hub.callback=' . urlencode($push_url) . '&hub.topic=' . urlencode($contact['poll']) . '&hub.verify=async&hub.verify_token=' . $verify_token;
1511 logger('subscribe_to_hub: ' . $hubmode . ' ' . $contact['name'] . ' to hub ' . $url . ' endpoint: ' . $push_url . ' with verifier ' . $verify_token);
1513 if(!strlen($contact['hub-verify']) OR ($contact['hub-verify'] != $verify_token)) {
1514 $r = q("UPDATE `contact` SET `hub-verify` = '%s' WHERE `id` = %d",
1515 dbesc($verify_token),
1516 intval($contact['id'])
1520 post_url($url,$params);
1522 logger('subscribe_to_hub: returns: ' . $a->get_curl_code(), LOGGER_DEBUG);
1528 function fix_private_photos($s, $uid, $item = null, $cid = 0) {
1530 if(get_config('system','disable_embedded'))
1535 logger('fix_private_photos: check for photos', LOGGER_DEBUG);
1536 $site = substr($a->get_baseurl(),strpos($a->get_baseurl(),'://'));
1541 $img_start = strpos($orig_body, '[img');
1542 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
1543 $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
1544 while( ($img_st_close !== false) && ($img_len !== false) ) {
1546 $img_st_close++; // make it point to AFTER the closing bracket
1547 $image = substr($orig_body, $img_start + $img_st_close, $img_len);
1549 logger('fix_private_photos: found photo ' . $image, LOGGER_DEBUG);
1552 if(stristr($image , $site . '/photo/')) {
1553 // Only embed locally hosted photos
1555 $i = basename($image);
1556 $i = str_replace(array('.jpg','.png','.gif'),array('','',''),$i);
1557 $x = strpos($i,'-');
1560 $res = substr($i,$x+1);
1561 $i = substr($i,0,$x);
1562 $r = q("SELECT * FROM `photo` WHERE `resource-id` = '%s' AND `scale` = %d AND `uid` = %d",
1569 // Check to see if we should replace this photo link with an embedded image
1570 // 1. No need to do so if the photo is public
1571 // 2. If there's a contact-id provided, see if they're in the access list
1572 // for the photo. If so, embed it.
1573 // 3. Otherwise, if we have an item, see if the item permissions match the photo
1574 // permissions, regardless of order but first check to see if they're an exact
1575 // match to save some processing overhead.
1577 if(has_permissions($r[0])) {
1579 $recips = enumerate_permissions($r[0]);
1580 if(in_array($cid, $recips)) {
1585 if(compare_permissions($item,$r[0]))
1590 $data = $r[0]['data'];
1591 $type = $r[0]['type'];
1593 // If a custom width and height were specified, apply before embedding
1594 if(preg_match("/\[img\=([0-9]*)x([0-9]*)\]/is", substr($orig_body, $img_start, $img_st_close), $match)) {
1595 logger('fix_private_photos: scaling photo', LOGGER_DEBUG);
1597 $width = intval($match[1]);
1598 $height = intval($match[2]);
1600 $ph = new Photo($data, $type);
1601 if($ph->is_valid()) {
1602 $ph->scaleImage(max($width, $height));
1603 $data = $ph->imageString();
1604 $type = $ph->getType();
1608 logger('fix_private_photos: replacing photo', LOGGER_DEBUG);
1609 $image = 'data:' . $type . ';base64,' . base64_encode($data);
1610 logger('fix_private_photos: replaced: ' . $image, LOGGER_DATA);
1616 $new_body = $new_body . substr($orig_body, 0, $img_start + $img_st_close) . $image . '[/img]';
1617 $orig_body = substr($orig_body, $img_start + $img_st_close + $img_len + strlen('[/img]'));
1618 if($orig_body === false)
1621 $img_start = strpos($orig_body, '[img');
1622 $img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
1623 $img_len = ($img_start !== false ? strpos(substr($orig_body, $img_start + $img_st_close + 1), '[/img]') : false);
1626 $new_body = $new_body . $orig_body;
1631 function has_permissions($obj) {
1632 if(($obj['allow_cid'] != '') || ($obj['allow_gid'] != '') || ($obj['deny_cid'] != '') || ($obj['deny_gid'] != ''))
1637 function compare_permissions($obj1,$obj2) {
1638 // first part is easy. Check that these are exactly the same.
1639 if(($obj1['allow_cid'] == $obj2['allow_cid'])
1640 && ($obj1['allow_gid'] == $obj2['allow_gid'])
1641 && ($obj1['deny_cid'] == $obj2['deny_cid'])
1642 && ($obj1['deny_gid'] == $obj2['deny_gid']))
1645 // This is harder. Parse all the permissions and compare the resulting set.
1647 $recipients1 = enumerate_permissions($obj1);
1648 $recipients2 = enumerate_permissions($obj2);
1651 if($recipients1 == $recipients2)
1656 // returns an array of contact-ids that are allowed to see this object
1658 function enumerate_permissions($obj) {
1659 $allow_people = expand_acl($obj['allow_cid']);
1660 $allow_groups = expand_groups(expand_acl($obj['allow_gid']));
1661 $deny_people = expand_acl($obj['deny_cid']);
1662 $deny_groups = expand_groups(expand_acl($obj['deny_gid']));
1663 $recipients = array_unique(array_merge($allow_people,$allow_groups));
1664 $deny = array_unique(array_merge($deny_people,$deny_groups));
1665 $recipients = array_diff($recipients,$deny);
1669 function item_getfeedtags($item) {
1672 $cnt = preg_match_all('|\#\[url\=(.*?)\](.*?)\[\/url\]|',$item['tag'],$matches);
1674 for($x = 0; $x < $cnt; $x ++) {
1676 $ret[$matches[2][$x]] = array('#',$matches[1][$x], $matches[2][$x]);
1680 $cnt = preg_match_all('|\@\[url\=(.*?)\](.*?)\[\/url\]|',$item['tag'],$matches);
1682 for($x = 0; $x < $cnt; $x ++) {
1684 $ret[] = array('@',$matches[1][$x], $matches[2][$x]);
1690 function item_expire($uid, $days, $network = "", $force = false) {
1692 if((! $uid) || ($days < 1))
1695 // $expire_network_only = save your own wall posts
1696 // and just expire conversations started by others
1698 $expire_network_only = get_pconfig($uid,'expire','network_only');
1699 $sql_extra = ((intval($expire_network_only)) ? " AND wall = 0 " : "");
1701 if ($network != "") {
1702 $sql_extra .= sprintf(" AND network = '%s' ", dbesc($network));
1703 // There is an index "uid_network_received" but not "uid_network_created"
1704 // This avoids the creation of another index just for one purpose.
1705 // And it doesn't really matter wether to look at "received" or "created"
1706 $range = "AND `received` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
1708 $range = "AND `created` < UTC_TIMESTAMP() - INTERVAL %d DAY ";
1710 $r = q("SELECT * FROM `item`
1711 WHERE `uid` = %d $range
1722 $expire_items = get_pconfig($uid, 'expire','items');
1723 $expire_items = (($expire_items===false)?1:intval($expire_items)); // default if not set: 1
1725 // Forcing expiring of items - but not notes and marked items
1727 $expire_items = true;
1729 $expire_notes = get_pconfig($uid, 'expire','notes');
1730 $expire_notes = (($expire_notes===false)?1:intval($expire_notes)); // default if not set: 1
1732 $expire_starred = get_pconfig($uid, 'expire','starred');
1733 $expire_starred = (($expire_starred===false)?1:intval($expire_starred)); // default if not set: 1
1735 $expire_photos = get_pconfig($uid, 'expire','photos');
1736 $expire_photos = (($expire_photos===false)?0:intval($expire_photos)); // default if not set: 0
1738 logger('expire: # items=' . count($r). "; expire items: $expire_items, expire notes: $expire_notes, expire starred: $expire_starred, expire photos: $expire_photos");
1740 foreach($r as $item) {
1742 // don't expire filed items
1744 if(strpos($item['file'],'[') !== false)
1747 // Only expire posts, not photos and photo comments
1749 if($expire_photos==0 && strlen($item['resource-id']))
1751 if($expire_starred==0 && intval($item['starred']))
1753 if($expire_notes==0 && $item['type']=='note')
1755 if($expire_items==0 && $item['type']!='note')
1758 drop_item($item['id'],false);
1761 proc_run('php',"include/notifier.php","expire","$uid");
1766 function drop_items($items) {
1769 if(! local_user() && ! remote_user())
1773 foreach($items as $item) {
1774 $owner = drop_item($item,false);
1775 if($owner && ! $uid)
1780 // multiple threads may have been deleted, send an expire notification
1783 proc_run('php',"include/notifier.php","expire","$uid");
1787 function drop_item($id,$interactive = true) {
1791 // locate item to be deleted
1793 $r = q("SELECT * FROM `item` WHERE `id` = %d LIMIT 1",
1800 notice( t('Item not found.') . EOL);
1801 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
1806 $owner = $item['uid'];
1810 // check if logged in user is either the author or owner of this item
1812 if(is_array($_SESSION['remote'])) {
1813 foreach($_SESSION['remote'] as $visitor) {
1814 if($visitor['uid'] == $item['uid'] && $visitor['cid'] == $item['contact-id']) {
1815 $cid = $visitor['cid'];
1822 if((local_user() == $item['uid']) || ($cid) || (! $interactive)) {
1824 // Check if we should do HTML-based delete confirmation
1825 if($_REQUEST['confirm']) {
1826 // <form> can't take arguments in its "action" parameter
1827 // so add any arguments as hidden inputs
1828 $query = explode_querystring($a->query_string);
1830 foreach($query['args'] as $arg) {
1831 if(strpos($arg, 'confirm=') === false) {
1832 $arg_parts = explode('=', $arg);
1833 $inputs[] = array('name' => $arg_parts[0], 'value' => $arg_parts[1]);
1837 return replace_macros(get_markup_template('confirm.tpl'), array(
1839 '$message' => t('Do you really want to delete this item?'),
1840 '$extra_inputs' => $inputs,
1841 '$confirm' => t('Yes'),
1842 '$confirm_url' => $query['base'],
1843 '$confirm_name' => 'confirmed',
1844 '$cancel' => t('Cancel'),
1847 // Now check how the user responded to the confirmation query
1848 if($_REQUEST['canceled']) {
1849 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
1852 logger('delete item: ' . $item['id'], LOGGER_DEBUG);
1855 $r = q("UPDATE `item` SET `deleted` = 1, `title` = '', `body` = '', `edited` = '%s', `changed` = '%s' WHERE `id` = %d",
1856 dbesc(datetime_convert()),
1857 dbesc(datetime_convert()),
1860 create_tags_from_item($item['id']);
1861 create_files_from_item($item['id']);
1862 delete_thread($item['id'], $item['parent-uri']);
1864 // clean up categories and tags so they don't end up as orphans
1867 $cnt = preg_match_all('/<(.*?)>/',$item['file'],$matches,PREG_SET_ORDER);
1869 foreach($matches as $mtch) {
1870 file_tag_unsave_file($item['uid'],$item['id'],$mtch[1],true);
1876 $cnt = preg_match_all('/\[(.*?)\]/',$item['file'],$matches,PREG_SET_ORDER);
1878 foreach($matches as $mtch) {
1879 file_tag_unsave_file($item['uid'],$item['id'],$mtch[1],false);
1883 // If item is a link to a photo resource, nuke all the associated photos
1884 // (visitors will not have photo resources)
1885 // This only applies to photos uploaded from the photos page. Photos inserted into a post do not
1886 // generate a resource-id and therefore aren't intimately linked to the item.
1888 if(strlen($item['resource-id'])) {
1889 q("DELETE FROM `photo` WHERE `resource-id` = '%s' AND `uid` = %d ",
1890 dbesc($item['resource-id']),
1891 intval($item['uid'])
1893 // ignore the result
1896 // If item is a link to an event, nuke the event record.
1898 if(intval($item['event-id'])) {
1899 q("DELETE FROM `event` WHERE `id` = %d AND `uid` = %d",
1900 intval($item['event-id']),
1901 intval($item['uid'])
1903 // ignore the result
1906 // If item has attachments, drop them
1908 foreach(explode(",",$item['attach']) as $attach){
1909 preg_match("|attach/(\d+)|", $attach, $matches);
1910 q("DELETE FROM `attach` WHERE `id` = %d AND `uid` = %d",
1911 intval($matches[1]),
1914 // ignore the result
1918 // clean up item_id and sign meta-data tables
1921 // Old code - caused very long queries and warning entries in the mysql logfiles:
1923 $r = q("DELETE FROM item_id where iid in (select id from item where parent = %d and uid = %d)",
1924 intval($item['id']),
1925 intval($item['uid'])
1928 $r = q("DELETE FROM sign where iid in (select id from item where parent = %d and uid = %d)",
1929 intval($item['id']),
1930 intval($item['uid'])
1934 // The new code splits the queries since the mysql optimizer really has bad problems with subqueries
1936 // Creating list of parents
1937 $r = q("select id from item where parent = %d and uid = %d",
1938 intval($item['id']),
1939 intval($item['uid'])
1944 foreach ($r AS $row) {
1945 if ($parentid != "")
1948 $parentid .= $row["id"];
1952 if ($parentid != "") {
1953 $r = q("DELETE FROM item_id where iid in (%s)", dbesc($parentid));
1955 $r = q("DELETE FROM sign where iid in (%s)", dbesc($parentid));
1958 // If it's the parent of a comment thread, kill all the kids
1960 if($item['uri'] == $item['parent-uri']) {
1961 $r = q("UPDATE `item` SET `deleted` = 1, `edited` = '%s', `changed` = '%s', `body` = '' , `title` = ''
1962 WHERE `parent-uri` = '%s' AND `uid` = %d ",
1963 dbesc(datetime_convert()),
1964 dbesc(datetime_convert()),
1965 dbesc($item['parent-uri']),
1966 intval($item['uid'])
1968 create_tags_from_itemuri($item['parent-uri'], $item['uid']);
1969 create_files_from_itemuri($item['parent-uri'], $item['uid']);
1970 delete_thread_uri($item['parent-uri'], $item['uid']);
1971 // ignore the result
1974 // ensure that last-child is set in case the comment that had it just got wiped.
1975 q("UPDATE `item` SET `last-child` = 0, `changed` = '%s' WHERE `parent-uri` = '%s' AND `uid` = %d ",
1976 dbesc(datetime_convert()),
1977 dbesc($item['parent-uri']),
1978 intval($item['uid'])
1980 // who is the last child now?
1981 $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",
1982 dbesc($item['parent-uri']),
1983 intval($item['uid'])
1986 q("UPDATE `item` SET `last-child` = 1 WHERE `id` = %d",
1992 $drop_id = intval($item['id']);
1994 // send the notification upstream/downstream as the case may be
1996 proc_run('php',"include/notifier.php","drop","$drop_id");
2000 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
2006 notice( t('Permission denied.') . EOL);
2007 goaway($a->get_baseurl() . '/' . $_SESSION['return_url']);
2014 function first_post_date($uid,$wall = false) {
2015 $r = q("select id, created from item
2016 where uid = %d and wall = %d and deleted = 0 and visible = 1 AND moderated = 0
2018 order by created asc limit 1",
2020 intval($wall ? 1 : 0)
2023 // logger('first_post_date: ' . $r[0]['id'] . ' ' . $r[0]['created'], LOGGER_DATA);
2024 return substr(datetime_convert('',date_default_timezone_get(),$r[0]['created']),0,10);
2029 /* modified posted_dates() {below} to arrange the list in years */
2030 function list_post_dates($uid, $wall) {
2031 $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
2033 $dthen = first_post_date($uid, $wall);
2037 // Set the start and end date to the beginning of the month
2038 $dnow = substr($dnow,0,8).'01';
2039 $dthen = substr($dthen,0,8).'01';
2043 // Starting with the current month, get the first and last days of every
2044 // month down to and including the month of the first post
2045 while(substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
2046 $dyear = intval(substr($dnow,0,4));
2047 $dstart = substr($dnow,0,8) . '01';
2048 $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
2049 $start_month = datetime_convert('','',$dstart,'Y-m-d');
2050 $end_month = datetime_convert('','',$dend,'Y-m-d');
2051 $str = day_translate(datetime_convert('','',$dnow,'F'));
2053 $ret[$dyear] = array();
2054 $ret[$dyear][] = array($str,$end_month,$start_month);
2055 $dnow = datetime_convert('','',$dnow . ' -1 month', 'Y-m-d');
2060 function posted_dates($uid,$wall) {
2061 $dnow = datetime_convert('',date_default_timezone_get(),'now','Y-m-d');
2063 $dthen = first_post_date($uid,$wall);
2067 // Set the start and end date to the beginning of the month
2068 $dnow = substr($dnow,0,8).'01';
2069 $dthen = substr($dthen,0,8).'01';
2072 // Starting with the current month, get the first and last days of every
2073 // month down to and including the month of the first post
2074 while(substr($dnow, 0, 7) >= substr($dthen, 0, 7)) {
2075 $dstart = substr($dnow,0,8) . '01';
2076 $dend = substr($dnow,0,8) . get_dim(intval($dnow),intval(substr($dnow,5)));
2077 $start_month = datetime_convert('','',$dstart,'Y-m-d');
2078 $end_month = datetime_convert('','',$dend,'Y-m-d');
2079 $str = day_translate(datetime_convert('','',$dnow,'F Y'));
2080 $ret[] = array($str,$end_month,$start_month);
2081 $dnow = datetime_convert('','',$dnow . ' -1 month', 'Y-m-d');
2087 function posted_date_widget($url,$uid,$wall) {
2090 if(! feature_enabled($uid,'archives'))
2093 // For former Facebook folks that left because of "timeline"
2095 /* if($wall && intval(get_pconfig($uid,'system','no_wall_archive_widget')))
2098 $visible_years = get_pconfig($uid,'system','archive_visible_years');
2099 if(! $visible_years)
2102 $ret = list_post_dates($uid,$wall);
2107 $cutoff_year = intval(datetime_convert('',date_default_timezone_get(),'now','Y')) - $visible_years;
2108 $cutoff = ((array_key_exists($cutoff_year,$ret))? true : false);
2110 $o = replace_macros(get_markup_template('posted_date_widget.tpl'),array(
2111 '$title' => t('Archives'),
2112 '$size' => $visible_years,
2113 '$cutoff_year' => $cutoff_year,
2114 '$cutoff' => $cutoff,
2117 '$showmore' => t('show more')