1<?php 2 3use dokuwiki\Extension\Event; 4 5/** 6 * DokuWiki Plugin linksuggest (Action Component) 7 * 8 * ajax autosuggest for links 9 * 10 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 11 * @author lisps 12 */ 13 14class action_plugin_linksuggest extends DokuWiki_Action_Plugin { 15 16 /** 17 * Register the eventhandlers 18 * 19 * @param Doku_Event_Handler $controller 20 */ 21 public function register(Doku_Event_Handler $controller) { 22 $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'page_link'); 23 $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'media_link'); 24 $controller->register_hook('DOKUWIKI_STARTED', 'AFTER', $this, '_add_config'); 25 } 26 public function _add_config(&$event, $param) { 27 global $JSINFO; 28 $JSINFO['append_header'] = $this->getConf('append_header'); 29 } 30 31 /** 32 * ajax Request Handler 33 * page_link 34 * 35 * @param $event 36 */ 37 public function page_link($event) { 38 if ($event->data !== 'plugin_linksuggest') { 39 return; 40 } 41 //no other ajax call handlers needed 42 $event->stopPropagation(); 43 $event->preventDefault(); 44 45 global $INPUT; 46 47 //current page/ns 48 $current_pageid = trim($INPUT->post->str('id')); //current id 49 $current_ns = getNS($current_pageid); 50 $q = trim($INPUT->post->str('q')); //entered string 51 52 //keep hashlink if exists 53 list($q, $hash) = array_pad(explode('#', $q, 2), 2, null); 54 55 $has_hash = !($hash === null); 56 $entered_ns = getNS($q); //namespace of entered string 57 $trailing = ':'; //needs to be remembered, such that actual user input can be returned 58 if($entered_ns === false) { 59 //no namespace given (i.e. none : in $q) 60 // .xxx, ..xxx, ~xxx, if in front of ns, cleaned in $entered_page 61 if (substr($q, 0, 2) === '..') { 62 $entered_ns = '..'; 63 } elseif (substr($q, 0, 1) === '.') { 64 $entered_ns = '.'; 65 66 } elseif (substr($q, 0, 1) === '~') { 67 $entered_ns = '~'; 68 } 69 $trailing = ''; 70 } 71 72 $entered_page = cleanID(noNS($q)); //page part of entered string 73 74 if ($entered_ns === '') { // [[:xxx -> absolute link 75 $matchedPages = $this->search_pages('', $entered_page, $has_hash); 76 } else if (strpos($q, '.') !== false //relative link (., .:, .., ..:, .ns: etc, and :..:, :.: ) 77 || substr($entered_ns, 0, 1) == '~') { // ~, ~:, 78 //resolve the ns based on current id 79 $ns = $entered_ns; 80 if($entered_ns === '~') { 81 //add a random page name, otherwise it ~ or ~: are interpret as ~:start 82 $ns .= 'uniqueadditionforlinksuggestplugin'; 83 } 84 85 if (class_exists('dokuwiki\File\PageResolver')) { 86 // Igor and later 87 $resolver = new dokuwiki\File\PageResolver($current_pageid); 88 $resolved_ns = $resolver->resolveId($ns); 89 } else { 90 // Compatibility with older releases 91 $resolved_ns = $ns; 92 resolve_pageid(getNS($current_pageid), $resolved_ns, $exists); 93 } 94 if($entered_ns === '~') { 95 $resolved_ns = substr($resolved_ns, 0,-35); //remove : and unique string 96 } 97 98 $matchedPages = $this->search_pages($resolved_ns, $entered_page, $has_hash); 99 } else if ($entered_ns === false && $current_ns) { // [[xxx while current page not in root-namespace 100 $matchedPages = array_merge( 101 $this->search_pages($current_ns, $entered_page, true),//search in current for pages 102 $this->search_pages('', $entered_page, $has_hash) //search in root both pgs and ns 103 ); 104 } else { 105 $matchedPages = $this->search_pages($entered_ns, $entered_page, $has_hash); 106 } 107 108 $data_suggestions = []; 109 $link = ''; 110 111 if ($hash !== null && $matchedPages[0]['type'] === 'f') { 112 //if hash is given and a page was found 113 $page = $matchedPages[0]['id']; 114 $meta = p_get_metadata($page, false, METADATA_RENDER_USING_CACHE); 115 116 if (isset($meta['internal']['toc'])) { 117 $toc = $meta['description']['tableofcontents']; 118 Event::createAndTrigger('TPL_TOC_RENDER', $toc, null, false); 119 if (is_array($toc) && count($toc) !== 0) { 120 foreach ($toc as $t) { //loop through toc and compare 121 if ($hash === '' || strpos($t['hid'], $hash) === 0) { 122 $data_suggestions[] = $t; 123 } 124 } 125 $link = $q; 126 } 127 } 128 } else { 129 130 foreach ($matchedPages as $entry) { 131 //a page in rootns 132 if($current_ns !== '' && !$entry['ns'] && $entry['type'] === 'f') { 133 $trailing = ':'; 134 } 135 136 $data_suggestions[] = [ 137 'id' => noNS($entry['id']), 138 //return literally ns what user has typed in before page name/namespace name that is suggested 139 'ns' => $entered_ns . $trailing, 140 'type' => $entry['type'], // d/f 141 'title' => $entry['title'] ?? '', //namespace have no title, for pages sometimes no title 142 'rootns' => $entry['ns'] ? 0 : 1, 143 ]; 144 } 145 } 146 147 echo json_encode([ 148 'data' => $data_suggestions, 149 'link' => $link 150 ]); 151 } 152 153 /** 154 * ajax Request Handler 155 * media_link 156 * 157 * @param Event $event 158 */ 159 public function media_link($event) { 160 if ($event->data !== 'plugin_imglinksuggest') { 161 return; 162 } 163 //no other ajax call handlers needed 164 $event->stopPropagation(); 165 $event->preventDefault(); 166 167 global $INPUT; 168 169 //current media/ns 170 $current_pageid = trim($INPUT->post->str('id')); //current id 171 $current_ns = getNS($current_pageid); 172 $q = trim($INPUT->post->str('q')); //entered string 173 174 $entered_ns = getNS($q); //namespace of entered string 175 $trailing = ':'; //needs to be remembered, such that actual user input can be returned 176 if($entered_ns === false) { 177 //no namespace given (i.e. none : in $q) 178 // .xxx, ..xxx, ~xxx, if in front of ns, cleaned in $entered_page 179 if (substr($q, 0, 2) === '..') { 180 $entered_ns = '..'; 181 } elseif (substr($q, 0, 1) === '.') { 182 $entered_ns = '.'; 183 184 } elseif (substr($q, 0, 1) === '~') { 185 $entered_ns = '~'; 186 } 187 $trailing = ''; 188 } 189 190 $entered_media = cleanID(noNS($q)); //page part of entered string 191 192 if ($entered_ns === '') { // [[:xxx -> absolute link 193 $matchedMedias = $this->search_medias('', $entered_media); 194 } else if (strpos($q, '.') !== false //relative link (., .:, .., ..:, .ns: etc, and :..:, :.: ) 195 || substr($entered_ns, 0, 1) == '~') { // ~, ~:, 196 //resolve the ns based on current id 197 $ns = $entered_ns; 198 if($entered_ns === '~') { 199 //add a random page name, otherwise it ~ or ~: are interpret as ~:start 200 $ns .= 'uniqueadditionforlinksuggestplugin'; 201 } 202 203 if (class_exists('dokuwiki\File\PageResolver')) { 204 // Igor and later 205 $resolver = new dokuwiki\File\MediaResolver($current_pageid); 206 $resolved_ns = $resolver->resolveId($ns); 207 } else { 208 // Compatibility with older releases 209 $resolved_ns = $ns; 210 resolve_mediaid(getNS($current_pageid), $resolved_ns, $exists); 211 } 212 if($entered_ns === '~') { 213 $resolved_ns = substr($resolved_ns, 0,-35); //remove : and unique string 214 } 215 216 $matchedMedias = $this->search_medias($resolved_ns, $entered_media); 217 } else if ($entered_ns === false && $current_ns) { // [[xxx while current page not in root-namespace 218 $matchedMedias = array_merge( 219 $this->search_medias($current_ns, $entered_media), //search in current for pages 220 $this->search_medias('', $entered_media) //search in root both pgs and ns 221 ); 222 } else { 223 $matchedMedias = $this->search_medias($entered_ns, $entered_media); 224 } 225 226 $data_suggestions = []; 227 foreach ($matchedMedias as $entry) { 228 //a page in rootns 229 if($current_ns !== '' && !$entry['ns'] && $entry['type'] === 'f') { 230 $trailing = ':'; 231 } 232 233 $data_suggestions[] = [ 234 'id' => noNS($entry['id']), 235 //return literally ns what user has typed in before page name/namespace name that is suggested 236 'ns' => $entered_ns . $trailing, 237 'type' => $entry['type'], // d/f 238 'rootns' => $entry['ns'] ? 0 : 1, 239 ]; 240 } 241 242 echo json_encode([ 243 'data' => $data_suggestions, 244 'link' => '' 245 ]); 246 } 247 248 249 /** 250 * List available pages, and eventually namespaces 251 * 252 * @param string $ns namespace to search in 253 * @param string $id 254 * @param bool $pagesonly true: pages only, false: pages and namespaces 255 * @return array 256 */ 257 protected function search_pages($ns, $id, $pagesonly = false) { 258 global $conf; 259 260 $data = []; 261 $nsd = utf8_encodeFN(str_replace(':', '/', $ns)); //dir 262 263 $opts = [ 264 'depth' => 1, 265 'listfiles' => true, 266 'listdirs' => !$pagesonly, 267 'pagesonly' => true, 268 'firsthead' => true, 269 'sneakyacl' => $conf['sneaky_index'], 270 ]; 271 if ($id) { 272 $opts['filematch'] = '^.*\/' . $id; 273 } 274 if ($id && !$pagesonly) { 275 $opts['dirmatch'] = '^.*\/' . $id; 276 } 277 search($data, $conf['datadir'], 'search_universal', $opts, $nsd); 278 279 return $data; 280 } 281 282 /** 283 * List available media 284 * 285 * @param string $ns 286 * @param string $id 287 * @return array 288 */ 289 protected function search_medias($ns, $id) { 290 global $conf; 291 292 $data = []; 293 $nsd = utf8_encodeFN(str_replace(':', '/', $ns)); //dir 294 295 $opts = [ 296 'depth' => 1, 297 'listfiles' => true, 298 'listdirs' => true, 299 'firsthead' => true, 300 'sneakyacl' => $conf['sneaky_index'], 301 ]; 302 if ($id) { 303 $opts['filematch'] = '^.*\/' . $id; 304 } 305 if ($id) { 306 $opts['dirmatch'] = '^.*\/' . $id; 307 } 308 search($data, $conf['mediadir'], 'search_universal', $opts, $nsd); 309 310 return $data; 311 } 312 313} 314