README.md
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