pingback.module

Tracking 5.x-1.x branch
  1. drupal
    1. 5 contributions/pingback/pingback.module
    2. 6 contributions/pingback/pingback.module

Functions & methods

NameDescription
pingback_comment_is_pingback
pingback_cron
pingback_discoverDiscover a pingback server with pingback autodiscovery schemes.
pingback_exit
pingback_form_alter
pingback_list_page
pingback_menu
pingback_nodeapi
pingback_perm
pingback_receiveXML-RPC callback: process pingback.ping() call.
pingback_sendSend pingbacks. Does nothing if the target does not have a pingback server.
pingback_send_by_nidSends pingbacks in all URLs in specified node.
pingback_xmlrpc
theme_pingback
_pingback_extract_urls
_pingback_url_to_nid
_pingback_valid_for_node
_pingback_valid_for_node_type

File

View source
  1. <?php
  2. function pingback_perm() {
  3. return array('administer pingbacks');
  4. }
  5. function pingback_menu($may_cache) {
  6. $items = array();
  7. if ($may_cache) {
  8. $items[] = array(
  9. 'path' => 'admin/settings/pingback',
  10. 'title' => t('Pingback'),
  11. 'description' => t('Configure pingbacks.'),
  12. 'callback' => 'drupal_get_form',
  13. 'callback arguments' => array('pingback_settings_form'),
  14. 'access' => user_access('administer pingbacks'),
  15. );
  16. }
  17. else {
  18. //I developed this module with Drupal 6 pattern/convention, so page callbacks are on their own files
  19. //unconditionally load those files in Drupal 5
  20. require_once drupal_get_path('module', 'pingback') . '/pingback.admin.inc';
  21. //set the server autodiscovery
  22. //paths like node/10/edit are NOT pingback-enabled
  23. if (arg(0) == 'node' && is_numeric(arg(1)) && arg(3) == NULL) {
  24. drupal_set_header('X-Pingback: ' .$GLOBALS['base_url'] . '/xmlrpc.php');
  25. //drupal_set_html_head();
  26. }
  27. }
  28. return $items;
  29. }
  30. function pingback_form_alter($form_id, &$form) {
  31. global $user;
  32. if ($form_id == 'node_type_form' && isset($form['identity']['type'])) {
  33. $type = $form['#node_type']->type;
  34. $form['workflow']['pingback'] = array(
  35. '#type' => 'radios',
  36. '#title' => t('Pingbacks'),
  37. '#options' => array(1 => t('Enabled'), 0 => t('Disabled')),
  38. '#default_value' => _pingback_valid_for_node_type($type),
  39. '#description' => t('Enable pingbacks for this node type.')
  40. );
  41. }
  42. else if (isset($form['type']) && $form['type']['#value'] .'_node_form' == $form_id) {
  43. $node = $form['#node'];
  44. if (_pingback_valid_for_node_type($node->type)) {
  45. // if there are any past successful pingbacks from this posting, add them to the node editing page.
  46. $past_successes_listing = array();
  47. $q = db_query("SELECT url FROM {pingback_sent} WHERE nid = %d", $node->nid);
  48. while ($pb = db_fetch_object($q)) {
  49. $past_successes_listing[] = $pb->url;
  50. }
  51. // add listing of successfully pingbacked URLs
  52. if (count($past_successes_listing)) {
  53. $form['pingback'] = array(
  54. '#type' => 'fieldset',
  55. '#title' => t('Pingbacks'),
  56. '#collapsible' => TRUE
  57. );
  58. $form['pingback'][] = array(
  59. '#type' => 'markup',
  60. '#value' => theme('item_list', $past_successes_listing, t('Successfully pingbacked URLs')),
  61. );
  62. //t('These URLs have been successfuly pinged by this post.')
  63. }
  64. }
  65. }
  66. //hide pingback input format if desired for anon users
  67. else if (
  68. $form_id == 'comment_form'
  69. && (!$user->uid)
  70. && variable_get('pingback_hide_format_for_anon', 0)
  71. //&& (isset($GLOBALS['pingback_bypass_format_hiding']) ? !$GLOBALS['pingback_bypass_format_hiding'] : TRUE)
  72. ) {
  73. //dpm($form);
  74. $alternate_formats = array();
  75. foreach ($form['comment_filter']['format'] as $k => $v) {
  76. //dpm($k);
  77. if (!element_property($k) && isset($v['#return_value'])) {
  78. if ($v['#return_value'] == variable_get('pingback_input_format', FILTER_FORMAT_DEFAULT)) {
  79. unset($form['comment_filter']['format'][$k]);
  80. }
  81. else {
  82. // Make a list of alternate formats for the comment form.
  83. $alternate_formats[] = $k;
  84. }
  85. }
  86. }
  87. if (count($alternate_formats) == 1) {
  88. // There is only one available format. Remove fieldset and go back to hidden form field.
  89. $new_form[$alternate_formats[0]] = array(
  90. '#type' => 'value',
  91. '#value' => $alternate_formats[0],
  92. '#parents' => $form['comment_filter']['format'][$alternate_formats[0]]['#parents'],
  93. );
  94. $new_form['format']['guidelines'] = array(
  95. '#title' => t('Formatting guidelines'),
  96. '#value' => $form['comment_filter']['format'][$alternate_formats[0]]['#description'],
  97. );
  98. $form['comment_filter']['format'] = $new_form;
  99. }
  100. }
  101. }
  102. /**
  103. * Menu callback: lists pingbacks. TODO: ability to delete them!
  104. */
  105. //pingback_list_pingbacks() a better name?
  106. function pingback_list_page() {
  107. //$result = db_query("SELECT ");
  108. return 'TODO';
  109. }
  110. function pingback_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {
  111. if (_pingback_valid_for_node_type($node->type)) {
  112. switch ($op) {
  113. case 'insert':
  114. case 'update':
  115. if (variable_get('pingback_mode', 'off') == 'submit') {
  116. global $pingback_nid;
  117. $pingback_nid = $node->nid;
  118. }
  119. else { //mode == 'cron'
  120. //queue this nid in variable pingback_nid_queue, but take care for not queuing existing nids
  121. $q = variable_get('pingback_nid_queue', array());
  122. if (!in_array($node->nid, $q)) {
  123. $q[] = $node->nid;
  124. variable_set('pingback_nid_queue', $q);
  125. }
  126. }
  127. break;
  128. }
  129. }
  130. }
  131. function pingback_cron() {
  132. $q = variable_get('pingback_nid_queue', array());
  133. $limit = variable_get('pingback_check_per_cron', 30);
  134. $count = 0;
  135. //dpr($q);
  136. while (($nid = array_shift($q)) && ($count++ < $limit)) {
  137. pingback_send_by_nid($nid, FALSE);
  138. //dpr("Sent pingbacks in node $nid");
  139. }
  140. variable_set('pingback_nid_queue', $q);
  141. }
  142. function pingback_exit() {
  143. global $pingback_nid;
  144. if (isset($pingback_nid)) {
  145. //reset the node_load() cache
  146. node_load($pingback_nid, NULL, TRUE);
  147. pingback_send_by_nid($pingback_nid, variable_get('pingback_notify_successful_pings', 1));
  148. }
  149. }
  150. function pingback_xmlrpc() {
  151. return array(
  152. array(
  153. 'pingback.ping',
  154. 'pingback_receive',
  155. array('string', 'string', 'string'),
  156. t('Handles pingback pings.'),
  157. ),
  158. );
  159. }
  160. /**
  161. * XML-RPC callback: process pingback.ping() call.
  162. */
  163. function pingback_receive($pagelinkedfrom, $pagelinkedto) {
  164. //return xmlrpc_server_error(0, 'abcdefgh');
  165. //big thanks to WordPress codebase, specifically file xmlrpc.php, method pingback_ping() for becoming the reference implementation and theft victim ;)
  166. //note: $pagelinkedto is a URL from our own site, $pagelinkedfrom is a foreign URL
  167. if (!variable_get('pingback_receive', 1)) return xmlrpc_server_error(33, t("The specified target URL cannot be used as a target. It either doesn't exist, or it is not a pingback-enabled resource."));
  168. //don't really understand this part, supposed to unescape ampersand entities?
  169. $pagelinkedfrom = str_replace('&amp;', '&', $pagelinkedfrom);
  170. $pagelinkedto = preg_replace('#&([^amp\;])#is', '&amp;$1', $pagelinkedto);
  171. $error_code = -1;
  172. // Check if the page linked to is in our site
  173. $pos1 = strpos($pagelinkedto, str_replace(array('http://www.','http://','https://www.','https://'), '', $GLOBALS['base_url']));
  174. if( !$pos1 ) {
  175. return new xmlrpc_server_error(0, t('Is there no link to us?'));
  176. }
  177. // let's find which post is linked to
  178. $nid = _pingback_url_to_nid($pagelinkedto);
  179. //dpm("(PB) URL='$pagelinkedto' ID='$post_ID' Found='$way'");
  180. $node = $nid ? node_load($nid) : FALSE;
  181. //watchdog('debug', '--- ' . $nid . ' --- ' . print_r($node, TRUE));
  182. if (!$node || !_pingback_valid_for_node($node)) // node not found
  183. return xmlrpc_server_error(33, t("The specified target URL cannot be used as a target. It either doesn't exist, or it is not a pingback-enabled resource."));
  184. if ($nid == _pingback_url_to_nid($pagelinkedfrom))
  185. return xmlrpc_server_error(0, t('The source URL and the target URL cannot both point to the same resource.'));
  186. if (!$node->status)
  187. return xmlrpc_server_error(33, t("The specified target URL cannot be used as a target. It either doesn't exist, or it is not a pingback-enabled resource."));
  188. // Let's check that the remote site didn't already pingback this entry
  189. $result = db_query("SELECT cid FROM {comments} WHERE nid = %d AND homepage = '%s' AND format = %d", $nid, $pagelinkedfrom, variable_get('pingback_input_format', FILTER_FORMAT_DEFAULT));
  190. if (db_num_rows($result)) // We already have a Pingback from this URL
  191. return xmlrpc_server_error(48, 'The pingback has already been registered.');
  192. // very stupid, but gives time to the 'from' server to publish !
  193. sleep(1);
  194. // Let's check the remote site
  195. $r = drupal_http_request($pagelinkedfrom);
  196. if ($r->error)
  197. return xmlrpc_server_error(16, 'The source URL does not exist.');
  198. //watchdog('debug', print_r($r, TRUE));
  199. $linea = $r->data;
  200. // Work around bug in strip_tags():
  201. $linea = str_replace('<!DOC', '<DOC', $linea);
  202. $linea = preg_replace( '/[\s\r\n\t]+/', ' ', $linea ); // normalize spaces
  203. $linea = preg_replace( "/ <(h1|h2|h3|h4|h5|h6|p|th|td|li|dt|dd|pre|caption|input|textarea|button|body)[^>]*>/", "\n\n", $linea );
  204. preg_match('|<title>([^<]*?)</title>|is', $linea, $matchtitle);
  205. $title = check_plain($matchtitle[1]);
  206. if ( empty( $title ) )
  207. return xmlrpc_server_error(32, 'We cannot find a title on that page.');
  208. $linea = strip_tags( $linea, '<a>' ); // just keep the tag we need
  209. $p = explode( "\n\n", $linea );
  210. $preg_target = preg_quote($pagelinkedto);
  211. foreach ( $p as $para ) {
  212. if ( strpos($para, $pagelinkedto) !== false ) { // it exists, but is it a link?
  213. preg_match("|<a[^>]+?".$preg_target."[^>]*>([^>]+?)</a>|", $para, $context);
  214. // If the URL isn't in a link context, keep looking
  215. if (empty($context)) continue;
  216. // We're going to use this fake tag to mark the context in a bit
  217. // the marker is needed in case the link text appears more than once in the paragraph
  218. //I edited <wpcontext></wpcontext> to <dpcontext></dpcontext> so it becomes more Drupal-ish!
  219. $excerpt = preg_replace('|\</?dpcontext\>|', '', $para);
  220. // prevent really long link text
  221. if ( strlen($context[1]) > 100 )
  222. $context[1] = substr($context[1], 0, 100) . '...';
  223. $marker = '<dpcontext>'.$context[1].'</dpcontext>'; // set up our marker
  224. $excerpt = str_replace($context[0], $marker, $excerpt); // swap out the link for our marker
  225. $excerpt = strip_tags($excerpt, '<dpcontext>'); // strip all tags but our context marker
  226. $excerpt = trim($excerpt);
  227. $preg_marker = preg_quote($marker);
  228. $excerpt = preg_replace("|.*?\s(.{0,100}$preg_marker.{0,100})\s.*|s", '$1', $excerpt);
  229. $excerpt = strip_tags($excerpt); // YES, again, to remove the marker wrapper
  230. break;
  231. }
  232. }
  233. if (empty($context)) // Link to target not found
  234. return xmlrpc_server_error(17, t('The source URL does not contain a link to the target URL, and so cannot be used as a source.'));
  235. //??? can someone explain about this?
  236. $pagelinkedfrom = preg_replace('#&([^amp\;])#is', '&amp;$1', $pagelinkedfrom);
  237. //$context = '[...] ' . wp_specialchars( $excerpt ) . ' [...]';
  238. //TODO: a custom filter for $excerpt
  239. $edit = array(
  240. 'nid' => $nid,
  241. 'subject' => t('Pingback'),
  242. 'comment' => '[...] ' . $excerpt . ' [...]',
  243. 'hostname' => $_SERVER['REMOTE_ADDR'],
  244. 'format' => variable_get('pingback_input_format', FILTER_FORMAT_DEFAULT),
  245. 'name' => $title,
  246. 'homepage' => $pagelinkedfrom,
  247. );
  248. comment_save($edit);
  249. /*
  250. //bypass the hiding in pingback_form_alter() because we want to use the input format
  251. $GLOBALS['pingback_bypass_format_hiding'] = TRUE;
  252. drupal_execute('comment_form', $edit, array());
  253. $GLOBALS['pingback_bypass_format_hiding'] = FALSE;
  254. watchdog('debug', print_r(form_get_errors(), TRUE));
  255. */
  256. $message = t('Pingback from @source to @target registered! Keep the web talking! :-)', array('@source' => $pagelinkedfrom, '@target' => $pagelinkedto));
  257. //comment.module already logs new comments
  258. //watchdog('pingback', $message);
  259. return $message;
  260. }
  261. /*--- theme_pingback_* ---*/
  262. function theme_pingback($pb, $links = 0) {
  263. return theme('comment', $pb, $links);
  264. }
  265. /*--- APIs ---*/
  266. /**
  267. * Discover a pingback server with pingback autodiscovery schemes.
  268. * @param $target the absolute URL to search for its server. This should have passed check_url() first.
  269. */
  270. function pingback_discover($target) {
  271. $server = '';
  272. //#1: send a HEAD to check for X-Pingback header
  273. $r = drupal_http_request($target, array(), 'HEAD');
  274. //dpm($r);
  275. if (!$r->error) {
  276. if (is_array($r->headers) && isset($r->headers['X-Pingback'])) {
  277. $server = $r->headers['X-Pingback'];
  278. }
  279. else {
  280. //#2: search for <link rel="pingback" href="(server)" /> tags
  281. $get = drupal_http_request($target);
  282. if (!$get->error) {
  283. //dpm($get->data);
  284. //this regexp is the one provided in the spec
  285. if (preg_match('#<link rel="pingback" href="([^"]+)" ?/?>#', $get->data, $matches)) {
  286. $server = $matches[1];
  287. }
  288. }
  289. }
  290. }
  291. if (!empty($server)) {
  292. return check_url($server);
  293. }
  294. else return '';
  295. }
  296. /**
  297. * Send pingbacks. Does nothing if the target does not have a pingback server.
  298. * @param $nid the source node ID.
  299. * @param $target the target absolute URL.
  300. * @param $source_is_absolute if this value is set to TRUE, $nid is interpreted as an absolute URL (which may originate not from the host site).
  301. * @return TRUE on success, FALSE otherwise.
  302. */
  303. function pingback_send($nid, $target, $source_is_absolute = FALSE) {
  304. if (!valid_url($target, TRUE)) return FALSE;
  305. if (!$source_is_absolute) {
  306. $source = url("node/$nid", NULL, NULL, TRUE);
  307. $result = db_query("SELECT nid FROM {pingback_sent} WHERE nid = %d AND url = '%s'", $nid, $target);
  308. if (db_num_rows($result) > 0) {
  309. //dpm('oops already sent');
  310. return FALSE;
  311. }
  312. }
  313. else {
  314. $source = $nid;
  315. if (!valid_url($source)) return FALSE;
  316. }
  317. //dpm($source);
  318. $retval = FALSE;
  319. //server autodiscovery
  320. $server = pingback_discover($target);
  321. //dpm($server);
  322. if (!empty($server)) {
  323. if(xmlrpc($server, 'pingback.ping', $source, $target)) {
  324. if (!$source_is_absolute) {
  325. db_query("INSERT INTO {pingback_sent} (nid, url, timestamp) VALUES (%d, '%s', %d)", $nid, $target, time());
  326. }
  327. watchdog('pingback', t('Pingback to %target from %source succeeded.', array('%source' => $source, '%target' => $target)));
  328. return TRUE;
  329. }
  330. else {
  331. watchdog('pingback', t('Pingback to %target from %source failed. Error @errno: @description', array('%source' => $source, '%target' => $target, '@errno' => xmlrpc_errno(), '@description' => xmlrpc_error_msg())), WATCHDOG_WARNING);
  332. return FALSE;
  333. }
  334. }
  335. return FALSE;
  336. }
  337. /**
  338. * Sends pingbacks in all URLs in specified node.
  339. */
  340. function pingback_send_by_nid($nid, $message = TRUE) {
  341. global $base_root;
  342. $node = node_load($nid);
  343. $prepared = node_prepare($node);
  344. $urls = _pingback_extract_urls($prepared->body);
  345. if (isset($node->pingback_sent)) {
  346. //$urls = array_diff(_pingback_extract_urls($prepared->body), $node->pingback_sent);
  347. }
  348. $succesful = array();
  349. foreach ($urls as $url) {
  350. //dpm("Sending to " . check_plain($url));
  351. if (pingback_send($node->nid, $url)) {
  352. //dpm('success!');
  353. if ($message) $successful[] = "<a href=\"$url\">$url</a>";
  354. }
  355. }
  356. if ($message && count($successful)) {
  357. drupal_set_message(t('!urls pingbacked successfully.', array('!urls' => implode(', ', $successful))));
  358. }
  359. //drupal_set_message("URLs: " . implode(', ', $urls));
  360. }
  361. function pingback_comment_is_pingback($comment) {
  362. return $comment->format == variable_get('pingback_input_format', FILTER_FORMAT_DEFAULT);
  363. }
  364. /*--- private functions ---*/
  365. function _pingback_valid_for_node_type($type) {
  366. return variable_get("pingback_$type", ($type == 'story' || $type == 'blog') ? 1 : 0);
  367. }
  368. function _pingback_valid_for_node($node) {
  369. return $node->comment == COMMENT_NODE_READ_WRITE;
  370. }
  371. function _pingback_extract_urls($text) {
  372. //regexp is stolen from trackback.module ;)
  373. preg_match_all("/(http|https):\/\/[a-zA-Z0-9@:%_~#?&=.,\/;-]*[a-zA-Z0-9@:%_~#&=\/;-]/", $text, $urls);
  374. return array_unique($urls[0]);
  375. }
  376. //maps any absolute url from this drupal site to nid if applicable.
  377. //can also be used to check whether an absolute path is in the site and points to a node (e.g. node/1)
  378. function _pingback_url_to_nid($url) {
  379. //first check if the url is really in our site, as well as getting the non-base-url part
  380. if (preg_match($a = '#^' . preg_quote($GLOBALS['base_url'], '#') . '/(.+)$#', $url, $matches)) {
  381. //dpm($matches[1]);
  382. //dpm(drupal_get_normal_path($matches[1]));
  383. if (!variable_get('clean_url', 0)) {
  384. // Clean URLs not enabled. Strip '?q=' from URL.
  385. $matches[1] = str_replace('?q=', '', $matches[1]);
  386. }
  387. if (preg_match($b = '#^node/([0-9]+)$#', drupal_get_normal_path($matches[1]), $matches2)) {
  388. return $matches2[1];
  389. } //else dpm($b);
  390. }
  391. //dpm($a);
  392. return FALSE;
  393. }