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; } /** 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 < count($match); $i++) { if (preg_match('/href="((https?:\/)?\/[^"]+)"/ms', $match[$i][1], $matches)) { $res[$matches[1]] = 1; } } } unset($match); # cite attributes on "blockquote" and "q" tags if (preg_match_all('/<(blockquote|q) ([^>]+)>/ms', $text, $match, PREG_SET_ORDER)) { for ($i = 0; $i < count($match); $i++) { if (preg_match('/cite="((https?:\/)?\/[^"]+)"/ms', $match[$i][2], $matches)) { $res[$matches[1]] = 1; } } } return array_keys($res); } /** Check remote header/content to find api trace. @param url string 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; $i < count($rdf_all); $i++) { $rdf = $rdf_all[$i][1]; if (preg_match('/dc:identifier="' . preg_quote($url, '/') . '"/msi', $rdf) || preg_match('/dc:identifier="' . preg_quote($sanitized_url, '/') . '"/msi', $rdf)) { if (preg_match('/trackback:ping="(.*?)"/msi', $rdf, $tb_link)) { return $tb_link[1]; } } } # No trackback ? OK, let see if we've got a X-Pingback header and it's a valid URL, it will be enough if ($pb_url && filter_var($pb_url, FILTER_VALIDATE_URL) && preg_match('!^https?:!', $pb_url)) { return $pb_url . '|' . $url; } # No X-Pingback header. A link rel=pingback, maybe ? $pattern_pingback = '!!msi'; if (preg_match($pattern_pingback, $page_content, $m)) { $pb_url = $m[1]; if (filter_var($pb_url, FILTER_VALIDATE_URL) && preg_match('!^https?:!', $pb_url)) { return $pb_url . '|' . $url; } } # Nothing, let's try webmention. Only support x/html content if ($wm_url) { $type = explode(';', $http->getHeader('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; } //@} /** 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); } } }