1*b7faabe8SLeonard Heyman# DokuWiki Plugin: localopen 2*b7faabe8SLeonard Heyman 3*b7faabe8SLeonard HeymanOpen local files and folders directly from DokuWiki pages — **without browser popups or protocol handlers**. 4*b7faabe8SLeonard Heyman 5*b7faabe8SLeonard 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. 6*b7faabe8SLeonard Heyman 7*b7faabe8SLeonard Heyman--- 8*b7faabe8SLeonard Heyman 9*b7faabe8SLeonard Heyman## ✨ Features 10*b7faabe8SLeonard Heyman 11*b7faabe8SLeonard Heyman- Open local files (`.docx`, `.pdf`, etc.) and folders from wiki links 12*b7faabe8SLeonard Heyman- No browser confirmation dialogs 13*b7faabe8SLeonard Heyman- No blank tabs or navigation away from the wiki 14*b7faabe8SLeonard Heyman- Works with: 15*b7faabe8SLeonard Heyman - Local drives (`C:\`, `Z:\`, etc.) 16*b7faabe8SLeonard Heyman - Network shares (`\\server\share`) 17*b7faabe8SLeonard Heyman- Uses standard HTTP instead of custom protocol handlers 18*b7faabe8SLeonard Heyman- Lightweight and fast 19*b7faabe8SLeonard Heyman 20*b7faabe8SLeonard Heyman--- 21*b7faabe8SLeonard Heyman 22*b7faabe8SLeonard Heyman## How It Works 23*b7faabe8SLeonard Heyman 24*b7faabe8SLeonard Heyman1. The plugin renders links like: \[\[localopen>c:\windows|Windows\]\] 25*b7faabe8SLeonard Heyman 26*b7faabe8SLeonard Heyman2. When clicked: 27*b7faabe8SLeonard Heyman- A background `fetch()` request is sent to: 28*b7faabe8SLeonard Heyman 29*b7faabe8SLeonard Heyman ``` 30*b7faabe8SLeonard Heyman http://127.0.0.1:2222/open?path=...&token=... 31*b7faabe8SLeonard Heyman ``` 32*b7faabe8SLeonard Heyman- A small Python service running locally receives the request 33*b7faabe8SLeonard Heyman- The service opens the file using Windows (`os.startfile`) 34*b7faabe8SLeonard Heyman 35*b7faabe8SLeonard Heyman The browser never navigates away from the page. 36*b7faabe8SLeonard Heyman 37*b7faabe8SLeonard Heyman--- 38*b7faabe8SLeonard Heyman 39*b7faabe8SLeonard Heyman## Requirements 40*b7faabe8SLeonard Heyman 41*b7faabe8SLeonard Heyman- DokuWiki 42*b7faabe8SLeonard Heyman- Python 3 (Windows) 43*b7faabe8SLeonard Heyman- A local Python service (included below) 44*b7faabe8SLeonard Heyman 45*b7faabe8SLeonard Heyman--- 46*b7faabe8SLeonard Heyman 47*b7faabe8SLeonard Heyman## Installation 48*b7faabe8SLeonard Heyman 49*b7faabe8SLeonard Heyman### 1. Install the Plugin 50*b7faabe8SLeonard Heyman 51*b7faabe8SLeonard HeymanCopy the plugin into: lib/plugins/localopen/ 52*b7faabe8SLeonard Heyman 53*b7faabe8SLeonard Heyman--- 54*b7faabe8SLeonard Heyman 55*b7faabe8SLeonard Heyman### 2. Set Your Token 56*b7faabe8SLeonard Heyman 57*b7faabe8SLeonard HeymanChoose a secret token (any random string) 58*b7faabe8SLeonard Heyman 59*b7faabe8SLeonard Heyman 60*b7faabe8SLeonard HeymanUpdate it in: 61*b7faabe8SLeonard Heyman 62*b7faabe8SLeonard Heyman- the plugin PHP file 63*b7faabe8SLeonard Heyman- the Python service 64*b7faabe8SLeonard Heyman 65*b7faabe8SLeonard Heyman--- 66*b7faabe8SLeonard Heyman 67*b7faabe8SLeonard Heyman### 3. Run the Local Python Service 68*b7faabe8SLeonard Heyman 69*b7faabe8SLeonard HeymanSave this as `localopen_service.py`: 70*b7faabe8SLeonard Heyman 71*b7faabe8SLeonard Heyman```python 72*b7faabe8SLeonard Heymanfrom http.server import BaseHTTPRequestHandler, HTTPServer 73*b7faabe8SLeonard Heymanfrom urllib.parse import urlparse, parse_qs, unquote 74*b7faabe8SLeonard Heymanimport os 75*b7faabe8SLeonard Heyman 76*b7faabe8SLeonard HeymanHOST = "127.0.0.1" 77*b7faabe8SLeonard HeymanPORT = 2222 78*b7faabe8SLeonard HeymanTOKEN = "ChangeThisToYourToken" 79*b7faabe8SLeonard Heyman 80*b7faabe8SLeonard HeymanALLOWED_PREFIXES = [ 81*b7faabe8SLeonard Heyman r"C:\\", 82*b7faabe8SLeonard Heyman r"D:\\", 83*b7faabe8SLeonard Heyman r"Z:\\", 84*b7faabe8SLeonard Heyman r"\\10.2.1.74\\", 85*b7faabe8SLeonard Heyman] 86*b7faabe8SLeonard Heyman 87*b7faabe8SLeonard Heymandef normalize_path(path): 88*b7faabe8SLeonard Heyman path = unquote(path) 89*b7faabe8SLeonard Heyman return path.strip().strip('"').strip("'") 90*b7faabe8SLeonard Heyman 91*b7faabe8SLeonard Heymandef is_allowed(path): 92*b7faabe8SLeonard Heyman p = path.lower() 93*b7faabe8SLeonard Heyman return any(p.startswith(prefix.lower()) for prefix in ALLOWED_PREFIXES) 94*b7faabe8SLeonard Heyman 95*b7faabe8SLeonard Heymanclass Handler(BaseHTTPRequestHandler): 96*b7faabe8SLeonard Heyman def do_GET(self): 97*b7faabe8SLeonard Heyman parsed = urlparse(self.path) 98*b7faabe8SLeonard Heyman 99*b7faabe8SLeonard Heyman if parsed.path != "/open": 100*b7faabe8SLeonard Heyman self.send_error(404) 101*b7faabe8SLeonard Heyman return 102*b7faabe8SLeonard Heyman 103*b7faabe8SLeonard Heyman qs = parse_qs(parsed.query) 104*b7faabe8SLeonard Heyman token = qs.get("token", [""])[0] 105*b7faabe8SLeonard Heyman path = normalize_path(qs.get("path", [""])[0]) 106*b7faabe8SLeonard Heyman 107*b7faabe8SLeonard Heyman if token != TOKEN: 108*b7faabe8SLeonard Heyman self.send_error(403) 109*b7faabe8SLeonard Heyman return 110*b7faabe8SLeonard Heyman 111*b7faabe8SLeonard Heyman if not is_allowed(path): 112*b7faabe8SLeonard Heyman self.send_error(403) 113*b7faabe8SLeonard Heyman return 114*b7faabe8SLeonard Heyman 115*b7faabe8SLeonard Heyman if not os.path.exists(path): 116*b7faabe8SLeonard Heyman self.send_error(404) 117*b7faabe8SLeonard Heyman return 118*b7faabe8SLeonard Heyman 119*b7faabe8SLeonard Heyman os.startfile(path) 120*b7faabe8SLeonard Heyman 121*b7faabe8SLeonard Heyman self.send_response(200) 122*b7faabe8SLeonard Heyman self.end_headers() 123*b7faabe8SLeonard Heyman 124*b7faabe8SLeonard Heyman def log_message(self, *args): 125*b7faabe8SLeonard Heyman pass 126*b7faabe8SLeonard Heyman 127*b7faabe8SLeonard HeymanHTTPServer((HOST, PORT), Handler).serve_forever() 128*b7faabe8SLeonard Heyman 129