* @link http://wiki.foosel.net/snippets/dokuwiki/linkback
*/
use dokuwiki\Form\Form;
use IXR\Client\Client;
require_once (DOKU_PLUGIN . 'linkback/http.php');
class action_plugin_linkback_send extends DokuWiki_Action_Plugin {
var $preact;
/**
* Register the eventhandlers.
*/
function register(Doku_Event_Handler $controller) {
$controller->register_hook('HTML_EDITFORM_OUTPUT', 'BEFORE', $this, 'handle_editform_output', array ());
$controller->register_hook('FORM_EDIT_OUTPUT', 'BEFORE', $this, 'handle_editform_output', array ());
$controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handle_action_act_preprocess_before', array());
$controller->register_hook('ACTION_ACT_PREPROCESS', 'AFTER', $this, 'handle_action_act_preprocess_after', array());
}
/**
* Handler for the ACTION_ACT_PREPROCESS event and BEFORE advise.
*
* Saves current action.
*/
function handle_action_act_preprocess_before(Doku_Event $event, $params) {
if (is_array($event->data))
list($this->preact) = array_keys($event->data);
else
$this->preact = $event->data;
}
/**
* Handler for the ACTION_ACT_PREPROCESS event and AFTER advise.
*
* Sends linkback if previous action was 'save' and new one is show.
*/
function handle_action_act_preprocess_after(Doku_Event $event, $params) {
global $ID;
global $conf;
// only perform linkbacks on save of a wikipage
if ($this->preact != 'save' || $event->data != 'show')
return;
// if guests are not allowed to perform linkbacks, return
if (!$this->getConf('allow_guests') && !$_SERVER['REMOTE_USER'])
return;
// get linkback meta file name
$file = metaFN($ID, '.linkbacks');
$data = array (
'send' => false,
'receive' => false,
'display' => false,
'sentpings' => array (),
'receivedpings' => array (),
'number' => 0,
);
if (@ file_exists($file)) {
$data = unserialize(io_readFile($file, false));
}
$data['send'] = (bool)$_REQUEST['plugin__linkback_toggle'];
if (!$data['send'])
return;
$meta = p_get_metadata($ID);
// prepare linkback info
$linkback_info = array ();
$linkback_info['title'] = tpl_pagetitle($ID, true);
$linkback_info['url'] = wl($ID, '', true);
$linkback_info['blog_name'] = $conf['title'];
$linkback_info['excerpt'] = $meta['description']['abstract'];
// get links
$ilist = p_cached_instructions(wikiFN($ID),false,$ID);
if (!is_array($ilist))
return;
$pages = $this->_parse_instructionlist($ilist);
$sentpings = array ();
foreach ($pages as $page) {
if (!$data['sentpings'][$page]) {
// try to ping pages not already pinged
$this->_ping_page($page, $linkback_info);
}
$sentpings[$page] = true;
}
$data['sentpings'] = $sentpings;
// save sent ping info
io_saveFile($file, serialize($data));
}
/**
* Parses a given instruction list and extracts external and -- if configured
* that way -- internal links.
*
* @param array $list instruction list as generated by the DokuWiki parser
*/
function _parse_instructionlist($list) {
$pages = array ();
foreach ($list as $item) {
if ($item[0] == 'externallink') {
$pages[] = $item[1][0];
} else
if ($item[0] == 'internallink' && $this->getConf('ping_internal')) {
$pages[] = wl($item[1][0], '', true);
}
}
return $pages;
}
/**
* Handles HTML_EDITFORM_OUTPUT event.
*/
function handle_editform_output(Doku_Event $event, $params) {
global $ID;
global $ACT;
global $INFO;
// Not in edit mode? Quit
if ($ACT != 'edit' && $ACT != 'preview')
return;
// page not writable? Quit
if (!$INFO['writable'])
return;
// if guests are not allowed to perform linkbacks, return
if (!$this->getConf('allow_guests') && !$_SERVER['REMOTE_USER'])
return;
// get linkback meta file name
$file = metaFN($ID, '.linkbacks');
$data = array (
'send' => false,
'receive' => false,
'display' => false,
'sentpings' => array (),
'receivedpings' => array (),
'number' => 0,
);
if (@ file_exists($file)) {
$data = unserialize(io_readFile($file, false));
} else {
$namespace_conf = $this->getConf('enabled_namespaces');
if ($namespace_conf == '*') {
$data['send'] = true;
} else {
$namespaces = explode(',', $namespace_conf);
$ns = getNS($ID);
foreach($namespaces as $namespace) {
if ($namespace == '') {
continue;
} else if ($namespace == '*') {
$data['send'] = true;
break;
} else if (strstr($ns, $namespace) === $ns) {
$data['send'] = true;
break;
}
}
}
}
/** @var Form|Doku_Form $form */
$form = $event->data;
if(is_a($form, Form::class)) {
/** @var Form $pos */
$pos = $form->findPositionByAttribute('id','wiki__editbar');
$form->addTagOpen('div', $pos);
//value of an unchecked checkbox is not submitted, set an extra hidden different value to fix prefilling
$form->setHiddenField('plugin__linkback_toggle','');
$checkbox = $form->addCheckbox('plugin__linkback_toggle', $this->getLang('linkback_enabledisable'), $pos + 1)
->id('plugin__linkback_toggle')
->addClass('edit')
->val('1');
if($data['send']) {
$checkbox->attr('checked', 'checked');
}
$form->addTagClose('div', $pos + 2);
} else {
$pos = $form->findElementById('wiki__editbar');
$form->insertElement($pos, form_makeOpenTag('div', array('id'=>'plugin__linkback_wrapper')));
$form->insertElement($pos + 1, form_makeCheckboxField('plugin__linkback_toggle', '1', $this->getLang('linkback_enabledisable'), 'plugin__linkback_toggle', 'edit', (($data['send']) ? array('checked' => 'checked') : array())));
$form->insertElement($pos + 2, form_makeCloseTag('div'));
}
}
/**
* Pings a given page with the given info.
*/
function _ping_page($page, $linkback_info) {
$range = $this->getConf('range') * 1024;
$http_client = new LinkbackHTTPClient();
$http_client->headers['Range'] = 'bytes=0-' . $range;
$http_client->max_bodysize = $range;
$http_client->max_bodysize_limit = true;
$data = $http_client->get($page, true);
if (!$data)
return false;
$order = explode(',', $this->getConf('order'));
foreach ($order as $type) {
if ($this->_ping_page_linkback(trim($type), $page, $http_client->resp_headers, $data, $linkback_info))
return true;
}
return false;
}
/**
* Discovers and executes the actual linkback of given type
*
* @param string $type type of linkback to send, can be "pingback" or "trackback"
* @param string $page URL of the page to ping
* @param array $headers headers received from page
* @param string $body first range bytes of the pages body
* @param array $linkback_info linkback info
*/
function _ping_page_linkback($type, $page, $headers, $body, $linkback_info) {
if (!$this->getConf('enable_' . $type)) {
return false;
}
switch ($type) {
case 'trackback' :
{
$pingurl = $this->_autodiscover_trackback($page, $body);
if (!$pingurl) {
return false;
}
return (bool) $this->_ping_page_trackback($pingurl, $linkback_info);
}
case 'pingback' :
{
$xmlrpc_server = $this->_autodiscover_pingback($headers, $body);
if (!$xmlrpc_server) {
return false;
}
return (bool) $this->_ping_page_pingback($xmlrpc_server, $linkback_info['url'], $page);
}
}
return false;
}
/**
* Sends a Pingback to the given url, using the supplied data.
*
* @param string $xmlrpc_server URL of remote XML-RPC server
* @param string $source_url URL from which to ping
* @param string $target_url URL to ping
*/
function _ping_page_pingback($xmlrpc_server, $source_url, $target_url) {
$client = new Client($xmlrpc_server);
return $client->query('pingback.ping', $source_url, $target_url);
}
/**
* Sends a Trackback to the given url, using the supplied data.
*
* @param string $pingurl URL to ping
* @param array $linkback_info Hash containing title, url and blog_name of linking post
* @return bool
*/
function _ping_page_trackback($pingurl, $linkback_info) {
$http_client = new \dokuwiki\HTTP\DokuHTTPClient();
$success = $http_client->post($pingurl, $linkback_info);
return ($success !== false);
}
/**
* Autodiscovers a pingback URL in the given HTTP headers and body.
*
* @param array $headers the headers received from to be pinged page.
* @param string $data the body received from the pinged page.
* @return false|string
*/
function _autodiscover_pingback(array $headers, string $data) {
if (isset ($headers['X-Pingback'])) {
return $headers['X-Pingback'];
}
$regex = '!!';
if (!preg_match($regex, $data, $match)) {
return false;
}
return $match[1];
}
/**
* Autodiscovers a trackback URL for the given page URL and site body.
*
* @param string $page the url of the page to be pinged.
* @param string $data the body received from the page to be pinged.
*/
function _autodiscover_trackback($page, $data) {
$page_anchorless = substr($page, 0, strrpos($page, '#'));
$regex = '!!is';
if (preg_match_all($regex, $data, $matches)) {
foreach ($matches[0] as $rdf) {
if (!preg_match('!dc:identifier="([^"]+)"!is', $rdf, $match_id))
continue;
$perm_link = $match_id[1];
if (!($perm_link == $page || $perm_link == $page_anchorless))
continue;
if (!(preg_match('!trackback:ping="([^"]+)"!is', $rdf, $match_plink) || preg_match('!about="([^"]+)"!is', $rdf, $match_plink)))
continue;
return $match_plink[1];
}
} else {
// fix for wordpress
$regex = '!!is';
if (preg_match($regex, $data, $match))
return $match[1];
}
return false;
}
}