* @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; } }