<?php if (!defined('PmWiki')) exit();

/*	=== Bloge-LinkbackServer ===
 *	Copyright 2009 Eemeli Aro <eemeli@gmail.com>
 *
 *	Receive Pingbacks and Trackbacks
 *
 *	Developed and tested using PmWiki 2.2.x
 *
 *	To use, add the following to a configuration file:

  		include_once("$FarmD/cookbook/bloge-linkback-server.php");

 *	To limit receiving linkback pings you should only include this file
 *	on pages which will accept linkbacks, for example by using per-group
 *	customizations.
 *
 *	This is a part of the Bloge bundle of recipes, but may be used by itself.
 *	For more information, please see the online documentation at
 *		http://www.pmwiki.org/wiki/Cookbook/Bloge and at
 *		http://www.pmwiki.org/wiki/Cookbook/Bloge-Linkback#server
 *
 *	This program is free software; you can redistribute it and/or modify
 *	it under the terms of the GNU General Public License as published by
 *	the Free Software Foundation; either version 2 of the License, or
 *	(at your option) any later version.
 */

$RecipeInfo['Bloge-LinkbackServer']['Version'] = '2009-08-12';

$HTMLHeaderFmt['linkback'] = 'function:LinkbackHeader';
function LinkbackHeader($pagename) {
	global $LinkbackHeader;
	SDVA($LinkbackHeader, array( 'pingback' => 1, 'trackback' => 1 ));
	$url = PageVar($pagename,'$PageUrl');
	if (!empty($LinkbackHeader['pingback']))
		echo "\n<link rel=\"pingback\" href=\"$url?action=pingback\" />";

	## NOTE: this is not fully correct, but is much shorter and will be
	##       recognised by at least Drupal, Movable Type & Pligg
	##       See also: <http://www.sixapart.com/pronet/docs/trackback_spec>
	if (!empty($LinkbackHeader['trackback']))
		echo "\n<!--<rdf:RDF><rdf:Description trackback:ping=\"$url?action=trackback\" /></rdf:RDF>-->";
}

if (($action!='pingback') && ($action!='trackback')) return;

SDV($LinkbackPage, 'Site.Linkbacks');

SDV($HandleActions['pingback'], 'HandleLinkback');
SDV($HandleAuth['pingback'], 'read');

SDV($HandleActions['trackback'], 'HandleLinkback');
SDV($HandleAuth['trackback'], 'read');

if (!function_exists('xmlrpc_decode_request')) {
function xmlrpc_decode_request($xml, &$method, $encoding='iso-8859-1') {
	if (!preg_match('#<methodCall>\s*(.*?)\s*</methodCall>#s',$xml,$m_mcall)) return FALSE;
	if (!preg_match('#<methodName>\s*([\w.:/]*?)\s*</methodName>#',$m_mcall[1],$m_name)) return FALSE;
	$method = $m_name[1];
	if (!preg_match('#<params>\s*(.*?)\s*</params>#s',$m_mcall[1],$m_params)) return FALSE;
	if (!preg_match_all('#<param>\s*<value>\s*<(\w+)>\s*(.*?)\s*</\1>\s*</value>\s*</param>#s', $m_params[1], $m_values, PREG_SET_ORDER)) return FALSE;
	$out = array();
	foreach( $m_values as $va ) $out[] = $va[2];
	return $out;
}
}

function LinkbackVerifySource($url, $self, &$errstr) {
	global $LinkbackServerCURLOptions;
	if ( !function_exists('curl_init') || empty($self) ) return -32400;
	if (empty($url)) return 16;

	$host = preg_replace('!^[a-z]+://([^/]+).*$!i', '$1', $url);
	if (gethostbyname($host) != $_SERVER['REMOTE_ADDR']) { $errstr='source IP mismatch'; return 49; }

	SDV($LinkbackServerCURLOptions, array(
		CURLOPT_FOLLOWLOCATION => TRUE,
		CURLOPT_MAXREDIRS => 10,
		CURLOPT_CONNECTTIMEOUT => 10,
		CURLOPT_RETURNTRANSFER => TRUE,
		CURLOPT_USERAGENT => "$Version Bloge-LinkbackServer-{$RecipeInfo['Bloge-LinkbackServer']['Version']}"
	));

	## HEAD url
	$ch = curl_init($url);
	curl_setopt_array($ch, $LinkbackServerCURLOptions + array(
		CURLOPT_NOBODY => TRUE,
		CURLOPT_HEADER => TRUE,
		CURLOPT_REFERER => $url
	));
	$reply = curl_exec($ch);
	if (empty($reply)) $errstr = curl_error($ch);
	curl_close($ch);

	if (empty($reply)) return 16;
	else if (!preg_match('/^Content-Type: .*(html|xml)/mi', $reply, $m)) return 17;

	## GET url
	$ch = curl_init($url);
	curl_setopt_array($ch, $LinkbackServerCURLOptions + array( CURLOPT_REFERER => $url ));
	$reply = curl_exec($ch);
	if (empty($reply)) $errstr = curl_error($ch);
	curl_close($ch);

	if (empty($reply)) return 16;
	if (!preg_match('#<a\s[^>]*\bhref=([\'"])'.preg_quote($self,'#').'\1[^>]*>\s*\S+\s*</a>#i', $reply)) return 17;

	return 0;
}

function LinkbackBlocklist($text) {
	global
		$EnableBlocklist, $FarmD, $EnablePost, $EditFunctions,
		$EnableLinkbackBlocklist, $LinkbackPage;

	if (!IsEnabled($EnableLinkbackBlocklist,0)) return FALSE;

	$EnableBlocklist = $EnableLinkbackBlocklist;

	include_once("$FarmD/scripts/blocklist.php");

	$EnablePost &= 1;
	BlockList($LinkbackPage, $text);
	if (!$EnablePost) return TRUE;

	$cbkey = array_search('CheckBlocklist', $EditFunctions);
	if ($cbkey !== FALSE) unset($EditFunctions[$cbkey]);

	return FALSE;
}

function LinkbackExecute($pagename, $auth, &$type, &$errstr) {
	global
		$action, $HTTP_RAW_POST_DATA, $Author, $AuthorLink, $IsPagePosted, $CurrentTime,
		$LinkbackPage, $LinkbackMarkup;

	$tgtpage = RetrieveAuthPage($pagename, 'read', FALSE, READPAGE_CURRENT);
	if (!$tgtpage) { $errstr = 'page read error'; return -32500; }
	$tgt = PageVar($pagename,'$PageUrl');

	$lbpage = RetrieveAuthPage($LinkbackPage, $auth, FALSE);
	if (!$lbpage) { $errstr = 'page read error'; return -32500; }

	if (empty($_REQUEST['url'])) {
		if ($action=='trackback') {
			$type = 'trackback';
			$errstr = 'no url parameter';
			return -32700;
		}
		$type = 'pingback';
		$params = xmlrpc_decode_request($HTTP_RAW_POST_DATA, &$method);
		if ( $params === FALSE ) return -32600;
		if ( $method != 'pingback.ping' ) return -32601;
		if ( count($params) != 2 ) return -32602;
		if ( $params[1] != $tgt ) { $errstr = 'targetURI doesn\'t match Pingback URI'; return 33; }
		$source = $params[0];
	} else {
		$type = 'trackback';
		$source = $_REQUEST['url'];
	}

	$sn = str_replace('.','--',$pagename);
	$lbsection = TextSection($lbpage['text'], "#$sn#");
	if (preg_match('#\s'.preg_quote($source,'#').'\s#', $lbsection)) return 48;

	if (LinkbackBlocklist($source)) { $errstr='blocked'; return 49; }

	$source_code = LinkbackVerifySource($source, $tgt, $errstr);
	if ($source_code) return $source_code;

	SDVA($LinkbackMarkup, array(
		'head' => "\$pageanchor\n!!! Linkbacks",
		'item' => "\n* \$source ($CurrentTime)"
	));

	$lbnew = $lbpage;
	foreach ( array('head','item') as $t ) $lbmarkup[$t] = str_replace(
		array('$pagename', '$pageanchor', '$source'),
		array( $pagename,  "[[#$sn]]",     $source),
		$LinkbackMarkup[$t]);
	if ($lbsection) $lbnew['text'] = str_replace($lbmarkup['head'], $lbmarkup['head'].$lbmarkup['item'], $lbnew['text']);
	else $lbnew['text'] .= "\n{$lbmarkup['head']}{$lbmarkup['item']}\n";
	$AuthorLink = $Author = "linkback from {$_SERVER['REMOTE_ADDR']}";
	UpdatePage($LinkbackPage, $lbpage, $lbnew);
	if (!$IsPagePosted) { $errstr = 'page write error'; return -32500; }

	return 0;
}

function HandleLinkback($pagename, $auth='read') {
	$errstr = '';
	Lock(2);
		$errcode = LinkbackExecute($pagename, $auth, $type, $errstr);
	Lock(0);

	switch($errcode) {
		//case 0: $str = 'generic fault code'; break;
		case 16: $str = 'The source URI does not exist.'; break;
		case 17: $str = 'The source URI does not contain a link to the target URI, and so cannot be used as a source.'; break;
		case 32: $str = 'The specified target URI does not exist.'; break;
		case 33: $str = 'The specified target URI cannot be used as a target.'; break;
		case 48: $str = 'The linkback has already been registered.'; break;
		case 49: $str = 'Access denied.'; break;
		//case 50: $str = 'The server could not communicate with an upstream server, or received an error from an upstream server, and therefore could not complete the request.'; break;
		case -32700: $str = 'parse error. not well formed'; break;
		//case -32701: $str = 'parse error. unsupported encoding'; break;
		//case -32702: $str = 'parse error. invalid character for encoding'; break;
		case -32600: $str = 'server error. invalid xml-rpc. not conforming to spec.'; break;
		case -32601: $str = 'server error. requested method not found'; break;
		case -32602: $str = 'server error. invalid method parameters'; break;
		//case -32603: $str = 'server error. internal xml-rpc error'; break;
		case -32500: $str = 'application error'; break;
		case -32400: $str = 'system error'; break;
		//case -32300: $str = 'transport error'; break;
		default: $str = 'ok';
	}
	if ($errstr) $str .= " ($errstr)";

	if ($type=='trackback') {
		$response = "<response>\n"
			. ( $errcode
				? "<error>1</error>\n<message>$str [$errcode]</message>"
				: "<error>0</error>" )
			. "\n</response>";
	} else {
		## default to pingback
		$response = "<methodResponse>\n"
			. ( $errcode
				? "<fault><value><struct>\n<member><name>faultCode</name><value><int>$errcode</int></value></member>\n<member><name>faultString</name><value><string>$str</string></value></member>\n</struct></value></fault>"
				: "<params><param><value><string>$str</string></value></param></params>" )
			. "\n</methodResponse>";
	}

	header('Content-type: text/xml');
	echo "<?xml version=\"1.0\"?>\n$response\n";

	exit();
}