# DokuWiki Plugin: localopen Open local files and folders directly from DokuWiki pages — **without browser popups or protocol handlers**. This plugin replaces `localexplorer://`-style links with a modern, secure approach using a local HTTP service and background requests (`fetch()`), resulting in a seamless, one-click experience. --- ## ✨ Features - Open local files (`.docx`, `.pdf`, etc.) and folders from wiki links - No browser confirmation dialogs - No blank tabs or navigation away from the wiki - Works with: - Local drives (`C:\`, `Z:\`, etc.) - Network shares (`\\server\share`) - Uses standard HTTP instead of custom protocol handlers - Lightweight and fast --- ## 🧠 How It Works 1. The plugin renders links like: \[\[localopen>c:\windows|Windows\]\] 2. When clicked: - A background `fetch()` request is sent to: ``` http://127.0.0.1:2222/open?path=...&token=... ``` - A small Python service running locally receives the request - The service opens the file using Windows (`os.startfile`) 👉 The browser never navigates away from the page. --- ## 📦 Requirements - DokuWiki - Python 3 (Windows) - A local Python service (included below) --- ## 🔧 Installation ### 1. Install the Plugin Copy the plugin into: lib/plugins/localopen/ --- ### 2. Set Your Token Choose a secret token (any random string) Update it in: - the plugin PHP file - the Python service --- ### 3. Run the Local Python Service Save this as `localopen_service.py`: ```python from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import urlparse, parse_qs, unquote import os import re import sys import subprocess import time HOST = "127.0.0.1" PORT = 2222 TOKEN = "PUT-YOUR-TOKEN-HERE" def normalize_path(path: str) -> str: path = path.strip().strip('"').strip("'") # Decode URL encoding first path = unquote(path) # Replace forward slashes with backslashes (Windows-native) path = path.replace("/", "\\") return path def is_allowed(path: str) -> bool: # Allow any drive letter like C:\, D:\, H:\, etc. if re.match(r'^[a-zA-Z]:\\', path): return True # Allow UNC paths (network shares) if path.startswith('\\\\'): return True return False def focus_window(title): subprocess.Popen([ # "powershell", "-NoProfile", "-Command", f"$wshell = New-Object -ComObject wscript.shell; $wshell.AppActivate('{title}')" ]) import os import subprocess def open_path(path): ext = os.path.splitext(path)[1].lower() if os.path.isdir(path): subprocess.Popen(["explorer.exe", path]) time.sleep(0.5) focus_window("File Explorer") else: os.startfile(path) time.sleep(1) if ext in [".xls", ".xlsx", ".xlsm"]: focus_window("Excel") elif ext == ".pdf": focus_window(os.path.basename(path)) class Handler(BaseHTTPRequestHandler): def do_GET(self): parsed = urlparse(self.path) if parsed.path != "/open": self.send_response(404) self.send_header("Content-Type", "text/plain; charset=utf-8") self.end_headers() self.wfile.write(b"Not found") return qs = parse_qs(parsed.query) token = qs.get("token", [""])[0] raw_path = qs.get("path", [""])[0] path = normalize_path(unquote(raw_path)) if token != TOKEN: self.send_response(403) self.send_header("Content-Type", "text/plain; charset=utf-8") self.end_headers() self.wfile.write(b"Forbidden") return if not path: self.send_response(400) self.send_header("Content-Type", "text/plain; charset=utf-8") self.end_headers() self.wfile.write(b"Missing path") return if not is_allowed(path): self.send_response(403) self.send_header("Content-Type", "text/plain; charset=utf-8") self.end_headers() self.wfile.write(b"Path not allowed") return # Convert to Windows path for existence check win_path = path.replace("/", "\\") if not os.path.exists(win_path): self.send_response(404) self.send_header("Content-Type", "text/plain; charset=utf-8") self.end_headers() self.wfile.write(f"Path does not exist: {win_path}".encode("utf-8")) return try: open_path(path) self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.end_headers() self.wfile.write(b"""Opened.""") except Exception as e: self.send_response(500) self.send_header("Content-Type", "text/plain; charset=utf-8") self.end_headers() self.wfile.write(f"Error: {e}".encode("utf-8")) def log_message(self, format, *args): # Stay silent pass def main(): try: server = HTTPServer((HOST, PORT), Handler) except OSError as e: sys.exit(f"Could not bind to {HOST}:{PORT}: {e}") server.serve_forever() if __name__ == "__main__": main()