1b7faabe8SLeonard Heyman# DokuWiki Plugin: localopen 2b7faabe8SLeonard Heyman 3b7faabe8SLeonard HeymanOpen local files and folders directly from DokuWiki pages — **without browser popups or protocol handlers**. 4b7faabe8SLeonard Heyman 5b7faabe8SLeonard HeymanThis 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. 6b7faabe8SLeonard Heyman 7b7faabe8SLeonard Heyman--- 8b7faabe8SLeonard Heyman 9b7faabe8SLeonard Heyman## ✨ Features 10b7faabe8SLeonard Heyman 11b7faabe8SLeonard Heyman- Open local files (`.docx`, `.pdf`, etc.) and folders from wiki links 12b7faabe8SLeonard Heyman- No browser confirmation dialogs 13b7faabe8SLeonard Heyman- No blank tabs or navigation away from the wiki 14b7faabe8SLeonard Heyman- Works with: 15b7faabe8SLeonard Heyman - Local drives (`C:\`, `Z:\`, etc.) 16b7faabe8SLeonard Heyman - Network shares (`\\server\share`) 17b7faabe8SLeonard Heyman- Uses standard HTTP instead of custom protocol handlers 18b7faabe8SLeonard Heyman- Lightweight and fast 19b7faabe8SLeonard Heyman 20b7faabe8SLeonard Heyman--- 21b7faabe8SLeonard Heyman 22b7faabe8SLeonard Heyman## How It Works 23b7faabe8SLeonard Heyman 24b7faabe8SLeonard Heyman1. The plugin renders links like: \[\[localopen>c:\windows|Windows\]\] 25b7faabe8SLeonard Heyman 26b7faabe8SLeonard Heyman2. When clicked: 27b7faabe8SLeonard Heyman- A background `fetch()` request is sent to: 28b7faabe8SLeonard Heyman 29b7faabe8SLeonard Heyman ``` 30b7faabe8SLeonard Heyman http://127.0.0.1:2222/open?path=...&token=... 31b7faabe8SLeonard Heyman ``` 32b7faabe8SLeonard Heyman- A small Python service running locally receives the request 33b7faabe8SLeonard Heyman- The service opens the file using Windows (`os.startfile`) 34b7faabe8SLeonard Heyman 35b7faabe8SLeonard Heyman The browser never navigates away from the page. 36b7faabe8SLeonard Heyman 37b7faabe8SLeonard Heyman--- 38b7faabe8SLeonard Heyman 39b7faabe8SLeonard Heyman## Requirements 40b7faabe8SLeonard Heyman 41b7faabe8SLeonard Heyman- DokuWiki 42b7faabe8SLeonard Heyman- Python 3 (Windows) 43b7faabe8SLeonard Heyman- A local Python service (included below) 44b7faabe8SLeonard Heyman 45b7faabe8SLeonard Heyman--- 46b7faabe8SLeonard Heyman 47b7faabe8SLeonard Heyman## Installation 48b7faabe8SLeonard Heyman 49b7faabe8SLeonard Heyman### 1. Install the Plugin 50b7faabe8SLeonard Heyman 51b7faabe8SLeonard HeymanCopy the plugin into: lib/plugins/localopen/ 52b7faabe8SLeonard Heyman 53b7faabe8SLeonard Heyman--- 54b7faabe8SLeonard Heyman 55b7faabe8SLeonard Heyman### 2. Set Your Token 56b7faabe8SLeonard Heyman 57b7faabe8SLeonard HeymanChoose a secret token (any random string) 58b7faabe8SLeonard Heyman 59b7faabe8SLeonard Heyman 60b7faabe8SLeonard HeymanUpdate it in: 61b7faabe8SLeonard Heyman 62b7faabe8SLeonard Heyman- the plugin PHP file 63b7faabe8SLeonard Heyman- the Python service 64b7faabe8SLeonard Heyman 65b7faabe8SLeonard Heyman--- 66b7faabe8SLeonard Heyman 67b7faabe8SLeonard Heyman### 3. Run the Local Python Service 68b7faabe8SLeonard Heyman 69*f14deb16SLeonard Heyman1. Save this as `localopen_service.py`: 70b7faabe8SLeonard Heyman 71b7faabe8SLeonard Heyman```python 72b7faabe8SLeonard Heymanfrom http.server import BaseHTTPRequestHandler, HTTPServer 73b7faabe8SLeonard Heymanfrom urllib.parse import urlparse, parse_qs, unquote 74b7faabe8SLeonard Heymanimport os 7502cf58fdSLeonard Heymanimport re 7602cf58fdSLeonard Heymanimport sys 7702cf58fdSLeonard Heymanimport subprocess 7802cf58fdSLeonard Heymanimport time 79b7faabe8SLeonard Heyman 80b7faabe8SLeonard HeymanHOST = "127.0.0.1" 81b7faabe8SLeonard HeymanPORT = 2222 8202cf58fdSLeonard HeymanTOKEN = "PUT-YOUR-TOKEN-HERE" 83b7faabe8SLeonard Heyman 8402cf58fdSLeonard Heymandef normalize_path(path: str) -> str: 8502cf58fdSLeonard Heyman path = path.strip().strip('"').strip("'") 86b7faabe8SLeonard Heyman 8702cf58fdSLeonard Heyman # Decode URL encoding first 88b7faabe8SLeonard Heyman path = unquote(path) 89b7faabe8SLeonard Heyman 9002cf58fdSLeonard Heyman # Replace forward slashes with backslashes (Windows-native) 9102cf58fdSLeonard Heyman path = path.replace("/", "\\") 9202cf58fdSLeonard Heyman 9302cf58fdSLeonard Heyman return path 9402cf58fdSLeonard Heyman 9502cf58fdSLeonard Heymandef is_allowed(path: str) -> bool: 9602cf58fdSLeonard Heyman # Allow any drive letter like C:\, D:\, H:\, etc. 9702cf58fdSLeonard Heyman if re.match(r'^[a-zA-Z]:\\', path): 9802cf58fdSLeonard Heyman return True 9902cf58fdSLeonard Heyman 10002cf58fdSLeonard Heyman # Allow UNC paths (network shares) 10102cf58fdSLeonard Heyman if path.startswith('\\\\'): 10202cf58fdSLeonard Heyman return True 10302cf58fdSLeonard Heyman 10402cf58fdSLeonard Heyman return False 10502cf58fdSLeonard Heyman 10602cf58fdSLeonard Heyman 10702cf58fdSLeonard Heymandef focus_window(title): 10802cf58fdSLeonard Heyman subprocess.Popen([ 10902cf58fdSLeonard Heyman # "powershell", 11002cf58fdSLeonard Heyman "-NoProfile", 11102cf58fdSLeonard Heyman "-Command", 11202cf58fdSLeonard Heyman f"$wshell = New-Object -ComObject wscript.shell; $wshell.AppActivate('{title}')" 11302cf58fdSLeonard Heyman ]) 11402cf58fdSLeonard Heyman 11502cf58fdSLeonard Heyman 11602cf58fdSLeonard Heymanimport os 11702cf58fdSLeonard Heymanimport subprocess 11802cf58fdSLeonard Heyman 11902cf58fdSLeonard Heymandef open_path(path): 12002cf58fdSLeonard Heyman ext = os.path.splitext(path)[1].lower() 12102cf58fdSLeonard Heyman 12202cf58fdSLeonard Heyman if os.path.isdir(path): 12302cf58fdSLeonard Heyman subprocess.Popen(["explorer.exe", path]) 12402cf58fdSLeonard Heyman time.sleep(0.5) 12502cf58fdSLeonard Heyman focus_window("File Explorer") 12602cf58fdSLeonard Heyman else: 12702cf58fdSLeonard Heyman os.startfile(path) 12802cf58fdSLeonard Heyman time.sleep(1) 12902cf58fdSLeonard Heyman 13002cf58fdSLeonard Heyman if ext in [".xls", ".xlsx", ".xlsm"]: 13102cf58fdSLeonard Heyman focus_window("Excel") 13202cf58fdSLeonard Heyman elif ext == ".pdf": 13302cf58fdSLeonard Heyman focus_window(os.path.basename(path)) 134b7faabe8SLeonard Heyman 135b7faabe8SLeonard Heymanclass Handler(BaseHTTPRequestHandler): 136b7faabe8SLeonard Heyman def do_GET(self): 137b7faabe8SLeonard Heyman parsed = urlparse(self.path) 138b7faabe8SLeonard Heyman 139b7faabe8SLeonard Heyman if parsed.path != "/open": 14002cf58fdSLeonard Heyman self.send_response(404) 14102cf58fdSLeonard Heyman self.send_header("Content-Type", "text/plain; charset=utf-8") 14202cf58fdSLeonard Heyman self.end_headers() 14302cf58fdSLeonard Heyman self.wfile.write(b"Not found") 144b7faabe8SLeonard Heyman return 145b7faabe8SLeonard Heyman 146b7faabe8SLeonard Heyman qs = parse_qs(parsed.query) 147b7faabe8SLeonard Heyman token = qs.get("token", [""])[0] 14802cf58fdSLeonard Heyman raw_path = qs.get("path", [""])[0] 14902cf58fdSLeonard Heyman path = normalize_path(unquote(raw_path)) 150b7faabe8SLeonard Heyman 151b7faabe8SLeonard Heyman if token != TOKEN: 15202cf58fdSLeonard Heyman self.send_response(403) 15302cf58fdSLeonard Heyman self.send_header("Content-Type", "text/plain; charset=utf-8") 15402cf58fdSLeonard Heyman self.end_headers() 15502cf58fdSLeonard Heyman self.wfile.write(b"Forbidden") 15602cf58fdSLeonard Heyman return 15702cf58fdSLeonard Heyman 15802cf58fdSLeonard Heyman if not path: 15902cf58fdSLeonard Heyman self.send_response(400) 16002cf58fdSLeonard Heyman self.send_header("Content-Type", "text/plain; charset=utf-8") 16102cf58fdSLeonard Heyman self.end_headers() 16202cf58fdSLeonard Heyman self.wfile.write(b"Missing path") 163b7faabe8SLeonard Heyman return 164b7faabe8SLeonard Heyman 165b7faabe8SLeonard Heyman if not is_allowed(path): 16602cf58fdSLeonard Heyman self.send_response(403) 16702cf58fdSLeonard Heyman self.send_header("Content-Type", "text/plain; charset=utf-8") 168b7faabe8SLeonard Heyman self.end_headers() 16902cf58fdSLeonard Heyman self.wfile.write(b"Path not allowed") 17002cf58fdSLeonard Heyman return 171b7faabe8SLeonard Heyman 17202cf58fdSLeonard Heyman # Convert to Windows path for existence check 17302cf58fdSLeonard Heyman win_path = path.replace("/", "\\") 17402cf58fdSLeonard Heyman 17502cf58fdSLeonard Heyman if not os.path.exists(win_path): 17602cf58fdSLeonard Heyman self.send_response(404) 17702cf58fdSLeonard Heyman self.send_header("Content-Type", "text/plain; charset=utf-8") 17802cf58fdSLeonard Heyman self.end_headers() 17902cf58fdSLeonard Heyman self.wfile.write(f"Path does not exist: {win_path}".encode("utf-8")) 18002cf58fdSLeonard Heyman return 18102cf58fdSLeonard Heyman 18202cf58fdSLeonard Heyman try: 18302cf58fdSLeonard Heyman open_path(path) 18402cf58fdSLeonard Heyman self.send_response(200) 18502cf58fdSLeonard Heyman self.send_header("Content-Type", "text/html; charset=utf-8") 18602cf58fdSLeonard Heyman self.end_headers() 18702cf58fdSLeonard Heyman self.wfile.write(b"""<html><body>Opened.</body></html>""") 18802cf58fdSLeonard Heyman except Exception as e: 18902cf58fdSLeonard Heyman self.send_response(500) 19002cf58fdSLeonard Heyman self.send_header("Content-Type", "text/plain; charset=utf-8") 19102cf58fdSLeonard Heyman self.end_headers() 19202cf58fdSLeonard Heyman self.wfile.write(f"Error: {e}".encode("utf-8")) 19302cf58fdSLeonard Heyman 19402cf58fdSLeonard Heyman def log_message(self, format, *args): 19502cf58fdSLeonard Heyman # Stay silent 196b7faabe8SLeonard Heyman pass 197b7faabe8SLeonard Heyman 19802cf58fdSLeonard Heymandef main(): 19902cf58fdSLeonard Heyman try: 20002cf58fdSLeonard Heyman server = HTTPServer((HOST, PORT), Handler) 20102cf58fdSLeonard Heyman except OSError as e: 20202cf58fdSLeonard Heyman sys.exit(f"Could not bind to {HOST}:{PORT}: {e}") 203b7faabe8SLeonard Heyman 20402cf58fdSLeonard Heyman server.serve_forever() 20502cf58fdSLeonard Heyman 20602cf58fdSLeonard Heymanif __name__ == "__main__": 20702cf58fdSLeonard Heyman main() 208*f14deb16SLeonard Heyman``` 209*f14deb16SLeonard Heyman 210*f14deb16SLeonard Heyman# Run the LocalOpen Python Service at Windows Login (Task Scheduler) 211*f14deb16SLeonard Heyman 212*f14deb16SLeonard HeymanThis guide configures your Python script to run **silently** when you log into Windows, using Task Scheduler. 213*f14deb16SLeonard Heyman 214*f14deb16SLeonard Heyman--- 215*f14deb16SLeonard Heyman 216*f14deb16SLeonard Heyman## Prerequisites 217*f14deb16SLeonard Heyman 218*f14deb16SLeonard Heyman- Python installed 219*f14deb16SLeonard Heyman- Your script (e.g., `local_linker.py`) working when run manually 220*f14deb16SLeonard Heyman- Path to `pythonw.exe` (important for no console window) 221*f14deb16SLeonard Heyman 222*f14deb16SLeonard HeymanExample Python path: C:\Users\me\AppData\Local\Programs\Python\Python310\pythonw.exe 223*f14deb16SLeonard Heyman 224*f14deb16SLeonard Heyman 225*f14deb16SLeonard Heyman--- 226*f14deb16SLeonard Heyman 227*f14deb16SLeonard Heyman## Step 1 — Open Task Scheduler 228*f14deb16SLeonard Heyman 229*f14deb16SLeonard Heyman1. Press `Win + R` 230*f14deb16SLeonard Heyman2. Enter: `taskschd.msc` 231*f14deb16SLeonard Heyman3. Press Enter 232*f14deb16SLeonard Heyman 233*f14deb16SLeonard Heyman--- 234*f14deb16SLeonard Heyman 235*f14deb16SLeonard Heyman## Step 2 — Create a New Task 236*f14deb16SLeonard Heyman 237*f14deb16SLeonard Heyman1. In Task Scheduler, click: `Create Task` 238*f14deb16SLeonard Heyman 239*f14deb16SLeonard Heyman> Do **not** use "Create Basic Task" 240*f14deb16SLeonard Heyman 241*f14deb16SLeonard Heyman--- 242*f14deb16SLeonard Heyman 243*f14deb16SLeonard Heyman## Step 3 — Configure the General Tab 244*f14deb16SLeonard Heyman 245*f14deb16SLeonard Heyman- **Name:** `LocalOpen Service` 246*f14deb16SLeonard Heyman- Select: 247*f14deb16SLeonard Heyman - ✅ Run only when user is logged on 248*f14deb16SLeonard Heyman - ✅ Run with highest privileges *(optional but recommended)* 249*f14deb16SLeonard Heyman 250*f14deb16SLeonard Heyman--- 251*f14deb16SLeonard Heyman 252*f14deb16SLeonard Heyman## Step 4 — Configure the Trigger 253*f14deb16SLeonard Heyman 254*f14deb16SLeonard Heyman1. Go to the **Triggers** tab 255*f14deb16SLeonard Heyman2. Click **New...** 256*f14deb16SLeonard Heyman 257*f14deb16SLeonard HeymanSet: 258*f14deb16SLeonard Heyman- **Begin the task:** `At log on` 259*f14deb16SLeonard Heyman- **User:** Your user account 260*f14deb16SLeonard Heyman 261*f14deb16SLeonard HeymanClick **OK** 262*f14deb16SLeonard Heyman 263*f14deb16SLeonard Heyman--- 264*f14deb16SLeonard Heyman 265*f14deb16SLeonard Heyman## Step 5 — Configure the Action 266*f14deb16SLeonard Heyman 267*f14deb16SLeonard Heyman1. Go to the **Actions** tab 268*f14deb16SLeonard Heyman2. Click **New...** 269*f14deb16SLeonard Heyman 270*f14deb16SLeonard Heyman### Program/script 271*f14deb16SLeonard HeymanC:\Users\me\AppData\Local\Programs\Python\Python310\pythonw.exe 272*f14deb16SLeonard Heyman 273*f14deb16SLeonard Heyman 274*f14deb16SLeonard Heyman### Add arguments 275*f14deb16SLeonard Heyman"C:\Users\me\OneDrive\Apps\General Python Scripts\local_linker.py" 276*f14deb16SLeonard Heyman 277*f14deb16SLeonard Heyman 278*f14deb16SLeonard Heyman### Start in 279*f14deb16SLeonard HeymanC:\Users\me\OneDrive\Apps\General Python Scripts 280*f14deb16SLeonard Heyman 281*f14deb16SLeonard Heyman 282*f14deb16SLeonard HeymanClick **OK** 283*f14deb16SLeonard Heyman 284*f14deb16SLeonard Heyman--- 285*f14deb16SLeonard Heyman 286*f14deb16SLeonard Heyman## Step 6 — Save the Task 287*f14deb16SLeonard Heyman 288*f14deb16SLeonard Heyman- Click **OK** 289*f14deb16SLeonard Heyman- Enter your Windows password if prompted 290*f14deb16SLeonard Heyman 291*f14deb16SLeonard Heyman--- 292*f14deb16SLeonard Heyman 293*f14deb16SLeonard Heyman## Step 7 — Test the Task 294*f14deb16SLeonard Heyman 295*f14deb16SLeonard Heyman1. In Task Scheduler: 296*f14deb16SLeonard Heyman - Right-click the task 297*f14deb16SLeonard Heyman - Click **Run** 298*f14deb16SLeonard Heyman 299*f14deb16SLeonard Heyman2. Test a link in your wiki 300*f14deb16SLeonard Heyman 301*f14deb16SLeonard Heyman--- 302*f14deb16SLeonard Heyman 303*f14deb16SLeonard Heyman## Verify It Is Running 304*f14deb16SLeonard Heyman 305*f14deb16SLeonard HeymanOpen PowerShell: 306*f14deb16SLeonard Heyman 307*f14deb16SLeonard Heyman```powershell 308*f14deb16SLeonard HeymanGet-Process pythonw 309