Compare commits

...

11 Commits

Author SHA1 Message Date
e59970e0b8 Rename project to luxtools, remove client tool
Some checks failed
DokuWiki Default Tasks / all (push) Has been cancelled
2026-01-05 16:51:42 +01:00
b64d4d91ff Client tool 2026-01-05 13:05:21 +01:00
c442c0df1e Inial implementation for folder opening 2026-01-05 11:25:18 +01:00
9a067eca16 Inital implementation of image listing 2026-01-05 10:49:00 +01:00
fe8d0bbffb Change project name to filetools 2026-01-05 10:37:51 +01:00
a3558c470e Change project name to File List Plus 2026-01-05 10:29:34 +01:00
Andreas Gohr
83e348f31e Version upped 2024-07-03 23:50:12 +02:00
Andreas Gohr
23c781f855 abort early when no paths are configured 2024-07-03 16:03:51 +02:00
Andreas Gohr
9ad5912307 Version upped 2024-04-03 23:50:13 +02:00
Andreas Gohr
c470c4266e Merge pull request #44 from dokufreaks/bot/autofix
🤖 Automatic code style fixes
2024-04-03 14:29:26 +02:00
splitbrain
202f571cbe 🤖 Automatic code style fixes 2024-04-03 12:26:54 +00:00
19 changed files with 730 additions and 200 deletions

View File

@@ -1,6 +1,6 @@
<?php
namespace dokuwiki\plugin\filelist;
namespace dokuwiki\plugin\luxtools;
class Crawler
{
@@ -58,7 +58,7 @@ class Crawler
$path = $root . $local;
// do not descent into wiki or data directories
if(Path::isWikiControlled($path)) return [];
if (Path::isWikiControlled($path)) return [];
if (($dir = opendir($path)) === false) return [];
$result = [];

View File

@@ -1,6 +1,6 @@
<?php
namespace dokuwiki\plugin\filelist;
namespace dokuwiki\plugin\luxtools;
class Output
{
@@ -28,7 +28,7 @@ class Output
public function renderAsList($params)
{
if ($this->renderer instanceof \Doku_Renderer_xhtml) {
$this->renderer->doc .= '<div class="filelist-plugin">';
$this->renderer->doc .= '<div class="filetools-plugin">';
}
$this->renderListItems($this->files, $params);
@@ -38,6 +38,40 @@ class Output
}
}
/**
* 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;
}
/** @var \Doku_Renderer_xhtml $renderer */
$renderer = $this->renderer;
$renderer->doc .= '<div class="filetools-plugin filetools-gallery">';
foreach ($this->files as $item) {
$url = $this->itemWebUrl($item, !empty($params['randlinks']));
$safeUrl = hsc($url);
$label = hsc($item['name']);
$renderer->doc .= '<a href="' . $safeUrl . '" class="media" title="' . $label . '">';
$renderer->doc .= '<img src="' . $safeUrl . '" alt="' . $label . '" width="150" loading="lazy" />';
$renderer->doc .= '</a>';
}
$renderer->doc .= '</div>';
}
/**
* Renders the files as a table, including details if configured that way.
*
@@ -46,7 +80,7 @@ class Output
public function renderAsTable($params)
{
if ($this->renderer instanceof \Doku_Renderer_xhtml) {
$this->renderer->doc .= '<div class="filelist-plugin">';
$this->renderer->doc .= '<div class="filetools-plugin">';
}
$items = $this->flattenResultTree($this->files);
@@ -277,7 +311,7 @@ class Output
protected function getLang($key)
{
$syntax = plugin_load('syntax', 'filelist');
$syntax = plugin_load('syntax', 'luxtools');
return $syntax->getLang($key);
}
}

View File

@@ -1,6 +1,6 @@
<?php
namespace dokuwiki\plugin\filelist;
namespace dokuwiki\plugin\luxtools;
class Path
{
@@ -60,7 +60,7 @@ class Path
$lastRoot = $line;
$paths[$line] = [
'root' => $line,
'web' => DOKU_BASE . 'lib/plugins/filelist/file.php?root=' . rawurlencode($line) . '&file=',
'web' => DOKU_BASE . 'lib/plugins/luxtools/file.php?root=' . rawurlencode($line) . '&file=',
];
}
}
@@ -81,6 +81,10 @@ class Path
$path = static::cleanPath($path, $addTrailingSlash);
$paths = $this->paths;
if ($paths === []) {
throw new \Exception('No paths configured');
}
$allowed = array_keys($paths);
usort($allowed, static fn($a, $b) => strlen($a) - strlen($b));
$allowed = array_map('preg_quote_cb', $allowed);

9
README
View File

@@ -1,14 +1,17 @@
filelist plugin for DokuWiki
LuxTools plugin for DokuWiki
Lists files matching a given glob pattern.
All documentation for this plugin can be found at
https://www.dokuwiki.org/plugin:filelist
https://www.dokuwiki.org/plugin:luxtools
If you install this plugin manually, make sure it is installed in
lib/plugins/filelist/ - if the folder is called different it
lib/plugins/luxtools/ - if the folder is called different it
will not work!
Syntax:
{{files>...}}
Please refer to http://www.dokuwiki.org/extensions for additional info
on how to install extensions in DokuWiki.

View File

@@ -1,13 +1,13 @@
<?php
namespace dokuwiki\plugin\filelist\test;
namespace dokuwiki\plugin\luxtools\test;
use DokuWikiTest;
/**
* General tests for the filelist plugin
* General tests for the luxtools plugin
*
* @group plugin_filelist
* @group plugin_luxtools
* @group plugins
*/
class GeneralTest extends DokuWikiTest
@@ -31,7 +31,7 @@ class GeneralTest extends DokuWikiTest
$this->assertArrayHasKey('desc', $info);
$this->assertArrayHasKey('url', $info);
$this->assertEquals('filelist', $info['base']);
$this->assertEquals('luxtools', $info['base']);
$this->assertRegExp('/^https?:\/\//', $info['url']);
$this->assertTrue(mail_isvalid($info['email']));
$this->assertRegExp('/^\d\d\d\d-\d\d-\d\d$/', $info['date']);
@@ -61,7 +61,7 @@ class GeneralTest extends DokuWikiTest
$this->assertEquals(
gettype($conf),
gettype($meta),
'Both ' . DOKU_PLUGIN . 'filelist/conf/default.php and ' . DOKU_PLUGIN . 'filelist/conf/metadata.php have to exist and contain the same keys.'
'Both ' . DOKU_PLUGIN . 'luxtools/conf/default.php and ' . DOKU_PLUGIN . 'luxtools/conf/metadata.php have to exist and contain the same keys.'
);
if ($conf !== null && $meta !== null) {
@@ -69,7 +69,7 @@ class GeneralTest extends DokuWikiTest
$this->assertArrayHasKey(
$key,
$meta,
'Key $meta[\'' . $key . '\'] missing in ' . DOKU_PLUGIN . 'filelist/conf/metadata.php'
'Key $meta[\'' . $key . '\'] missing in ' . DOKU_PLUGIN . 'luxtools/conf/metadata.php'
);
}
@@ -77,7 +77,7 @@ class GeneralTest extends DokuWikiTest
$this->assertArrayHasKey(
$key,
$conf,
'Key $conf[\'' . $key . '\'] missing in ' . DOKU_PLUGIN . 'filelist/conf/default.php'
'Key $conf[\'' . $key . '\'] missing in ' . DOKU_PLUGIN . 'luxtools/conf/default.php'
);
}
}

View File

@@ -1,14 +1,14 @@
<?php
namespace dokuwiki\plugin\filelist\test;
namespace dokuwiki\plugin\luxtools\test;
use dokuwiki\plugin\filelist\Path;
use dokuwiki\plugin\luxtools\Path;
use DokuWikiTest;
/**
* Path related tests for the filelist plugin
* Path related tests for the luxtools plugin
*
* @group plugin_filelist
* @group plugin_luxtools
* @group plugins
*/
class PathTest extends DokuWikiTest
@@ -40,15 +40,15 @@ EOT
$expect = [
'C:/xampp/htdocs/wiki/' => [
'root' => 'C:/xampp/htdocs/wiki/',
'web' => '/lib/plugins/filelist/file.php?root=C%3A%2Fxampp%2Fhtdocs%2Fwiki%2F&file=',
'web' => '/lib/plugins/luxtools/file.php?root=C%3A%2Fxampp%2Fhtdocs%2Fwiki%2F&file=',
],
'\\\\server/share/path/' => [
'root' => '\\\\server/share/path/',
'web' => '/lib/plugins/filelist/file.php?root=%5C%5Cserver%2Fshare%2Fpath%2F&file=',
'web' => '/lib/plugins/luxtools/file.php?root=%5C%5Cserver%2Fshare%2Fpath%2F&file=',
],
'/linux/file/path/' => [
'root' => '/linux/file/path/',
'web' => '/lib/plugins/filelist/file.php?root=%2Flinux%2Ffile%2Fpath%2F&file=',
'web' => '/lib/plugins/luxtools/file.php?root=%2Flinux%2Ffile%2Fpath%2F&file=',
],
'/linux/another/path/' => [
'root' => '/linux/another/path/',

View File

@@ -1,35 +1,35 @@
<?php
namespace dokuwiki\plugin\filelist\test;
namespace dokuwiki\plugin\luxtools\test;
use DokuWikiTest;
use DOMWrap\Document;
/**
* Tests for the filelist plugin.
* Tests for the luxtools plugin.
*
* These test assume that the directory filelist has the following content:
* These test assume that the directory luxtools has the following content:
* - exampledir (directory)
* - example2.txt (text file)
* - example.txt (text file)
* - exampleimage.png (image file)
*
* @group plugin_filelist
* @group plugin_luxtools
* @group plugins
*/
class plugin_filelist_test extends DokuWikiTest
class plugin_luxtools_test extends DokuWikiTest
{
public function setUp(): void
{
global $conf;
$this->pluginsEnabled[] = 'filelist';
$this->pluginsEnabled[] = 'luxtools';
parent::setUp();
// Setup config so that access to the TMP directory will be allowed
$conf ['plugin']['filelist']['paths'] = TMP_DIR . '/filelistdata/' . "\n" . 'W> http://localhost/';
$conf ['plugin']['luxtools']['paths'] = TMP_DIR . '/filelistdata/' . "\n" . 'W> http://localhost/';
}
@@ -76,7 +76,7 @@ class plugin_filelist_test extends DokuWikiTest
global $conf;
// Render filelist
$instructions = p_get_instructions('{{filelist>' . TMP_DIR . '/filelistdata/*&style=list&direct=1}}');
$instructions = p_get_instructions('{{files>' . TMP_DIR . '/filelistdata/*&style=list&direct=1}}');
$xhtml = p_render('xhtml', $instructions, $info);
// We should find:
@@ -94,7 +94,7 @@ class plugin_filelist_test extends DokuWikiTest
public function test_recursive()
{
// Render filelist
$instructions = p_get_instructions('{{filelist>' . TMP_DIR . '/filelistdata/*&style=list&direct=1&recursive=1}}');
$instructions = p_get_instructions('{{files>' . TMP_DIR . '/filelistdata/*&style=list&direct=1&recursive=1}}');
$xhtml = p_render('xhtml', $instructions, $info);
// We should find:
@@ -119,21 +119,21 @@ class plugin_filelist_test extends DokuWikiTest
public function testUnorderedList()
{
// Render filelist
$instructions = p_get_instructions('{{filelist>' . TMP_DIR . '/filelistdata/*&style=list&direct=1&recursive=1}}');
$instructions = p_get_instructions('{{files>' . TMP_DIR . '/filelistdata/*&style=list&direct=1&recursive=1}}');
$xhtml = p_render('xhtml', $instructions, $info);
$doc = new Document();
$doc->html($xhtml);
$structure = [
'div.filelist-plugin' => 1,
'div.filelist-plugin > ul' => 1,
'div.filelist-plugin > ul > li' => 3,
'div.filelist-plugin > ul > li:nth-child(1)' => 1,
'div.filelist-plugin > ul > li:nth-child(1) a' => 'example.txt',
'div.filelist-plugin > ul > li:nth-child(2) ul' => 1,
'div.filelist-plugin > ul > li:nth-child(2) ul > li' => 1,
'div.filelist-plugin > ul > li:nth-child(2) ul > li a' => 'example2.txt',
'div.filetools-plugin' => 1,
'div.filetools-plugin > ul' => 1,
'div.filetools-plugin > ul > li' => 3,
'div.filetools-plugin > ul > li:nth-child(1)' => 1,
'div.filetools-plugin > ul > li:nth-child(1) a' => 'example.txt',
'div.filetools-plugin > ul > li:nth-child(2) ul' => 1,
'div.filetools-plugin > ul > li:nth-child(2) ul > li' => 1,
'div.filetools-plugin > ul > li:nth-child(2) ul > li a' => 'example2.txt',
];
$this->structureCheck($doc, $structure);
@@ -146,21 +146,21 @@ class plugin_filelist_test extends DokuWikiTest
public function testOrderedList()
{
// Render filelist
$instructions = p_get_instructions('{{filelist>' . TMP_DIR . '/filelistdata/*&style=olist&direct=1&recursive=1}}');
$instructions = p_get_instructions('{{files>' . TMP_DIR . '/filelistdata/*&style=olist&direct=1&recursive=1}}');
$xhtml = p_render('xhtml', $instructions, $info);
$doc = new Document();
$doc->html($xhtml);
$structure = [
'div.filelist-plugin' => 1,
'div.filelist-plugin > ol' => 1,
'div.filelist-plugin > ol > li' => 3,
'div.filelist-plugin > ol > li:nth-child(1)' => 1,
'div.filelist-plugin > ol > li:nth-child(1) a' => 'example.txt',
'div.filelist-plugin > ol > li:nth-child(2) ol' => 1,
'div.filelist-plugin > ol > li:nth-child(2) ol > li' => 1,
'div.filelist-plugin > ol > li:nth-child(2) ol > li a' => 'example2.txt',
'div.filetools-plugin' => 1,
'div.filetools-plugin > ol' => 1,
'div.filetools-plugin > ol > li' => 3,
'div.filetools-plugin > ol > li:nth-child(1)' => 1,
'div.filetools-plugin > ol > li:nth-child(1) a' => 'example.txt',
'div.filetools-plugin > ol > li:nth-child(2) ol' => 1,
'div.filetools-plugin > ol > li:nth-child(2) ol > li' => 1,
'div.filetools-plugin > ol > li:nth-child(2) ol > li a' => 'example2.txt',
];
$this->structureCheck($doc, $structure);
@@ -175,21 +175,62 @@ class plugin_filelist_test extends DokuWikiTest
global $conf;
// Render filelist
$instructions = p_get_instructions('{{filelist>' . TMP_DIR . '/filelistdata/*&style=table&direct=1&recursive=1}}');
$instructions = p_get_instructions('{{files>' . TMP_DIR . '/filelistdata/*&style=table&direct=1&recursive=1}}');
$xhtml = p_render('xhtml', $instructions, $info);
$doc = new Document();
$doc->html($xhtml);
$structure = [
'div.filelist-plugin' => 1,
'div.filelist-plugin table' => 1,
'div.filelist-plugin table > tbody > tr' => 3,
'div.filelist-plugin table > tbody > tr:nth-child(1) a' => 'example.txt',
'div.filelist-plugin table > tbody > tr:nth-child(2) a' => 'exampledir/example2.txt',
'div.filelist-plugin table > tbody > tr:nth-child(3) a' => 'exampleimage.png',
'div.filetools-plugin' => 1,
'div.filetools-plugin table' => 1,
'div.filetools-plugin table > tbody > tr' => 3,
'div.filetools-plugin table > tbody > tr:nth-child(1) a' => 'example.txt',
'div.filetools-plugin table > tbody > tr:nth-child(2) a' => 'exampledir/example2.txt',
'div.filetools-plugin table > tbody > tr:nth-child(3) a' => 'exampleimage.png',
];
$this->structureCheck($doc, $structure);
}
/**
* This function checks that the images syntax renders a thumbnail gallery.
*/
public function test_images_gallery()
{
$instructions = p_get_instructions('{{images>' . TMP_DIR . '/filelistdata/*&direct=1}}');
$xhtml = p_render('xhtml', $instructions, $info);
$doc = new Document();
$doc->html($xhtml);
$structure = [
'div.filetools-plugin.filetools-gallery' => 1,
'div.filetools-plugin.filetools-gallery a' => 1,
'div.filetools-plugin.filetools-gallery img' => 1,
];
$this->structureCheck($doc, $structure);
$this->assertStringContainsString('exampleimage.png', $xhtml);
}
/**
* This function checks that the open syntax renders an inline button.
*/
public function test_open_button()
{
$instructions = p_get_instructions('{{open>/tmp/somewhere|Open here}}');
$xhtml = p_render('xhtml', $instructions, $info);
$doc = new Document();
$doc->html($xhtml);
$structure = [
'button.filetools-open' => 1,
];
$this->structureCheck($doc, $structure);
$this->assertStringContainsString('Open here', $xhtml);
$this->assertStringContainsString('data-path="/tmp/somewhere"', $xhtml);
}
}

View File

@@ -1,10 +1,14 @@
<?php
/**
* Options for the filelist plugin
* Options for the filetools plugin
*/
$conf['paths'] = '';
$conf['allow_in_comments'] = 0;
$conf['defaults'] = '';
$conf['extensions'] = '';
// Local opener service used by {{open>...}}.
$conf['open_service_url'] = 'http://127.0.0.1:8765';
$conf['open_service_token'] = '';

View File

@@ -2,7 +2,7 @@
/**
* Metadata for configuration manager plugin
* Additions for the filelist plugin
* Additions for the filetools plugin
*
* @author Gina Haeussge <osd@foosel.net>
*/
@@ -11,3 +11,6 @@ $meta['paths'] = array('');
$meta['allow_in_comments'] = array('onoff');
$meta['defaults'] = array('string');
$meta['extensions'] = array('string');
$meta['open_service_url'] = array('string');
$meta['open_service_token'] = array('string');

View File

@@ -2,7 +2,7 @@
// phpcs:disable PSR1.Files.SideEffects.FoundWithSymbols
use dokuwiki\plugin\filelist\Path;
use dokuwiki\plugin\luxtools\Path;
if (!defined('DOKU_INC')) define('DOKU_INC', __DIR__ . '/../../../');
if (!defined('NOSESSION')) define('NOSESSION', true); // we do not use a session or authentication here (better caching)
@@ -11,7 +11,7 @@ require_once(DOKU_INC . 'inc/init.php');
global $INPUT;
$syntax = plugin_load('syntax', 'filelist');
$syntax = plugin_load('syntax', 'luxtools');
if (!$syntax) die('plugin disabled?');
$pathUtil = new Path($syntax->getConf('paths'));

View File

@@ -1,3 +1,6 @@
<?php
$lang['allow_in_comments'] = 'Filelistsyntax in Kommentaren erlauben.';
$lang['allow_in_comments'] = 'Files-Syntax in Kommentaren erlauben.';
$lang['open_service_url'] = 'URL des lokalen Öffner-Dienstes für {{open>...}} (z.B. http://127.0.0.1:8765).';
$lang['open_service_token'] = 'Token für den lokalen Öffner-Dienst (X-Filetools-Token).';

View File

@@ -1,5 +1,8 @@
<?php
$lang['allow_in_comments'] = 'Whether to allow the filelist syntax to be used in comments.';
$lang['allow_in_comments'] = 'Whether to allow the files syntax to be used in comments.';
$lang['defaults'] = 'Default options. Use the same syntax as in inline configuration';
$lang['extensions'] = 'Comma-separated list of allowed file extensions to list';
$lang['open_service_url'] = 'Local opener service URL for the {{open>...}} button (e.g. http://127.0.0.1:8765).';
$lang['open_service_token'] = 'Token sent to the local opener service (X-Filetools-Token).';

View File

@@ -4,6 +4,9 @@
*
* @author Mark C. Prins <mprins@users.sf.net>
*/
$lang['allow_in_comments'] = 'Of de filelist syntax toegestaan is voor gebruik in commentaar.';
$lang['allow_in_comments'] = 'Of de files syntax toegestaan is voor gebruik in commentaar.';
$lang['defaults'] = 'Default options. Gebruik dezelfde syntax als de inline configuratie.';
$lang['extensions'] = 'Komma-gescheiden lijst van toegestane bestandsextensies voor de lijst.';
$lang['open_service_url'] = 'Lokale opener service-URL voor de {{open>...}} knop (bijv. http://127.0.0.1:8765).';
$lang['open_service_token'] = 'Token dat naar de lokale opener service wordt gestuurd (X-Filetools-Token).';

View File

@@ -1,7 +1,7 @@
base filelist
author Gina Häußge, Dokufreaks
email freaks@dokuwiki.org
date 2024-03-13
name Filelist Plugin
base luxtools
author Gina Häußge, Dokufreaks, luxick
email dokuwiki@luxick.de
date 2026-01-05
name LuxTools
desc Lists files matching a given glob pattern.
url https://www.dokuwiki.org/plugin:filelist
url https://www.dokuwiki.org/plugin:luxtools

123
script.js Normal file
View File

@@ -0,0 +1,123 @@
/* global window, document */
(function () {
'use strict';
function getServiceUrl(el) {
var url = el.getAttribute('data-service-url') || '';
url = (url || '').trim();
if (!url) return '';
// strip trailing slashes
return url.replace(/\/+$/, '');
}
function getServiceToken(el) {
var token = el.getAttribute('data-service-token') || '';
return (token || '').trim();
}
function pingOpenViaImage(el, rawPath) {
var baseUrl = getServiceUrl(el);
if (!baseUrl) return;
var token = getServiceToken(el);
var url = baseUrl + '/open?path=' + encodeURIComponent(rawPath);
if (token) url += '&token=' + encodeURIComponent(token);
// Fire-and-forget without CORS.
try {
var img = new window.Image();
img.src = url;
} catch (e) {
// ignore
}
}
function openViaService(el, rawPath) {
var baseUrl = getServiceUrl(el);
if (!baseUrl) return Promise.reject(new Error('No opener service configured'));
var headers = {
'Content-Type': 'application/json'
};
var token = getServiceToken(el);
if (token) headers['X-Filetools-Token'] = token;
return window.fetch(baseUrl + '/open', {
method: 'POST',
mode: 'cors',
credentials: 'omit',
headers: headers,
body: JSON.stringify({ path: rawPath })
}).then(function (res) {
if (!res.ok) {
return res.json().catch(function () { return null; }).then(function (body) {
var msg = (body && body.message) ? body.message : ('HTTP ' + res.status);
throw new Error(msg);
});
}
return res.json().catch(function () { return { ok: true }; });
});
}
function normalizeToFileUrl(path) {
if (!path) return '';
// already a file URL
if (/^file:\/\//i.test(path)) return path;
// UNC path: \\server\share\path
if (/^\\\\/.test(path)) {
var p = path.replace(/^\\\\/, '');
p = p.replace(/\\/g, '/');
return 'file://///' + p;
}
// Windows drive: C:\path\to\file
if (/^[a-zA-Z]:\\/.test(path)) {
var drive = path[0].toUpperCase();
var rest = path.slice(2).replace(/\\/g, '/');
return 'file:///' + drive + ':' + rest;
}
// POSIX absolute: /home/user/file
if (path[0] === '/') {
return 'file://' + path;
}
// Fall back to using the provided string.
return path;
}
function onClick(event) {
var el = event.target;
if (!el || !el.classList || !el.classList.contains('filetools-open')) return;
var raw = el.getAttribute('data-path') || '';
if (!raw) return;
// Prefer local opener service.
openViaService(el, raw)
.catch(function (err) {
// If the browser blocks the request before it reaches localhost (mixed-content,
// extensions, stricter CORS handling), fall back to a no-CORS GET ping.
pingOpenViaImage(el, raw);
// Fallback to old behavior (often blocked in modern browsers).
var url = normalizeToFileUrl(raw);
if (!url) return;
console.warn('Local opener service failed, falling back to file:// navigation:', err);
try {
window.open(url, '_blank', 'noopener');
} catch (e) {
try {
window.location.href = url;
} catch (e2) {
console.error('Failed to open file URL:', e2);
}
}
});
}
document.addEventListener('click', onClick, false);
})();

View File

@@ -1,140 +1,31 @@
<?php
use dokuwiki\Extension\SyntaxPlugin;
use dokuwiki\plugin\filelist\Crawler;
use dokuwiki\plugin\filelist\Output;
use dokuwiki\plugin\filelist\Path;
require_once(__DIR__ . '/syntax/files.php');
/**
* Filelist Plugin: Lists files matching a given glob pattern.
* LuxTools plugin bootstrap.
*
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
* @author Gina Haeussge <osd@foosel.net>
* The actual {{files>...}} syntax implementation lives in syntax/files.php.
*/
class syntax_plugin_filelist extends SyntaxPlugin
class syntax_plugin_luxtools extends syntax_plugin_luxtools_files
{
/** @inheritdoc */
public function getType()
{
return 'substition';
}
/** @inheritdoc */
public function getPType()
{
return 'block';
}
/** @inheritdoc */
public function getSort()
{
return 222;
}
/** @inheritdoc */
public function connectTo($mode)
{
$this->Lexer->addSpecialPattern('\{\{filelist>.+?\}\}', $mode, 'plugin_filelist');
}
/** @inheritdoc */
public function handle($match, $state, $pos, Doku_Handler $handler)
{
global $INPUT;
// do not allow the syntax in discussion plugin comments
if (!$this->getConf('allow_in_comments') && $INPUT->has('comment')) {
return false;
}
$match = substr($match, strlen('{{filelist>'), -2);
[$path, $flags] = explode('&', $match, 2);
// load default config options
$flags = $this->getConf('defaults') . '&' . $flags;
$flags = explode('&', $flags);
$params = [
'sort' => 'name',
'order' => 'asc',
'style' => 'list',
'tableheader' => 0,
'recursive' => 0,
'titlefile' => '_title.txt',
'cache' => 0,
'randlinks' => 0,
'showsize' => 0,
'showdate' => 0,
'listsep' => ', ',
];
foreach ($flags as $flag) {
[$name, $value] = sexplode('=', $flag, 2, '');
$params[trim($name)] = trim(trim($value), '"'); // quotes can be use to keep whitespace
}
// separate path and pattern
$path = Path::cleanPath($path, false);
$parts = explode('/', $path);
$pattern = array_pop($parts);
$base = implode('/', $parts) . '/';
return [$base, $pattern, $params];
}
/**
* Create output
*/
public function render($format, Doku_Renderer $renderer, $data)
{
[$base, $pattern, $params] = $data;
if ($format != 'xhtml' && $format != 'odt') {
return false;
}
// disable caching
if ($params['cache'] === 0) {
$renderer->nocache();
}
try {
$pathHelper = new Path($this->getConf('paths'));
$pathInfo = $pathHelper->getPathInfo($base);
} catch (Exception $e) {
$renderer->cdata('[n/a: ' . $this->getLang('error_outsidejail') . ']');
return true;
}
$crawler = new Crawler($this->getConf('extensions'));
$crawler->setSortBy($params['sort']);
$crawler->setSortReverse($params['order'] === 'desc');
$result = $crawler->crawl(
$pathInfo['root'],
$pathInfo['local'],
$pattern,
$params['recursive'],
$params['titlefile']
);
// if we got nothing back, display a message
if ($result == []) {
$renderer->cdata('[n/a: ' . $this->getLang('error_nomatch') . ']');
return true;
}
$output = new Output($renderer, $pathInfo['root'], $pathInfo['web'], $result);
switch ($params['style']) {
case 'list':
case 'olist':
$output->renderAsList($params);
break;
case 'table':
$output->renderAsTable($params);
break;
}
return true;
// Intentionally empty: syntax is registered by syntax_plugin_luxtools_files.
}
}
/**
* Compatibility alias for older codebases that referenced the legacy class name.
*
* Note: plugin id/base is now `luxtools`.
*/
class syntax_plugin_filetools extends syntax_plugin_luxtools_files
{
/** @inheritdoc */
public function connectTo($mode)
{
// Intentionally empty: syntax is registered by syntax_plugin_luxtools_files.
}
}

138
syntax/files.php Normal file
View File

@@ -0,0 +1,138 @@
<?php
use dokuwiki\Extension\SyntaxPlugin;
use dokuwiki\plugin\luxtools\Crawler;
use dokuwiki\plugin\luxtools\Output;
use dokuwiki\plugin\luxtools\Path;
/**
* LuxTools Plugin: Files syntax.
*
* Lists files matching a given glob pattern.
*/
class syntax_plugin_luxtools_files extends SyntaxPlugin
{
/** @inheritdoc */
public function getType()
{
return 'substition';
}
/** @inheritdoc */
public function getPType()
{
return 'block';
}
/** @inheritdoc */
public function getSort()
{
return 222;
}
/** @inheritdoc */
public function connectTo($mode)
{
$this->Lexer->addSpecialPattern('\{\{files>.+?\}\}', $mode, 'plugin_luxtools_files');
}
/** @inheritdoc */
public function handle($match, $state, $pos, Doku_Handler $handler)
{
global $INPUT;
// do not allow the syntax in discussion plugin comments
if (!$this->getConf('allow_in_comments') && $INPUT->has('comment')) {
return false;
}
$match = substr($match, strlen('{{files>'), -2);
[$path, $flags] = explode('&', $match, 2);
// load default config options
$flags = $this->getConf('defaults') . '&' . $flags;
$flags = explode('&', $flags);
$params = [
'sort' => 'name',
'order' => 'asc',
'style' => 'list',
'tableheader' => 0,
'recursive' => 0,
'titlefile' => '_title.txt',
'cache' => 0,
'randlinks' => 0,
'showsize' => 0,
'showdate' => 0,
'listsep' => ', ',
];
foreach ($flags as $flag) {
[$name, $value] = sexplode('=', $flag, 2, '');
$params[trim($name)] = trim(trim($value), '"'); // quotes can be use to keep whitespace
}
// separate path and pattern
$path = Path::cleanPath($path, false);
$parts = explode('/', $path);
$pattern = array_pop($parts);
$base = implode('/', $parts) . '/';
return [$base, $pattern, $params];
}
/**
* Create output
*/
public function render($format, Doku_Renderer $renderer, $data)
{
[$base, $pattern, $params] = $data;
if ($format != 'xhtml' && $format != 'odt') {
return false;
}
// disable caching
if ($params['cache'] === 0) {
$renderer->nocache();
}
try {
$pathHelper = new Path($this->getConf('paths'));
$pathInfo = $pathHelper->getPathInfo($base);
} catch (Exception $e) {
$renderer->cdata('[n/a: ' . $this->getLang('error_outsidejail') . ']');
return true;
}
$crawler = new Crawler($this->getConf('extensions'));
$crawler->setSortBy($params['sort']);
$crawler->setSortReverse($params['order'] === 'desc');
$result = $crawler->crawl(
$pathInfo['root'],
$pathInfo['local'],
$pattern,
$params['recursive'],
$params['titlefile']
);
// if we got nothing back, display a message
if ($result == []) {
$renderer->cdata('[n/a: ' . $this->getLang('error_nomatch') . ']');
return true;
}
$output = new Output($renderer, $pathInfo['root'], $pathInfo['web'], $result);
switch ($params['style']) {
case 'list':
case 'olist':
$output->renderAsList($params);
break;
case 'table':
$output->renderAsTable($params);
break;
}
return true;
}
}

189
syntax/images.php Normal file
View File

@@ -0,0 +1,189 @@
<?php
use dokuwiki\Extension\SyntaxPlugin;
use dokuwiki\plugin\luxtools\Crawler;
use dokuwiki\plugin\luxtools\Output;
use dokuwiki\plugin\luxtools\Path;
/**
* LuxTools Plugin: Image gallery syntax.
*
* Renders a thumbnail gallery of images matching a glob pattern.
*/
class syntax_plugin_luxtools_images extends SyntaxPlugin
{
/** @inheritdoc */
public function getType()
{
return 'substition';
}
/** @inheritdoc */
public function getPType()
{
return 'block';
}
/** @inheritdoc */
public function getSort()
{
return 222;
}
/** @inheritdoc */
public function connectTo($mode)
{
$this->Lexer->addSpecialPattern('\{\{images>.+?\}\}', $mode, 'plugin_luxtools_images');
}
/** @inheritdoc */
public function handle($match, $state, $pos, Doku_Handler $handler)
{
global $INPUT;
// do not allow the syntax in discussion plugin comments
if (!$this->getConf('allow_in_comments') && $INPUT->has('comment')) {
return false;
}
$match = substr($match, strlen('{{images>'), -2);
[$path, $flags] = explode('&', $match, 2);
// load default config options
$flags = $this->getConf('defaults') . '&' . $flags;
$flags = explode('&', $flags);
$params = [
'sort' => 'name',
'order' => 'asc',
'recursive' => 0,
'titlefile' => '_title.txt',
'cache' => 0,
'randlinks' => 0,
];
foreach ($flags as $flag) {
[$name, $value] = sexplode('=', $flag, 2, '');
$params[trim($name)] = trim(trim($value), '"'); // quotes can be use to keep whitespace
}
// separate path and pattern
$path = Path::cleanPath($path, false);
$parts = explode('/', $path);
$pattern = array_pop($parts);
$base = implode('/', $parts) . '/';
return [$base, $pattern, $params];
}
/**
* Create output
*/
public function render($format, Doku_Renderer $renderer, $data)
{
[$base, $pattern, $params] = $data;
if ($format != 'xhtml' && $format != 'odt') {
return false;
}
// disable caching
if ($params['cache'] === 0) {
$renderer->nocache();
}
try {
$pathHelper = new Path($this->getConf('paths'));
$pathInfo = $pathHelper->getPathInfo($base);
} catch (Exception $e) {
$renderer->cdata('[n/a: ' . $this->getLang('error_outsidejail') . ']');
return true;
}
$crawler = new Crawler($this->getConf('extensions'));
$crawler->setSortBy($params['sort']);
$crawler->setSortReverse($params['order'] === 'desc');
$result = $crawler->crawl(
$pathInfo['root'],
$pathInfo['local'],
$pattern,
$params['recursive'],
$params['titlefile']
);
$items = $this->flattenResultTree($result);
$items = $this->filterImages($items);
// if we got nothing back, display a message
if ($items == []) {
$renderer->cdata('[n/a: ' . $this->getLang('error_nomatch') . ']');
return true;
}
$output = new Output($renderer, $pathInfo['root'], $pathInfo['web'], $items);
if ($format == 'xhtml') {
$output->renderAsGallery($params);
return true;
}
// Fallback for non-XHTML formats: render as a list of links
$params['style'] = 'list';
$params['showsize'] = 0;
$params['showdate'] = 0;
$params['listsep'] = ', ';
$output->renderAsList($params);
return true;
}
/**
* Flattens the crawl result tree into a list of file items.
*
* @param array $items
* @param string $prefix
* @return array
*/
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;
}
/**
* Keep only image files.
*
* @param array $items
* @return array
*/
protected function filterImages($items)
{
$images = [];
foreach ($items as $item) {
if (!isset($item['path']) || !is_string($item['path'])) continue;
if (!is_file($item['path'])) continue;
try {
[, $mime,] = mimetype($item['path'], false);
} catch (Throwable $e) {
continue;
}
if (is_string($mime) && str_starts_with($mime, 'image/')) {
$images[] = $item;
}
}
return $images;
}
}

91
syntax/open.php Normal file
View File

@@ -0,0 +1,91 @@
<?php
use dokuwiki\Extension\SyntaxPlugin;
/**
* LuxTools Plugin: Open local path syntax.
*
* Renders an inline button. Clicking it triggers client-side JS that attempts
* to open the configured path in the default file manager (best-effort).
*/
class syntax_plugin_luxtools_open extends SyntaxPlugin
{
/** @inheritdoc */
public function getType()
{
return 'substition';
}
/** @inheritdoc */
public function getPType()
{
// inline
return 'normal';
}
/** @inheritdoc */
public function getSort()
{
return 222;
}
/** @inheritdoc */
public function connectTo($mode)
{
$this->Lexer->addSpecialPattern('\{\{open>.+?\}\}', $mode, 'plugin_luxtools_open');
}
/** @inheritdoc */
public function handle($match, $state, $pos, Doku_Handler $handler)
{
$match = substr($match, strlen('{{open>'), -2);
[$path, $caption] = array_pad(explode('|', $match, 2), 2, '');
$path = trim($path);
$caption = trim($caption);
if ($caption === '') $caption = $path !== '' ? $path : 'Open';
// Basic scheme filtering to avoid javascript: style injections.
// Allow either file:// URLs, or plain paths (Windows/UNC/Linux style).
if (preg_match('/^[a-zA-Z][a-zA-Z0-9+.-]*:/', $path)) {
if (!str_starts_with(strtolower($path), 'file://')) {
return false;
}
}
return [$path, $caption];
}
/** @inheritdoc */
public function render($format, Doku_Renderer $renderer, $data)
{
if ($data === false) return false;
[$path, $caption] = $data;
if ($format !== 'xhtml') {
// no meaningful representation in non-browser formats
$renderer->cdata($caption);
return true;
}
if ($path === '') {
$renderer->cdata('[n/a]');
return true;
}
$serviceUrl = trim((string)$this->getConf('open_service_url'));
$serviceToken = trim((string)$this->getConf('open_service_token'));
$attrs = ' type="button" class="filetools-open"'
. ' data-path="' . hsc($path) . '"';
if ($serviceUrl !== '') {
$attrs .= ' data-service-url="' . hsc($serviceUrl) . '"';
}
if ($serviceToken !== '') {
$attrs .= ' data-service-token="' . hsc($serviceToken) . '"';
}
$renderer->doc .= '<button' . $attrs . '>' . hsc($caption) . '</button>';
return true;
}
}