Subversion Repositories oidplus

Rev

Rev 1130 | Rev 1162 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

  1. <?php
  2.  
  3. /*
  4.  * OIDplus 2.0
  5.  * Copyright 2019 - 2023 Daniel Marschall, ViaThinkSoft
  6.  *
  7.  * Licensed under the Apache License, Version 2.0 (the "License");
  8.  * you may not use this file except in compliance with the License.
  9.  * You may obtain a copy of the License at
  10.  *
  11.  *     http://www.apache.org/licenses/LICENSE-2.0
  12.  *
  13.  * Unless required by applicable law or agreed to in writing, software
  14.  * distributed under the License is distributed on an "AS IS" BASIS,
  15.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16.  * See the License for the specific language governing permissions and
  17.  * limitations under the License.
  18.  */
  19.  
  20. namespace ViaThinkSoft\OIDplus;
  21.  
  22. // phpcs:disable PSR1.Files.SideEffects
  23. \defined('INSIDE_OIDPLUS') or die;
  24. // phpcs:enable PSR1.Files.SideEffects
  25.  
  26. class OIDplusPagePublicResources extends OIDplusPagePluginPublic {
  27.  
  28.         /**
  29.          * @return array|mixed|string|string[]
  30.          */
  31.         private function getMainTitle() {
  32.                 return _L('Documents and Resources');
  33.         }
  34.  
  35.         /**
  36.          * @param bool $html
  37.          * @return void
  38.          * @throws OIDplusException
  39.          */
  40.         public function init(bool $html=true) {
  41.                 OIDplus::config()->prepareConfigKey('resource_plugin_autoopen_level', 'Resource plugin: How many levels should be open in the treeview when OIDplus is loaded?', '1', OIDplusConfig::PROTECTION_EDITABLE, function($value) {
  42.                         if (!is_numeric($value) || ($value < 0)) {
  43.                                 throw new OIDplusException(_L('Please enter a valid value.'));
  44.                         }
  45.                 });
  46.                 OIDplus::config()->delete('resource_plugin_title');
  47.                 OIDplus::config()->delete('resource_plugin_path');
  48.                 OIDplus::config()->prepareConfigKey('resource_plugin_hide_empty_path','Resource plugin: Hide empty paths? (0=no, 1=yes)', '1', OIDplusConfig::PROTECTION_EDITABLE, function($value) {
  49.                         if (!is_numeric($value) || (($value != 0) && ($value != 1))) {
  50.                                 throw new OIDplusException(_L('Please enter a valid value (0=no, 1=yes).'));
  51.                         }
  52.                 });
  53.         }
  54.  
  55.         /**
  56.          * @param string $file
  57.          * @return string
  58.          * @throws OIDplusConfigInitializationException|OIDplusException
  59.          */
  60.         private static function getDocumentContent(string $file): string {
  61.                 $file = self::realname($file);
  62.                 $file2 = preg_replace('/\.([^.]+)$/', '$'.OIDplus::getCurrentLang().'.\1', $file);
  63.                 if (file_exists($file2)) $file = $file2;
  64.  
  65.                 $cont = file_get_contents($file);
  66.                 if (!$cont) return '';
  67.  
  68.                 list($html, $js, $css) = extractHtmlContents($cont);
  69.                 $cont = '';
  70.                 if (!empty($js))  $cont .= "<script>\n$js\n</script>";
  71.                 if (!empty($css)) $cont .= "<style>\n$css\n</style>";
  72.                 $cont .= stripHtmlComments($html);
  73.  
  74.                 return $cont;
  75.         }
  76.  
  77.         /**
  78.          * @param string $file
  79.          * @return array|mixed|string
  80.          * @throws OIDplusConfigInitializationException
  81.          * @throws OIDplusException
  82.          */
  83.         private static function getDocumentTitle(string $file) {
  84.                 $file = self::realname($file);
  85.                 $file2 = preg_replace('/\.([^.]+)$/', '$'.OIDplus::getCurrentLang().'.\1', $file);
  86.                 if (file_exists($file2)) $file = $file2;
  87.  
  88.                 $cont = file_get_contents($file);
  89.  
  90.                 // make sure the program works even if the user provided HTML is not UTF-8
  91.                 $cont = convert_to_utf8_no_bom($cont);
  92.  
  93.                 $m = array();
  94.                 if (preg_match('@<title>(.+)</title>@ismU', $cont, $m)) return $m[1];
  95.                 if (preg_match('@<h1>(.+)</h1>@ismU', $cont, $m)) return $m[1];
  96.                 if (preg_match('@<h2>(.+)</h2>@ismU', $cont, $m)) return $m[1];
  97.                 if (preg_match('@<h3>(.+)</h3>@ismU', $cont, $m)) return $m[1];
  98.                 if (preg_match('@<h4>(.+)</h4>@ismU', $cont, $m)) return $m[1];
  99.                 if (preg_match('@<h5>(.+)</h5>@ismU', $cont, $m)) return $m[1];
  100.                 if (preg_match('@<h6>(.+)</h6>@ismU', $cont, $m)) return $m[1];
  101.                 return pathinfo($file, PATHINFO_FILENAME); // filename without extension
  102.         }
  103.  
  104.         /**
  105.          * @param string $source
  106.          * @return bool
  107.          * @throws OIDplusException
  108.          */
  109.         protected static function mayAccessResource(string $source): bool {
  110.                 if (OIDplus::authUtils()->isAdminLoggedIn()) return true;
  111.  
  112.                 $candidates = array(
  113.                         OIDplus::localpath().'userdata/resources/security.ini',
  114.                         OIDplus::localpath().'res/security.ini'
  115.                 );
  116.                 foreach ($candidates as $ini_file) {
  117.                         if (file_exists($ini_file)) {
  118.                                 $data = @parse_ini_file($ini_file, true);
  119.                                 if (isset($data['Security']) && isset($data['Security'][$source])) {
  120.                                         $level = $data['Security'][$source];
  121.                                         if ($level == 'PUBLIC') {
  122.                                                 return true;
  123.                                         } else if ($level == 'RA') {
  124.                                                 return
  125.                                                         ((OIDplus::authUtils()->raNumLoggedIn() > 0) ||
  126.                                                         (OIDplus::authUtils()->isAdminLoggedIn()));
  127.                                         } else if ($level == 'ADMIN') {
  128.                                                 return OIDplus::authUtils()->isAdminLoggedIn();
  129.                                         } else {
  130.                                                 throw new OIDplusException(_L('Unexpected security level in %1 (expect PUBLIC, RA or ADMIN)', $ini_file));
  131.                                         }
  132.                                 }
  133.                         }
  134.                 }
  135.                 return true;
  136.         }
  137.  
  138.         /**
  139.          * @param string $reldir
  140.          * @param bool $onlydir
  141.          * @return array
  142.          * @throws OIDplusException
  143.          */
  144.         private static function myglob(string $reldir, bool $onlydir=false): array {
  145.                 $out = array();
  146.  
  147.                 $root = OIDplus::localpath().'userdata/resources/';
  148.                 $res = $onlydir ? @glob($root.ltrim($reldir,'/'), GLOB_ONLYDIR) : @glob($root.ltrim($reldir,'/'));
  149.                 if ($res) foreach ($res as &$x) {
  150.                         $x = substr($x, strlen($root));
  151.                         if (strpos($x,'$') !== false) continue;
  152.                         $out[] = $x;
  153.                 }
  154.  
  155.                 $root = OIDplus::localpath().'res/';
  156.                 $res = $onlydir ? @glob($root.ltrim($reldir,'/'), GLOB_ONLYDIR) : @glob($root.ltrim($reldir,'/'));
  157.                 if ($res) foreach ($res as $x) {
  158.                         $x = substr($x, strlen($root));
  159.                         if (strpos($x,'$') !== false) continue;
  160.                         $out[] = $x;
  161.                 }
  162.  
  163.                 $out = array_unique($out);
  164.  
  165.                 return array_filter($out, function($v, $k) {
  166.                         return self::mayAccessResource($v);
  167.                 }, ARRAY_FILTER_USE_BOTH);
  168.         }
  169.  
  170.         /**
  171.          * @param string $rel
  172.          * @return string|null
  173.          */
  174.         private static function realname(string $rel) {
  175.                 $candidate1 = OIDplus::localpath().'userdata/resources/'.$rel;
  176.                 $candidate2 = OIDplus::localpath().'res/'.$rel;
  177.                 if (file_exists($candidate1) || is_dir($candidate1)) return $candidate1;
  178.                 if (file_exists($candidate2) || is_dir($candidate2)) return $candidate2;
  179.                 return null;
  180.         }
  181.  
  182.         /**
  183.          * @param string $source
  184.          * @param string $target
  185.          * @return bool
  186.          */
  187.         protected static function checkRedirect(string $source, string &$target): bool {
  188.                 $candidates = array(
  189.                         OIDplus::localpath().'userdata/resources/redirect.ini',
  190.                         OIDplus::localpath().'res/redirect.ini'
  191.                 );
  192.                 foreach ($candidates as $ini_file) {
  193.                         if (file_exists($ini_file)) {
  194.                                 $data = @parse_ini_file($ini_file, true);
  195.                                 if (isset($data['Redirects']) && isset($data['Redirects'][$source])) {
  196.                                         $target = $data['Redirects'][$source];
  197.                                         return true;
  198.                                 }
  199.                         }
  200.                 }
  201.                 return false;
  202.         }
  203.  
  204.         /**
  205.          * @param string $id
  206.          * @param array $out
  207.          * @param bool $handled
  208.          * @return void
  209.          * @throws OIDplusConfigInitializationException
  210.          * @throws OIDplusException
  211.          */
  212.         public function gui(string $id, array &$out, bool &$handled) {
  213.                 if (explode('$',$id,2)[0] === 'oidplus:resources') {
  214.                         $handled = true;
  215.  
  216.                         $tmp = explode('$',$id);
  217.                         $file = $tmp[1] ?? '';
  218.                         unset($tmp);
  219.  
  220.                         // Security checks
  221.  
  222.                         if (
  223.                                 ($file != '') && (
  224.                                 (strpos($file, chr(0)) !== false) || // Directory traversal (LFI,RFI) helper
  225.                                 (strpos($file, '../') !== false) || ($file[0] == '/') || ($file[0] == '~') || // <-- Local File Injection (LFI)
  226.                                 ($file[0] == '.') || (strpos($file, '/.') !== false) ||                       // <-- Calling hidden files e.g. ".htpasswd"
  227.                                 (strpos($file, '://') !== false)                                              // <-- Remote File Injection (RFI)
  228.                            )) {
  229.                                 if (strpos($file, chr(0)) !== false) {
  230.                                         $file = str_replace(chr(0), '[NUL]', $file);
  231.                                 }
  232.                                 // This will not be logged anymore, because people could spam the log files otherwise
  233.                                 //OIDplus::logger()->log("[WARN]A!", "LFI/RFI attack blocked (requested file '$file')");
  234.                                 $out['title'] = _L('Access denied');
  235.                                 $out['icon'] = 'img/error.png';
  236.                                 $out['text'] = '<p>'._L('This request is invalid').'</p>';
  237.                                 return;
  238.                         }
  239.  
  240.                         $out['text'] = '';
  241.  
  242.                         // Check for permission
  243.  
  244.                         if ($file != '') {
  245.                                 if (!self::mayAccessResource($file)) {
  246.                                         $out['title'] = _L('Access denied');
  247.                                         $out['icon'] = 'img/error.png';
  248.                                         $out['text'] = '<p>'._L('Authentication error. Please log in.').'</p>';
  249.                                         return;
  250.                                 }
  251.                         }
  252.  
  253.                         // Redirections
  254.  
  255.                         if ($file != '') {
  256.                                 $target = '';
  257.                                 if (self::checkRedirect($file, $target)) {
  258.                                         $out['title'] = _L('Please wait...');
  259.                                         $out['text'] = '<p>'._L('You are being redirected...').'</p><script>window.location.href = '.js_escape($target).';</script>';
  260.                                         return;
  261.                                 }
  262.                         }
  263.  
  264.                         // First, "Go back to" line
  265.  
  266.                         if ($file != '') {
  267.                                 $dir = dirname($file);
  268.  
  269.                                 if ($dir == '.') {
  270.                                         if (file_exists(__DIR__.'/img/main_icon16.png')) {
  271.                                                 $tree_icon = OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'img/main_icon16.png';
  272.                                         } else {
  273.                                                 $tree_icon = null; // default icon (folder)
  274.                                         }
  275.  
  276.                                         $ic = empty($tree_icon) ? '' : '<img src="'.$tree_icon.'" alt="">';
  277.  
  278.                                         $lng_gobackto = _L('Go back to').':';
  279.                                         $out['text'] .= '<p><a '.OIDplus::gui()->link('oidplus:resources').'><img src="img/arrow_back.png" width="16" alt="'._L('Go back').'"> '.$lng_gobackto.' '.$ic.' '.htmlentities($this->getMainTitle()).'</a></p>';
  280.                                 } else {
  281.                                         $realdir = self::realname($dir);
  282.  
  283.                                         $tree_icon = OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'show_icon.php?mode=folder_icon16&lang='.OIDplus::getCurrentLang().'&file='.urlencode($dir);
  284.                                         /*
  285.                                         $icon_candidate = pathinfo($realdir)['dirname'].'/'.pathinfo($realdir)['filename'].'_tree.png';
  286.                                         if (file_exists($icon_candidate)) {
  287.                                                 $tree_icon = $icon_candidate;
  288.                                         } else if (file_exists(__DIR__.'/img/folder_icon16.png')) {
  289.                                                 $tree_icon = OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'img/folder_icon16.png';
  290.                                         } else {
  291.                                                 $tree_icon = null; // no icon
  292.                                         }
  293.                                         */
  294.  
  295.                                         $ic = /*empty($tree_icon) ? '' : */'<img src="'.$tree_icon.'" alt="">';
  296.  
  297.                                         $out['text'] .= '<p><a '.OIDplus::gui()->link('oidplus:resources$'.rtrim($dir,'/').'/').'><img src="img/arrow_back.png" width="16" alt="'._L('Go back').'"> '._L('Go back to').': '.$ic.' '.htmlentities(self::getFolderTitle($realdir)).'</a></p><br>';
  298.                                 }
  299.                         }
  300.  
  301.                         // Then the content
  302.  
  303.                         $realfile = self::realname($file);
  304.                         // $realfile2 = preg_replace('/\.([^.]+)$/', '$'.OIDplus::getCurrentLang().'.\1', $realfile);
  305.                         // if (file_exists($realfile2)) $realfile = $realfile2;
  306.  
  307.                         if (file_exists($realfile) && (!is_dir($realfile))) {
  308.                                 if ((substr($file,-4,4) == '.url') || (substr($file,-5,5) == '.link')) {
  309.                                         $out['title'] = $this->getHyperlinkTitle($realfile);
  310.  
  311.                                         $out['icon'] = OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'show_icon.php?mode=leaf_url_icon&lang='.OIDplus::getCurrentLang().'&file='.urlencode($file);
  312.                                         /*
  313.                                         $icon_candidate = pathinfo($realfile)['dirname'].'/'.pathinfo($realfile)['filename'].'_big.png';
  314.                                         if (file_exists($icon_candidate)) {
  315.                                                 $out['icon'] = $icon_candidate;
  316.                                         } else if (file_exists(__DIR__.'/img/leaf_url_icon.png')) {
  317.                                                 $out['icon'] = OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'img/leaf_url_icon.png';
  318.                                         } else {
  319.                                                 $out['icon'] = '';
  320.                                         }
  321.                                         */
  322.  
  323.                                         // Should not happen though, due to conditionalselect
  324.                                         $out['text'] .= '<a href="'.htmlentities(self::getHyperlinkURL($realfile)).'" target="_blank">'._L('Open in new window').'</a>';
  325.                                 } else if ((substr($file,-4,4) == '.htm') || (substr($file,-5,5) == '.html')) {
  326.                                         $out['title'] = $this->getDocumentTitle($file);
  327.  
  328.                                         $out['icon'] = OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'show_icon.php?mode=leaf_doc_icon&lang='.OIDplus::getCurrentLang().'&file='.urlencode($file);
  329.                                         /*
  330.                                         $icon_candidate = pathinfo($realfile)['dirname'].'/'.pathinfo($realfile)['filename'].'_big.png';
  331.                                         if (file_exists($icon_candidate)) {
  332.                                                 $out['icon'] = $icon_candidate;
  333.                                         } else if (file_exists(__DIR__.'/img/leaf_doc_icon.png')) {
  334.                                                 $out['icon'] = OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'img/leaf_doc_icon.png';
  335.                                         } else {
  336.                                                 $out['icon'] = '';
  337.                                         }
  338.                                         */
  339.  
  340.                                         $out['text'] .= self::getDocumentContent($file);
  341.                                 } else {
  342.                                         $out['title'] = _L('Unknown file type');
  343.                                         $out['icon'] = 'img/error.png';
  344.                                         $out['text'] = '<p>'._L('The system does not know how to handle this file type.').'</p>';
  345.                                 }
  346.                         } else if (is_dir($realfile)) {
  347.                                 $out['title'] = ($file == '') ? $this->getMainTitle() : self::getFolderTitle($realfile);
  348.  
  349.                                 if ($file == '') {
  350.                                         $out['icon'] = file_exists(__DIR__.'/img/main_icon.png') ? OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'img/main_icon.png' : '';
  351.                                 } else {
  352.                                         $out['icon'] = OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'show_icon.php?mode=folder_icon&lang='.OIDplus::getCurrentLang().'&file='.urlencode($file);
  353.                                         /*
  354.                                         $icon_candidate = pathinfo($realfile)['dirname'].'/'.pathinfo($realfile)['filename'].'_big.png';
  355.                                         if (file_exists($icon_candidate)) {
  356.                                                 $out['icon'] = $icon_candidate;
  357.                                         } else if (file_exists(__DIR__.'/img/folder_icon.png')) {
  358.                                                 $out['icon'] = OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'img/folder_icon.png';
  359.                                         } else {
  360.                                                 $out['icon'] = null; // no icon
  361.                                         }
  362.                                         */
  363.                                 }
  364.  
  365.                                 if (file_exists(__DIR__.'/img/main_icon16.png')) {
  366.                                         $tree_icon = OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'img/main_icon16.png';
  367.                                 } else {
  368.                                         $tree_icon = null; // default icon (folder)
  369.                                 }
  370.  
  371.                                 $count = 0;
  372.  
  373.                                 $dirs = self::myglob(rtrim($file,'/').'/'.'*', true);
  374.                                 natcasesort($dirs);
  375.                                 foreach ($dirs as $dir) {
  376.                                         $realdir = self::realname($dir);
  377.                                         $tree_icon = OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'show_icon.php?mode=folder_icon16&lang='.OIDplus::getCurrentLang().'&file='.urlencode($dir);
  378.                                         /*
  379.                                         $icon_candidate = pathinfo($realdir)['dirname'].'/'.pathinfo($realdir)['filename'].'_tree.png';
  380.                                         if (file_exists($icon_candidate)) {
  381.                                                 $tree_icon = $icon_candidate;
  382.                                         } else if (file_exists(__DIR__.'/img/folder_icon16.png')) {
  383.                                                 $tree_icon = OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'img/folder_icon16.png';
  384.                                         } else {
  385.                                                 $tree_icon = null; // no icon
  386.                                         }
  387.                                         */
  388.  
  389.                                         $ic = /*empty($tree_icon) ? '' : */'<img src="'.$tree_icon.'" alt="">';
  390.  
  391.                                         $out['text'] .= '<p><a '.OIDplus::gui()->link('oidplus:resources$'.rtrim($dir,'/').'/').'>'.$ic.' '.htmlentities(self::getFolderTitle($realdir)).'</a></p>';
  392.                                         $count++;
  393.                                 }
  394.  
  395.                                 $files = array_merge(
  396.                                         self::myglob(rtrim($file,'/').'/'.'*.htm'), // TODO: also PHP?
  397.                                         self::myglob(rtrim($file,'/').'/'.'*.html'),
  398.                                         self::myglob(rtrim($file,'/').'/'.'*.url'),
  399.                                         self::myglob(rtrim($file,'/').'/'.'*.link')
  400.                                 );
  401.                                 natcasesort($files);
  402.                                 foreach ($files as $file) {
  403.                                         $realfile = self::realname($file);
  404.                                         if ((substr($file,-4,4) == '.url') || (substr($file,-5,5) == '.link')) {
  405.                                                 $tree_icon = OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'show_icon.php?mode=leaf_url_icon16&lang='.OIDplus::getCurrentLang().'&file='.urlencode($file);
  406.                                                 /*
  407.                                                 $icon_candidate = pathinfo($realfile)['dirname'].'/'.pathinfo($realfile)['filename'].'_tree.png';
  408.                                                 if (file_exists($icon_candidate)) {
  409.                                                         $tree_icon = $icon_candidate;
  410.                                                 } else if (file_exists(__DIR__.'/img/leaf_url_icon16.png')) {
  411.                                                         $tree_icon = OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'img/leaf_url_icon16.png';
  412.                                                 } else {
  413.                                                         $tree_icon = null; // default icon (folder)
  414.                                                 }
  415.                                                 */
  416.  
  417.                                                 $ic = /*empty($tree_icon) ? '' : */'<img src="'.$tree_icon.'" alt="">';
  418.  
  419.                                                 $out['text'] .= '<p><a href="'.htmlentities(self::getHyperlinkURL($realfile)).'" target="_blank">'.$ic.' '.htmlentities($this->getHyperlinkTitle($realfile)).'</a></p>';
  420.                                                 $count++;
  421.                                         } else {
  422.                                                 $tree_icon = OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'show_icon.php?mode=leaf_doc_icon16&lang='.OIDplus::getCurrentLang().'&file='.urlencode($file);
  423.                                                 /*
  424.                                                 $icon_candidate = pathinfo($realfile)['dirname'].'/'.pathinfo($realfile)['filename'].'_tree.png';
  425.                                                 if (file_exists($icon_candidate)) {
  426.                                                         $tree_icon = $icon_candidate;
  427.                                                 } else if (file_exists(__DIR__.'/img/leaf_doc_icon16.png')) {
  428.                                                         $tree_icon = OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'img/leaf_doc_icon16.png';
  429.                                                 } else {
  430.                                                         $tree_icon = null; // default icon (folder)
  431.                                                 }
  432.                                                 */
  433.  
  434.                                                 $ic = /*empty($tree_icon) ? '' : */'<img src="'.$tree_icon.'" alt="">';
  435.  
  436.                                                 $out['text'] .= '<p><a '.OIDplus::gui()->link('oidplus:resources$'.$file).'>'.$ic.' '.htmlentities($this->getDocumentTitle($file)).'</a></p>';
  437.                                                 $count++;
  438.                                         }
  439.                                 }
  440.  
  441.                                 if ($count == 0) {
  442.                                         $out['text'] .= '<p>'._L('This folder does not contain any elements').'</p>';
  443.                                 }
  444.                         } else {
  445.                                 $out['title'] = _L('Not found');
  446.                                 $out['icon'] = 'img/error.png';
  447.                                 $out['text'] = '<p>'._L('This resource doesn\'t exist anymore.').'</p>';
  448.                         }
  449.                 }
  450.         }
  451.  
  452.         /**
  453.          * @param array $children
  454.          * @param string|null $rootdir
  455.          * @param int $depth
  456.          * @return void
  457.          * @throws OIDplusConfigInitializationException
  458.          * @throws OIDplusException
  459.          */
  460.         private function tree_rec(array &$children, string $rootdir=null, int $depth=0)/*: void*/ {
  461.                 if (is_null($rootdir)) $rootdir = '';
  462.                 if ($depth > 100) return; // something is wrong!
  463.  
  464.                 $dirs = self::myglob($rootdir.'*'.'/', true);
  465.                 natcasesort($dirs);
  466.                 foreach ($dirs as $dir) {
  467.                         $tmp = array();
  468.  
  469.                         $this->tree_rec($tmp, $dir, $depth+1);
  470.  
  471.                         $realdir = self::realname($dir);
  472.  
  473.                         $tree_icon = OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'show_icon.php?mode=folder_icon16&lang='.OIDplus::getCurrentLang().'&file='.urlencode($dir);
  474.                         /*
  475.                         $icon_candidate = pathinfo($realdir)['dirname'].'/'.pathinfo($realdir)['filename'].'_tree.png';
  476.                         if (file_exists($icon_candidate)) {
  477.                                 $tree_icon = $icon_candidate;
  478.                         } else if (file_exists(__DIR__.'/img/folder_icon16.png')) {
  479.                                 $tree_icon = OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'img/folder_icon16.png';
  480.                         } else {
  481.                                 $tree_icon = null; // default icon (folder)
  482.                         }
  483.                         */
  484.  
  485.                         $children[] = array(
  486.                                 'id' => 'oidplus:resources$'.$dir,
  487.                                 'icon' => $tree_icon,
  488.                                 'text' => self::getFolderTitle($realdir),
  489.                                 'children' => $tmp,
  490.                                 'state' => array("opened" => $depth <= OIDplus::config()->getValue('resource_plugin_autoopen_level', 1)-1)
  491.                         );
  492.                 }
  493.  
  494.                 $files = array_merge(
  495.                         self::myglob($rootdir.'*.htm'), // TODO: Also PHP?
  496.                         self::myglob($rootdir.'*.html'),
  497.                         self::myglob($rootdir.'*.url'),
  498.                         self::myglob($rootdir.'*.link')
  499.                 );
  500.                 natcasesort($files);
  501.                 foreach ($files as $file) {
  502.                         $realfile = self::realname($file);
  503.                         if ((substr($file,-4,4) == '.url') || (substr($file,-5,5) == '.link')) {
  504.                                 $tree_icon = OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'show_icon.php?mode=leaf_url_icon16&lang='.OIDplus::getCurrentLang().'&file='.urlencode($file);
  505.                                 /*
  506.                                 $icon_candidate = pathinfo($realfile)['dirname'].'/'.pathinfo($realfile)['filename'].'_tree.png';
  507.                                 if (file_exists($icon_candidate)) {
  508.                                         $tree_icon = $icon_candidate;
  509.                                 } else if (file_exists(__DIR__.'/img/leaf_url_icon16.png')) {
  510.                                         $tree_icon = OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'img/leaf_url_icon16.png';
  511.                                 } else {
  512.                                         $tree_icon = null; // default icon (folder)
  513.                                 }
  514.                                 */
  515.  
  516.                                 $children[] = array(
  517.                                         'id' => 'oidplus:resources$'.$file,
  518.                                         'conditionalselect' => 'window.open('.js_escape(self::getHyperlinkURL($realfile)).'); false;',
  519.                                         'icon' => $tree_icon,
  520.                                         'text' => $this->getHyperlinkTitle($realfile),
  521.                                         'state' => array("opened" => $depth <= OIDplus::config()->getValue('resource_plugin_autoopen_level', 1)-1),
  522.                                         'a_attr' => array(
  523.                                                 'href' => self::getHyperlinkURL($realfile),
  524.                                                 'target' => '_blank'
  525.                                         )
  526.                                 );
  527.                         } else {
  528.                                 $tree_icon = OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'show_icon.php?mode=leaf_doc_icon16&lang='.OIDplus::getCurrentLang().'&file='.urlencode($file);
  529.                                 /*
  530.                                 $icon_candidate = pathinfo($realfile)['dirname'].'/'.pathinfo($realfile)['filename'].'_tree.png';
  531.                                 if (file_exists($icon_candidate)) {
  532.                                         $tree_icon = $icon_candidate;
  533.                                 } else if (file_exists(__DIR__.'/img/leaf_doc_icon16.png')) {
  534.                                         $tree_icon = OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'img/leaf_doc_icon16.png';
  535.                                 } else {
  536.                                         $tree_icon = null; // default icon (folder)
  537.                                 }
  538.                                 */
  539.                                 $children[] = array(
  540.                                         'id' => 'oidplus:resources$'.$file,
  541.                                         'icon' => $tree_icon,
  542.                                         'text' => $this->getDocumentTitle($file),
  543.                                         'state' => array("opened" => $depth <= OIDplus::config()->getValue('resource_plugin_autoopen_level', 1)-1)
  544.                                 );
  545.                         }
  546.                 }
  547.         }
  548.  
  549.         /**
  550.          * @param array $json
  551.          * @param array $out
  552.          * @return void
  553.          */
  554.         private function publicSitemap_rec(array $json, array &$out) {
  555.                 foreach ($json as $x) {
  556.                         if (isset($x['id']) && $x['id']) {
  557.                                 $out[] = $x['id'];
  558.                         }
  559.                         if (isset($x['children'])) {
  560.                                 $this->publicSitemap_rec($x['children'], $out);
  561.                         }
  562.                 }
  563.         }
  564.  
  565.         /**
  566.          * @param array $out
  567.          * @return void
  568.          */
  569.         public function publicSitemap(array &$out) {
  570.                 $json = array();
  571.                 $this->tree($json, null/*RA EMail*/, false/*HTML tree algorithm*/, "*"/*display all*/);
  572.                 $this->publicSitemap_rec($json, $out);
  573.         }
  574.  
  575.         /**
  576.          * @param array $json
  577.          * @param string|null $ra_email
  578.          * @param bool $nonjs
  579.          * @param string $req_goto
  580.          * @return bool
  581.          * @throws OIDplusConfigInitializationException
  582.          * @throws OIDplusException
  583.          */
  584.         public function tree(array &$json, string $ra_email=null, bool $nonjs=false, string $req_goto=''): bool {
  585.                 $children = array();
  586.  
  587.                 $this->tree_rec($children, '/');
  588.  
  589.                 if (!OIDplus::config()->getValue('resource_plugin_hide_empty_path', true) || (count($children) > 0)) {
  590.                         if (file_exists(__DIR__.'/img/main_icon16.png')) {
  591.                                 $tree_icon = OIDplus::webpath(__DIR__,OIDplus::PATH_RELATIVE).'img/main_icon16.png';
  592.                         } else {
  593.                                 $tree_icon = null; // default icon (folder)
  594.                         }
  595.  
  596.                         $json[] = array(
  597.                                 'id' => 'oidplus:resources',
  598.                                 'icon' => $tree_icon,
  599.                                 'state' => array("opened" => true),
  600.                                 'text' => $this->getMainTitle(),
  601.                                 'children' => $children
  602.                         );
  603.                 }
  604.  
  605.                 return true;
  606.         }
  607.  
  608.         /**
  609.          * @param string $request
  610.          * @return array|false
  611.          */
  612.         public function tree_search(string $request) {
  613.                 return false;
  614.         }
  615.  
  616.         /**
  617.          * @param string $file
  618.          * @return array|mixed|string|string[]|null
  619.          * @throws OIDplusConfigInitializationException
  620.          * @throws OIDplusException
  621.          */
  622.         private static function getHyperlinkTitle(string $file) {
  623.                 $file2 = preg_replace('/\.([^.]+)$/', '$'.OIDplus::getCurrentLang().'.\1', $file);
  624.                 if (file_exists($file2)) $file = $file2;
  625.  
  626.                 if (substr($file,-4,4) == '.url') {
  627.                         return preg_replace('/\\.[^.\\s]{3,4}$/', '', basename($file));
  628.                 } else if (substr($file,-5,5) == '.link') {
  629.                         /*
  630.                         [Link]
  631.                         Title=Report a bug
  632.                         URL=https://github.com/danielmarschall/oidplus/issues
  633.                         */
  634.  
  635.                         $data = @parse_ini_file($file, true);
  636.                         if (!$data) {
  637.                                 throw new OIDplusException(_L('File %1 has an invalid INI format!',$file));
  638.                         }
  639.                         if (!isset($data['Link'])) {
  640.                                 throw new OIDplusException(_L('Could not find "%1" section at %2','Link',$file));
  641.                         }
  642.                         if (!isset($data['Link']['Title'])) {
  643.                                 throw new OIDplusException(_L('"%1" is missing in %2','Title',$file));
  644.                         }
  645.                         return $data['Link']['Title'];
  646.                 } else {
  647.                         throw new OIDplusException(_L('Unexpected file extension for file %1',$file));
  648.                 }
  649.         }
  650.  
  651.         /**
  652.          * @param string $file
  653.          * @return mixed
  654.          * @throws OIDplusConfigInitializationException
  655.          * @throws OIDplusException
  656.          */
  657.         private static function getHyperlinkURL(string $file) {
  658.                 $file2 = preg_replace('/\.([^.]+)$/', '$'.OIDplus::getCurrentLang().'.\1', $file);
  659.                 if (file_exists($file2)) $file = $file2;
  660.  
  661.                 if (substr($file,-4,4) == '.url') {
  662.                         /*
  663.                         [InternetShortcut]
  664.                         URL=http://www.example.com/
  665.                         */
  666.  
  667.                         $data = @parse_ini_file($file, true);
  668.                         if (!$data) {
  669.                                 throw new OIDplusException(_L('File %1 has an invalid INI format!',$file));
  670.                         }
  671.                         if (!isset($data['InternetShortcut'])) {
  672.                                 throw new OIDplusException(_L('Could not find "%1" section at %2','InternetShortcut',$file));
  673.                         }
  674.                         if (!isset($data['InternetShortcut']['URL'])) {
  675.                                 throw new OIDplusException(_L('"%1" is missing in %2','URL',$file));
  676.                         }
  677.                         return $data['InternetShortcut']['URL'];
  678.                 } else if (substr($file,-5,5) == '.link') {
  679.                         /*
  680.                         [Link]
  681.                         Title=Report a bug
  682.                         URL=https://github.com/danielmarschall/oidplus/issues
  683.                         */
  684.  
  685.                         $data = @parse_ini_file($file, true);
  686.                         if (!$data) {
  687.                                 throw new OIDplusException(_L('File %1 has an invalid INI format!',$file));
  688.                         }
  689.                         if (!isset($data['Link'])) {
  690.                                 throw new OIDplusException(_L('Could not find "%1" section at %2','Link',$file));
  691.                         }
  692.                         if (!isset($data['Link']['URL'])) {
  693.                                 throw new OIDplusException(_L('"%1" is missing in %2','URL',$file));
  694.                         }
  695.                         return $data['Link']['URL'];
  696.                 } else {
  697.                         throw new OIDplusException(_L('Unexpected file extension for file %1',$file));
  698.                 }
  699.  
  700.         }
  701.  
  702.         /**
  703.          * @param string $dir
  704.          * @return mixed|string
  705.          * @throws OIDplusConfigInitializationException
  706.          * @throws OIDplusException
  707.          */
  708.         private static function getFolderTitle(string $dir) {
  709.                 $data = @parse_ini_file("$dir/folder\$".OIDplus::getCurrentLang().".ini", true);
  710.                 if ($data && isset($data['Folder']) && isset($data['Folder']['Title'])) {
  711.                         return $data['Folder']['Title'];
  712.                 }
  713.  
  714.                 $data = @parse_ini_file("$dir/folder.ini", true);
  715.                 if ($data && isset($data['Folder']) && isset($data['Folder']['Title'])) {
  716.                         return $data['Folder']['Title'];
  717.                 }
  718.  
  719.                 return basename($dir);
  720.         }
  721. }
  722.