From e59970e0b876e1cce34598a169d2606e1983d685 Mon Sep 17 00:00:00 2001 From: luxick Date: Mon, 5 Jan 2026 16:51:42 +0100 Subject: [PATCH] Rename project to luxtools, remove client tool --- Crawler.php | 2 +- Output.php | 4 +- Path.php | 4 +- README | 6 +- _test/GeneralTest.php | 14 +-- _test/PathTest.php | 14 +-- _test/SyntaxTest.php | 14 +-- file.php | 4 +- local-opener/go.mod | 3 - local-opener/main.go | 267 ------------------------------------------ plugin.info.txt | 6 +- syntax.php | 23 +++- syntax/files.php | 12 +- syntax/images.php | 12 +- syntax/open.php | 6 +- 15 files changed, 66 insertions(+), 325 deletions(-) delete mode 100644 local-opener/go.mod delete mode 100644 local-opener/main.go diff --git a/Crawler.php b/Crawler.php index 5a2311f..aaecc9b 100644 --- a/Crawler.php +++ b/Crawler.php @@ -1,6 +1,6 @@ getLang($key); } } diff --git a/Path.php b/Path.php index 09b5f17..3c4c323 100644 --- a/Path.php +++ b/Path.php @@ -1,6 +1,6 @@ $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=', ]; } } diff --git a/README b/README index 72e30db..8735490 100644 --- a/README +++ b/README @@ -1,12 +1,12 @@ -File Tools 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:filetools +https://www.dokuwiki.org/plugin:luxtools 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! Syntax: diff --git a/_test/GeneralTest.php b/_test/GeneralTest.php index a306a2f..5b23e0b 100644 --- a/_test/GeneralTest.php +++ b/_test/GeneralTest.php @@ -1,13 +1,13 @@ assertArrayHasKey('desc', $info); $this->assertArrayHasKey('url', $info); - $this->assertEquals('filetools', $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 . '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) { @@ -69,7 +69,7 @@ class GeneralTest extends DokuWikiTest $this->assertArrayHasKey( $key, $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( $key, $conf, - 'Key $conf[\'' . $key . '\'] missing in ' . DOKU_PLUGIN . 'filetools/conf/default.php' + 'Key $conf[\'' . $key . '\'] missing in ' . DOKU_PLUGIN . 'luxtools/conf/default.php' ); } } diff --git a/_test/PathTest.php b/_test/PathTest.php index 22b94dd..397c301 100644 --- a/_test/PathTest.php +++ b/_test/PathTest.php @@ -1,14 +1,14 @@ [ '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/' => [ '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/' => [ '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/' => [ 'root' => '/linux/another/path/', diff --git a/_test/SyntaxTest.php b/_test/SyntaxTest.php index 924570c..f694014 100644 --- a/_test/SyntaxTest.php +++ b/_test/SyntaxTest.php @@ -1,35 +1,35 @@ pluginsEnabled[] = 'filetools'; + $this->pluginsEnabled[] = 'luxtools'; parent::setUp(); // 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/'; } diff --git a/file.php b/file.php index 33cf229..04b3089 100644 --- a/file.php +++ b/file.php @@ -2,7 +2,7 @@ // 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('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', 'filetools'); +$syntax = plugin_load('syntax', 'luxtools'); if (!$syntax) die('plugin disabled?'); $pathUtil = new Path($syntax->getConf('paths')); diff --git a/local-opener/go.mod b/local-opener/go.mod deleted file mode 100644 index 7fbadd1..0000000 --- a/local-opener/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module filetools-local-opener - -go 1.22 diff --git a/local-opener/main.go b/local-opener/main.go deleted file mode 100644 index 8c44122..0000000 --- a/local-opener/main.go +++ /dev/null @@ -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) -} diff --git a/plugin.info.txt b/plugin.info.txt index 24ccf86..cef780e 100644 --- a/plugin.info.txt +++ b/plugin.info.txt @@ -1,7 +1,7 @@ -base filetools +base luxtools author Gina Häußge, Dokufreaks, luxick email dokuwiki@luxick.de date 2026-01-05 -name File Tools +name LuxTools desc Lists files matching a given glob pattern. -url https://www.dokuwiki.org/plugin:filetools +url https://www.dokuwiki.org/plugin:luxtools diff --git a/syntax.php b/syntax.php index 86b70f8..f460c97 100644 --- a/syntax.php +++ b/syntax.php @@ -3,18 +3,29 @@ require_once(__DIR__ . '/syntax/files.php'); /** - * File Tools plugin compatibility shim. - * - * Keep this class so existing code that does `plugin_load('syntax', 'filetools')` - * continues to work (config/lang access). + * LuxTools plugin bootstrap. * * 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 */ 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. } } diff --git a/syntax/files.php b/syntax/files.php index 240dd6c..b9797c9 100644 --- a/syntax/files.php +++ b/syntax/files.php @@ -1,16 +1,16 @@ Lexer->addSpecialPattern('\{\{files>.+?\}\}', $mode, 'plugin_filetools_files'); + $this->Lexer->addSpecialPattern('\{\{files>.+?\}\}', $mode, 'plugin_luxtools_files'); } /** @inheritdoc */ diff --git a/syntax/images.php b/syntax/images.php index 7d21fad..a870d66 100644 --- a/syntax/images.php +++ b/syntax/images.php @@ -1,16 +1,16 @@ Lexer->addSpecialPattern('\{\{images>.+?\}\}', $mode, 'plugin_filetools_images'); + $this->Lexer->addSpecialPattern('\{\{images>.+?\}\}', $mode, 'plugin_luxtools_images'); } /** @inheritdoc */ diff --git a/syntax/open.php b/syntax/open.php index 6239dd9..9c58b8d 100644 --- a/syntax/open.php +++ b/syntax/open.php @@ -3,12 +3,12 @@ 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 * 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 */ public function getType() @@ -32,7 +32,7 @@ class syntax_plugin_filetools_open extends SyntaxPlugin /** @inheritdoc */ public function connectTo($mode) { - $this->Lexer->addSpecialPattern('\{\{open>.+?\}\}', $mode, 'plugin_filetools_open'); + $this->Lexer->addSpecialPattern('\{\{open>.+?\}\}', $mode, 'plugin_luxtools_open'); } /** @inheritdoc */