You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

562 lines
17 KiB

  1. <?php
  2. /**
  3. * Indexmenu Action Plugin: Indexmenu Component.
  4. *
  5. * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
  6. * @author Samuele Tognini <samuele@samuele.netsons.org>
  7. */
  8. use dokuwiki\Extension\ActionPlugin;
  9. use dokuwiki\Extension\Event;
  10. use dokuwiki\Extension\EventHandler;
  11. use dokuwiki\plugin\indexmenu\Search;
  12. use dokuwiki\Ui\Index;
  13. /**
  14. * Class action_plugin_indexmenu
  15. */
  16. class action_plugin_indexmenu extends ActionPlugin
  17. {
  18. /**
  19. * plugin should use this method to register its handlers with the dokuwiki's event controller
  20. *
  21. * @param EventHandler $controller DokuWiki's event controller object.
  22. */
  23. public function register(EventHandler $controller)
  24. {
  25. if ($this->getConf('only_admins')) {
  26. $controller->register_hook('IO_WIKIPAGE_WRITE', 'BEFORE', $this, 'removeSyntaxIfNotAdmin');
  27. }
  28. if ($this->getConf('page_index') != '') {
  29. $controller->register_hook('TPL_ACT_RENDER', 'BEFORE', $this, 'loadOwnIndexPage');
  30. }
  31. $controller->register_hook('DOKUWIKI_STARTED', 'AFTER', $this, 'extendJSINFO');
  32. $controller->register_hook('PARSER_CACHE_USE', 'BEFORE', $this, 'purgeCache');
  33. if ($this->getConf('show_sort')) {
  34. $controller->register_hook('TPL_CONTENT_DISPLAY', 'BEFORE', $this, 'showSortNumberAtTopOfPage');
  35. }
  36. $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'ajaxCalls');
  37. $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'addStylesForSkins');
  38. }
  39. /**
  40. * Check if user has permission to insert indexmenu
  41. *
  42. * @param Event $event
  43. *
  44. * @author Samuele Tognini <samuele@samuele.netsons.org>
  45. */
  46. public function removeSyntaxIfNotAdmin(Event $event)
  47. {
  48. global $INFO;
  49. if (!$INFO['ismanager']) {
  50. $event->data[0][1] = preg_replace("/{{indexmenu(|_n)>.+?}}/", "", $event->data[0][1]);
  51. }
  52. }
  53. /**
  54. * Add additional info to $JSINFO
  55. *
  56. * @param Event $event
  57. *
  58. * @author Gerrit Uitslag <klapinklapin@gmail.com>
  59. * @author Samuele Tognini <samuele@samuele.netsons.org>
  60. */
  61. public function extendJSINFO(Event $event)
  62. {
  63. global $INFO, $JSINFO;
  64. $JSINFO['isadmin'] = (int)$INFO['isadmin'];
  65. $JSINFO['isauth'] = isset($INFO['userinfo']) ? (int) $INFO['userinfo'] : 0;
  66. }
  67. /**
  68. * Check for pages changes and eventually purge cache.
  69. *
  70. * @param Event $event
  71. *
  72. * @author Samuele Tognini <samuele@samuele.netsons.org>
  73. */
  74. public function purgeCache(Event $event)
  75. {
  76. global $ID;
  77. global $conf;
  78. global $INPUT;
  79. global $INFO;
  80. /** @var cache_parser $cache */
  81. $cache = &$event->data;
  82. if (!isset($cache->page)) return;
  83. //purge only xhtml cache
  84. if ($cache->mode != "xhtml") return;
  85. //Check if it is an indexmenu page
  86. if (!p_get_metadata($ID, 'indexmenu hasindexmenu')) return;
  87. $aclcache = $this->getConf('aclcache');
  88. if ($conf['useacl']) {
  89. $newkey = false;
  90. if ($aclcache == 'user') {
  91. //Cache per user
  92. if ($INPUT->server->str('REMOTE_USER')) {
  93. $newkey = $INPUT->server->str('REMOTE_USER');
  94. }
  95. } elseif ($aclcache == 'groups') {
  96. //Cache per groups
  97. if (isset($INFO['userinfo']['grps'])) {
  98. $newkey = implode('#', $INFO['userinfo']['grps']);
  99. }
  100. }
  101. if ($newkey) {
  102. $cache->key .= "#" . $newkey;
  103. $cache->cache = getCacheName($cache->key, $cache->ext);
  104. }
  105. }
  106. //Check if a page is more recent than purgefile.
  107. if (@filemtime($cache->cache) < @filemtime($conf['cachedir'] . '/purgefile')) {
  108. $event->preventDefault();
  109. $event->stopPropagation();
  110. $event->result = false;
  111. }
  112. }
  113. /**
  114. * Render a defined page as index.
  115. *
  116. * @param Event $event
  117. *
  118. * @author Samuele Tognini <samuele@samuele.netsons.org>
  119. */
  120. public function loadOwnIndexPage(Event $event)
  121. {
  122. if ('index' != $event->data) return;
  123. if (!file_exists(wikiFN($this->getConf('page_index')))) return;
  124. global $lang;
  125. echo '<h1><a id="index">' . $lang['btn_index'] . "</a></h1>\n";
  126. echo p_wiki_xhtml($this->getConf('page_index'));
  127. $event->preventDefault();
  128. $event->stopPropagation();
  129. }
  130. /**
  131. * Display the indexmenu sort number.
  132. *
  133. * @param Event $event
  134. *
  135. * @author Samuele Tognini <samuele@samuele.netsons.org>
  136. */
  137. public function showSortNumberAtTopOfPage(Event $event)
  138. {
  139. global $ID, $ACT, $INFO;
  140. if ($INFO['isadmin'] && $ACT == 'show') {
  141. if ($n = p_get_metadata($ID, 'indexmenu_n')) {
  142. echo '<div class="info">';
  143. echo $this->getLang('showsort') . $n;
  144. echo '</div>';
  145. }
  146. }
  147. }
  148. /**
  149. * Handles ajax requests for indexmenu
  150. *
  151. * @param Event $event
  152. */
  153. public function ajaxCalls(Event $event)
  154. {
  155. if ($event->data !== 'indexmenu') {
  156. return;
  157. }
  158. //no other ajax call handlers needed
  159. $event->stopPropagation();
  160. $event->preventDefault();
  161. global $INPUT;
  162. switch ($INPUT->str('req')) {
  163. case 'local':
  164. //list themes
  165. $this->getlocalThemes();
  166. break;
  167. case 'toc':
  168. //print toc preview
  169. if ($INPUT->has('id')) {
  170. echo $this->printToc($INPUT->str('id'));
  171. }
  172. break;
  173. case 'index':
  174. //for dTree
  175. //retrieval of data of the extra nodes for the indexmenu (if ajax loading set with max#m(#n)
  176. if ($INPUT->has('idx')) {
  177. echo $this->printIndex($INPUT->str('idx'));
  178. }
  179. break;
  180. case 'fancytree':
  181. //data for new index build with Fancytree
  182. $this->getDataFancyTree();
  183. break;
  184. }
  185. }
  186. /**
  187. * Handles ajax requests for FancyTree
  188. *
  189. * @return void
  190. */
  191. private function getDataFancyTree()
  192. {
  193. global $INPUT;
  194. $ns = $INPUT->str('ns', '');
  195. $ns = rtrim($ns, ':');
  196. //key of directory has extra : on the end
  197. $level = -1; //opened levels. -1=all levels open
  198. $max = 1; //levels to load by lazyloading. Before the default was 0. CHANGED to 1.
  199. $skipFileCombined = [];
  200. $skipNsCombined = [];
  201. if ($INPUT->int('max') > 0) {
  202. $max = $INPUT->int('max'); // max#n#m, if init: #n, otherwise #m
  203. $level = $max;
  204. }
  205. if ($INPUT->int('level', -10) >= -1) {
  206. $level = $INPUT->int('level');
  207. }
  208. $isInit = $INPUT->bool('init');
  209. $currentPage = $INPUT->str('currentpage');
  210. if ($isInit) {
  211. $subnss = $INPUT->arr('subnss');
  212. // if 'navbar' is enabled add current ns to list
  213. if ($INPUT->bool('navbar')) {
  214. $currentNs = getNS($currentPage);
  215. if ($currentNs !== false) {
  216. $subnss[] = [$currentNs, 1];
  217. }
  218. }
  219. // alternative, via javascript.. https://wwwendt.de/tech/fancytree/doc/jsdoc/Fancytree.html#loadKeyPath
  220. } else {
  221. //not set via javascript at the moment.. ajax opens per level, so subnss has no use here
  222. $subnss = $INPUT->str('subnss');
  223. if ($subnss !== '') {
  224. $subnss = [[cleanID($subnss), 1]];
  225. }
  226. }
  227. $skipf = $INPUT->str('skipfile');
  228. $skipFileCombined[] = $this->getConf('skip_file');
  229. if (!empty($skipf)) {
  230. $index = 0;
  231. //prefix is '=' or '+'
  232. if ($skipf[0] == '+') {
  233. $index = 1;
  234. }
  235. $skipFileCombined[$index] = substr($skipf, 1);
  236. }
  237. $skipn = $INPUT->str('skipns');
  238. $skipNsCombined[] = $this->getConf('skip_index');
  239. if (!empty($skipn)) {
  240. $index = 0;
  241. //prefix is '=' or '+'
  242. if ($skipn[0] == '+') {
  243. $index = 1;
  244. }
  245. $skipNsCombined[$index] = substr($skipn, 1);
  246. }
  247. $opts = [
  248. //only set for init, lazy requests equal to max
  249. 'level' => $level,
  250. //nons only needed for init as it has no nested nodes
  251. 'nons' => $INPUT->bool('nons'),
  252. 'nopg' => $INPUT->bool('nopg'),
  253. //init with complex array, empty if lazy loading
  254. 'subnss' => $subnss,
  255. 'max' => $max,
  256. 'skipnscombined' => $skipNsCombined,
  257. 'skipfilecombined' => $skipFileCombined,
  258. 'headpage' => $this->getConf('headpage'),
  259. 'hide_headpage' => $this->getConf('hide_headpage'),
  260. ];
  261. $sort = [
  262. 'sort' => $INPUT->str('sort'),
  263. 'msort' => $INPUT->str('msort'),
  264. 'rsort' => $INPUT->bool('rsort'),
  265. 'nsort' => $INPUT->bool('nsort'),
  266. 'group' => $INPUT->bool('group'),
  267. 'hsort' => $INPUT->bool('hsort')
  268. ];
  269. $opts['tempNew'] = true; //TODO temporary for recognizing treenew in the search function
  270. $search = new Search($sort);
  271. $data = $search->search($ns, $opts);
  272. $fancytreeData = $search->buildFancytreeData($data, $isInit, $currentPage, $opts['nopg']);
  273. //add eventually debug info
  274. if ($isInit) {
  275. //for lazy loading are other items than children not supported.
  276. // $fancytreeData['opts'] = $opts;
  277. // $fancytreeData['sort'] = $sort;
  278. // $fancytreeData['debug'] = $data;
  279. } else {
  280. //returns only children, therefore, add debug info to first child
  281. // $fancytreeData[0]['opts'] = $opts;
  282. // $fancytreeData[0]['sort'] = $sort;
  283. // $fancytreeData[0]['debug'] = $data;
  284. }
  285. header('Content-Type: application/json');
  286. echo json_encode($fancytreeData);
  287. }
  288. /**
  289. * Print a list of local themes
  290. *
  291. * @author Samuele Tognini <samuele@samuele.netsons.org>
  292. * @author Gerrit Uitslag <klapinklapin@gmail.com>
  293. */
  294. private function getlocalThemes()
  295. {
  296. header('Content-Type: application/json');
  297. $themebase = 'lib/plugins/indexmenu/images';
  298. $handle = @opendir(DOKU_INC . $themebase);
  299. $themes = [];
  300. while (false !== ($file = readdir($handle))) {
  301. if (
  302. is_dir(DOKU_INC . $themebase . '/' . $file)
  303. && $file != "."
  304. && $file != ".."
  305. && $file != "repository"
  306. && $file != "tmp"
  307. && $file != ".svn"
  308. ) {
  309. $themes[] = $file;
  310. }
  311. }
  312. closedir($handle);
  313. sort($themes);
  314. echo json_encode([
  315. 'themebase' => $themebase,
  316. 'themes' => $themes
  317. ]);
  318. }
  319. /**
  320. * Print a toc preview
  321. *
  322. * @param string $id
  323. * @return string
  324. *
  325. * @author Samuele Tognini <samuele@samuele.netsons.org>
  326. * @author Andreas Gohr <andi@splitbrain.org>
  327. */
  328. private function printToc($id)
  329. {
  330. $id = cleanID($id);
  331. if (auth_quickaclcheck($id) < AUTH_READ) return '';
  332. $meta = p_get_metadata($id);
  333. $toc = $meta['description']['tableofcontents'] ?? [];
  334. if (count($toc) > 1) {
  335. //display ToC of two or more headings
  336. $out = $this->renderToc($toc);
  337. } else {
  338. //display page abstract
  339. $out = $this->renderAbstract($id, $meta);
  340. }
  341. return $out;
  342. }
  343. /**
  344. * Return the TOC rendered to XHTML
  345. *
  346. * @param $toc
  347. * @return string
  348. *
  349. * @author Andreas Gohr <andi@splitbrain.org>
  350. * @author Gerrit Uitslag <klapinklapin@gmail.com>
  351. */
  352. private function renderToc($toc)
  353. {
  354. global $lang;
  355. $out = '<div class="tocheader">';
  356. $out .= $lang['toc'];
  357. $out .= '</div>';
  358. $out .= '<div class="indexmenu_toc_inside">';
  359. $out .= html_buildlist($toc, 'toc', [$this, 'formatIndexmenuListTocItem'], null, true);
  360. $out .= '</div>';
  361. return $out;
  362. }
  363. /**
  364. * Return the page abstract rendered to XHTML
  365. *
  366. * @param $id
  367. * @param array $meta by reference
  368. * @return string
  369. */
  370. private function renderAbstract($id, $meta)
  371. {
  372. $out = '<div class="tocheader">';
  373. $out .= '<a href="' . wl($id) . '">';
  374. $out .= $meta['title'] ? hsc($meta['title']) : hsc(noNS($id));
  375. $out .= '</a>';
  376. $out .= '</div>';
  377. if ($meta['description']['abstract']) {
  378. $out .= '<div class="indexmenu_toc_inside">';
  379. $out .= p_render('xhtml', p_get_instructions($meta['description']['abstract']), $info);
  380. $out .= '</div></div>';
  381. }
  382. return $out;
  383. }
  384. /**
  385. * Callback for html_buildlist
  386. *
  387. * @param $item
  388. * @return string
  389. */
  390. public function formatIndexmenuListTocItem($item)
  391. {
  392. global $INPUT;
  393. $id = cleanID($INPUT->str('id'));
  394. if (isset($item['hid'])) {
  395. $link = '#' . $item['hid'];
  396. } else {
  397. $link = $item['link'];
  398. }
  399. //prefix anchers with page id
  400. if ($link[0] == '#') {
  401. $link = wl($id, $link, false, '');
  402. }
  403. return '<a href="' . $link . '">' . hsc($item['title']) . '</a>';
  404. }
  405. /**
  406. * Print index nodes
  407. *
  408. * @param $ns
  409. * @return string
  410. *
  411. * @author Rene Hadler <rene.hadler@iteas.at>
  412. * @author Samuele Tognini <samuele@samuele.netsons.org>
  413. * @author Andreas Gohr <andi@splitbrain.org>
  414. */
  415. private function printIndex($ns)
  416. {
  417. global $conf, $INPUT;
  418. $idxm = new syntax_plugin_indexmenu_indexmenu();
  419. $ns = $idxm->parseNs(rawurldecode($ns));
  420. $level = -1;
  421. $max = 0;
  422. $data = [];
  423. $skipfilecombined = [];
  424. $skipnscombined = [];
  425. if ($INPUT->int('max') > 0) {
  426. $max = $INPUT->int('max');
  427. $level = $max;
  428. }
  429. $nss = $INPUT->str('nss', '', true);
  430. $sort['sort'] = $INPUT->str('sort', '', true);
  431. $sort['msort'] = $INPUT->str('msort', '', true);
  432. $sort['rsort'] = $INPUT->bool('rsort', false, true);
  433. $sort['nsort'] = $INPUT->bool('nsort', false, true);
  434. $sort['group'] = $INPUT->bool('group', false, true);
  435. $sort['hsort'] = $INPUT->bool('hsort', false, true);
  436. $search = new Search($sort);
  437. $fsdir = "/" . utf8_encodeFN(str_replace(':', '/', $ns));
  438. $skipf = utf8_decodeFN($INPUT->str('skipfile'));
  439. $skipfilecombined[] = $this->getConf('skip_file');
  440. if (!empty($skipf)) {
  441. $index = 0;
  442. if ($skipf[0] == '+') {
  443. $index = 1;
  444. }
  445. $skipfilecombined[$index] = substr($skipf, 1);
  446. }
  447. $skipn = utf8_decodeFN($INPUT->str('skipns'));
  448. $skipnscombined[] = $this->getConf('skip_index');
  449. if (!empty($skipn)) {
  450. $index = 0;
  451. if ($skipn[0] == '+') {
  452. $index = 1;
  453. }
  454. $skipnscombined[$index] = substr($skipn, 1);
  455. }
  456. $opts = [
  457. 'level' => $level,
  458. 'nons' => $INPUT->bool('nons', false, true),
  459. 'nss' => [[$nss, 1]],
  460. 'max' => $max,
  461. 'js' => false,
  462. 'nopg' => $INPUT->bool('nopg', false, true),
  463. 'skipnscombined' => $skipnscombined,
  464. 'skipfilecombined' => $skipfilecombined,
  465. 'headpage' => $idxm->getConf('headpage'),
  466. 'hide_headpage' => $idxm->getConf('hide_headpage')
  467. ];
  468. if ($sort['sort'] || $sort['msort'] || $sort['rsort'] || $sort['hsort']) {
  469. $search->customSearch($data, $conf['datadir'], [$search, 'searchIndexmenuItems'], $opts, $fsdir);
  470. } else {
  471. search($data, $conf['datadir'], [$search, 'searchIndexmenuItems'], $opts, $fsdir);
  472. }
  473. $out = '';
  474. if ($INPUT->int('nojs') === 1) {
  475. $idx = new Index();
  476. $out_tmp = html_buildlist($data, 'idx', [$idxm, 'formatIndexmenuItem'], [$idx, 'tagListItem']);
  477. $out .= preg_replace('/<ul class="idx">(.*)<\/ul>/s', "$1", $out_tmp);
  478. } else {
  479. $nodes = $idxm->builddTreeNodes($data, '', false);
  480. $out = "ajxnodes = [";
  481. $out .= rtrim($nodes[0], ",");
  482. $out .= "];";
  483. }
  484. return $out;
  485. }
  486. /**
  487. * Add Js & Css after template is displayed
  488. *
  489. * @param Event $event
  490. */
  491. public function addStylesForSkins(Event $event)
  492. {
  493. // $event->data["link"][] = [
  494. // "type" => "text/css",
  495. // "rel" => "stylesheet",
  496. // "href" => DOKU_BASE . "lib/plugins/indexmenu/scripts/fancytree/... etc etc"
  497. // ];
  498. // $event->data["link"][] = [
  499. // "type" => "text/css",
  500. // "rel" => "stylesheet",
  501. // "href" => "//fonts.googleapis.com/icon?family=Material+Icons"
  502. // ];
  503. // $event->data["link"][] = [
  504. // "type" => "text/css",
  505. // "rel" => "stylesheet",
  506. // "href" => "//code.getmdl.io/1.3.0/material.indigo-pink.min.css"
  507. // ];
  508. }
  509. }