1<?php 2/** 3 * DokuWiki Plugin multiorphan (Action Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author i-net software <tools@inetsoftware.de> 7 */ 8 9// must be run within Dokuwiki 10if(!defined('DOKU_INC')) { 11 die(); 12} 13 14class action_plugin_multiorphan_multiorphan extends DokuWiki_Action_Plugin { 15 16 private $checkInstructions = array('plugin', 'externallink', 'interwikilink', 'locallink', 'windowssharelink'); 17 private $pagesInstructions = array('internallink', 'camelcaselink'); 18 private $mediaInstructions = array('internalmedia'); 19 20 private $renderer = null; 21 private $checkExternal = false; 22 private $includeWindowsShares = false; 23 24 /** 25 * Registers a callback function for a given event 26 * 27 * @param Doku_Event_Handler $controller DokuWiki's event controller object 28 * @return void 29 */ 30 public function register(Doku_Event_Handler $controller) { 31 32 $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax_call_unknown'); 33 $controller->register_hook('MULTIORPHAN_INSTRUCTION_LINKED', 'BEFORE', $this, 'handle_unknown_instructions'); 34 $controller->register_hook('DOKUWIKI_STARTED', 'AFTER', $this, 'extend_JSINFO'); 35 } 36 37 /** 38 * [Custom event handler which performs action] 39 * 40 * @param Doku_Event $event event object by reference 41 * @param mixed $param [the parameters passed as fifth argument to register_hook() when this 42 * handler was registered] 43 * @return false|null 44 */ 45 46 public function handle_ajax_call_unknown(Doku_Event &$event, $param) { 47 48 global $INPUT, $conf, $AUTH; 49 50 if ( $event->data != 'multiorphan' ) { 51 return false; 52 } 53 if ((!$helper = $this->loadHelper('multiorphan'))) { 54 return false; 55 } 56 if ( !checkSecurityToken() ) { 57 return false; 58 } 59 $event->preventDefault(); 60 61 $namespace = $INPUT->str('ns'); 62 $ns_dir = utf8_encodeFN(str_replace(':','/',$namespace)); 63 $this->checkExternal = $INPUT->bool('checkExternal'); 64 $this->includeWindowsShares = $INPUT->bool('includeWindowsShares'); 65 $result = array(); 66 67 switch( $INPUT->str('do') ) { 68 69 case 'loadpages': { 70 71 $type = 'both'; //$INPUT->str('type'); 72 $includeHidden = $INPUT->bool('includeHidden', false); 73 74 if ( $type == 'both' || $type == 'pages') { 75 $pages = array(); 76 search($pages,$conf['datadir'],'search_universal',array( 77 'showhidden' => $includeHidden, 78 'skipacl' => $includeHidden, 79 'pagesonly' => true, 80 'listfiles' => true, 81 'idmatch' => trim($INPUT->str('filter')) 82 ),$ns_dir); 83 array_walk($pages, array($this, '__map_ids')); 84 } 85 86 if ( $type == 'both' || $type == 'media') { 87 $media = array(); 88 search($media,$conf['mediadir'],'search_media',array( 89 'showhidden' => $includeHidden, 90 'skipacl' => $includeHidden, 91 'pattern' => '/' . str_replace('/', '\/', trim($INPUT->str('filter'))) . '/' 92 ),$ns_dir); 93 array_walk($media, array($this, '__map_ids')); 94 } 95 96 $result = array( 97 'pages' => $pages, 98 'media' => $media 99 ); 100 101 break; 102 } 103 104 case 'checkpage': { 105 106 $id = $INPUT->str('id'); 107 $result = $this->__check_pages($id); 108 break; 109 } 110 111 case 'deletePage' : { 112 113 $link = urldecode($INPUT->str('link')); 114 saveWikiText($link, '', "Remove page via multiORPHANS"); 115 break; 116 } 117 118 case 'viewMedia' : { 119 120 $link = $INPUT->str('link'); 121 ob_start(); 122 tpl_mediaFileDetails($link, null); 123 $result = array( 'dialogContent' => ob_get_contents()); 124 ob_end_clean(); 125 126 // If there is no content, this could be a link only 127 if ( !empty( $result['dialogContent'] ) ) { 128 break; 129 } 130 } 131 132 case 'deleteMedia' : { 133 134 $link = urldecode($INPUT->str('link')); 135 $status = media_delete($link, $AUTH); 136 break; 137 } 138 139 default: { 140 $result = array( 141 'error' => 'I do not know what to do.' 142 ); 143 break; 144 } 145 } 146 147 print json_encode($result); 148 149 } 150 151 /** 152 * Remove not needed information from search 153 */ 154 private function __map_ids(&$element) { 155 $element = $element['id']; 156 } 157 158 /** 159 * Checks a page for the contained links and media. 160 * Returns an array: page|media => array of ids with count 161 */ 162 private function __check_pages($id) { 163 164 global $conf; 165 166 $file = wikiFN($id); 167 $instructions = p_cached_instructions($file, false, $id); 168 $links = array('pages' => array(), 'media' => array(), 'href' => wl($id) ); 169 170 $this->walk_instructions( $links, $id, $instructions ); 171 return $links; 172 } 173 174 /** 175 * Walks a list of instructions to find links 176 */ 177 public function walk_instructions( &$links, $id, $instructions ) { 178 global $ID; 179 180 if (!is_array($instructions)) { 181 return; 182 } 183 184 $internalID = $ID; 185 $ID = $id; 186 foreach ($instructions as $ins) { 187 188 if ($ins[0] == 'nest' ) { 189 $this->walk_instructions( $links, $id, $ins[1][0] ); 190 continue; 191 } 192 193 $data = $this->_getDataContainer( $id, $ins); 194 $evt = new Doku_Event('MULTIORPHAN_INSTRUCTION_LINKED', $data); 195 196 // If prevented, this is definitely an orphan. 197 if (!is_null($data['type']) || (in_array($ins[0], $this->checkInstructions) && $evt->advise_before())) { 198 $this->_addEntryToLinkList( $links, $data); 199 } 200 201 unset($evt); 202 } 203 $ID = $internalID; 204 } 205 206 private function _getDataContainer( $id, $instructions ) { 207 208 return array( 209 'pageID' => $id, 210 'instructions' => $instructions[1], 211 'checkNamespace' => getNS($id), 212 'entryID' => !empty($instructions[1][0]) ? $instructions[1][0] : null, 213 'syntax' => $instructions[0], 214 'type' => $this->getInternalMediaType($instructions[0]), 215 'exists' => null, 216 'additionalEntries' => array(), 217 ); 218 } 219 220 private function _addEntryToLinkList( &$links, $data ) { 221 222 if ( !$data || is_null($data['type'])) { 223 // still not media type so ignore the entry. 224 return; 225 } 226 227 $mid = $data['entryID']; 228 $hash = null; 229 if (strpos($mid, '#') !== false) { 230 list($mid, $hash) = explode('#', $mid); //record pages without hashs 231 } 232 233 $isLocalLink = $data['syntax'] == 'locallink'; 234 if ( $isLocalLink ) { 235 $mid = $data['pageID']; 236 $hash = cleanID($data['instructions'][0]); 237 $data['type'] = 'pages'; 238 } 239 240 if (( !is_bool($data['exists']) || !$data['exists']) && $data['type'] == 'media') { 241 resolve_mediaid($data['checkNamespace'], $mid, $data['exists']); 242 } else if (!is_bool($data['exists']) || !$data['exists']) { 243 resolve_pageid($data['checkNamespace'], $mid, $data['exists']); 244 if ( $data['exists'] && !empty( $hash) ) { 245 // check for 'locallink' in a different page than the current one 246 $linkData = array( 247 'pageID' => $mid, 248 'entryID' => $hash, 249 'exists' => null, 250 ); 251 252 $this->_check_locallink( $linkData ); 253 $data['exists'] = $linkData['exists']; 254 } 255 } 256 257 $itemIndex = $mid . (!empty($hash) ? '#'.$hash : ''); 258 if (!isset($links[$data['type']][$itemIndex])) { 259 $links[$data['type']][$itemIndex] = array( 260 'href' => $this->hrefForType( $data['type'], $itemIndex), 261 'amount' => 0 262 ); 263 } 264 265 $links[$data['type']][$itemIndex]['amount'] += (is_bool($data['exists']) && $data['exists']) ? 1 : 0; 266 267 if ( !is_array($data['additionalEntries']) ) { 268 return; 269 } 270 271 foreach( $data['additionalEntries'] as $additionalEntry ) { 272 $this->_addEntryToLinkList( $links, array_merge( $this->_getDataContainer( $data['id'], $data['instructions']), $additionalEntry)); 273 } 274 } 275 276 private function _plugin_input_to_header( &$input, &$data ) { 277 278 // print_r($input); 279 switch( $input[1][0] ) { 280 case 'box2': 281 if ( $input[1][1][0] == 'title' ) { 282 $input = array( 'header', array( $input[1][1][1]) ); 283 } 284 break; 285 case 'include_include': 286 // Get included instructions 287 $plugin = plugin_load('syntax', 'include_include'); 288 if($plugin != null) { 289 $plugin->render($this->renderer->getFormat(), $this->renderer, $input[1][1]); 290 } 291 292 $instructions = $this->renderer->instructions; 293 $this->renderer->nest_close(); 294 $this->_check_locallink( $data, $instructions); 295 break; 296 } 297 } 298 299 private function _check_locallink( &$data, $instructions = null ) { 300 $this->_init_renderer(); 301 $renderer = &$this->renderer; 302 $data['type'] = 'pages'; 303 $data['exists'] = false; 304 $result = array(); 305 306 if ( is_null($instructions) ) { 307 $instructions = p_cached_instructions(wikiFN($data['pageID']), false, $data['pageID']); 308 } 309 310 if ( !is_null($instructions) ) { 311 $result = array_filter($instructions, function( $input ) use ( $data, $renderer ) { 312 // Closure requires PHP >= 5.3 313 314 if ( $input[0] == 'plugin' ) { 315 $this->_plugin_input_to_header( $input, $data ); 316 } 317 318 if ( $input[0] != 'header' ) { 319 return $data['exists']; 320 } 321 322 $hid = $renderer->_headerToLink( $input[1][0] ); 323 $check = $renderer->_headerToLink( $data['entryID'] ); 324 325 return ($hid == $check); 326 }); 327 } 328 329 $data['exists'] = $data['exists'] || count($result) > 0; 330 } 331 332 /** 333 * Handles unknown instructions using the Event. 334 */ 335 public function handle_unknown_instructions(Doku_Event &$event) { 336 337 //print "Beginn:\n"; 338 //print_r($event->data); 339 $instructions = $event->data['instructions']; 340 switch( $event->data['syntax'] ) { 341 342 case 'locallink': { 343 $this->_check_locallink( $event->data ); 344 } 345 case 'interwikilink': { 346 347 if ( ! $this->checkExternal ) { return false; } 348 $this->_init_renderer(); 349 $exists = false; 350 $event->data['entryID'] = $this->renderer->_resolveInterWiki($instructions[2], $instructions[3], $exists); 351 } 352 case 'externallink': { 353 354 if ( ! $this->checkExternal ) { return false; } 355 356 $httpClient = new dokuwiki\HTTP\DokuHTTPClient(); 357 $httpClient->keep_alive = false; // just close it already. 358 $httpClient->max_bodysize = 0; 359 $data = $httpClient->sendRequest( $event->data['entryID'], null, 'GET' ); 360 $event->data['exists'] = ( $httpClient->status >= 200 && $httpClient->status <= 200 ) || $httpClient->status == 304; 361 $event->data['status'] = $httpClient->status; 362 $event->data['type'] = 'urls'; 363 if ( !empty( $httpClient->error ) ) { 364 $event->data['error'] = $httpClient->error; 365 } 366 367 return true; 368 } 369 case 'windowssharelink': { 370 if ( ! $this->includeWindowsShares ) { return false; } 371 372 if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { 373 return false; 374 } 375 $event->data['exists'] = file_exists($event->data['entryID']); 376 $event->data['type'] = 'media'; 377 return true; 378 } 379 case 'plugin': { 380 switch( $instructions[0] ) { 381 case 'include_include': 382 $event->data['entryID'] = $instructions[1][1]; 383 $event->data['type'] = 'pages'; 384 return true; 385 case 'imagemapping': 386 if ( $instructions[1][1] == 'area' ) { 387 $event->data['type'] = $this->getInternalMediaType($instructions[1][4]); 388 $event->data['entryID'] = $instructions[1][6]; 389 } else 390 { 391 $event->data['type'] = $this->getInternalMediaType($instructions[1][1]); 392 $event->data['entryID'] = $instructions[1][2]; 393 } 394 return true; 395 case 'mp3play': 396 $event->data['entryID'] = $instructions[1]['mp3']; 397 $event->data['type'] = 'media'; 398 return true; 399 case 'imagebox': 400 if ( $instructions[1][0] === 1 ) { 401 $event->data['entryID'] = $instructions[1][1]['src']; 402 $event->data['exists'] = $instructions[1][1]['exist']; 403 $event->data['type'] = 'media'; 404 return true; 405 } 406 default: 407 // print_r($instructions); 408 } 409 } 410 } 411 412 return false; 413 } 414 415 public function extend_JSINFO($event, $param) { 416 global $JSINFO; 417 $JSINFO['schemes'] = array_values(getSchemes()); 418 } 419 420 private function _init_renderer() { 421 if ( $this->renderer != null ) { 422 return; 423 } 424 425 @include_once( dirname( __FILE__ ) . "/../inc/MultiOrphanDummyRenderer.php"); 426 $this->renderer = new MultiOrphanDummyRenderer(); 427 $this->renderer->interwiki = getInterwiki(); 428 } 429 430 private function getInternalMediaType($ins) { 431 return in_array($ins, $this->mediaInstructions) ? 'media' : (in_array($ins, $this->pagesInstructions) ? 'pages' : null); 432 } 433 434 private function hrefForType( $type, $id ) { 435 switch( $type ) { 436 case 'pages': 437 list($link, $hash) = explode('#', $id, 2); 438 if ( !empty( $hash) ) { 439 $this->_init_renderer(); 440 $hash = '#' . $this->renderer->_headerToLink( $hash ); 441 } 442 443 return wl($link) . $hash; 444 case 'urls': 445 return $id; 446 case 'media': 447 return ml($id); 448 default: 449 return null; 450 } 451 } 452} 453 454// vim:ts=4:sw=4:et: 455