PageRenderTime 40ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/system/plugins/pingback/pingback.plugin.php

https://github.com/HabariMag/habarimag-old
PHP | 397 lines | 250 code | 48 blank | 99 comment | 55 complexity | 62182a10e7d37de972f5f6bc527252db MD5 | raw file
Possible License(s): Apache-2.0
  1. <?php
  2. /**
  3. * Habari Pingback plugin, enables pingback support between sites.
  4. * @link http://www.hixie.ch/specs/pingback/pingback The Pingback spec
  5. *
  6. * @package Habari
  7. */
  8. class Pingback extends Plugin
  9. {
  10. /**
  11. * Register the Pingback event type with the event log
  12. */
  13. public function action_plugin_activation( $file )
  14. {
  15. if ( realpath( $file ) == __FILE__ ) {
  16. EventLog::register_type( 'Pingback' );
  17. }
  18. }
  19. /**
  20. * Unregister the Pingback event type on deactivation
  21. * @todo Should we be doing this?
  22. */
  23. public function action_plugin_deactivation( $file )
  24. {
  25. if ( realpath( $file ) == __FILE__ ) {
  26. EventLog::unregister_type( 'Pingback' );
  27. }
  28. }
  29. /**
  30. * Pingback links from the post content when a post is inserted into the database.
  31. * @param post The post from which to send pingbacks
  32. */
  33. public function action_post_insert_after( $post )
  34. {
  35. // only execute if this is a published post
  36. if ( Post::status( 'published' ) != $post->status ) {
  37. return;
  38. }
  39. $this->pingback_all_links( $post->content, $post->permalink, $post );
  40. }
  41. /**
  42. * Pingback mentioned links when a post is updated.
  43. * @param Post $post The post is updated
  44. * We invoke this function regardless of what might have been updated
  45. * in the post because:
  46. * - this will only execute if the post is published
  47. * - the pingback_all_links function keeps track of links its
  48. * already pinged, so if the content hasnt changed no
  49. * pings will be sent`
  50. */
  51. public function action_post_update_after( $post )
  52. {
  53. // only execute if this is a published post
  54. if ( Post::status( 'published' ) != $post->status) {
  55. return;
  56. }
  57. $this->pingback_all_links( $post->content, $post->permalink, $post );
  58. }
  59. /**
  60. * Add the Pingback header on single post/page requests
  61. * Not to the entire site. Clever.
  62. */
  63. public function action_add_template_vars()
  64. {
  65. $action = Controller::get_action();
  66. if ( $action == 'display_post' ) {
  67. header( 'X-Pingback: ' . URL::get( 'xmlrpc' ) );
  68. }
  69. else {
  70. header( 'X-action: ' . $action );
  71. }
  72. }
  73. /**
  74. * Receive a Pingback via XMLRPC
  75. * @param array $params An array of XMLRPC parameters from the remote call
  76. * @return string The success state of the pingback
  77. */
  78. public function xmlrpc_pingback__ping( $params )
  79. {
  80. try {
  81. list( $source_uri, $target_uri )= $params;
  82. // This should really be done by an Habari core function
  83. $target_parse = InputFilter::parse_url( $target_uri );
  84. $target_stub = $target_parse['path'];
  85. $base_url = Site::get_path( 'base', true );
  86. if ( '/' != $base_url) {
  87. $target_stub = str_replace( $base_url, '', $target_stub );
  88. }
  89. $target_stub = trim( $target_stub, '/' );
  90. if ( strpos( $target_stub, '?' ) !== false ) {
  91. list( $target_stub, $query_string )= explode( '?', $target_stub );
  92. }
  93. // Can this be used as a target?
  94. $target_slug = URL::parse( $target_stub )->named_arg_values['slug'];
  95. if ( $target_slug === false ) {
  96. throw new XMLRPCException( 33 );
  97. }
  98. // Does the target exist?
  99. $target_post = Post::get( array( 'slug' => $target_slug ) );
  100. if ( $target_post === false ) {
  101. throw new XMLRPCException( 32 );
  102. }
  103. // Is comment allowed?
  104. if ( $target_post->info->comments_disabled ) {
  105. throw new XMLRPCException( 33 );
  106. }
  107. // Is this Pingback already registered?
  108. if ( Comments::get( array( 'post_id' => $target_post->id, 'url' => $source_uri, 'type' => Comment::PINGBACK ) )->count() > 0 ) {
  109. throw new XMLRPCException( 48 );
  110. }
  111. // Retrieve source contents
  112. try {
  113. $rr = new RemoteRequest( $source_uri );
  114. $rr->execute();
  115. if ( ! $rr->executed() ) {
  116. throw new XMLRPCException( 16 );
  117. }
  118. $source_contents = $rr->get_response_body();
  119. }
  120. catch ( XMLRPCException $e ) {
  121. // catch our special type of exception and re-throw it
  122. throw $e;
  123. }
  124. catch ( Exception $e ) {
  125. throw new XMLRPCException( -32300 );
  126. }
  127. // encoding is converted into internal encoding.
  128. // @todo check BOM at beginning of file before checking for a charset attribute
  129. $habari_encoding = MultiByte::hab_encoding();
  130. if ( preg_match( "/<meta[^>]+charset=([A-Za-z0-9\-\_]+)/i", $source_contents, $matches ) && strtolower( $habari_encoding ) != strtolower( $matches[1] ) ) {
  131. $ret = MultiByte::convert_encoding( $source_contents, $habari_encoding, $matches[1] );
  132. if ( $ret !== false ) {
  133. $source_contents = $ret;
  134. }
  135. }
  136. // Find the page's title
  137. preg_match( '/<title>(.*)<\/title>/is', $source_contents, $matches );
  138. $source_title = $matches[1];
  139. // Find the reciprocal links and their context
  140. preg_match( '/<body[^>]*>(.+)<\/body>/is', $source_contents, $matches );
  141. $source_contents_filtered = preg_replace( '/\s{2,}/is', ' ', strip_tags( $matches[1], '<a>' ) );
  142. if ( !preg_match( '%.{0,100}?<a[^>]*?href\\s*=\\s*("|\'|)' . $target_uri . '\\1[^>]*?'.'>(.+?)</a>.{0,100}%s', $source_contents_filtered, $source_excerpt ) ) {
  143. throw new XMLRPCException( 17 );
  144. }
  145. /** Sanitize Data */
  146. $source_excerpt = '&hellip;' . InputFilter::filter( $source_excerpt[0] ) . '&hellip;';
  147. $source_title = InputFilter::filter($source_title);
  148. $source_uri = InputFilter::filter($source_uri);
  149. /* Sanitize the URL */
  150. if (!empty($source_uri)) {
  151. $parsed = InputFilter::parse_url( $source_uri );
  152. if ( $parsed['is_relative'] ) {
  153. // guess if they meant to use an absolute link
  154. $parsed = InputFilter::parse_url( 'http://' . $source_uri );
  155. if ( ! $parsed['is_error'] ) {
  156. $source_uri = InputFilter::glue_url( $parsed );
  157. }
  158. else {
  159. // disallow relative URLs
  160. $source_uri = '';
  161. }
  162. }
  163. if ( $parsed['is_pseudo'] || ( $parsed['scheme'] !== 'http' && $parsed['scheme'] !== 'https' ) ) {
  164. // allow only http(s) URLs
  165. $source_uri = '';
  166. }
  167. else {
  168. // reconstruct the URL from the error-tolerant parsing
  169. // http:moeffju.net/blog/ -> http://moeffju.net/blog/
  170. $source_uri = InputFilter::glue_url( $parsed );
  171. }
  172. }
  173. // Add a new pingback comment
  174. $pingback = new Comment( array(
  175. 'post_id' => $target_post->id,
  176. 'name' => $source_title,
  177. 'email' => '',
  178. 'url' => $source_uri,
  179. 'ip' => sprintf( "%u", ip2long( Utils::get_ip() ) ),
  180. 'content' => $source_excerpt,
  181. 'status' => Comment::STATUS_UNAPPROVED,
  182. 'date' => HabariDateTime::date_create(),
  183. 'type' => Comment::PINGBACK,
  184. ) );
  185. $pingback->insert();
  186. // Respond to the Pingback
  187. return 'The pingback has been registered';
  188. }
  189. catch ( XMLRPCException $e ) {
  190. $e->output_fault_xml();
  191. }
  192. }
  193. /**
  194. * Send a single Pingback
  195. * @param string $source_uri The URI source of the ping (here)
  196. * @param string $target_uri The URI destination of the ping (there, the site linked to in the content)
  197. * @param Post $post The post object that is initiating the ping, used to track the pings that were sent
  198. * @todo If receive error code of already pinged, add to the successful.
  199. */
  200. public function send_pingback( $source_uri, $target_uri, $post = NULL )
  201. {
  202. // RemoteRequest makes it easier to retrieve the headers.
  203. try {
  204. $rr = new RemoteRequest( $target_uri );
  205. $rr->execute();
  206. if ( ! $rr->executed() ) {
  207. return false;
  208. }
  209. }
  210. catch ( Exception $e ) {
  211. // log the pingback error
  212. EventLog::log( _t( 'Unable to retrieve target, can\'t detect pingback endpoint. (Source: %1$s | Target: %2$s)', array( $source_uri, $target_uri ) ), 'err', 'Pingback' );
  213. return false;
  214. }
  215. $headers = $rr->get_response_headers();
  216. $body = $rr->get_response_body();
  217. // Find a Pingback endpoint.
  218. if ( isset( $headers['X-Pingback'] ) ) {
  219. $pingback_endpoint = $headers['X-Pingback'];
  220. }
  221. elseif ( preg_match( '/<link rel="pingback" href="([^"]+)" ?\/?'.'>/is', $body, $matches ) ) {
  222. $pingback_endpoint = $matches[1];
  223. }
  224. else {
  225. // No Pingback endpoint found.
  226. return false;
  227. }
  228. try {
  229. $response = XMLRPCClient::open( $pingback_endpoint )->pingback->ping( $source_uri, $target_uri );
  230. }
  231. catch ( Exception $e ) {
  232. EventLog::log( _t( 'Invalid Pingback endpoint - %1$s (Source: %2$s | Target: %3$s)', array( $pingback_endpoint, $source_uri, $target_uri ) ), 'err', 'Pingback' );
  233. return false;
  234. }
  235. if ( isset( $response->faultString ) ) {
  236. EventLog::log( _t( 'Pingback error: %1$s - %2$s (Source: %3$s | Target: %4$s)', array( $response->faultCode, $response->faultString, $source_uri, $target_uri ) ), 'err', 'Pingback' );
  237. return false;
  238. }
  239. else {
  240. // The pingback has been registered and is stored as a successful pingback.
  241. if ( is_object( $post ) ) {
  242. if ( isset( $post->info->pingbacks_successful ) ) {
  243. $pingbacks_successful = $post->info->pingbacks_successful;
  244. $pingbacks_successful[]= $target_uri;
  245. $post->info->pingbacks_successful = $pingbacks_successful;
  246. }
  247. else {
  248. $post->info->pingbacks_successful = array( $target_uri );
  249. }
  250. $post->info->commit();
  251. }
  252. return true;
  253. }
  254. }
  255. /**
  256. * Scan all links in the content and send them a Pingback.
  257. * @param string $content The post content to search
  258. * @param string $source_uri The source of the content
  259. * @param Post $post The post object of the source of the ping
  260. * @param boolean $force If true, force the system to ping all links even if that had been pinged before
  261. */
  262. public function pingback_all_links( $content, $source_uri, $post = NULL, $force = false )
  263. {
  264. $tokenizer = new HTMLTokenizer( $content, false );
  265. $tokens = $tokenizer->parse();
  266. // slice out only A tags
  267. $slices = $tokens->slice( array( 'a' ), array( ) );
  268. $urls = array();
  269. foreach ( $slices as $slice ) {
  270. // if there is no href attribute, just skip it, though there is something wrong
  271. if ( !isset( $slice[0]['attrs']['href'] ) ) {
  272. continue;
  273. }
  274. else {
  275. $url = $slice[0]['attrs']['href'];
  276. }
  277. // make sure it's a valid URL before we waste our time
  278. $parsed = InputFilter::parse_url( $url );
  279. if ( $parsed['is_error'] || $parsed['is_pseudo'] || $parsed['is_relative'] ) {
  280. continue;
  281. }
  282. else {
  283. $urls[] = $url;
  284. }
  285. }
  286. if ( is_object( $post ) && isset( $post->info->pingbacks_successful ) ) {
  287. $fn = ( $force === true ) ? 'array_merge' : 'array_diff';
  288. $links = $fn( $urls, $post->info->pingbacks_successful );
  289. }
  290. else {
  291. $links = $urls;
  292. }
  293. $links = array_unique( $links );
  294. foreach ( $links as $target_uri ) {
  295. if ( $this->send_pingback( $source_uri, $target_uri, $post ) ) {
  296. EventLog::log( _t( 'Sent pingbacks for "%1$s", target: %2$s', array( $post->title, $target_uri ) ), 'info', 'Pingback' );
  297. }
  298. }
  299. }
  300. /**
  301. * Add the pingback options to the options page
  302. * @param array $items The array of option on the options page
  303. * @return array The array of options including new options for pingback
  304. */
  305. public function filter_admin_option_items($items)
  306. {
  307. $items[_t('Publishing')]['pingback_send'] = array(
  308. 'label' => _t('Send Pingbacks to Links'),
  309. 'type' => 'checkbox',
  310. 'helptext' => '',
  311. );
  312. return $items;
  313. }
  314. /**
  315. * Returns a full qualified URL of the specified post based on the comments count.
  316. *
  317. * Passed strings are localized prior to parsing therefore to localize "%d Comments" in french, it would be "%d Commentaires".
  318. *
  319. * Since we use sprintf() in the final concatenation, you must format passed strings accordingly.
  320. *
  321. * @param Theme $theme The current theme object
  322. * @param Post $post Post object used to build the pingback link
  323. * @param string $zero String to return when there are no pingbacks
  324. * @param string $one String to return when there is one pingback
  325. * @param string $many String to return when there are more than one pingback
  326. * @return string String to display for pingback count
  327. */
  328. public function theme_pingback_count( $theme, $post, $zero = '', $one = '', $many = '' )
  329. {
  330. $count = $post->comments->pingbacks->approved->count;
  331. if ( empty( $zero ) ) {
  332. $zero = _t( '%d Pingbacks' );
  333. }
  334. if ( empty( $one ) ) {
  335. $one = _t( '%d Pingback' );
  336. }
  337. if ( empty( $many ) ) {
  338. $many = _t( '%d Pingbacks' );
  339. }
  340. if ($count >= 1) {
  341. $text = _n( $one, $many, $count );
  342. }
  343. else {
  344. $text = $zero;
  345. }
  346. return sprintf( $text, $count );
  347. }
  348. }
  349. ?>