Dotclear

source: inc/core/class.dc.trackback.php @ 1689:6a99bdc5ed01

Revision 1689:6a99bdc5ed01, 15.4 KB checked in by Florent Cotton <florent.cotton@…>, 11 years ago (diff)

Réception des trackbacks/pingbacks évitant autant que possible les doublons. (close #1629)

RevLine 
[0]1<?php
2# -- BEGIN LICENSE BLOCK ---------------------------------------
3#
4# This file is part of Dotclear 2.
5#
[1179]6# Copyright (c) 2003-2013 Olivier Meunier & Association Dotclear
[0]7# Licensed under the GPL version 2.0 license.
8# See LICENSE file or
9# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
10#
11# -- END LICENSE BLOCK -----------------------------------------
12if (!defined('DC_RC_PATH')) { return; }
13
14/**
15@ingroup DC_CORE
[1684]16@brief Trackbacks/Pingbacks sender and server
[0]17
[1684]18Sends and receives trackbacks/pingbacks. Also handles trackbacks/pingbacks auto discovery.
[0]19*/
20class dcTrackback
21{
22     public $core;       ///< <b>dcCore</b> dcCore instance
23     public $table;      ///< <b>string</b> done pings table name
24     
25     /**
26     Object constructor
27     
28     @param    core      <b>dcCore</b>       dcCore instance
29     */
30     public function __construct($core)
31     {
32          $this->core =& $core;
33          $this->con =& $this->core->con;
34          $this->table = $this->core->prefix.'ping';
35     }
36     
37     /// @name Send trackbacks
38     //@{
39     /**
40     Get all pings sent for a given post.
41     
42     @param    post_id   <b>integer</b>      Post ID
43     @return   <b>record</b>
44     */
45     public function getPostPings($post_id)
46     {
47          $strReq = 'SELECT ping_url, ping_dt '.
48                    'FROM '.$this->table.' '.
49                    'WHERE post_id = '.(integer) $post_id;
50         
51          return $this->con->select($strReq);
52     }
53     
54     /**
55     Sends a ping to given <var>$url</var>.
56     
57     @param    url            <b>string</b>       URL to ping
58     @param    post_id        <b>integer</b>      Post ID
59     @param    post_title     <b>string</b>       Post title
60     @param    post_excerpt   <b>string</b>       Post excerpt
61     @param    post_url       <b>string</b>       Post URL
62     */
63     public function ping($url,$post_id,$post_title,$post_excerpt,$post_url)
64     {
65          if ($this->core->blog === null) {
66               return false;
67          }
68         
69          $post_id = (integer) $post_id;
70         
71          # Check for previously done trackback
72          $strReq = 'SELECT post_id, ping_url FROM '.$this->table.' '.
73                    'WHERE post_id = '.$post_id.' '.
74                    "AND ping_url = '".$this->con->escape($url)."' ";
75         
76          $rs = $this->con->select($strReq);
77         
78          if (!$rs->isEmpty()) {
79               throw new Exception(sprintf(__('%s has still been pinged'),$url));
80          }
81         
[1671]82          $ping_parts = explode('|',$url);
[0]83         
[1671]84          # Let's walk by the trackback way
85          if (count($ping_parts) < 2) {
86               $data = array(
87                    'title' => $post_title,
88                    'excerpt' => $post_excerpt,
89                    'url' => $post_url,
90                    'blog_name' => trim(html::escapeHTML(html::clean($this->core->blog->name)))
91                    //,'__debug' => false
92               );
93               
94               # Ping
95               try
96               {
97                    $http = self::initHttp($url,$path);
98                    $http->post($path,$data,'UTF-8');
99                    $res = $http->getContent();
100               }
101               catch (Exception $e)
102               {
103                    throw new Exception(__('Unable to ping URL'));
104               }
105               
106               $pattern =
107               '|<response>.*<error>(.*)</error>(.*)'.
108               '(<message>(.*)</message>(.*))?'.
109               '</response>|msU';
110               
111               if (!preg_match($pattern,$res,$match))
112               {
113                    throw new Exception(sprintf(__('%s is not a ping URL'),$url));
114               }
115               
116               $ping_error = trim($match[1]);
117               $ping_msg = (!empty($match[4])) ? $match[4] : '';
[0]118          }
[1671]119          # Damnit ! Let's play pingback
120          else {
121               try {
122                    $xmlrpc = new xmlrpcClient($ping_parts[0]);
123                    $res = $xmlrpc->query('pingback.ping', $post_url, $ping_parts[1]);
124                    $ping_error = '0';
125               }
126               catch (xmlrpcException $e) {
127                    $ping_error = $e->getCode();
128                    $ping_msg = $e->getMessage(); 
129               }
130               catch (Exception $e) {
131                    throw new Exception(__('Unable to ping URL'));
132               }
[0]133          }
134         
135          if ($ping_error != '0') {
136               throw new Exception(sprintf(__('%s, ping error:'),$url).' '.$ping_msg);
137          } else {
138               # Notify ping result in database
139               $cur = $this->con->openCursor($this->table);
140               $cur->post_id = $post_id;
141               $cur->ping_url = $url;
142               $cur->ping_dt = date('Y-m-d H:i:s');
143               
144               $cur->insert();
145          }
146     }
147     //@}
148     
[1689]149     private function pingAlreadyDone($post_id, $from_url)
150     {
151          $params = array(
152               'post_id' => $post_id,
153               'comment_site' => $from_url,
154               'comment_trackback' => 1,
155          );
156         
157          $rs = $this->core->blog->getComments($params, true);
158          if ($rs && !$rs->isEmpty()) {
159               return ($rs->f(0));
160          }
161         
162          return false;
163     }
164     
[1684]165     private function addBacklink($post_id, $url, $blog_name, $title, $excerpt, &$comment)
166     {
167          if (empty($blog_name)) {
168               $blog_name = 'Anonymous blog';
169          }
170         
171          $comment =
172          "<!-- TB -->\n".
173          '<p><strong>'.($title ? $title : $blog_name)."</strong></p>\n".
174          '<p>'.$excerpt.'</p>';
175         
176          $cur = $this->core->con->openCursor($this->core->prefix.'comment');
177          $cur->comment_author = (string) $blog_name;
178          $cur->comment_site = (string) $url;
179          $cur->comment_content = (string) $comment;
180          $cur->post_id = $post_id;
181          $cur->comment_trackback = 1;
182          $cur->comment_status = $this->core->blog->settings->system->trackbacks_pub ? 1 : -1;
183          $cur->comment_ip = http::realIP();
184         
185          # --BEHAVIOR-- publicBeforeTrackbackCreate
186          $this->core->callBehavior('publicBeforeTrackbackCreate',$cur);
187          if ($cur->post_id) {
188               $comment_id = $this->core->blog->addComment($cur);
189               
190               # --BEHAVIOR-- publicAfterTrackbackCreate
191               $this->core->callBehavior('publicAfterTrackbackCreate',$cur,$comment_id);
192          }
193     }
194     
[0]195     /// @name Receive trackbacks
196     //@{
197     /**
198     Receives a trackback and insert it as a comment of given post.
199     
200     @param    post_id        <b>integer</b>      Post ID
201     */
202     public function receive($post_id)
203     {
204          header('Content-Type: text/xml; charset=UTF-8');
205          if (empty($_POST)) {
206               http::head(405,'Method Not Allowed');
207               echo
208               '<?xml version="1.0" encoding="utf-8"?>'."\n".
209               "<response>\n".
210               "  <error>1</error>\n".
211               "  <message>POST request needed</message>\n".
212               "</response>";
213               return;
214          }
215         
216          $post_id = (integer) $post_id;
217         
218          $title = !empty($_POST['title']) ? $_POST['title'] : '';
219          $excerpt = !empty($_POST['excerpt']) ? $_POST['excerpt'] : '';
220          $url = !empty($_POST['url']) ? $_POST['url'] : '';
221          $blog_name = !empty($_POST['blog_name']) ? $_POST['blog_name'] : '';
222          $charset = '';
223          $comment = '';
[1689]224                   
[0]225          $err = false;
226          $msg = '';
227         
228          if ($this->core->blog === null)
229          {
230               $err = true;
231               $msg = 'No blog.';
232          }
233          elseif ($url == '')
234          {
235               $err = true;
236               $msg = 'URL parameter is required.';
237          }
238          elseif ($blog_name == '') {
239               $err = true;
240               $msg = 'Blog name is required.';
241          }
242         
243          if (!$err)
244          {
245               $post = $this->core->blog->getPosts(array('post_id'=>$post_id,'post_type'=>''));
246               
247               if ($post->isEmpty())
248               {
249                    $err = true;
250                    $msg = 'No such post.';
251               }
252               elseif (!$post->trackbacksActive())
253               {
254                    $err = true;
255                    $msg = 'Trackbacks are not allowed for this post or weblog.';
256               }
[1689]257
258               $url = trim(html::clean($url));
259               if ($this->pingAlreadyDone($post->post_id, $url)) {
260                    $err = true;
261                    $msg = 'The trackback has already been registered';
262               }
[0]263          }
264         
265          if (!$err)
266          {
267               $charset = self::getCharsetFromRequest();
268               
269               if (!$charset) {
[1684]270                    $charset = self::detectCharset($title.' '.$excerpt.' '.$blog_name);
[0]271               }
272               
273               if (strtolower($charset) != 'utf-8') {
274                    $title = iconv($charset,'UTF-8',$title);
275                    $excerpt = iconv($charset,'UTF-8',$excerpt);
276                    $blog_name = iconv($charset,'UTF-8',$blog_name);
277               }
278               
279               $title = trim(html::clean($title));
280               $title = html::decodeEntities($title);
281               $title = html::escapeHTML($title);
282               $title = text::cutString($title,60);
283               
284               $excerpt = trim(html::clean($excerpt));
285               $excerpt = html::decodeEntities($excerpt);
286               $excerpt = preg_replace('/\s+/ms',' ',$excerpt);
287               $excerpt = text::cutString($excerpt,252); 
288               $excerpt = html::escapeHTML($excerpt).'...';
289               
290               $blog_name = trim(html::clean($blog_name));
291               $blog_name = html::decodeEntities($blog_name);
292               $blog_name = html::escapeHTML($blog_name);
293               $blog_name = text::cutString($blog_name,60);
294               
295               try
296               {
[1684]297                    $this->addBacklink($post_id, $url, $blog_name, $title, $excerpt, $comment);
[0]298               }
299               catch (Exception $e)
300               {
301                    $err = 1;
302                    $msg = 'Something went wrong : '.$e->getMessage();
303               }
304          }
305         
306          $resp =
307          '<?xml version="1.0" encoding="utf-8"?>'."\n".
308          "<response>\n".
309          '  <error>'.(integer) $err."</error>\n";
310         
311          if ($msg) {
312               $resp .= '  <message>'.$msg."</message>\n";
313          }
314         
315          if (!empty($_POST['__debug'])) {
[1684]316               $resp .= 
317               "  <debug>\n".
318               '    <title>'.$title."</title>\n".
319               '    <excerpt>'.$excerpt."</excerpt>\n".
320               '    <url>'.$url."</url>\n".
321               '    <blog_name>'.$blog_name."</blog_name>\n".
322               '    <charset>'.$charset."</charset>\n".
323               '    <comment>'.$comment."</comment>\n".
324               "  </debug>\n";
[0]325          }
326         
327          echo $resp."</response>";
328     }
329     //@}
[1674]330
331     /// @name Receive pingbacks
332     //@{
333     /**
334     Receives a pingback and insert it as a comment of given post.
335     
336     @param    from_url       <b>string</b>       Source URL
337     @param    to_url              <b>string</b>       Target URL
338     */
339     public function receive_pb($from_url, $to_url)
340     {
341          $reg = '!^'.preg_quote($this->core->blog->url).'(.*)!';
342          $type = $args = $next = '';
343         
344          # Are you dumb?
345          if (!preg_match($reg, $to_url, $m)) {
346               throw new Exception(__('Any chance you ping one of my contents? No? Really?'), 0);
347          }
348         
349          # Does the targeted URL look like a registered post type?
350          $url_part = $m[1];
351          $p_type = '';
352          $post_types = $this->core->getPostTypes();
353          foreach ($post_types as $k => $v) {
354               $reg = '!^'.preg_quote(str_replace('%s', '', $v['public_url'])).'(.*)!';
355               if (preg_match($reg, $url_part, $n)) {
356                    $p_type = $k;
357                    $post_url = $n[1];
358                    break;
359               }
360          }
361         
362          if (empty($p_type)) {
363               throw new Exception(__('Sorry but you can not ping this type of content.'), 33);
364          }
365
366          # Time to see if we've got a winner...
367          $params = array(
368               'post_type' => $p_type,
369               'post_url' => $post_url,
370          );
371          $posts = $this->core->blog->getPosts($params);
372         
373          # Missed!
374          if ($posts->isEmpty()) {
375               throw new Exception(__('Oops. Kinda "not found" stuff. Please check the target URL twice.'), 33);
376          }
377         
378          # Nice try. But, sorry, no.
379          if (!$posts->trackbacksActive()) {
380               throw new Exception(__('Sorry, dude. This entry does not accept pingback at the moment.'), 33);
381          }
382
[1689]383          if ($this->pingAlreadyDone($posts->post_id, $from_url)) {
384               throw new Exception(__('Don\'t repeat yourself, please.'), 48);
385          }
386         
[1674]387          # OK. We've found our champion. Time to check the remote part.
388          try {
389               $http = self::initHttp($from_url, $from_path);
390               
391               # First round : just to be sure the ping comes from an acceptable resource type.
392               $http->setHeadersOnly(true);
393               $http->get($from_path);
394               $c_type = explode(';', $http->getHeader('content-type'));
395
396               # Bad luck. Bye, bye...
397               if (!in_array($c_type[0],array('text/html', 'application/xhtml+xml'))) {
398                    throw new Exception(__('Your source URL does not look like a supported content type. Sorry. Bye, bye!'), 0);
399               }
400               
401               # Second round : let's go fetch and parse the remote content
402               $http->setHeadersOnly(false);
403               $http->get($from_path);
404               $remote_content = $http->getContent();
405
[1684]406               $charset = self::getCharsetFromRequest($http->getHeader('content-type'));
[1674]407
[1684]408               if (!$charset) {
409                    $charset = self::detectCharset($remote_content);
410               }
411               
[1674]412               if (strtolower($charset) != 'utf-8') {
413                    $remote_content = iconv($charset,'UTF-8',$remote_content);
414               }
415               
416               # We want a title...
417               if (!preg_match('!<title>([^<].*?)</title>!mis', $remote_content, $m)) {
418                    throw new Exception(__('Where\'s your title?'), 0);
419               }
420               $title = trim(html::clean($m[1]));
421               $title = html::decodeEntities($title);
422               $title = html::escapeHTML($title);
423               $title = text::cutString($title,60);
424               
425               preg_match('!<body[^>]*?>(.*)?</body>!msi', $remote_content, $m);
426               $source = $m[1];
427               $source = preg_replace('![\r\n\s]+!ms',' ',$source);
428               $source = preg_replace( "/<\/*(h\d|p|th|td|li|dt|dd|pre|caption|input|textarea|button)[^>]*>/", "\n\n", $source );
429               $source = strip_tags($source, '<a>');
430               $source = explode("\n\n",$source);
431               
432               $excerpt = '';
433               foreach ($source as $line) {
434                    if (strpos($line, $to_url) !== false) {
435                         if (preg_match("!<a[^>]+?".$to_url."[^>]*>([^>]+?)</a>!", $line, $m)) {
436                              $excerpt = strip_tags($line);
437                              break;
438                         }
439                    }
440               }
441               if ($excerpt) {
[1684]442                    $excerpt = '(&#8230;) '.text::cutString(html::escapeHTML($excerpt),200).' (&#8230;)';
[1674]443               }
444               else {
[1684]445                    $excerpt = '(&#8230;)';
[1674]446               }
447
[1684]448               $this->addBacklink($posts->post_id, $from_url, '', $title, $excerpt, $comment);
[1674]449          }
450          catch (Exception $e) {
451               throw new Exception(__('Sorry, an internal problem has occured.'), 0);
452          }
453         
454          return __('Thanks, mate. It was a pleasure.');
455     }
456     //@}
[0]457     
458     private static function initHttp($url,&$path)
459     {
460          $client = netHttp::initClient($url,$path);
461          $client->setTimeout(5);
462          $client->setUserAgent('Dotclear - http://www.dotclear.org/');
463          $client->useGzip(false);
464          $client->setPersistReferers(false);
465         
466          return $client;
467     }
468     
[1684]469     private static function getCharsetFromRequest($header = '')
[0]470     {
[1684]471          if (!$header && isset($_SERVER['CONTENT_TYPE'])) {
472               $header = $_SERVER['CONTENT_TYPE'];
473          }
474         
475          if ($header) {
476               if (preg_match('|charset=([a-zA-Z0-9-]+)|',$header,$m)) {
[0]477                    return $m[1];
478               }
479          }
480         
481          return null;
482     }
[1684]483
484     private static function detectCharset($string)
485     {
486          return mb_detect_encoding($remote_content,
487                    'UTF-8,ISO-8859-1,ISO-8859-2,ISO-8859-3,'.
488                    'ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,'.
489                    'ISO-8859-9,ISO-8859-10,ISO-8859-13,ISO-8859-14,ISO-8859-15');
490     }
491         
[0]492     /// @name Trackbacks auto discovery
493     //@{
494     /**
495     Returns an array containing all discovered trackbacks URLs in
496     <var>$text</var>.
497     
498     @param    text      <b>string</b>       Input text
499     @return   <b>array</b>
500     */
501     public function discover($text)
502     {
503          $res = array();
504         
505          foreach ($this->getTextLinks($text) as $link)
506          {
507               if (($url = $this->getPingURL($link)) !== null) {
508                    $res[] = $url;
509               }
510          }
511         
512          return $res;
513     }
514     //@}
515     
516     private function getTextLinks($text)
517     {
518          $res = array();
519         
520          # href attribute on "a" tags
521          if (preg_match_all('/<a ([^>]+)>/ms', $text, $match, PREG_SET_ORDER))
522          {
523               for ($i = 0; $i<count($match); $i++)
524               {
[1672]525                    if (preg_match('/href="((https?:\/)?\/[^"]+)"/ms', $match[$i][1], $matches)) {
[0]526                         $res[$matches[1]] = 1;
527                    }
528               }
529          }
530          unset($match);
531         
532          # cite attributes on "blockquote" and "q" tags
533          if (preg_match_all('/<(blockquote|q) ([^>]+)>/ms', $text, $match, PREG_SET_ORDER))
534          {
535               for ($i = 0; $i<count($match); $i++)
536               {
[1672]537                    if (preg_match('/cite="((https?:\/)?\/[^"]+)"/ms', $match[$i][2], $matches)) {
[0]538                         $res[$matches[1]] = 1;
539                    }
540               }
541          }
542         
543          return array_keys($res);
544     }
545     
546     private function getPingURL($url)
547     {
[1672]548          if (strpos($url,'/') === 0) {
549               $url = http::getHost().$url;
550          }
551         
[0]552          try
553          {
554               $http = self::initHttp($url,$path);
555               $http->get($path);
556               $page_content = $http->getContent();
[1671]557               $pb_url = $http->getHeader('x-pingback');
[0]558          }
559          catch (Exception $e)
560          {
561               return false;
562          }
563         
[1671]564          # If we've got a X-Pingback header and it's a valid URL, it will be enough
565          if ($pb_url && filter_var($pb_url,FILTER_VALIDATE_URL) && preg_match('!^https?:!',$pb_url)) {
566               return $pb_url.'|'.$url;
567          }
568         
[1673]569          # No X-Pingback header. A link rel=pingback, maybe ?
570          $pattern_pingback = '!<link rel="pingback" href="(.*?)"( /)?>!msi';
571         
572          if (preg_match($pattern_pingback,$page_content,$m)) {
573               $pb_url = $m[1];
574               if (filter_var($pb_url,FILTER_VALIDATE_URL) && preg_match('!^https?:!',$pb_url)) {
575                    return $pb_url.'|'.$url;
576               }
577          }
578
579          # No pingback ? OK, let's check for a trackback data chunk...
[0]580          $pattern_rdf =
581          '/<rdf:RDF.*?>.*?'.
582          '<rdf:Description\s+(.*?)\/>'.
583          '.*?<\/rdf:RDF>'.
584          '/msi';
585         
586          preg_match_all($pattern_rdf,$page_content,$rdf_all,PREG_SET_ORDER);
587         
[1671]588          $url_path = parse_url($url, PHP_URL_PATH);
589          $sanitized_url = str_replace($url_path, html::sanitizeURL($url_path), $url);
590         
[0]591          for ($i=0; $i<count($rdf_all); $i++)
592          {
593               $rdf = $rdf_all[$i][1];
[1671]594               if (preg_match('/dc:identifier="'.preg_quote($url,'/').'"/msi',$rdf) ||
595                    preg_match('/dc:identifier="'.preg_quote($sanitized_url,'/').'"/msi',$rdf)) {
[0]596                    if (preg_match('/trackback:ping="(.*?)"/msi',$rdf,$tb_link)) {
597                         return $tb_link[1];
598                    }
599               }
600          }
601         
602          return null;
603     }
604}
605?>
Note: See TracBrowser for help on using the repository browser.

Sites map