renderer = $renderer; $this->basedir = $basedir; $this->webdir = $webdir; $this->files = $files; } public function renderAsList($params) { $this->openContainer($params); $this->renderListItems($this->files, $params); $this->closeContainer(); } /** * Render a thumbnail gallery (XHTML only). * * Expects a flat list of file items in $this->files. * Clicking a thumbnail opens the original image. * * @param array $params * @return void */ public function renderAsGallery($params) { if (!($this->renderer instanceof \Doku_Renderer_xhtml)) { $params['style'] = 'list'; $this->renderAsList($params); return; } $thumbW = 150; $thumbH = 150; $thumbQ = 80; // Allow generating larger thumbnails (e.g. 2x) while still displaying them // at 150x150 for sharper results on HiDPI screens. $thumbScale = 1.0; $syntax = plugin_load('syntax', 'luxtools'); if ($syntax) { $rawScale = (string)$syntax->getConf('gallery_thumb_scale'); if ($rawScale !== '') { $thumbScale = (float)$rawScale; } } if (!is_finite($thumbScale) || $thumbScale < 1.0) $thumbScale = 1.0; if ($thumbScale > 4.0) $thumbScale = 4.0; $genThumbW = (int)max(1, (int)round($thumbW * $thumbScale)); $genThumbH = (int)max(1, (int)round($thumbH * $thumbScale)); $placeholderUrl = DOKU_BASE . 'lib/images/blank.gif'; $placeholderId = $syntax ? trim((string)$syntax->getConf('thumb_placeholder')) : ''; if ($placeholderId !== '' && function_exists('ml')) { // ml() builds a fetch.php URL for a MediaManager item $placeholderUrl = ml($placeholderId, ['w' => $genThumbW, 'h' => $genThumbH], true, '&'); } /** @var \Doku_Renderer_xhtml $renderer */ $renderer = $this->renderer; $renderer->doc .= ''; } /** * Append query parameters to a URL. * * Preserves existing query and fragment and uses RFC3986 encoding. * * @param string $url * @param array $params * @return string */ protected function withQueryParams(string $url, array $params): string { if ($params === []) return $url; $fragment = ''; $hashPos = strpos($url, '#'); if ($hashPos !== false) { $fragment = substr($url, $hashPos); $url = substr($url, 0, $hashPos); } $glue = (strpos($url, '?') === false) ? '?' : '&'; return $url . $glue . http_build_query($params, '', '&', PHP_QUERY_RFC3986) . $fragment; } /** * Compute the expected thumbnail cache path for an item. * * Mirrors the hashing scheme in file.php so we can detect whether a thumb * already exists and can be used immediately. * * @param array $item * @param int $w * @param int $h * @param int $q * @return string|null */ protected function thumbCachePathForItem(array $item, int $w, int $h, int $q): ?string { if (!isset($item['path']) || !is_string($item['path']) || $item['path'] === '') return null; if (!isset($item['mtime'])) return null; $path = $item['path']; $mtime = (int)$item['mtime']; // Decide output format the same way file.php does. try { [, $mime,] = mimetype($path, false); } catch (\Throwable $e) { return null; } if (!is_string($mime) || !str_starts_with($mime, 'image/')) return null; $dstFormat = ($mime === 'image/png' || $mime === 'image/gif') ? 'png' : 'jpg'; global $conf; if (!isset($conf['cachedir']) || !is_string($conf['cachedir']) || trim($conf['cachedir']) === '') return null; $hash = sha1($path . '|' . $mtime . '|w=' . $w . '|h=' . $h . '|q=' . $q . '|f=' . $dstFormat); $sub = substr($hash, 0, 2); $cacheDir = rtrim($conf['cachedir'], '/'); return $cacheDir . '/luxtools/thumbs/' . $sub . '/' . $hash . '.' . $dstFormat; } /** * Renders the files as a table, including details if configured that way. * * @param array $params the parameters of the filelist call */ public function renderAsTable($params) { $this->openContainer($params); $items = $this->flattenResultTree($this->files); $this->renderTableItems($items, $params); $this->closeContainer(); } /** * Render a flat list (files and/or directories) as a table. * * @param array $params * @return void */ public function renderAsFlatTable($params) { $this->openContainer($params); $this->renderTableItems($this->files, $params); $this->closeContainer(); } /** * Open the wrapping container with an optional max-height and scroll behaviour. */ protected function openContainer($params): void { if (!($this->renderer instanceof \Doku_Renderer_xhtml)) { return; } $style = $this->containerStyle($params); $this->renderer->doc .= '
'; } /** * Close the wrapping container if XHTML renderer is in use. */ protected function closeContainer(): void { if (!($this->renderer instanceof \Doku_Renderer_xhtml)) { return; } $this->renderer->doc .= '
'; } /** * Build the inline style attribute for the container based on the maxheight param. */ protected function containerStyle($params): string { if (!isset($params['maxheight'])) { return ''; } $maxHeight = (int)$params['maxheight']; if ($maxHeight < 0) { return ''; } return ' style="max-height: ' . $maxHeight . 'px; overflow-y: auto;"'; } /** * Renders the files as a table, including details if configured that way. * * @param array $params the parameters of the filelist call */ protected function renderTableItems($items, $params) { $renderer = $this->renderer; // count the columns $columns = 1; if ($params['showsize']) { $columns++; } if ($params['showdate']) { $columns++; } $renderer->table_open($columns); $hasOpenLocation = isset($params['openlocation']) && is_string($params['openlocation']) && trim($params['openlocation']) !== ''; $hasHeader = !empty($params['tableheader']); if ($hasOpenLocation || $hasHeader) { $renderer->tablethead_open(); // Small row above the header with an "Open Location" link. if ($hasOpenLocation && ($renderer instanceof \Doku_Renderer_xhtml)) { $openItem = [ 'name' => $this->getLang('openlocation'), 'path' => $params['openlocation'], 'isdir' => true, // Render without an icon (no mf_* class). 'noicon' => true, ]; /** @var \Doku_Renderer_xhtml $renderer */ $renderer->doc .= ''; $this->renderDirectoryLink($openItem); $renderer->doc .= ''; } if ($hasHeader) { $renderer->tablerow_open(); $renderer->tableheader_open(); $renderer->cdata($this->getLang('filename')); $renderer->tableheader_close(); if ($params['showsize']) { $renderer->tableheader_open(); $renderer->cdata($this->getLang('filesize')); $renderer->tableheader_close(); } if ($params['showdate']) { $renderer->tableheader_open(); $renderer->cdata($this->getLang('lastmodified')); $renderer->tableheader_close(); } $renderer->tablerow_close(); } $renderer->tablethead_close(); } $renderer->tabletbody_open(); foreach ($items as $item) { $renderer->tablerow_open(); $renderer->tablecell_open(); $this->renderItemLink($item, $params['randlinks']); $renderer->tablecell_close(); if ($params['showsize']) { $renderer->tablecell_open(1, 'right'); if (!empty($item['isdir'])) { $renderer->cdata(''); } else { $renderer->cdata(filesize_h($item['size'])); } $renderer->tablecell_close(); } if ($params['showdate']) { $renderer->tablecell_open(); $renderer->cdata(dformat($item['mtime'])); $renderer->tablecell_close(); } $renderer->tablerow_close(); } $renderer->tabletbody_close(); $renderer->table_close(); } /** * Recursively renders a tree of files as list items. * * @param array $items the files to render * @param array $params the parameters of the filelist call * @param int $level the level to render * @return void */ protected function renderListItems($items, $params, $level = 1) { if ($params['style'] == 'olist') { $this->renderer->listo_open(); } else { $this->renderer->listu_open(); } foreach ($items as $file) { if ($file['children'] === false && $file['treesize'] === 0) continue; // empty directory $this->renderer->listitem_open($level); $this->renderer->listcontent_open(); if ($file['children'] !== false && $file['treesize'] > 0) { // render the directory and its subtree $this->renderer->cdata($file['name']); $this->renderListItems($file['children'], $params, $level + 1); } elseif ($file['children'] === false) { // render the file link $this->renderItemLink($file, $params['randlinks']); // render filesize if ($params['showsize']) { $this->renderer->cdata($params['listsep'] . filesize_h($file['size'])); } // render lastmodified if ($params['showdate']) { $this->renderer->cdata($params['listsep'] . dformat($file['mtime'])); } } $this->renderer->listcontent_close(); $this->renderer->listitem_close(); } if ($params['style'] == 'olist') { $this->renderer->listo_close(); } else { $this->renderer->listu_close(); } } protected function renderItemLink($item, $cachebuster = false) { if (!empty($item['isdir'])) { $this->renderDirectoryLink($item); return; } if ($this->renderer instanceof \Doku_Renderer_xhtml) { $this->renderItemLinkXHTML($item, $cachebuster); } else { $this->renderItemLinkAny($item, $cachebuster); } } /** * Render a directory like a normal media link, but with open behaviour. * * @param array $item * @return void */ protected function renderDirectoryLink($item) { $caption = $item['name'] ?? ''; $path = $item['path'] ?? ''; if ($caption === '') { $caption = '[n/a]'; } if (!($this->renderer instanceof \Doku_Renderer_xhtml)) { $this->renderer->cdata($caption); return; } if (!is_string($path) || $path === '') { $this->renderer->cdata('[n/a]'); return; } $path = $this->mapOpenPath($path); global $conf; /** @var \Doku_Renderer_xhtml $renderer */ $renderer = $this->renderer; $syntax = plugin_load('syntax', 'luxtools'); $serviceUrl = $syntax ? trim((string)$syntax->getConf('open_service_url')) : ''; $serviceToken = $syntax ? trim((string)$syntax->getConf('open_service_token')) : ''; // Prepare a DokuWiki-style link. // Use the same icon mechanism as normal media links (via $link['class']). $link = [ 'target' => $conf['target']['extern'], 'style' => '', 'pre' => '', 'suf' => '', 'name' => $caption, 'url' => '#', 'title' => $renderer->_xmlEntities($path), 'more' => '', ]; $noIcon = !empty($item['noicon']); $link['class'] = $noIcon ? 'luxtools-open' : 'luxtools-open media mediafile mf_folder'; $link['more'] .= ' data-path="' . hsc($path) . '"'; if ($conf['relnofollow']) $link['more'] .= ' rel="nofollow"'; if ($serviceUrl !== '') $link['more'] .= ' data-service-url="' . hsc($serviceUrl) . '"'; if ($serviceToken !== '') $link['more'] .= ' data-service-token="' . hsc($serviceToken) . '"'; $renderer->doc .= $renderer->_formatLink($link); } /** * Map a filesystem path to an alias path (if configured). * * @param string $path * @return string */ protected function mapOpenPath($path) { if ($this->openPathMapper === false) return $path; if ($this->openPathMapper === null) { $syntax = plugin_load('syntax', 'luxtools'); $pathConfig = $syntax ? (string)$syntax->getConf('paths') : ''; if (trim($pathConfig) === '') { $this->openPathMapper = false; return $path; } $this->openPathMapper = new Path($pathConfig); } return $this->openPathMapper->mapToAliasPath($path); } /** * Render a file link on the XHTML renderer */ protected function renderItemLinkXHTML($item, $cachebuster = false) { global $conf; /** @var \Doku_Renderer_xhtml $renderer */ $renderer = $this->renderer; //prepare for formating $link['target'] = $conf['target']['extern']; $link['style'] = ''; $link['pre'] = ''; $link['suf'] = ''; $link['more'] = ''; $link['url'] = $this->itemWebUrl($item, $cachebuster); $link['name'] = $item['name']; $link['title'] = $renderer->_xmlEntities($link['url']); if ($conf['relnofollow']) $link['more'] .= ' rel="nofollow"'; [$ext,] = mimetype(basename($item['local'])); $link['class'] = 'media mediafile mf_' . $ext; $renderer->doc .= $renderer->_formatLink($link); } /** * Render a file link on any Renderer * @param array $item * @param bool $cachebuster * @return void */ protected function renderItemLinkAny($item, $cachebuster = false) { $this->renderer->externalmedialink($this->itemWebUrl($item, $cachebuster), $item['name']); } /** * Construct the Web URL for a given item * * @param array $item The item data as returned by the Crawler * @param bool $cachbuster add a cachebuster to the URL? * @return string */ protected function itemWebUrl($item, $cachbuster = false) { if (str_ends_with($this->webdir, '=')) { $url = $this->webdir . rawurlencode($item['local']); } else { $url = $this->webdir . $item['local']; } if ($cachbuster) { if (strpos($url, '?') === false) { $url .= '?t=' . $item['mtime']; } else { $url .= '&t=' . $item['mtime']; } } return $url; } /** * Flattens the filelist by recursively walking through all subtrees and * merging them with a prefix attached to the filenames. * * @param array $items the tree to flatten * @param string $prefix the prefix to attach to all processed nodes * @return array a flattened representation of the tree */ protected function flattenResultTree($items, $prefix = '') { $result = []; foreach ($items as $file) { if ($file['children'] !== false) { $result = array_merge( $result, $this->flattenResultTree($file['children'], $prefix . $file['name'] . '/') ); } else { $file['name'] = $prefix . $file['name']; $result[] = $file; } } return $result; } protected function getLang($key) { $syntax = plugin_load('syntax', 'luxtools'); return $syntax->getLang($key); } }