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

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

		if ($action=='edit') include_once("$FarmD/cookbook/bloge-linkback-client.php");

 *	To limit sending linkback pings you should only include this file for such
 *	pages, 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#client
 *
 *	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-LinkbackClient']['Version'] = '2009-08-12';

$EditFunctions[] = 'LinkbackFindLinks';

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

if (!function_exists('xmlrpc_encode_request')) {
function xmlrpc_encode_request($method, $params, $output_options=NULL) {
	$xml = "<?xml version=\"1.0\"?>\n<methodCall>\n<methodName>pingback.ping</methodName>\n<params>";
	foreach ( (array)$params as $p ) {
		$type = gettype($p);
		switch(gettype($p)) {
			case 'integer':
				$type = 'int';
				break;
			case 'array':	# not implemented
			case 'object':	# not implemented
			case 'resource':
			case 'NULL':
			case 'unknown type':
				$type = 'string';
				$p = print_r($p,TRUE);
				break;
		}
		$xml .= "\n\t<param><value><$type>$p</$type></value></param>";
	}
	$xml .= "\n</params>\n</methodCall>";
	return $xml;
}
}

function LinkbackDiscovery($url, $self, &$type) {
	global $LinkbackClientCURLOptions;
	if ( !function_exists('curl_init') || empty($url) ) return FALSE;

	## HEAD url
	$ch = curl_init($url);
	curl_setopt_array($ch, $LinkbackClientCURLOptions + array(
		CURLOPT_NOBODY => TRUE,
		CURLOPT_HEADER => TRUE,
		CURLOPT_REFERER => $self
	));
	$reply = curl_exec($ch);
	curl_close($ch);

	## Pingback header + sanity checks
	if (empty($reply)) return FALSE;
	else if (preg_match('/^X-Pingback: (.+)$/m', $reply, $m)) { $type = 'pingback'; return $m[1]; }
	else if (!preg_match('/^Content-Type:.*(html|xml)/mi', $reply, $m)) return FALSE;

	## GET url
	$ch = curl_init($url);
	curl_setopt_array($ch, $LinkbackClientCURLOptions + array(CURLOPT_REFERER => $self));
	$reply = curl_exec($ch);
	curl_close($ch);

	if (preg_match('#<link rel="pingback" href="([^"]+)" ?/?>#', $reply, $m)) {
		$type = 'pingback';
		$target = $m[1];
	} else if (preg_match('#<rdf:RDF.*?trackback:ping="([^"]+)".*?</rdf:RDF>#s', $reply, $m)) {
		$type = 'trackback';
		$target = $m[1];
	} else return FALSE;

	return str_replace(
		array('&amp;', '&lt;', '&gt;', '&quot;'),
		array('&',     '<',    '>',    '"'),
		$target);
}

function LinkbackSendPing( $url, $ref, $post, &$reply ) {
	global $Version, $RecipeInfo, $LinkbackClientCURLOptions;
	if ( !function_exists('curl_init') || empty($url) || empty($post) ) return FALSE;

	$ok = TRUE;
	$ch = curl_init($url);
	curl_setopt_array($ch, $LinkbackClientCURLOptions + array(
		CURLOPT_POST => TRUE,
		CURLOPT_POSTFIELDS => $post,
		CURLOPT_REFERER => $ref
	));
	if ($post[0]=='<') curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: text/xml'));
	$msg = curl_exec($ch);
	if (empty($msg)) { $ok = FALSE; $msg = curl_error($ch) . " ($url)"; }
	curl_close($ch);

	if ($reply!==FALSE) $reply = $msg;
	return $ok;
}


function LinkbackExternalLinks($txt) {
	global $LinkPattern, $UrlExcludeChars, $IMap, $LinkbackClientFilterSites;

	SDVA($LinkbackClientFilterSites, array(
		'self' => '!^https?://'.preg_quote($_SERVER['SERVER_NAME'],'!').'!',
		'pmwiki' => '!^http://([^/]+\.)?pmwiki\.org!',
		'wikipedia' => '!^http://([^/]+\.)?wikipedia\.(com|org)!'
	));

	preg_match_all(
		"/\\b(?>($LinkPattern))([^\\s$UrlExcludeChars]*[^\\s.,?!$UrlExcludeChars])/",
		$txt, $matches, PREG_SET_ORDER );
	$targets = array();
	foreach( $matches as $link ) $targets[] = PUE(str_replace('$1',$link[2],$IMap[$link[1]]));

	return MatchPageNames(array_unique($targets), $LinkbackClientFilterSites);
}

function LinkbackFindLinks($pagename, &$page, &$new) {
	global $IsPagePosted, $Now, $MessagesFmt;
	if (!$IsPagePosted || !empty($_POST['postdraft']) || !empty($_POST['postedit'])) return;

	$diffkey = preg_grep("/^diff:$Now:/", array_keys($new));
	if (count($diffkey)!=1) return;

	preg_match_all('/^< .+$/m', $new[reset($diffkey)], $dm);
	if (empty($dm[0])) return;
	$targets = LinkbackExternalLinks( implode(' ',$dm[0]) );
	if ($targets) register_shutdown_function('LinkbackPingLinks', $pagename, $targets, getcwd());
}

function LinkbackParseResponse($xml, $debug=FALSE) {
	## pingback
	if (preg_match('#<methodResponse>\s*(.*?)\s*</methodResponse>#s',$xml,$m_r)) {
		if (preg_match('#
			<params> \s* <param> \s* <value> \s*
				<(\w+)> \s* (.*?) \s* </\1> \s*
			</value> \s* </param> \s* </params>
		#sx', $m_r[1], $m_return)) return $debug ? "success ({$m_return[2]})" : '';
		if (preg_match('#
			<fault> \s* <value> \s* <struct> \s*
				(.*?) \s*
			</struct> \s* </value> \s* </fault>
		#sx', $m_r[1], $m_fault)
			&& ( preg_match_all('#<member>\s*(.*?)\s*</member>#s', $m_fault[1], $m_fm, PREG_SET_ORDER) == 2 )
		) {
			foreach( $m_fm as $member ) {
				if (preg_match('#<name>\s*fault(Code|String)\s*</name>#', $member[1], $m_member_name)) {
					if ( ($m_member_name[1]=='Code') && preg_match('#<value>\s*<(?:int|i4)>\s*([\d-]*)\s*</(?:int|i4)>\s*</value>#', $member[1], $m_member_code) )
						$errcode = $m_member_code[1];
					else if ( ($m_member_name[1]=='String') && preg_match('#<value>\s*<string>\s*(.*?)\s*</string>\s*</value>#s', $member[1], $m_member_str) )
						$errstr = $m_member_str[1];
				}
			}
			if ( isset($errcode) && isset($errstr) ) return "error #$errcode ($errstr)";
		}
	}

	## trackback
	if (preg_match('#
		<response> \s*
			<error> \s* (\d*) \s* </error> \s*
			(?: <message> \s* (.*?) \s* </message> \s* )?
		</response>
	#six', $xml, $m_r)) {
		if (empty($m_r[1])) return $debug ? 'success' : '';
		else return "error ({$m_r[2]})";
	}

	return $debug ? $xml : 'XML parse error';
}

function LinkbackPingLinks($pagename, $targets, $dir='') {
	global $CurrentTime, $LinkbackClientLogPage;

	if ($dir) { flush(); chdir($dir); }
	SDV($LinkbackClientLogPage, 'Site.LinkbackClientLog');

	$log = "Bloge-LinkbackClient autodiscovery: $pagename ($CurrentTime)\n\n";
	$self = PageVar($pagename, '$PageUrl');
	foreach($targets as $tgt) {
		$log .= "$tgt\n";
		$pingtgt = LinkbackDiscovery($tgt, $self, $type);
		if (!$pingtgt) continue;
		switch($type) {
			case 'pingback':
				$post = xmlrpc_encode_request('pingback.ping', array($self,$tgt));
				$log .= "  > pingback @ $pingtgt\n";
				break;
			case 'trackback':
				$post =
					'url='.urlencode($self)
					//.'&excerpt='.urlencode(PageVar($pagename, '$Description'))
					//.'&blog_name='.urlencode('foo')
					.'&title='.urlencode(PageVar($pagename, '$Title'));
				$log .= "  > trackback @ $pingtgt\n";
				break;
			default:
				continue 2;
		}
		LinkbackSendPing($pingtgt, $tgt, $post, $reply);
		$out = LinkbackParseResponse($reply, TRUE);
		$log .= "    > $out\n";
	}

	if (!$LinkbackClientLogPage) return;
	$page = ReadPage($LinkbackClientLogPage);
	$page['text'] .= "[@{$log}@]\n----\n\n";
	WritePage($LinkbackClientLogPage, $page);
}