diff --git a/Output.php b/Output.php index 913db25..8b339f7 100644 --- a/Output.php +++ b/Output.php @@ -562,10 +562,37 @@ class Output */ protected function itemWebUrl($item, $cachbuster = false) { - if (str_ends_with($this->webdir, '=')) { - $url = $this->webdir . rawurlencode($item['local']); + $webdir = $this->webdir; + + // When using the built-in file-serving endpoint, include the current page id + // so file.php can enforce DokuWiki ACLs for that page. + if ( + is_string($webdir) + && $webdir !== '' + && strpos($webdir, 'lib/plugins/luxtools/file.php') !== false + && strpos($webdir, 'id=') === false + ) { + global $ID; + $pageId = isset($ID) ? (string)$ID : ''; + if ($pageId !== '') { + if (function_exists('cleanID')) { + $pageId = (string)cleanID($pageId); + } + if ($pageId !== '') { + $encoded = rawurlencode($pageId); + if (strpos($webdir, '&file=') !== false) { + $webdir = str_replace('&file=', '&id=' . $encoded . '&file=', $webdir); + } elseif (strpos($webdir, '?file=') !== false) { + $webdir = str_replace('?file=', '?id=' . $encoded . '&file=', $webdir); + } + } + } + } + + if (str_ends_with($webdir, '=')) { + $url = $webdir . rawurlencode($item['local']); } else { - $url = $this->webdir . $item['local']; + $url = $webdir . $item['local']; } if ($cachbuster) { diff --git a/Path.php b/Path.php index 40eee0e..0d4e510 100644 --- a/Path.php +++ b/Path.php @@ -49,11 +49,6 @@ class Path $alias = static::cleanPath($line); $paths[$lastRoot]['alias'] = $alias; $paths[$alias] = &$paths[$lastRoot]; // alias references the original - } elseif (str_starts_with($line, 'W>')) { - // this is a web path for the last read root - $line = trim(substr($line, 2)); - if (!isset($paths[$lastRoot])) continue; // no last path, no web path - $paths[$lastRoot]['web'] = $line; } else { // this is a new path $line = static::cleanPath($line); diff --git a/README.md b/README.md index f1487bf..25e07a0 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,11 @@ It also ships a small file-serving endpoint (`lib/plugins/luxtools/file.php`) us to deliver files and generate cached thumbnails. -## Important security note +## Note on security The file-serving endpoint (`lib/plugins/luxtools/file.php`) runs inside DokuWiki -and can enforce a simple access restriction based on the currently logged-in -user. - -This is intentionally *not* full per-page ACL integration; it is a pragmatic -allowlist to avoid “anyone with a guessed URL can fetch the file”. - +and enforces access via per-page ACL: the requester must have at least read +access to the wiki page that rendered the link. ## Installation @@ -60,33 +56,25 @@ luxtools is configured via its dedicated admin page: Key settings: -- **access_allow** - Allowed users/groups for the file-serving endpoint. - - Entries can be separated by newlines, commas, or whitespace. - - Use `@group` to allow a whole group. - - Leave empty to allow any authenticated (logged-in) user. - - Anonymous users are always denied. - - **paths** Allowed base filesystem roots (one per line). Each root can be followed by: - `A> /Alias/` (optional) alias used in wiki syntax and open links - - `W> https://...` (optional) web base URL used for links instead of `file.php` Example: ``` /srv/share/Datascape/ A> /Scape/ - W> https://files.example.example/Datascape/ ``` - If no `W>` line is configured, luxtools links will use the plugin endpoint: + luxtools links use the plugin endpoint: `lib/plugins/luxtools/file.php?root=...&file=...` - Note: if you configure a `W>` web URL to an external file server, that server - must enforce access itself; DokuWiki ACLs and `access_allow` only apply to - `file.php`. + The generated URLs also include the current wiki page id (`id=...`) so + `file.php` can enforce ACLs for the host page. + + - **scratchpad_paths** Scratchpad file map (one file path per line, followed by an `A>` alias line). diff --git a/ScratchpadMap.php b/ScratchpadMap.php index 2c8165b..11c590b 100644 --- a/ScratchpadMap.php +++ b/ScratchpadMap.php @@ -75,11 +75,6 @@ class ScratchpadMap continue; } - // Ignore W> lines for compatibility with the Path config style - if (str_starts_with($line, 'W>')) { - continue; - } - // Treat as file path (no trailing slash enforced) $filePath = Path::cleanPath($line, false); if ($filePath === '' || str_ends_with($filePath, '/')) { diff --git a/_test/PathTest.php b/_test/PathTest.php index 4c1d738..6ecbf4a 100644 --- a/_test/PathTest.php +++ b/_test/PathTest.php @@ -27,7 +27,6 @@ C:\\xampp\\htdocs\\wiki\\ /linux/file/path/ /linux/another/path/../..//another/blargh/../path A> alias - W> webfoo EOT ); } @@ -53,12 +52,12 @@ EOT '/linux/another/path/' => [ 'root' => '/linux/another/path/', 'alias' => 'alias/', - 'web' => 'webfoo', + 'web' => '/lib/plugins/luxtools/file.php?root=%2Flinux%2Fanother%2Fpath%2F&file=', ], 'alias/' => [ 'root' => '/linux/another/path/', 'alias' => 'alias/', - 'web' => 'webfoo', + 'web' => '/lib/plugins/luxtools/file.php?root=%2Flinux%2Fanother%2Fpath%2F&file=', ], ]; diff --git a/_test/ScratchpadMapTest.php b/_test/ScratchpadMapTest.php index 85707c0..8346450 100644 --- a/_test/ScratchpadMapTest.php +++ b/_test/ScratchpadMapTest.php @@ -22,7 +22,6 @@ A> start C:\\pads\\notes.md A> notes -W> ignored \\\\server\\share\\pads\\team.txt A> team diff --git a/_test/SyntaxTest.php b/_test/SyntaxTest.php index 1da276c..2db7ac2 100644 --- a/_test/SyntaxTest.php +++ b/_test/SyntaxTest.php @@ -29,7 +29,8 @@ class plugin_luxtools_test extends DokuWikiTest parent::setUp(); // Setup config so that access to the TMP directory will be allowed - $conf ['plugin']['luxtools']['paths'] = TMP_DIR . '/filelistdata/' . "\n" . 'A> /Scape' . "\n" . 'W> http://localhost/'; + // Use the built-in file.php endpoint. + $conf ['plugin']['luxtools']['paths'] = TMP_DIR . '/filelistdata/' . "\n" . 'A> /Scape'; } @@ -244,6 +245,27 @@ class plugin_luxtools_test extends DokuWikiTest $this->assertStringContainsString('height="150"', $xhtml); } + /** + * Ensure the built-in file endpoint includes the host page id so file.php can + * enforce per-page ACL. + */ + public function test_file_links_include_page_id_for_acl() + { + global $ID; + $ID = 'luxtools:acltest'; + + $instructions = p_get_instructions('{{files>' . TMP_DIR . '/filelistdata/*&style=list&direct=1}}'); + $xhtml = p_render('xhtml', $instructions, $info); + + $doc = new Document(); + $doc->html($xhtml); + + $href = (string)$doc->find('div.luxtools-plugin a')->first()->attr('href'); + $this->assertNotSame('', $href); + $this->assertStringContainsString('lib/plugins/luxtools/file.php', $href); + $this->assertStringContainsString('id=luxtools%3Aacltest', $href); + } + /** * This function checks that the open syntax renders an inline link. */ diff --git a/admin/main.php b/admin/main.php index ccae556..447f7d7 100644 --- a/admin/main.php +++ b/admin/main.php @@ -10,7 +10,6 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin { /** @var string[] */ protected $configKeys = [ - 'access_allow', 'paths', 'scratchpad_paths', 'allow_in_comments', @@ -49,10 +48,6 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin } $newConf = []; - $accessAllow = $INPUT->str('access_allow'); - $accessAllow = str_replace(["\r\n", "\r"], "\n", $accessAllow); - $newConf['access_allow'] = $accessAllow; - // Normalize newlines to "\n" for consistent parsing $paths = $INPUT->str('paths'); $paths = str_replace(["\r\n", "\r"], "\n", $paths); @@ -93,12 +88,6 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin echo '