Calling all Drupal developers!
Help us get this on the first page of Digg. DIGG NOW!
Help us get this on the first page of Digg. DIGG NOW!
<?php
// $Id: pingback.module,v 1.0.0.1 2007/07/26 23:17:13 dww Exp $
/**
* Implementation of hook_perm().
*/
function pingback_perm() {
return array('administer pingbacks');
}
/**
* Implementation of hook_menu().
*/
function pingback_menu() {
$items['admin/settings/pingback'] = array(
'title' => 'Pingback',
'description' => 'Configure pingbacks',
'page callback' => 'drupal_get_form',
'page arguments' => array('pingback_settings_form'),
'access arguments' => array('administer pingbacks'),
'file' => 'pingback.admin.inc',
);
return $items;
}
/**
* Implementation of hook_form_alter().
*/
function pingback_form_alter(&$form, &$form_state, $form_id) {
global $user;
if ($form_id == 'node_type_form' && isset($form['identity']['type'])) {
$type = $form['#node_type']->type;
$form['workflow']['pingback'] = array(
'#type' => 'radios',
'#title' => t('Pingbacks'),
'#options' => array(1 => t('Enabled'), 0 => t('Disabled')),
'#default_value' => _pingback_valid_for_node_type($type),
'#description' => t('Enable pingbacks for this node type.')
);
}
else if (isset($form['type']) && $form['type']['#value'] .'_node_form' == $form_id) {
$node = $form['#node'];
if (_pingback_valid_for_node_type($node->type)) {
// if there are any past successful pingbacks from this posting, add them to the node editing page.
$past_successes_listing = array();
$q = db_query("SELECT url FROM {pingback_sent} WHERE nid = %d", $node->nid);
while ($pb = db_fetch_object($q)) {
$past_successes_listing[] = $pb->url;
}
// add listing of successfully pingbacked URLs
if (count($past_successes_listing)) {
$form['pingback'] = array(
'#type' => 'fieldset',
'#title' => t('Pingbacks'),
'#collapsible' => TRUE
);
$form['pingback'][] = array(
'#type' => 'markup',
'#value' => theme('item_list', $past_successes_listing, t('Successfully pingbacked URLs')),
);
//t('These URLs have been successfuly pinged by this post.')
}
}
}
//hide pingback input format if desired for anon users
else if (
$form_id == 'comment_form'
&& (!$user->uid)
&& variable_get('pingback_hide_format_for_anon', 0)
//&& (isset($GLOBALS['pingback_bypass_format_hiding']) ? !$GLOBALS['pingback_bypass_format_hiding'] : TRUE)
) {
//dpm($form);
$alternate_formats = array();
foreach ($form['comment_filter']['format'] as $k => $v) {
//dpm($k);
if (!element_property($k) && isset($v['#return_value'])) {
if ($v['#return_value'] == variable_get('pingback_input_format', FILTER_FORMAT_DEFAULT)) {
unset($form['comment_filter']['format'][$k]);
}
else {
// Make a list of alternate formats for the comment form.
$alternate_formats[] = $k;
}
}
}
if (count($alternate_formats) == 1) {
// There is only one available format. Remove fieldset and go back to hidden form field.
$new_form[$alternate_formats[0]] = array(
'#type' => 'value',
'#value' => $alternate_formats[0],
'#parents' => $form['comment_filter']['format'][$alternate_formats[0]]['#parents'],
);
$new_form['format']['guidelines'] = array(
'#title' => t('Formatting guidelines'),
'#value' => $form['comment_filter']['format'][$alternate_formats[0]]['#description'],
);
$form['comment_filter']['format'] = $new_form;
}
}
}
/**
* Implementation of hook_theme().
*/
function pingback_theme() {
return array(
'pingback' => array(
'file' => 'pingback.module',
'function' => 'theme_pingback',
)
);
}
/**
* Menu callback: lists pingbacks. TODO: ability to delete them!
*/
//pingback_list_pingbacks() a better name?
function pingback_list_page() {
//$result = db_query("SELECT ");
return 'TODO';
}
function pingback_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {
if (_pingback_valid_for_node_type($node->type)) {
switch ($op) {
case 'insert':
case 'update':
if (variable_get('pingback_mode', 'off') == 'submit') {
global $_pingback_nid;
$_pingback_nid = $node->nid;
}
else { //mode == 'cron'
//queue this nid in variable pingback_nid_queue, but take care for not queuing existing nids
$q = variable_get('pingback_nid_queue', array());
if (!in_array($node->nid, $q)) {
$q[] = $node->nid;
variable_set('pingback_nid_queue', $q);
}
}
break;
case 'view':
// Insert pingback header when a node is being viewed.
if (arg(0) == 'node' && is_numeric(arg(1)) && arg(3) == NULL) {
drupal_set_header('X-Pingback: '. $GLOBALS['base_url'] .'/xmlrpc.php');
}
break;
}
}
}
/**
* Implementation of hook_cron().
*/
function pingback_cron() {
$q = variable_get('pingback_nid_queue', array());
$limit = variable_get('pingback_check_per_cron', 30);
$count = 0;
//dpr($q);
while (($nid = array_shift($q)) && ($count++ < $limit)) {
pingback_send_by_nid($nid, FALSE);
//dpr("Sent pingbacks in node $nid");
}
variable_set('pingback_nid_queue', $q);
}
function pingback_exit() {
global $_pingback_nid;
if (isset($_pingback_nid)) {
//reset the node_load() cache
node_load($_pingback_nid, NULL, TRUE);
pingback_send_by_nid($_pingback_nid, variable_get('pingback_notify_successful_pings', 1));
}
}
function pingback_xmlrpc() {
return array(
array(
'pingback.ping',
'pingback_receive',
array('string', 'string', 'string'),
t('Handles pingback pings.'),
),
);
}
/**
* XML-RPC callback: process pingback.ping() call.
*/
function pingback_receive($pagelinkedfrom, $pagelinkedto) {
//return xmlrpc_server_error(0, 'abcdefgh');
//big thanks to WordPress codebase, specifically file xmlrpc.php, method pingback_ping() for becoming the reference implementation and theft victim ;)
//note: $pagelinkedto is a URL from our own site, $pagelinkedfrom is a foreign URL
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."));
//don't really understand this part, supposed to unescape ampersand entities?
$pagelinkedfrom = str_replace('&', '&', $pagelinkedfrom);
$pagelinkedto = preg_replace('#&([^amp\;])#is', '&$1', $pagelinkedto);
$error_code = -1;
// Check if the page linked to is in our site
$pos1 = strpos($pagelinkedto, str_replace(array('http://www.', 'http://', 'https://www.', 'https://'), '', $GLOBALS['base_url']));
if (!$pos1) {
return new xmlrpc_server_error(0, t('Is there no link to us?'));
}
// let's find which post is linked to
$nid = _pingback_url_to_nid($pagelinkedto);
//dpm("(PB) URL='$pagelinkedto' ID='$post_ID' Found='$way'");
$node = $nid ? node_load($nid) : FALSE;
//watchdog('debug', '--- ' . $nid . ' --- ' . print_r($node, TRUE));
if (!$node || !_pingback_valid_for_node($node)) // node not found
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."));
if ($nid == _pingback_url_to_nid($pagelinkedfrom))
return xmlrpc_server_error(0, t('The source URL and the target URL cannot both point to the same resource.'));
if (!$node->status)
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."));
// Let's check that the remote site didn't already pingback this entry
$result = db_result(db_query("SELECT COUNT(*) FROM {comments} WHERE nid = %d AND homepage = '%s' AND format = %d", $nid, $pagelinkedfrom, variable_get('pingback_input_format', FILTER_FORMAT_DEFAULT)));
if ($result > 0) // We already have a Pingback from this URL
return xmlrpc_server_error(48, 'The pingback has already been registered.');
// very stupid, but gives time to the 'from' server to publish !
sleep(1);
// Let's check the remote site
$r = drupal_http_request($pagelinkedfrom);
if ($r->error)
return xmlrpc_server_error(16, 'The source URL does not exist.');
//watchdog('debug', print_r($r, TRUE));
$linea = $r->data;
// Work around bug in strip_tags():
$linea = str_replace('<!DOC', '<DOC', $linea);
$linea = preg_replace( '/[\s\r\n\t]+/', ' ', $linea ); // normalize spaces
$linea = preg_replace( "/ <(h1|h2|h3|h4|h5|h6|p|th|td|li|dt|dd|pre|caption|input|textarea|button|body)[^>]*>/", "\n\n", $linea );
preg_match('|<title>([^<]*?)</title>|is', $linea, $matchtitle);
$title = check_plain($matchtitle[1]);
if ( empty( $title ) )
return xmlrpc_server_error(32, 'We cannot find a title on that page.');
$linea = strip_tags( $linea, '<a>' ); // just keep the tag we need
$p = explode( "\n\n", $linea );
$preg_target = preg_quote($pagelinkedto);
foreach ( $p as $para ) {
if ( strpos($para, $pagelinkedto) !== false ) { // it exists, but is it a link?
preg_match("|<a[^>]+?". $preg_target ."[^>]*>([^>]+?)</a>|", $para, $context);
// If the URL isn't in a link context, keep looking
if (empty($context)) continue;
// We're going to use this fake tag to mark the context in a bit
// the marker is needed in case the link text appears more than once in the paragraph
//I edited <wpcontext></wpcontext> to <dpcontext></dpcontext> so it becomes more Drupal-ish!
$excerpt = preg_replace('|\</?dpcontext\>|', '', $para);
// prevent really long link text
if ( strlen($context[1]) > 100 )
$context[1] = substr($context[1], 0, 100) .'...';
$marker = '<dpcontext>'. $context[1] .'</dpcontext>'; // set up our marker
$excerpt = str_replace($context[0], $marker, $excerpt); // swap out the link for our marker
$excerpt = strip_tags($excerpt, '<dpcontext>'); // strip all tags but our context marker
$excerpt = trim($excerpt);
$preg_marker = preg_quote($marker);
$excerpt = preg_replace("|.*?\s(.{0,100}$preg_marker.{0,100})\s.*|s", '$1', $excerpt);
$excerpt = strip_tags($excerpt); // YES, again, to remove the marker wrapper
break;
}
}
if (empty($context)) // Link to target not found
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.'));
//??? can someone explain about this?
$pagelinkedfrom = preg_replace('#&([^amp\;])#is', '&$1', $pagelinkedfrom);
//$context = '[...] ' . wp_specialchars( $excerpt ) . ' [...]';
//TODO: a custom filter for $excerpt
$edit = array(
'nid' => $nid,
'subject' => t('Pingback'),
'comment' => '[...] '. $excerpt .' [...]',
'hostname' => ip_address(),
'format' => variable_get('pingback_input_format', FILTER_FORMAT_DEFAULT),
'name' => $title,
'homepage' => $pagelinkedfrom,
);
comment_save($edit);
/*
//bypass the hiding in pingback_form_alter() because we want to use the input format
$GLOBALS['pingback_bypass_format_hiding'] = TRUE;
drupal_execute('comment_form', $edit, array());
$GLOBALS['pingback_bypass_format_hiding'] = FALSE;
watchdog('debug', print_r(form_get_errors(), TRUE));
*/
$message = t('Pingback from @source to @target registered! Keep the web talking! :-)', array('@source' => $pagelinkedfrom, '@target' => $pagelinkedto));
//comment.module already logs new comments
//watchdog('pingback', $message);
return $message;
}
/* --- theme_pingback_* --- */
function theme_pingback($pb, $links = 0) {
return theme('comment', $pb, $links);
}
/* --- APIs --- */
/**
* Discover a pingback server with pingback autodiscovery schemes.
* @param $target the absolute URL to search for its server. This should have passed check_url() first.
*/
function pingback_discover($target) {
$server = '';
//#1: send a HEAD to check for X-Pingback header
$r = drupal_http_request($target, array(), 'HEAD');
//dpm($r);
if (!$r->error) {
if (is_array($r->headers) && isset($r->headers['X-Pingback'])) {
$server = $r->headers['X-Pingback'];
}
else {
//#2: search for <link rel="pingback" href="(server)" /> tags
$get = drupal_http_request($target);
if (!$get->error) {
//dpm($get->data);
//this regexp is the one provided in the spec
if (preg_match('#<link rel="pingback" href="([^"]+)" ?/?>#', $get->data, $matches)) {
$server = $matches[1];
}
}
}
}
if (!empty($server)) {
return check_url($server);
}
else return '';
}
/**
* Send pingbacks. Does nothing if the target does not have a pingback server.
* @param $nid the source node ID.
* @param $target the target absolute URL.
* @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).
* @return TRUE on success, FALSE otherwise.
*/
function pingback_send($nid, $target, $source_is_absolute = FALSE) {
if (!valid_url($target, TRUE)) {
watchdog('pingback', 'Target not valid URL: @url', array('@url' => $target), WATCHDOG_WARNING);
return FALSE;
}
if (!$source_is_absolute) {
$source = url("node/$nid", array('absolute' => TRUE));
$result = db_result(db_query("SELECT COUNT(*) FROM {pingback_sent} WHERE nid = %d AND url = '%s'", $nid, $target));
if ($result > 0) {
watchdog('pingback', 'Pingback already sent for: @nid', array('@nid' => $nid), WATCHDOG_WARNING);
//dpm('oops already sent');
return FALSE;
}
}
else {
$source = $nid;
if (!valid_url($source)) {
watchdog('pingback', 'Source not valid URL: @url', array('@url' => $source), WATCHDOG_WARNING);
return FALSE;
}
}
//dpm($source);
$retval = FALSE;
//server autodiscovery
$server = pingback_discover($target);
//dpm($server);
if (!empty($server)) {
if (xmlrpc($server, 'pingback.ping', $source, $target)) {
if (!$source_is_absolute) {
db_query("INSERT INTO {pingback_sent} (nid, url, timestamp) VALUES (%d, '%s', %d)", $nid, $target, time());
}
watchdog('pingback', 'Pingback to %target from %source succeeded.', array('%source' => $source, '%target' => $target));
return TRUE;
}
else {
watchdog('pingback', 'Pingback to %target from %source failed. Error @errno: @description', array('%source' => $source, '%target' => $target, '@errno' => xmlrpc_errno(), '@description' => xmlrpc_error_msg()), WATCHDOG_WARNING);
return FALSE;
}
}
// watchdog('pingback', 'Server not found', array(), WATCHDOG_WARNING);
return FALSE;
}
/**
* Sends pingbacks in all URLs in specified node.
*/
function pingback_send_by_nid($nid, $message = TRUE) {
global $base_root;
$node = node_load($nid);
$prepared = node_prepare($node);
$urls = _pingback_extract_urls($prepared->body);
if (isset($node->pingback_sent)) {
//$urls = array_diff(_pingback_extract_urls($prepared->body), $node->pingback_sent);
}
$succesful = array();
foreach ($urls as $url) {
//dpm("Sending to " . check_plain($url));
if (pingback_send($node->nid, $url)) {
//dpm('success!');
// watchdog('pingback', 'Pingback successful for: @url', array('@url' => $url));
if ($message) $successful[] = "<a href=\"$url\">$url</a>";
}
else {
// watchdog('pingback', 'Pingback failed for: @url', array('@url' => $url), WATCHDOG_WARNING);
}
}
if ($message && count($successful)) {
drupal_set_message(t('!urls pingbacked successfully.', array('!urls' => implode(', ', $successful))));
}
//drupal_set_message("URLs: " . implode(', ', $urls));
}
function pingback_comment_is_pingback($comment) {
return $comment->format == variable_get('pingback_input_format', FILTER_FORMAT_DEFAULT);
}
/* --- private functions --- */
function _pingback_valid_for_node_type($type) {
return variable_get("pingback_$type", ($type == 'story' || $type == 'blog') ? 1 : 0);
}
function _pingback_valid_for_node($node) {
return $node->comment == COMMENT_NODE_READ_WRITE;
}
function _pingback_extract_urls($text) {
//regexp is stolen from trackback.module ;)
preg_match_all("/(http|https):\/\/[a-zA-Z0-9@:%_~#?&=.,\/;-]*[a-zA-Z0-9@:%_~#&=\/;-]/", $text, $urls);
return array_unique($urls[0]);
}
//maps any absolute url from this drupal site to nid if applicable.
//can also be used to check whether an absolute path is in the site and points to a node (e.g. node/1)
function _pingback_url_to_nid($url) {
//first check if the url is really in our site, as well as getting the non-base-url part
if (preg_match($a = '#^'. preg_quote($GLOBALS['base_url'], '#') .'/(.+)$#', $url, $matches)) {
//dpm($matches[1]);
//dpm(drupal_get_normal_path($matches[1]));
if (!variable_get('clean_url', 0)) {
// Clean URLs not enabled. Strip '?q=' from URL.
$matches[1] = str_replace('?q=', '', $matches[1]);
}
if (preg_match($b = '#^node/([0-9]+)$#', drupal_get_normal_path($matches[1]), $matches2)) {
return $matches2[1];
} //else dpm($b);
}
//dpm($a);
return FALSE;
}