dcCore dcCore instance public $table; ///< string done pings table name /** Object constructor @param core dcCore dcCore instance */ public function __construct($core) { $this->core =& $core; $this->con =& $this->core->con; $this->table = $this->core->prefix.'ping'; } /// @name Send //@{ /** Get all pings sent for a given post. @param post_id integer Post ID @return record */ public function getPostPings($post_id) { $strReq = 'SELECT ping_url, ping_dt '. 'FROM '.$this->table.' '. 'WHERE post_id = '.(integer) $post_id; return $this->con->select($strReq); } /** Sends a ping to given $url. @param url string URL to ping @param post_id integer Post ID @param post_title string Post title @param post_excerpt string Post excerpt @param post_url string Post URL */ public function ping($url,$post_id,$post_title,$post_excerpt,$post_url) { if ($this->core->blog === null) { return false; } $post_id = (integer) $post_id; # Check for previously done trackback $strReq = 'SELECT post_id, ping_url FROM '.$this->table.' '. 'WHERE post_id = '.$post_id.' '. "AND ping_url = '".$this->con->escape($url)."' "; $rs = $this->con->select($strReq); if (!$rs->isEmpty()) { throw new Exception(sprintf(__('%s has still been pinged'),$url)); } $ping_parts = explode('|',$url); # Maybe a webmention if (count($ping_parts) == 3) { $payload = http_build_query(array( 'source' => $post_url, 'target' => $ping_parts[1] )); try { $http = self::initHttp($ping_parts[0],$path); $http->setMoreHeader('Content-Type: application/x-www-form-urlencoded'); $http->post($path,$payload,'UTF-8'); # Read response status $status = $http->getStatus(); $ping_error = '0'; } catch (Exception $e) { throw new Exception(__('Unable to ping URL')); } if (!in_array($status,array('200','201','202'))) { $ping_error = $http->getStatus(); $ping_msg = __('Bad server response code'); } } # No, let's walk by the trackback way elseif (count($ping_parts) < 2) { $data = array( 'title' => $post_title, 'excerpt' => $post_excerpt, 'url' => $post_url, 'blog_name' => trim(html::escapeHTML(html::clean($this->core->blog->name))) //,'__debug' => false ); # Ping try { $http = self::initHttp($url,$path); $http->post($path,$data,'UTF-8'); $res = $http->getContent(); } catch (Exception $e) { throw new Exception(__('Unable to ping URL')); } $pattern = '|.*(.*)(.*)'. '((.*)(.*))?'. '|msU'; if (!preg_match($pattern,$res,$match)) { throw new Exception(sprintf(__('%s is not a ping URL'),$url)); } $ping_error = trim($match[1]); $ping_msg = (!empty($match[4])) ? $match[4] : ''; } # Damnit ! Let's play pingback else { try { $xmlrpc = new xmlrpcClient($ping_parts[0]); $res = $xmlrpc->query('pingback.ping', $post_url, $ping_parts[1]); $ping_error = '0'; } catch (xmlrpcException $e) { $ping_error = $e->getCode(); $ping_msg = $e->getMessage(); } catch (Exception $e) { throw new Exception(__('Unable to ping URL')); } } if ($ping_error != '0') { throw new Exception(sprintf(__('%s, ping error:'),$url).' '.$ping_msg); } else { # Notify ping result in database $cur = $this->con->openCursor($this->table); $cur->post_id = $post_id; $cur->ping_url = $url; $cur->ping_dt = date('Y-m-d H:i:s'); $cur->insert(); } } //@} /// @name Receive //@{ /** Receives a trackback and insert it as a comment of given post. @param post_id integer Post ID */ public function receiveTrackback($post_id) { header('Content-Type: text/xml; charset=UTF-8'); if (empty($_POST)) { http::head(405,'Method Not Allowed'); echo ''."\n". "\n". " 1\n". " POST request needed\n". ""; return; } $post_id = (integer) $post_id; $title = !empty($_POST['title']) ? $_POST['title'] : ''; $excerpt = !empty($_POST['excerpt']) ? $_POST['excerpt'] : ''; $url = !empty($_POST['url']) ? $_POST['url'] : ''; $blog_name = !empty($_POST['blog_name']) ? $_POST['blog_name'] : ''; $charset = ''; $comment = ''; $err = false; $msg = ''; if ($this->core->blog === null) { $err = true; $msg = 'No blog.'; } elseif ($url == '') { $err = true; $msg = 'URL parameter is required.'; } elseif ($blog_name == '') { $err = true; $msg = 'Blog name is required.'; } if (!$err) { $post = $this->core->blog->getPosts(array('post_id'=>$post_id,'post_type'=>'')); if ($post->isEmpty()) { $err = true; $msg = 'No such post.'; } elseif (!$post->trackbacksActive()) { $err = true; $msg = 'Trackbacks are not allowed for this post or weblog.'; } $url = trim(html::clean($url)); if ($this->pingAlreadyDone($post->post_id,$url)) { $err = true; $msg = 'The trackback has already been registered'; } } if (!$err) { $charset = self::getCharsetFromRequest(); if (!$charset) { $charset = self::detectCharset($title.' '.$excerpt.' '.$blog_name); } if (strtolower($charset) != 'utf-8') { $title = iconv($charset,'UTF-8',$title); $excerpt = iconv($charset,'UTF-8',$excerpt); $blog_name = iconv($charset,'UTF-8',$blog_name); } $title = trim(html::clean($title)); $title = html::decodeEntities($title); $title = html::escapeHTML($title); $title = text::cutString($title,60); $excerpt = trim(html::clean($excerpt)); $excerpt = html::decodeEntities($excerpt); $excerpt = preg_replace('/\s+/ms',' ',$excerpt); $excerpt = text::cutString($excerpt,252); $excerpt = html::escapeHTML($excerpt).'...'; $blog_name = trim(html::clean($blog_name)); $blog_name = html::decodeEntities($blog_name); $blog_name = html::escapeHTML($blog_name); $blog_name = text::cutString($blog_name,60); try { $this->addBacklink($post_id,$url,$blog_name,$title,$excerpt,$comment); } catch (Exception $e) { $err = 1; $msg = 'Something went wrong : '.$e->getMessage(); } } $resp = ''."\n". "\n". ' '.(integer) $err."\n"; if ($msg) { $resp .= ' '.$msg."\n"; } if (!empty($_POST['__debug'])) { $resp .= " \n". ' '.$title."\n". ' '.$excerpt."\n". ' '.$url."\n". ' '.$blog_name."\n". ' '.$charset."\n". ' '.$comment."\n". " \n"; } echo $resp.""; } /** Receives a pingback and insert it as a comment of given post. @param from_url string Source URL @param to_url string Target URL */ public function receivePingback($from_url,$to_url) { try { $posts = $this->getTargetPost($to_url); if ($this->pingAlreadyDone($posts->post_id,$from_url)) { throw new Exception(__('Don\'t repeat yourself, please.'), 48); } $remote_content = $this->getRemoteContent($from_url); # We want a title... if (!preg_match('!([^<].*?)!mis',$remote_content,$m)) { throw new Exception(__('Where\'s your title?'), 0); } $title = trim(html::clean($m[1])); $title = html::decodeEntities($title); $title = html::escapeHTML($title); $title = text::cutString($title,60); preg_match('!]*?>(.*)?!msi',$remote_content,$m); $source = $m[1]; $source = preg_replace('![\r\n\s]+!ms',' ',$source); $source = preg_replace( "/<\/*(h\d|p|th|td|li|dt|dd|pre|caption|input|textarea|button)[^>]*>/","\n\n",$source ); $source = strip_tags($source,''); $source = explode("\n\n",$source); $excerpt = ''; foreach ($source as $line) { if (strpos($line, $to_url) !== false) { if (preg_match("!]+?".$to_url."[^>]*>([^>]+?)!",$line,$m)) { $excerpt = strip_tags($line); break; } } } if ($excerpt) { $excerpt = '(…) '.text::cutString(html::escapeHTML($excerpt),200).' (…)'; } else { $excerpt = '(…)'; } $this->addBacklink($posts->post_id,$from_url,'',$title,$excerpt,$comment); } catch (Exception $e) { throw new Exception(__('Sorry, an internal problem has occured.'),0); } return __('Thanks, mate. It was a pleasure.'); } /** Receives a webmention and insert it as a comment of given post. NB: plugin Fair Trackback check source content to find url. @return null Null on success, else throw an exception */ public function receiveWebmention() { $err = $post_id = false; header('Content-Type: text/html; charset=UTF-8'); try { # Check if post and target are valid URL if (empty($_POST['source']) || empty($_POST['target'])) { throw new Exception('Source or target is not valid',0); } $from_url = urldecode($_POST['source']); $to_url = urldecode($_POST['target']); self::checkURLs($from_url,$to_url); # Try to find post $posts = $this->getTargetPost($to_url); $post_id = $posts->post_id; # Check if it's an updated mention if ($this->pingAlreadyDone($post_id,$from_url)) { $this->delBacklink($post_id,$from_url); } # Create a comment for received webmention $remote_content = $this->getRemoteContent($from_url); # We want a title... if (!preg_match('!([^<].*?)!mis',$remote_content,$m)) { throw new Exception(__('Where\'s your title?'), 0); } $title = trim(html::clean($m[1])); $title = html::decodeEntities($title); $title = html::escapeHTML($title); $title = text::cutString($title,60); preg_match('!]*?>(.*)?!msi',$remote_content,$m); $source = $m[1]; $source = preg_replace('![\r\n\s]+!ms',' ',$source); $source = preg_replace( "/<\/*(h\d|p|th|td|li|dt|dd|pre|caption|input|textarea|button)[^>]*>/","\n\n",$source ); $source = strip_tags($source,''); $source = explode("\n\n",$source); $excerpt = ''; foreach ($source as $line) { if (strpos($line, $to_url) !== false) { if (preg_match("!]+?".$to_url."[^>]*>([^>]+?)!",$line,$m)) { $excerpt = strip_tags($line); break; } } } if ($excerpt) { $excerpt = '(…) '.text::cutString(html::escapeHTML($excerpt),200).' (…)'; } else { $excerpt = '(…)'; } $this->addBacklink($post_id,$from_url,'',$title,$excerpt,$comment); # All done, thanks $code = $this->core->blog->settings->system->trackbacks_pub ? 200 : 202; http::head($code); return; } catch (Exception $e) { $err = $e->getMessage(); } http::head(400); echo $err ?: 'Something went wrong.'; return; } /** Check if a post previously received a ping a from an URL. @param post_id integer Post ID @param from_url string Source URL @return boolean */ private function pingAlreadyDone($post_id,$from_url) { $params = array( 'post_id' => $post_id, 'comment_site' => $from_url, 'comment_trackback' => 1, ); $rs = $this->core->blog->getComments($params,true); if ($rs && !$rs->isEmpty()) { return ($rs->f(0)); } return false; } /** Create a comment marked as trackback for a given post. @param post_id integer Post ID @param url string Discovered URL @param blog name string Source blog name @param title string Comment title @param excerpt string Source excerpt @param comment string Comment content */ private function addBacklink($post_id,$url,$blog_name,$title,$excerpt,&$comment) { if (empty($blog_name)) { // Let use title as text link for this backlink $blog_name = ($title ?: 'Anonymous blog'); } $comment = "\n". '

'.($title ?: $blog_name)."

\n". '

'.$excerpt.'

'; $cur = $this->core->con->openCursor($this->core->prefix.'comment'); $cur->comment_author = (string) $blog_name; $cur->comment_site = (string) $url; $cur->comment_content = (string) $comment; $cur->post_id = $post_id; $cur->comment_trackback = 1; $cur->comment_status = $this->core->blog->settings->system->trackbacks_pub ? 1 : -1; $cur->comment_ip = http::realIP(); # --BEHAVIOR-- publicBeforeTrackbackCreate $this->core->callBehavior('publicBeforeTrackbackCreate',$cur); if ($cur->post_id) { $comment_id = $this->core->blog->addComment($cur); # --BEHAVIOR-- publicAfterTrackbackCreate $this->core->callBehavior('publicAfterTrackbackCreate',$cur,$comment_id); } } /** Delete previously received comment made from an URL for a given post. @param post_id integer Post ID @param url string Source URL */ private function delBacklink($post_id,$url) { $this->con->execute( 'DELETE FROM '.$this->core->prefix.'comment '. 'WHERE post_id = '.((integer) $post_id).' '. "AND comment_site = '".$this->core->con->escape((string) $url)."' ". 'AND comment_trackback = 1 ' ); } /** Find Charset from HTTP headers. @param header string Source header @return string */ private static function getCharsetFromRequest($header = '') { if (!$header && isset($_SERVER['CONTENT_TYPE'])) { $header = $_SERVER['CONTENT_TYPE']; } if ($header) { if (preg_match('|charset=([a-zA-Z0-9-]+)|',$header,$m)) { return $m[1]; } } return null; } /** Detect encoding. @param content string Source URL @return string */ private static function detectCharset($content) { return mb_detect_encoding($content, 'UTF-8,ISO-8859-1,ISO-8859-2,ISO-8859-3,'. 'ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,'. 'ISO-8859-9,ISO-8859-10,ISO-8859-13,ISO-8859-14,ISO-8859-15'); } /** Retreive local post from a given URL @param to_url string Target URL @return string */ private function getTargetPost($to_url) { $reg = '!^'.preg_quote($this->core->blog->url).'(.*)!'; $type = $args = $next = ''; # Are you dumb? if (!preg_match($reg,$to_url,$m)) { throw new Exception(__('Any chance you ping one of my contents? No? Really?'),0); } # Does the targeted URL look like a registered post type? $url_part = $m[1]; $p_type = ''; $post_types = $this->core->getPostTypes(); foreach ($post_types as $k => $v) { $reg = '!^'.preg_quote(str_replace('%s','',$v['public_url'])).'(.*)!'; if (preg_match($reg,$url_part,$n)) { $p_type = $k; $post_url = $n[1]; break; } } if (empty($p_type)) { throw new Exception(__('Sorry but you can not ping this type of content.'),33); } # Time to see if we've got a winner... $params = array( 'post_type' => $p_type, 'post_url' => $post_url, ); $posts = $this->core->blog->getPosts($params); # Missed! if ($posts->isEmpty()) { throw new Exception(__('Oops. Kinda "not found" stuff. Please check the target URL twice.'),33); } # Nice try. But, sorry, no. if (!$posts->trackbacksActive()) { throw new Exception(__('Sorry, dude. This entry does not accept pingback at the moment.'),33); } return $posts; } /** Returns content of a distant page @param from_url string Target URL @return string */ private function getRemoteContent($from_url) { $http = self::initHttp($from_url,$from_path); # First round : just to be sure the ping comes from an acceptable resource type. $http->setHeadersOnly(true); $http->get($from_path); $c_type = explode(';',$http->getHeader('content-type')); # Bad luck. Bye, bye... if (!in_array($c_type[0],array('text/html','application/xhtml+xml'))) { throw new Exception(__('Your source URL does not look like a supported content type. Sorry. Bye, bye!'),0); } # Second round : let's go fetch and parse the remote content $http->setHeadersOnly(false); $http->get($from_path); $remote_content = $http->getContent(); # Convert content charset $charset = self::getCharsetFromRequest($http->getHeader('content-type')); if (!$charset) { $charset = self::detectCharset($remote_content); } if (strtolower($charset) != 'utf-8') { $remote_content = iconv($charset,'UTF-8',$remote_content); } return $remote_content; } //@} /// @name Discover //@{ /** Returns an array containing all discovered trackbacks URLs in $text. @param text string Input text @return array */ public function discover($text) { $res = array(); foreach ($this->getTextLinks($text) as $link) { if (($url = $this->getPingURL($link)) !== null) { $res[] = $url; } } return $res; } /** Find links into a text. @param text string Text to scan @return array */ private function getTextLinks($text) { $res = array(); # href attribute on "a" tags if (preg_match_all('/]+)>/ms',$text,$match,PREG_SET_ORDER)) { for ($i = 0; $i]+)>/ms',$text,$match,PREG_SET_ORDER)) { for ($i = 0; $istring URL to scan @return string */ private function getPingURL($url) { if (strpos($url,'/') === 0) { $url = http::getHost().$url; } try { $http = self::initHttp($url,$path); $http->get($path); $page_content = $http->getContent(); $pb_url = $http->getHeader('x-pingback'); $wm_url = $http->getHeader('link'); } catch (Exception $e) { return false; } # Let's check for an elderly trackback data chunk... $pattern_rdf = '/.*?'. ''. '.*?<\/rdf:RDF>'. '/msi'; preg_match_all($pattern_rdf,$page_content,$rdf_all,PREG_SET_ORDER); $url_path = parse_url($url, PHP_URL_PATH); $sanitized_url = str_replace($url_path, html::sanitizeURL($url_path),$url); for ($i=0; $igetHeader('content-type')); if (!in_array($type[0],array('text/html','application/xhtml+xml'))) { $wm_url = false; } } # Check HTTP headers for a Link: ; rel="webmention" $wm_api = false; if ($wm_url) { if(preg_match('~<((?:https?://)?[^>]+)>; rel="?(?:https?://webmention.org/?|webmention)"?~',$wm_url,$match)) { if (filter_var($match[1],FILTER_VALIDATE_URL) && preg_match('!^https?:!',$match[1])) { $wm_api = $match[1]; } } } # Else check content for if ($wm_url && !$wm_api) { $content = preg_replace('//Us','',$page_content); if (preg_match('/<(?:link|a)[ ]+href="([^"]*)"[ ]+rel="[^" ]* ?webmention ?[^" ]*"[ ]*\/?>/i',$content,$match) || preg_match('/<(?:link|a)[ ]+rel="[^" ]* ?webmention ?[^" ]*"[ ]+href="([^"]*)"[ ]*\/?>/i',$content,$match)) { $wm_api = $match[1]; } } # We have a winner, let's add some tricks to make diference if ($wm_api) { return $wm_api.'|'.$url.'|webmention'; } return null; } //@} /** HTTP helper. @param url string URL @param path string Path @return object */ private static function initHttp($url,&$path) { $client = netHttp::initClient($url,$path); $client->setTimeout(5); $client->setUserAgent('Dotclear - http://www.dotclear.org/'); $client->useGzip(false); $client->setPersistReferers(false); return $client; } /** URL helper. @param from_url string URL a @param to_url string URL b */ public static function checkURLs($from_url,$to_url) { if (!(filter_var($from_url,FILTER_VALIDATE_URL) && preg_match('!^https?://!',$from_url))) { throw new Exception(__('No valid source URL provided? Try again!'), 0); } if (!(filter_var($to_url,FILTER_VALIDATE_URL) && preg_match('!^https?://!',$to_url))) { throw new Exception(__('No valid target URL provided? Try again!'), 0); } if (html::sanitizeURL(urldecode($from_url)) == html::sanitizeURL(urldecode($to_url))) { throw new Exception(__('LOL!'), 0); } } }