Rename project to luxtools, remove client tool
Some checks failed
DokuWiki Default Tasks / all (push) Has been cancelled

This commit is contained in:
2026-01-05 16:51:42 +01:00
parent b64d4d91ff
commit e59970e0b8
15 changed files with 66 additions and 325 deletions

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace dokuwiki\plugin\filetools; namespace dokuwiki\plugin\luxtools;
class Crawler class Crawler
{ {

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace dokuwiki\plugin\filetools; namespace dokuwiki\plugin\luxtools;
class Output class Output
{ {
@@ -311,7 +311,7 @@ class Output
protected function getLang($key) protected function getLang($key)
{ {
$syntax = plugin_load('syntax', 'filetools'); $syntax = plugin_load('syntax', 'luxtools');
return $syntax->getLang($key); return $syntax->getLang($key);
} }
} }

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace dokuwiki\plugin\filetools; namespace dokuwiki\plugin\luxtools;
class Path class Path
{ {
@@ -60,7 +60,7 @@ class Path
$lastRoot = $line; $lastRoot = $line;
$paths[$line] = [ $paths[$line] = [
'root' => $line, 'root' => $line,
'web' => DOKU_BASE . 'lib/plugins/filetools/file.php?root=' . rawurlencode($line) . '&file=', 'web' => DOKU_BASE . 'lib/plugins/luxtools/file.php?root=' . rawurlencode($line) . '&file=',
]; ];
} }
} }

6
README
View File

@@ -1,12 +1,12 @@
File Tools plugin for DokuWiki LuxTools plugin for DokuWiki
Lists files matching a given glob pattern. Lists files matching a given glob pattern.
All documentation for this plugin can be found at All documentation for this plugin can be found at
https://www.dokuwiki.org/plugin:filetools https://www.dokuwiki.org/plugin:luxtools
If you install this plugin manually, make sure it is installed in If you install this plugin manually, make sure it is installed in
lib/plugins/filetools/ - if the folder is called different it lib/plugins/luxtools/ - if the folder is called different it
will not work! will not work!
Syntax: Syntax:

View File

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

View File

@@ -1,14 +1,14 @@
<?php <?php
namespace dokuwiki\plugin\filetools\test; namespace dokuwiki\plugin\luxtools\test;
use dokuwiki\plugin\filetools\Path; use dokuwiki\plugin\luxtools\Path;
use DokuWikiTest; use DokuWikiTest;
/** /**
* Path related tests for the filetools plugin * Path related tests for the luxtools plugin
* *
* @group plugin_filetools * @group plugin_luxtools
* @group plugins * @group plugins
*/ */
class PathTest extends DokuWikiTest class PathTest extends DokuWikiTest
@@ -40,15 +40,15 @@ EOT
$expect = [ $expect = [
'C:/xampp/htdocs/wiki/' => [ 'C:/xampp/htdocs/wiki/' => [
'root' => 'C:/xampp/htdocs/wiki/', 'root' => 'C:/xampp/htdocs/wiki/',
'web' => '/lib/plugins/filetools/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/' => [ '\\\\server/share/path/' => [
'root' => '\\\\server/share/path/', 'root' => '\\\\server/share/path/',
'web' => '/lib/plugins/filetools/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/' => [ '/linux/file/path/' => [
'root' => '/linux/file/path/', 'root' => '/linux/file/path/',
'web' => '/lib/plugins/filetools/file.php?root=%2Flinux%2Ffile%2Fpath%2F&file=', 'web' => '/lib/plugins/luxtools/file.php?root=%2Flinux%2Ffile%2Fpath%2F&file=',
], ],
'/linux/another/path/' => [ '/linux/another/path/' => [
'root' => '/linux/another/path/', 'root' => '/linux/another/path/',

View File

@@ -1,35 +1,35 @@
<?php <?php
namespace dokuwiki\plugin\filetools\test; namespace dokuwiki\plugin\luxtools\test;
use DokuWikiTest; use DokuWikiTest;
use DOMWrap\Document; use DOMWrap\Document;
/** /**
* Tests for the filetools plugin. * Tests for the luxtools plugin.
* *
* These test assume that the directory filetools has the following content: * These test assume that the directory luxtools has the following content:
* - exampledir (directory) * - exampledir (directory)
* - example2.txt (text file) * - example2.txt (text file)
* - example.txt (text file) * - example.txt (text file)
* - exampleimage.png (image file) * - exampleimage.png (image file)
* *
* @group plugin_filetools * @group plugin_luxtools
* @group plugins * @group plugins
*/ */
class plugin_filetools_test extends DokuWikiTest class plugin_luxtools_test extends DokuWikiTest
{ {
public function setUp(): void public function setUp(): void
{ {
global $conf; global $conf;
$this->pluginsEnabled[] = 'filetools'; $this->pluginsEnabled[] = 'luxtools';
parent::setUp(); parent::setUp();
// Setup config so that access to the TMP directory will be allowed // Setup config so that access to the TMP directory will be allowed
$conf ['plugin']['filetools']['paths'] = TMP_DIR . '/filelistdata/' . "\n" . 'W> http://localhost/'; $conf ['plugin']['luxtools']['paths'] = TMP_DIR . '/filelistdata/' . "\n" . 'W> http://localhost/';
} }

View File

@@ -2,7 +2,7 @@
// phpcs:disable PSR1.Files.SideEffects.FoundWithSymbols // phpcs:disable PSR1.Files.SideEffects.FoundWithSymbols
use dokuwiki\plugin\filetools\Path; use dokuwiki\plugin\luxtools\Path;
if (!defined('DOKU_INC')) define('DOKU_INC', __DIR__ . '/../../../'); 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) 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; global $INPUT;
$syntax = plugin_load('syntax', 'filetools'); $syntax = plugin_load('syntax', 'luxtools');
if (!$syntax) die('plugin disabled?'); if (!$syntax) die('plugin disabled?');
$pathUtil = new Path($syntax->getConf('paths')); $pathUtil = new Path($syntax->getConf('paths'));

View File

@@ -1,3 +0,0 @@
module filetools-local-opener
go 1.22

View File

@@ -1,267 +0,0 @@
package main
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
)
type allowList []string
func (a *allowList) String() string { return strings.Join(*a, ",") }
func (a *allowList) Set(value string) error {
value = strings.TrimSpace(value)
if value == "" {
return nil
}
*a = append(*a, value)
return nil
}
type openRequest struct {
Path string `json:"path"`
}
type openResponse struct {
OK bool `json:"ok"`
Message string `json:"message"`
}
func main() {
listen := flag.String("listen", "127.0.0.1:8765", "listen address (host:port), should be loopback")
token := flag.String("token", "", "shared secret token; if empty, requests are allowed without authentication")
var allowed allowList
flag.Var(&allowed, "allow", "allowed path prefix (repeatable); if none, any path is allowed")
flag.Parse()
if !isLoopbackListenAddr(*listen) {
log.Fatalf("refusing to listen on non-loopback address: %s", *listen)
}
if strings.TrimSpace(*token) == "" {
generated, err := generateToken()
if err != nil {
log.Fatalf("failed to generate token: %v", err)
}
*token = generated
log.Printf("generated token (set this in the plugin config): %s", *token)
}
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
withCORS(w, r)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "time": time.Now().Format(time.RFC3339)})
})
mux.HandleFunc("/open", func(w http.ResponseWriter, r *http.Request) {
withCORS(w, r)
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
var req openRequest
switch r.Method {
case http.MethodGet:
req.Path = r.URL.Query().Get("path")
case http.MethodPost:
dec := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32*1024))
dec.DisallowUnknownFields()
if err := dec.Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, openResponse{OK: false, Message: "invalid json"})
return
}
default:
writeJSON(w, http.StatusMethodNotAllowed, openResponse{OK: false, Message: "GET or POST required"})
return
}
if !checkToken(r, *token) {
// Allow token to be supplied via query string for GET fallback.
qt := strings.TrimSpace(r.URL.Query().Get("token"))
if qt == "" || !subtleStringEqual(qt, strings.TrimSpace(*token)) {
writeJSON(w, http.StatusUnauthorized, openResponse{OK: false, Message: "unauthorized"})
return
}
}
target, err := normalizePath(req.Path)
if err != nil {
writeJSON(w, http.StatusBadRequest, openResponse{OK: false, Message: err.Error()})
return
}
if len(allowed) > 0 && !isAllowed(target, allowed) {
writeJSON(w, http.StatusForbidden, openResponse{OK: false, Message: "path not allowed"})
return
}
if err := openFolder(target); err != nil {
writeJSON(w, http.StatusInternalServerError, openResponse{OK: false, Message: err.Error()})
return
}
if r.Method == http.MethodGet {
// For GET callers (image-ping), a 204 avoids console noise from non-image responses.
w.WriteHeader(http.StatusNoContent)
return
}
writeJSON(w, http.StatusOK, openResponse{OK: true, Message: "opened"})
})
srv := &http.Server{
Addr: *listen,
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 30 * time.Second,
}
log.Printf("listening on http://%s", *listen)
log.Printf("os=%s arch=%s", runtime.GOOS, runtime.GOARCH)
log.Fatal(srv.ListenAndServe())
}
func isLoopbackListenAddr(addr string) bool {
host, _, err := net.SplitHostPort(addr)
if err != nil {
return false
}
ip := net.ParseIP(host)
if ip == nil {
return host == "localhost"
}
return ip.IsLoopback()
}
func generateToken() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}
func withCORS(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if origin != "" {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Vary", "Origin")
} else {
w.Header().Set("Access-Control-Allow-Origin", "*")
}
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-Filetools-Token")
}
func checkToken(r *http.Request, required string) bool {
required = strings.TrimSpace(required)
if required == "" {
return true
}
got := r.Header.Get("X-Filetools-Token")
got = strings.TrimSpace(got)
return got != "" && subtleStringEqual(got, required)
}
func subtleStringEqual(a, b string) bool {
if len(a) != len(b) {
return false
}
var v byte
for i := 0; i < len(a); i++ {
v |= a[i] ^ b[i]
}
return v == 0
}
func normalizePath(input string) (string, error) {
p := strings.TrimSpace(input)
if p == "" {
return "", errors.New("missing path")
}
// Accept file:// URLs.
if strings.HasPrefix(strings.ToLower(p), "file://") {
p = strings.TrimPrefix(p, "file://")
// file:///C:/... becomes /C:/... (strip one leading slash)
p = strings.TrimPrefix(p, "/")
p = strings.TrimPrefix(p, "/")
p = strings.TrimPrefix(p, "/")
p = strings.ReplaceAll(p, "/", string(os.PathSeparator))
}
p = filepath.Clean(p)
if !filepath.IsAbs(p) {
return "", errors.New("path must be absolute")
}
// Ensure path exists.
st, err := os.Stat(p)
if err != nil {
return "", fmt.Errorf("path not found")
}
if !st.IsDir() {
// If a file is provided, open its containing folder.
p = filepath.Dir(p)
}
return p, nil
}
func isAllowed(path string, allowed []string) bool {
path = filepath.Clean(path)
for _, a := range allowed {
a = filepath.Clean(a)
if a == "." || a == string(os.PathSeparator) {
return true
}
// Case-insensitive on Windows.
if runtime.GOOS == "windows" {
if strings.HasPrefix(strings.ToLower(path), strings.ToLower(a)) {
return true
}
} else {
if strings.HasPrefix(path, a) {
return true
}
}
}
return false
}
func openFolder(path string) error {
switch runtime.GOOS {
case "windows":
// explorer requires backslashes.
p := strings.ReplaceAll(path, "/", "\\")
cmd := exec.Command("explorer.exe", p)
return cmd.Start()
case "linux":
cmd := exec.Command("xdg-open", path)
return cmd.Start()
default:
return fmt.Errorf("unsupported OS: %s", runtime.GOOS)
}
}
func writeJSON(w http.ResponseWriter, status int, resp openResponse) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(resp)
}

View File

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

View File

@@ -3,18 +3,29 @@
require_once(__DIR__ . '/syntax/files.php'); require_once(__DIR__ . '/syntax/files.php');
/** /**
* File Tools plugin compatibility shim. * LuxTools plugin bootstrap.
*
* Keep this class so existing code that does `plugin_load('syntax', 'filetools')`
* continues to work (config/lang access).
* *
* The actual {{files>...}} syntax implementation lives in syntax/files.php. * The actual {{files>...}} syntax implementation lives in syntax/files.php.
*/ */
class syntax_plugin_filetools extends syntax_plugin_filetools_files class syntax_plugin_luxtools extends syntax_plugin_luxtools_files
{ {
/** @inheritdoc */ /** @inheritdoc */
public function connectTo($mode) public function connectTo($mode)
{ {
// Intentionally empty: syntax is registered by syntax_plugin_filetools_files. // 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.
} }
} }

View File

@@ -1,16 +1,16 @@
<?php <?php
use dokuwiki\Extension\SyntaxPlugin; use dokuwiki\Extension\SyntaxPlugin;
use dokuwiki\plugin\filetools\Crawler; use dokuwiki\plugin\luxtools\Crawler;
use dokuwiki\plugin\filetools\Output; use dokuwiki\plugin\luxtools\Output;
use dokuwiki\plugin\filetools\Path; use dokuwiki\plugin\luxtools\Path;
/** /**
* File Tools Plugin: Files syntax. * LuxTools Plugin: Files syntax.
* *
* Lists files matching a given glob pattern. * Lists files matching a given glob pattern.
*/ */
class syntax_plugin_filetools_files extends SyntaxPlugin class syntax_plugin_luxtools_files extends SyntaxPlugin
{ {
/** @inheritdoc */ /** @inheritdoc */
public function getType() public function getType()
@@ -33,7 +33,7 @@ class syntax_plugin_filetools_files extends SyntaxPlugin
/** @inheritdoc */ /** @inheritdoc */
public function connectTo($mode) public function connectTo($mode)
{ {
$this->Lexer->addSpecialPattern('\{\{files>.+?\}\}', $mode, 'plugin_filetools_files'); $this->Lexer->addSpecialPattern('\{\{files>.+?\}\}', $mode, 'plugin_luxtools_files');
} }
/** @inheritdoc */ /** @inheritdoc */

View File

@@ -1,16 +1,16 @@
<?php <?php
use dokuwiki\Extension\SyntaxPlugin; use dokuwiki\Extension\SyntaxPlugin;
use dokuwiki\plugin\filetools\Crawler; use dokuwiki\plugin\luxtools\Crawler;
use dokuwiki\plugin\filetools\Output; use dokuwiki\plugin\luxtools\Output;
use dokuwiki\plugin\filetools\Path; use dokuwiki\plugin\luxtools\Path;
/** /**
* File Tools Plugin: Image gallery syntax. * LuxTools Plugin: Image gallery syntax.
* *
* Renders a thumbnail gallery of images matching a glob pattern. * Renders a thumbnail gallery of images matching a glob pattern.
*/ */
class syntax_plugin_filetools_images extends SyntaxPlugin class syntax_plugin_luxtools_images extends SyntaxPlugin
{ {
/** @inheritdoc */ /** @inheritdoc */
public function getType() public function getType()
@@ -33,7 +33,7 @@ class syntax_plugin_filetools_images extends SyntaxPlugin
/** @inheritdoc */ /** @inheritdoc */
public function connectTo($mode) public function connectTo($mode)
{ {
$this->Lexer->addSpecialPattern('\{\{images>.+?\}\}', $mode, 'plugin_filetools_images'); $this->Lexer->addSpecialPattern('\{\{images>.+?\}\}', $mode, 'plugin_luxtools_images');
} }
/** @inheritdoc */ /** @inheritdoc */

View File

@@ -3,12 +3,12 @@
use dokuwiki\Extension\SyntaxPlugin; use dokuwiki\Extension\SyntaxPlugin;
/** /**
* File Tools Plugin: Open local path syntax. * LuxTools Plugin: Open local path syntax.
* *
* Renders an inline button. Clicking it triggers client-side JS that attempts * Renders an inline button. Clicking it triggers client-side JS that attempts
* to open the configured path in the default file manager (best-effort). * to open the configured path in the default file manager (best-effort).
*/ */
class syntax_plugin_filetools_open extends SyntaxPlugin class syntax_plugin_luxtools_open extends SyntaxPlugin
{ {
/** @inheritdoc */ /** @inheritdoc */
public function getType() public function getType()
@@ -32,7 +32,7 @@ class syntax_plugin_filetools_open extends SyntaxPlugin
/** @inheritdoc */ /** @inheritdoc */
public function connectTo($mode) public function connectTo($mode)
{ {
$this->Lexer->addSpecialPattern('\{\{open>.+?\}\}', $mode, 'plugin_filetools_open'); $this->Lexer->addSpecialPattern('\{\{open>.+?\}\}', $mode, 'plugin_luxtools_open');
} }
/** @inheritdoc */ /** @inheritdoc */