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