1<?php
2/**
3 * DokuWiki Plugin etherpadlite (Action Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Michael Braun <michael-dev@fami-braun.de>
7 */
8
9// must be run within Dokuwiki
10if (!defined('DOKU_INC')) die();
11
12if (!defined('DOKU_LF')) define('DOKU_LF', "\n");
13if (!defined('DOKU_TAB')) define('DOKU_TAB', "\t");
14if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
15
16require_once DOKU_PLUGIN.'action.php';
17require_once DOKU_PLUGIN.'etherpadlite/externals/etherpad-lite-client/etherpad-lite-client.php';
18
19class action_plugin_etherpadlite_etherpadlite extends DokuWiki_Action_Plugin {
20
21    public function register(Doku_Event_Handler $controller) {
22        $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'handle_tpl_metaheader_output');
23        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax');
24        $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handle_logoutconvenience');
25    }
26
27    private function createEPInstance() {
28        if (isset($this->instance)) return;
29        $this->domain = trim($this->getConf('etherpadlite_domain'));
30        if ($this->domain == "")
31          $this->domain = $_SERVER["HTTP_HOST"];
32        $this->ep_url = rtrim(trim($this->getConf('etherpadlite_url')),"/");
33        $ep_key = trim($this->getConf('etherpadlite_apikey'));
34        $this->ep_instance = new EtherpadLiteClient($ep_key, $this->ep_url."/api");
35        $this->ep_group = trim($this->getConf('etherpadlite_group'));
36        $this->ep_url_args = trim($this->getConf('etherpadlite_urlargs'));
37        $this->groupid = $this->ep_instance->createGroupIfNotExistsFor($this->ep_group);
38        $this->groupid = (string) $this->groupid->groupID;
39        return;
40    }
41
42    private function getPageID() {
43      global $meta, $rev;
44      assert(is_array($meta[$rev]));
45      if (!empty($this->ep_group)) {
46        return $this->groupid."\$".$meta[$rev]["pageid"];
47      } else {
48        return $meta[$rev]["pageid"];
49      }
50    }
51
52    private function renameCurrentPage() {
53      global $meta, $rev, $ID, $pageid;
54
55      assert(is_array($meta[$rev]));
56      $pageid = $this->getPageID();
57
58      $text = $this->ep_instance->getText($pageid);
59      $text = (string) $text->text;
60
61      $newpageid = md5(uniqid("dokuwiki:".md5($ID).":$rev:", true));
62      if (!empty($this->ep_group)) {
63        $this->ep_instance->createGroupPad($this->groupid, $newpageid, $text);
64      } else {
65        $this->ep_instance->createPad($newpageid, $text);
66      }
67      $this->ep_instance->deletePad($pageid);
68
69      $meta[$rev]["pageid"] = $newpageid;
70      $pageid = $this->getPageID();
71    }
72
73    public function handle_logoutconvenience(&$event,$param) {
74        global $ACT;
75        if ($ACT=='logout' && isset($_SESSION["ep_sessionID"])) {
76             $this->createEPInstance();
77             if (!empty($this->ep_group)) {
78                 $this->ep_instance->deleteSession($_SESSION["ep_sessionID"]);
79                 unset($_SESSION["ep_sessionID"]);
80             }
81        }
82    }
83
84    public function handle_ajax(&$event, $param) {
85        if (class_exists("action_plugin_ipgroup")) {
86          $plugin = new action_plugin_ipgroup();
87          $plugin->start($event, $param);
88        }
89
90        $call = $event->data;
91        if(method_exists($this, "handle_ajax_$call")) {
92           $json = new JSON();
93
94           header('Content-Type: application/json');
95           try {
96             $ret = $this->handle_ajax_inner($call);
97           } catch (Exception $e) {
98             $ret = Array("file" => __FILE__, "line" => __LINE__, "error" => "Server-Fehler (Pad-Plugin): ".$e->getMessage(), "trace" => $e->getTraceAsString(), "url" => $this->ep_url);
99           }
100           print $json->encode($ret);
101           $event->preventDefault();
102        }
103    }
104
105    private function handle_ajax_inner($call) {
106        global $conf, $ID, $REV, $INFO, $rev, $meta, $pageid, $USERINFO;
107        $this->createEPInstance();
108
109        $this->client = $_SERVER['REMOTE_USER'];
110        if(!$this->client) $this->client = clientIP(true);
111        $this->clientname = $USERINFO["name"];
112        if (empty($this->clientname)) $this->clientname = $this->client;
113
114        $ID = cleanID($_POST['id']);
115        if(empty($ID)) return;
116        if (auth_quickaclcheck($ID) < AUTH_READ) {
117          return array("file" => __FILE__, "line" => __LINE__, "error" => $this->getLang('Permission denied'));
118        }
119
120        $REV = (int) $_POST["rev"];
121        $INFO = pageinfo();
122        $rev = (int) (($INFO['rev'] == '') ? $INFO['lastmod'] : $INFO['rev']);
123        if ($rev == 0) {
124          return array("file" => __FILE__, "line" => __LINE__, "error" => $this->getLang('You need to create (save) the non-empty page first.'));
125        }
126
127        $meta = p_get_metadata($ID, "etherpadlite", METADATA_DONT_RENDER);
128        $oldmeta = $meta;
129        if (!is_array($meta)) $meta = Array();
130
131        if (isset($meta[$rev])) {
132          $pageid = $this->getPageID();
133        } else {
134          $pageid = NULL;
135        }
136
137        if (isset($_POST["isSaveable"])) {
138          $_POST["isSaveable"] = ($_POST["isSaveable"] == "true");
139        } else {
140          $_POST["isSaveable"] = false;
141        }
142
143        if (!isset($_POST["accessPassword"])) {
144          $_POST["accessPassword"] = "";
145        }
146
147        if (isset($_POST["readOnly"])) {
148          $_POST["readOnly"] = ($_POST["readOnly"] == "true");
149        }
150
151        if (isset($meta[$rev]) && ($meta[$rev]["owner"] != $this->client)) {
152          # PAD exists and is not owned by us
153          $canWrite = ((!isset($meta[$rev]["writepw"]) || ($meta[$rev]["writepw"] == (string) $_POST["accessPassword"]))
154                      && $INFO['writable']);
155          $canRead = (((($meta[$rev]["readMode"] == "wikiread") || $INFO['writable'])
156                      && (!isset($meta[$rev]["readpw"]) || $meta[$rev]["readpw"] == (string) $_POST["accessPassword"])
157                      ) || $canWrite);
158        } else { # no such pad or pad alread owned by me
159          $canWrite = $_POST["isSaveable"] && $INFO['writable'];
160          $canRead  = $INFO['writable'];
161          $_POST["readOnly"] = !$canWrite;
162        }
163
164        # default to write-access request if pad not exists, otherwise prefer write-access over readonly-access
165        if (!isset($_POST["readOnly"])) {
166          if ($pageid !== NULL) {
167            $_POST["readOnly"] = !$canWrite;
168          } else {
169            $_POST["readOnly"] = false;
170          }
171        }
172        # the master editor is always editable
173        $_POST["readOnly"] = $_POST["readOnly"] && !$_POST["isSaveable"];
174        # check if pad is owned by somebody else than how can save it (wikilock)
175        if (isset($meta[$rev]) && ($meta[$rev]["owner"] != $this->client) && $_POST["isSaveable"]) {
176          return array("file" => __FILE__, "line" => __LINE__, "error" => sprintf($this->getLang('Permission denied - pad is owned by %s, who needs to lock (edit) the page.'), $meta[$rev]["owner"]));
177        }
178        if ((!$canWrite) && (!$canRead || (!$_POST["readOnly"]))) {
179          return array("file" => __FILE__, "line" => __LINE__, "error" => $this->getLang('Permission denied'), "askPassword" => (isset($meta[$rev]["readpw"]) || isset($meta[$rev]["writepw"])));
180        }
181        if($_POST["isSaveable"] && checklock($ID)) {
182          return array("file" => __FILE__, "line" => __LINE__, "error" => $this->getLang('Permission denied - page locked by somebody else'));
183        }
184        if ($_POST["isSaveable"]) {
185          lock($ID);
186        }
187        $ret = $this->{"handle_ajax_$call"}();
188        if ($meta != $oldmeta)
189          p_set_metadata($ID, Array("etherpadlite" => $meta));
190        return $ret;
191    }
192
193    private function getPageInfo() {
194        global $conf, $ID, $REV, $INFO, $rev, $meta, $pageid;
195        if (!empty($this->ep_group)) {
196            $canPassword = ($meta[$rev]["owner"] == $this->client);
197        } else {
198            $canPassword = false;
199        }
200
201        $hasPassword = (bool) ($this->ep_instance->isPasswordProtected($pageid)->isPasswordProtected);
202        $ret = Array("hasPassword" => $hasPassword, "canPassword" => $canPassword, "encpw" => ($hasPassword ? "***" : ""));
203        $ret["encMode"] = $meta[$rev]["encMode"];
204        $ret["encAMode"] = $meta[$rev]["encAMode"];
205        $ret["readMode"] = $meta[$rev]["readMode"];
206        $ret["writeMode"] = "wikiwrite";
207
208        if (isset($meta[$rev]["readpw"])) {
209          $ret["readpw"] = "***";
210          $ret["readMode"] .= "+password";
211        } else
212          $ret["readpw"] = "";
213
214        if (isset($meta[$rev]["writepw"])) {
215          $ret["writepw"] = "***";
216          $ret["writeMode"] .= "+password";
217        } else
218          $ret["writepw"] = "";
219
220        $ret["name"] = "$pageid";
221
222        if ($_POST['readOnly']) {
223          $roid = (string) $this->ep_instance->getReadOnlyID($pageid)->readOnlyID;
224          $ret["url"] = $this->ep_url."/ro/".$roid;
225        } else {
226          $ret["url"] = $this->ep_url."/p/".$pageid;
227        }
228        $ret["url"] .= "?".$this->ep_url_args;
229
230        $isOwner = ($meta[$rev]["owner"] == $this->client);
231        $ret["isOwner"] = $isOwner;
232
233        $ret["isReadonly"] = $_POST["readOnly"];
234
235        return $ret;
236    }
237
238    public function handle_ajax_pad_security() {
239        global $conf, $ID, $REV, $INFO, $rev, $meta, $pageid;
240
241        if(!checkSecurityToken()) {
242          return Array("file" => __FILE__, "line" => __LINE__, "error" => $this->getLang("CSRF protection."));
243        }
244
245        if (!is_array($meta)) return Array("file" => __FILE__, "line" => __LINE__, "error" => $this->getLang("Permission denied"));
246        if (!isset($meta[$rev])) return Array("file" => __FILE__, "line" => __LINE__, "error" => $this->getLang("Permission denied"));
247        if ($meta[$rev]["owner"] != $this->client) return Array("file" => __FILE__, "line" => __LINE__, "error" => $this->getLang("Permission denied"));
248
249        if ($_POST["encMode"] == "noenc") {
250          $_POST["encpw"] = "";
251          if (strpos($_POST["readMode"],"password") === false)
252            $_POST["readpw"] = "";
253          if (strpos($_POST["writeMode"],"password") === false)
254            $_POST["writepw"] = "";
255          $_POST["readMode"] = str_replace("+password","",$_POST["readMode"]);
256        } else {
257          $_POST["encMode"] = "enc";
258          $_POST["readpw"] = "";
259          $_POST["writepw"] = "";
260          $_POST["readMode"] = $_POST["encAMode"];
261        }
262
263        $this->renameCurrentPage();
264
265        $password = $_POST["encpw"];
266        if ($password != "***") {
267          if ($password == "") $password = NULL;
268          $this->ep_instance->setPassword($pageid, $password);
269        }
270
271        $password = $_POST["readpw"];
272        if ($password != "***") {
273          if ($password == "") {
274            unset($meta[$rev]["readpw"]);
275          } else {
276            $meta[$rev]["readpw"] = $password;
277          }
278        }
279
280        $password = $_POST["writepw"];
281        if ($password != "***") {
282          if ($password == "") {
283            unset($meta[$rev]["writepw"]);
284          } else {
285            $meta[$rev]["writepw"] = $password;
286          }
287        }
288
289        $meta[$rev]["encMode"] = $_POST["encMode"];
290        $meta[$rev]["encAMode"] = $_POST["encAMode"];
291        $meta[$rev]["readMode"] = $_POST["readMode"];
292
293        return $this->getPageInfo();
294    }
295
296    public function handle_ajax_pad_getText() {
297        global $conf, $ID, $REV, $INFO, $rev, $meta, $pageid;
298
299        if (!is_array($meta)) return Array("file" => __FILE__, "line" => __LINE__, "error" => $this->getLang("Permission denied"));
300        if (!isset($meta[$rev])) return Array("file" => __FILE__, "line" => __LINE__, "error" => $this->getLang("Permission denied"));
301
302        $text = $this->ep_instance->getText($pageid);
303        $text = (string) $text->text;
304
305        return Array("status" => "OK", "text" => $text);
306    }
307
308    public function handle_ajax_pad_close() {
309        global $conf, $ID, $REV, $INFO, $rev, $meta, $pageid;
310
311        if(!checkSecurityToken()) {
312          return Array("file" => __FILE__, "line" => __LINE__, "error" => $this->getLang("CSRF protection."));
313        }
314
315        if (!is_array($meta)) return Array("file" => __FILE__, "line" => __LINE__, 'error' => $this->getLang("Permission denied"));
316        if (!isset($meta[$rev])) return Array("file" => __FILE__, "line" => __LINE__, 'error' => $this->getLang("Permission denied"));
317        if ($meta[$rev]["owner"] != $this->client) return Array("file" => __FILE__, "line" => __LINE__, 'error' => $this->getLang("Permission denied"));
318
319        $text = $this->ep_instance->getText($pageid);
320        $text = (string) $text->text;
321        # save as draft before deleting
322        if($conf['usedraft']) {
323          $draft = array('id'     => $ID,
324            'prefix' => substr($_POST['prefix'], 0, -1),
325            'text'   => $text,
326            'suffix' => $_POST['suffix'],
327            'date'   => (int) $_POST['date'],
328            'client' => $this->client,
329            );
330          $cname = getCacheName($draft['client'].$ID,'.draft');
331          if (!io_saveFile($cname,serialize($draft))) {
332            return Array("file" => __FILE__, "line" => __LINE__, 'error' => $this->getLang("pad could not be safed as draft"));
333          }
334        }
335        $this->ep_instance->deletePad($pageid);
336
337        unset($meta[$rev]);
338
339        return Array("status" => "OK", "text" => $text);
340    }
341
342    public function handle_ajax_has_pad() {
343        global $conf, $ID, $REV, $INFO, $rev, $meta, $pageid, $USERINFO;
344
345        return Array("exists" => isset($meta[$rev]));
346    }
347
348    public function handle_ajax_pad_open() {
349        global $conf, $ID, $REV, $INFO, $rev, $meta, $pageid, $USERINFO;
350
351        if(!checkSecurityToken()) {
352          return Array("file" => __FILE__, "line" => __LINE__, "error" => $this->getLang("CSRF protection."));
353        }
354
355        if (!empty($this->ep_group)) {
356          if (!isset($_SESSION["ep_sessionID"])) {
357            $authorid = $this->ep_instance->createAuthorIfNotExistsFor($this->client, $this->clientname);
358            $authorid = (string) $authorid->authorID;
359            $cookies = $this->ep_instance->createSession($this->groupid, $authorid, time() + 7 * 24 * 60 * 60);
360            $sessionID = (string) $cookies->sessionID;
361            $_SESSION["ep_sessionID"] = $sessionID;
362          }
363          $host = parse_url($this->ep_url, PHP_URL_HOST);
364          setcookie("sessionID",$_SESSION["ep_sessionID"], 0, "/", $host);
365          setcookie("sessionID",$_SESSION["ep_sessionID"], 0, "/", $this->domain);
366        }
367
368        if (!isset($meta[$rev])) {
369            if (!$_POST['isSaveable'] || $_POST["readOnly"])
370                return Array("file" => __FILE__, "line" => __LINE__, 'error' => $this->getLang("There is no such pad."));
371            /** new pad */
372            if (isset($_POST["text"])) {
373                $text = $_POST["text"];
374            } else {
375                $text = rawWiki($ID,$rev);
376                if(!$text) {
377                    $text = pageTemplate($ID);
378                }
379            }
380            $pageid = md5(uniqid("dokuwiki:".md5($ID).":$rev:", true));
381            if (!empty($this->ep_group)) {
382                $this->ep_instance->createGroupPad($this->groupid, $pageid, $text);
383            } else {
384                $this->ep_instance->createPad($pageid, $text);
385            }
386            $meta[$rev] = Array();
387            $meta[$rev]["pageid"] = $pageid;
388            $meta[$rev]["owner"] = $this->client;
389            $meta[$rev]["encMode"] = "noenc";
390            $meta[$rev]["encAMode"] = "wikiwrite";
391            $meta[$rev]["readMode"] = "wikiwrite";
392
393        } else {
394            $pageid = $meta[$rev]["pageid"];
395            /* in case pad is already deleted, recreate it. Should not happen, but this resolves this kind of conflict. */
396            try {
397                if (!empty($this->ep_group)) {
398                    $this->ep_instance->createGroupPad($this->groupid, $pageid, "");
399                } else {
400                    $this->ep_instance->createPad($pageid, "");
401                }
402            } catch (Exception $e) {
403            }
404        }
405        $pageid = $this->getPageID();
406
407        $ret = $this->getPageInfo();
408        $ret = array_merge($ret, Array("sessionID" => $_SESSION["ep_sessionID"], "domain" => $this->domain));
409
410        return $ret;
411    }
412
413    public function handle_tpl_metaheader_output(Doku_Event &$event, $param) {
414        global $ACT, $INFO;
415        $this->include_script($event, 'document.domain = "'.$this->domain.'";');
416
417        if (!in_array($ACT, array('edit', 'create', 'preview',
418                                  'locked', 'recover'))) {
419            return;
420        }
421        $config = array(
422            'id' => $INFO['id'],
423            'rev' => (($INFO['rev'] == '') ? $INFO['lastmod'] : $INFO['rev']),
424            'base' => DOKU_BASE.'lib/plugins/etherpadlite/',
425            'act' => $ACT
426        );
427        $path = 'scripts/etherpadlite.js';
428
429        $json = new JSON();
430        $this->include_script($event, 'var etherpad_lite_config = '.$json->encode($config));
431        $this->link_script($event, DOKU_BASE.'lib/plugins/etherpadlite/'.$path);
432
433    }
434
435    private function include_script($event, $code) {
436        $event->data['script'][] = array(
437            'type' => 'text/javascript',
438            'charset' => 'utf-8',
439            '_data' => $code,
440        );
441    }
442
443    private function link_script($event, $url) {
444        $event->data['script'][] = array(
445            'type' => 'text/javascript',
446            'charset' => 'utf-8',
447            'src' => $url,
448        );
449    }
450}
451
452// vim:ts=4:sw=4:et:
453