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