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 691. Save 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``` 209 210# Run the LocalOpen Python Service at Windows Login (Task Scheduler) 211 212This guide configures your Python script to run **silently** when you log into Windows, using Task Scheduler. 213 214--- 215 216## Prerequisites 217 218- Python installed 219- Your script (e.g., `local_linker.py`) working when run manually 220- Path to `pythonw.exe` (important for no console window) 221 222Example Python path: C:\Users\me\AppData\Local\Programs\Python\Python310\pythonw.exe 223 224 225--- 226 227## Step 1 — Open Task Scheduler 228 2291. Press `Win + R` 2302. Enter: `taskschd.msc` 2313. Press Enter 232 233--- 234 235## Step 2 — Create a New Task 236 2371. In Task Scheduler, click: `Create Task` 238 239> Do **not** use "Create Basic Task" 240 241--- 242 243## Step 3 — Configure the General Tab 244 245- **Name:** `LocalOpen Service` 246- Select: 247 - ✅ Run only when user is logged on 248 - ✅ Run with highest privileges *(optional but recommended)* 249 250--- 251 252## Step 4 — Configure the Trigger 253 2541. Go to the **Triggers** tab 2552. Click **New...** 256 257Set: 258- **Begin the task:** `At log on` 259- **User:** Your user account 260 261Click **OK** 262 263--- 264 265## Step 5 — Configure the Action 266 2671. Go to the **Actions** tab 2682. Click **New...** 269 270### Program/script 271C:\Users\me\AppData\Local\Programs\Python\Python310\pythonw.exe 272 273 274### Add arguments 275"C:\Users\me\OneDrive\Apps\General Python Scripts\local_linker.py" 276 277 278### Start in 279C:\Users\me\OneDrive\Apps\General Python Scripts 280 281 282Click **OK** 283 284--- 285 286## Step 6 — Save the Task 287 288- Click **OK** 289- Enter your Windows password if prompted 290 291--- 292 293## Step 7 — Test the Task 294 2951. In Task Scheduler: 296 - Right-click the task 297 - Click **Run** 298 2992. Test a link in your wiki 300 301--- 302 303## Verify It Is Running 304 305Open PowerShell: 306 307```powershell 308Get-Process pythonw 309