xref: /plugin/localopen/README.md (revision f14deb164c2bf1c7648bd907566bf759fe418a8f)
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