1<?php 2 3/** 4 * Send component of the DokuWiki Linkback action plugin. 5 * 6 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 7 * @author Gina Haeussge <osd@foosel.net> 8 * @link http://wiki.foosel.net/snippets/dokuwiki/linkback 9 */ 10 11use dokuwiki\Form\Form; 12use IXR\Client\Client; 13 14require_once (DOKU_PLUGIN . 'linkback/http.php'); 15 16class action_plugin_linkback_send extends DokuWiki_Action_Plugin { 17 18 var $preact; 19 20 /** 21 * Register the eventhandlers. 22 */ 23 function register(Doku_Event_Handler $controller) { 24 $controller->register_hook('HTML_EDITFORM_OUTPUT', 'BEFORE', $this, 'handle_editform_output', array ()); 25 $controller->register_hook('FORM_EDIT_OUTPUT', 'BEFORE', $this, 'handle_editform_output', array ()); 26 $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handle_action_act_preprocess_before', array()); 27 $controller->register_hook('ACTION_ACT_PREPROCESS', 'AFTER', $this, 'handle_action_act_preprocess_after', array()); 28 } 29 30 /** 31 * Handler for the ACTION_ACT_PREPROCESS event and BEFORE advise. 32 * 33 * Saves current action. 34 */ 35 function handle_action_act_preprocess_before(Doku_Event $event, $params) { 36 if (is_array($event->data)) 37 list($this->preact) = array_keys($event->data); 38 else 39 $this->preact = $event->data; 40 } 41 42 /** 43 * Handler for the ACTION_ACT_PREPROCESS event and AFTER advise. 44 * 45 * Sends linkback if previous action was 'save' and new one is show. 46 */ 47 function handle_action_act_preprocess_after(Doku_Event $event, $params) { 48 global $ID; 49 global $conf; 50 51 // only perform linkbacks on save of a wikipage 52 if ($this->preact != 'save' || $event->data != 'show') 53 return; 54 55 // if guests are not allowed to perform linkbacks, return 56 if (!$this->getConf('allow_guests') && !$_SERVER['REMOTE_USER']) 57 return; 58 59 // get linkback meta file name 60 $file = metaFN($ID, '.linkbacks'); 61 $data = array ( 62 'send' => false, 63 'receive' => false, 64 'display' => false, 65 'sentpings' => array (), 66 'receivedpings' => array (), 67 'number' => 0, 68 69 ); 70 if (@ file_exists($file)) { 71 $data = unserialize(io_readFile($file, false)); 72 } 73 $data['send'] = (bool)$_REQUEST['plugin__linkback_toggle']; 74 75 if (!$data['send']) 76 return; 77 78 $meta = p_get_metadata($ID); 79 80 // prepare linkback info 81 $linkback_info = array (); 82 $linkback_info['title'] = tpl_pagetitle($ID, true); 83 $linkback_info['url'] = wl($ID, '', true); 84 $linkback_info['blog_name'] = $conf['title']; 85 $linkback_info['excerpt'] = $meta['description']['abstract']; 86 87 // get links 88 $ilist = p_cached_instructions(wikiFN($ID),false,$ID); 89 if (!is_array($ilist)) 90 return; 91 $pages = $this->_parse_instructionlist($ilist); 92 93 $sentpings = array (); 94 foreach ($pages as $page) { 95 if (!$data['sentpings'][$page]) { 96 // try to ping pages not already pinged 97 $this->_ping_page($page, $linkback_info); 98 } 99 $sentpings[$page] = true; 100 } 101 $data['sentpings'] = $sentpings; 102 103 // save sent ping info 104 io_saveFile($file, serialize($data)); 105 } 106 107 /** 108 * Parses a given instruction list and extracts external and -- if configured 109 * that way -- internal links. 110 * 111 * @param array $list instruction list as generated by the DokuWiki parser 112 */ 113 function _parse_instructionlist($list) { 114 $pages = array (); 115 116 foreach ($list as $item) { 117 if ($item[0] == 'externallink') { 118 $pages[] = $item[1][0]; 119 } else 120 if ($item[0] == 'internallink' && $this->getConf('ping_internal')) { 121 $pages[] = wl($item[1][0], '', true); 122 } 123 } 124 125 return $pages; 126 } 127 128 /** 129 * Handles HTML_EDITFORM_OUTPUT event. 130 */ 131 function handle_editform_output(Doku_Event $event, $params) { 132 global $ID; 133 global $ACT; 134 global $INFO; 135 136 // Not in edit mode? Quit 137 if ($ACT != 'edit' && $ACT != 'preview') 138 return; 139 140 // page not writable? Quit 141 if (!$INFO['writable']) 142 return; 143 144 // if guests are not allowed to perform linkbacks, return 145 if (!$this->getConf('allow_guests') && !$_SERVER['REMOTE_USER']) 146 return; 147 148 // get linkback meta file name 149 $file = metaFN($ID, '.linkbacks'); 150 $data = array ( 151 'send' => false, 152 'receive' => false, 153 'display' => false, 154 'sentpings' => array (), 155 'receivedpings' => array (), 156 'number' => 0, 157 158 ); 159 if (@ file_exists($file)) { 160 $data = unserialize(io_readFile($file, false)); 161 } else { 162 $namespace_conf = $this->getConf('enabled_namespaces'); 163 if ($namespace_conf == '*') { 164 $data['send'] = true; 165 } else { 166 $namespaces = explode(',', $namespace_conf); 167 $ns = getNS($ID); 168 foreach($namespaces as $namespace) { 169 if ($namespace == '') { 170 continue; 171 } else if ($namespace == '*') { 172 $data['send'] = true; 173 break; 174 } else if (strstr($ns, $namespace) === $ns) { 175 $data['send'] = true; 176 break; 177 } 178 } 179 } 180 } 181 182 /** @var Form|Doku_Form $form */ 183 $form = $event->data; 184 if(is_a($form, Form::class)) { 185 /** @var Form $pos */ 186 $pos = $form->findPositionByAttribute('id','wiki__editbar'); 187 188 $form->addTagOpen('div', $pos); 189 190 //value of an unchecked checkbox is not submitted, set an extra hidden different value to fix prefilling 191 $form->setHiddenField('plugin__linkback_toggle',''); 192 $checkbox = $form->addCheckbox('plugin__linkback_toggle', $this->getLang('linkback_enabledisable'), $pos + 1) 193 ->id('plugin__linkback_toggle') 194 ->addClass('edit') 195 ->val('1'); 196 if($data['send']) { 197 $checkbox->attr('checked', 'checked'); 198 } 199 200 $form->addTagClose('div', $pos + 2); 201 202 } else { 203 $pos = $form->findElementById('wiki__editbar'); 204 $form->insertElement($pos, form_makeOpenTag('div', array('id'=>'plugin__linkback_wrapper'))); 205 $form->insertElement($pos + 1, form_makeCheckboxField('plugin__linkback_toggle', '1', $this->getLang('linkback_enabledisable'), 'plugin__linkback_toggle', 'edit', (($data['send']) ? array('checked' => 'checked') : array()))); 206 $form->insertElement($pos + 2, form_makeCloseTag('div')); 207 } 208 } 209 210 /** 211 * Pings a given page with the given info. 212 */ 213 function _ping_page($page, $linkback_info) { 214 $range = $this->getConf('range') * 1024; 215 216 $http_client = new LinkbackHTTPClient(); 217 $http_client->headers['Range'] = 'bytes=0-' . $range; 218 $http_client->max_bodysize = $range; 219 $http_client->max_bodysize_limit = true; 220 221 $data = $http_client->get($page, true); 222 if (!$data) 223 return false; 224 225 $order = explode(',', $this->getConf('order')); 226 foreach ($order as $type) { 227 if ($this->_ping_page_linkback(trim($type), $page, $http_client->resp_headers, $data, $linkback_info)) 228 return true; 229 } 230 231 return false; 232 } 233 234 /** 235 * Discovers and executes the actual linkback of given type 236 * 237 * @param string $type type of linkback to send, can be "pingback" or "trackback" 238 * @param string $page URL of the page to ping 239 * @param array $headers headers received from page 240 * @param string $body first range bytes of the pages body 241 * @param array $linkback_info linkback info 242 */ 243 function _ping_page_linkback($type, $page, $headers, $body, $linkback_info) { 244 if (!$this->getConf('enable_' . $type)) { 245 return false; 246 } 247 248 switch ($type) { 249 case 'trackback' : 250 { 251 $pingurl = $this->_autodiscover_trackback($page, $body); 252 if (!$pingurl) { 253 return false; 254 } 255 return (bool) $this->_ping_page_trackback($pingurl, $linkback_info); 256 } 257 case 'pingback' : 258 { 259 $xmlrpc_server = $this->_autodiscover_pingback($headers, $body); 260 if (!$xmlrpc_server) { 261 return false; 262 } 263 return (bool) $this->_ping_page_pingback($xmlrpc_server, $linkback_info['url'], $page); 264 } 265 } 266 return false; 267 } 268 269 /** 270 * Sends a Pingback to the given url, using the supplied data. 271 * 272 * @param string $xmlrpc_server URL of remote XML-RPC server 273 * @param string $source_url URL from which to ping 274 * @param string $target_url URL to ping 275 */ 276 function _ping_page_pingback($xmlrpc_server, $source_url, $target_url) { 277 $client = new Client($xmlrpc_server); 278 return $client->query('pingback.ping', $source_url, $target_url); 279 } 280 281 /** 282 * Sends a Trackback to the given url, using the supplied data. 283 * 284 * @param string $pingurl URL to ping 285 * @param array $linkback_info Hash containing title, url and blog_name of linking post 286 * @return bool 287 */ 288 function _ping_page_trackback($pingurl, $linkback_info) { 289 $http_client = new \dokuwiki\HTTP\DokuHTTPClient(); 290 $success = $http_client->post($pingurl, $linkback_info); 291 return ($success !== false); 292 } 293 294 /** 295 * Autodiscovers a pingback URL in the given HTTP headers and body. 296 * 297 * @param array $headers the headers received from to be pinged page. 298 * @param string $data the body received from the pinged page. 299 * @return false|string 300 */ 301 function _autodiscover_pingback(array $headers, string $data) { 302 if (isset ($headers['X-Pingback'])) { 303 return $headers['X-Pingback']; 304 } 305 $regex = '!<link rel="pingback" href="([^"]+)" ?/?>!'; 306 if (!preg_match($regex, $data, $match)) { 307 return false; 308 } 309 return $match[1]; 310 } 311 312 /** 313 * Autodiscovers a trackback URL for the given page URL and site body. 314 * 315 * @param string $page the url of the page to be pinged. 316 * @param string $data the body received from the page to be pinged. 317 */ 318 function _autodiscover_trackback($page, $data) { 319 $page_anchorless = substr($page, 0, strrpos($page, '#')); 320 321 $regex = '!<rdf:RDF.*?</rdf:RDF>!is'; 322 if (preg_match_all($regex, $data, $matches)) { 323 foreach ($matches[0] as $rdf) { 324 if (!preg_match('!dc:identifier="([^"]+)"!is', $rdf, $match_id)) 325 continue; 326 $perm_link = $match_id[1]; 327 if (!($perm_link == $page || $perm_link == $page_anchorless)) 328 continue; 329 if (!(preg_match('!trackback:ping="([^"]+)"!is', $rdf, $match_plink) || preg_match('!about="([^"]+)"!is', $rdf, $match_plink))) 330 continue; 331 return $match_plink[1]; 332 } 333 } else { 334 // fix for wordpress 335 $regex = '!<a href="([^"]*?)" rel="trackback">!is'; 336 if (preg_match($regex, $data, $match)) 337 return $match[1]; 338 } 339 return false; 340 } 341 342} 343