1# DokuWiki Plugin: localopen 2 3Open local files and folders directly from DokuWiki pages — **without browser popups or protocol handlers**. 4 5This 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. 6 7--- 8 9## ✨ Features 10 11- Open local files (`.docx`, `.pdf`, etc.) and folders from wiki links 12- No browser confirmation dialogs 13- No blank tabs or navigation away from the wiki 14- Works with: 15 - Local drives (`C:\`, `Z:\`, etc.) 16 - Network shares (`\\server\share`) 17- Uses standard HTTP instead of custom protocol handlers 18- Lightweight and fast 19 20--- 21 22## How It Works 23 241. The plugin renders links like: \[\[localopen>c:\windows|Windows\]\] 25 262. When clicked: 27- A background `fetch()` request is sent to: 28 29 ``` 30 http://127.0.0.1:2222/open?path=...&token=... 31 ``` 32- A small Python service running locally receives the request 33- The service opens the file using Windows (`os.startfile`) 34 35 The browser never navigates away from the page. 36 37--- 38 39## Requirements 40 41- DokuWiki 42- Python 3 (Windows) 43- A local Python service (included below) 44 45--- 46 47## Installation 48 49### 1. Install the Plugin 50 51Copy the plugin into: lib/plugins/localopen/ 52 53--- 54 55### 2. Set Your Token 56 57Choose a secret token (any random string) 58 59 60Update it in: 61 62- the plugin PHP file 63- the Python service 64 65--- 66 67### 3. Run the Local Python Service 68 69Save this as `localopen_service.py`: 70 71```python 72from http.server import BaseHTTPRequestHandler, HTTPServer 73from urllib.parse import urlparse, parse_qs, unquote 74import os 75import re 76import sys 77import subprocess 78import time 79 80HOST = "127.0.0.1" 81PORT = 2222 82TOKEN = "PUT-YOUR-TOKEN-HERE" 83 84def normalize_path(path: str) -> str: 85 path = path.strip().strip('"').strip("'") 86 87 # Decode URL encoding first 88 path = unquote(path) 89 90 # Replace forward slashes with backslashes (Windows-native) 91 path = path.replace("/", "\\") 92 93 return path 94 95def is_allowed(path: str) -> bool: 96 # Allow any drive letter like C:\, D:\, H:\, etc. 97 if re.match(r'^[a-zA-Z]:\\', path): 98 return True 99 100 # Allow UNC paths (network shares) 101 if path.startswith('\\\\'): 102 return True 103 104 return False 105 106 107def focus_window(title): 108 subprocess.Popen([ 109 # "powershell", 110 "-NoProfile", 111 "-Command", 112 f"$wshell = New-Object -ComObject wscript.shell; $wshell.AppActivate('{title}')" 113 ]) 114 115 116import os 117import subprocess 118 119def open_path(path): 120 ext = os.path.splitext(path)[1].lower() 121 122 if os.path.isdir(path): 123 subprocess.Popen(["explorer.exe", path]) 124 time.sleep(0.5) 125 focus_window("File Explorer") 126 else: 127 os.startfile(path) 128 time.sleep(1) 129 130 if ext in [".xls", ".xlsx", ".xlsm"]: 131 focus_window("Excel") 132 elif ext == ".pdf": 133 focus_window(os.path.basename(path)) 134 135class Handler(BaseHTTPRequestHandler): 136 def do_GET(self): 137 parsed = urlparse(self.path) 138 139 if parsed.path != "/open": 140 self.send_response(404) 141 self.send_header("Content-Type", "text/plain; charset=utf-8") 142 self.end_headers() 143 self.wfile.write(b"Not found") 144 return 145 146 qs = parse_qs(parsed.query) 147 token = qs.get("token", [""])[0] 148 raw_path = qs.get("path", [""])[0] 149 path = normalize_path(unquote(raw_path)) 150 151 if token != TOKEN: 152 self.send_response(403) 153 self.send_header("Content-Type", "text/plain; charset=utf-8") 154 self.end_headers() 155 self.wfile.write(b"Forbidden") 156 return 157 158 if not path: 159 self.send_response(400) 160 self.send_header("Content-Type", "text/plain; charset=utf-8") 161 self.end_headers() 162 self.wfile.write(b"Missing path") 163 return 164 165 if not is_allowed(path): 166 self.send_response(403) 167 self.send_header("Content-Type", "text/plain; charset=utf-8") 168 self.end_headers() 169 self.wfile.write(b"Path not allowed") 170 return 171 172 # Convert to Windows path for existence check 173 win_path = path.replace("/", "\\") 174 175 if not os.path.exists(win_path): 176 self.send_response(404) 177 self.send_header("Content-Type", "text/plain; charset=utf-8") 178 self.end_headers() 179 self.wfile.write(f"Path does not exist: {win_path}".encode("utf-8")) 180 return 181 182 try: 183 open_path(path) 184 self.send_response(200) 185 self.send_header("Content-Type", "text/html; charset=utf-8") 186 self.end_headers() 187 self.wfile.write(b"""<html><body>Opened.</body></html>""") 188 except Exception as e: 189 self.send_response(500) 190 self.send_header("Content-Type", "text/plain; charset=utf-8") 191 self.end_headers() 192 self.wfile.write(f"Error: {e}".encode("utf-8")) 193 194 def log_message(self, format, *args): 195 # Stay silent 196 pass 197 198def main(): 199 try: 200 server = HTTPServer((HOST, PORT), Handler) 201 except OSError as e: 202 sys.exit(f"Could not bind to {HOST}:{PORT}: {e}") 203 204 server.serve_forever() 205 206if __name__ == "__main__": 207 main() 208