1<?php 2/** 3 * Copyright 2003-2017 Horde LLC (http://www.horde.org/) 4 * 5 * See the enclosed file COPYING for license information (GPL). If you 6 * did not receive this file, see http://www.horde.org/licenses/gpl. 7 * 8 * @category Horde 9 * @license http://www.horde.org/licenses/gpl GPL 10 * @author Jan Schneider <jan@horde.org> 11 * @author Tyler Colbert <tyler@colberts.us> 12 * @package Wicked 13 */ 14 15/** 16 * Page class for regular pages. 17 * 18 * @category Horde 19 * @license http://www.horde.org/licenses/gpl GPL 20 * @author Jan Schneider <jan@horde.org> 21 * @author Tyler Colbert <tyler@colberts.us> 22 * @package Wicked 23 */ 24class Wicked_Page_StandardPage extends Wicked_Page 25{ 26 /** 27 * Display modes supported by this page. 28 * 29 * @var array 30 */ 31 public $supportedModes = array( 32 Wicked::MODE_DISPLAY => true, 33 Wicked::MODE_EDIT => true, 34 Wicked::MODE_REMOVE => true, 35 Wicked::MODE_HISTORY => true, 36 Wicked::MODE_DIFF => true); 37 38 /** 39 * A Horde_Locks instance for un-/locking this page. 40 * 41 * @var Horde_Lock 42 */ 43 protected $_locks = null; 44 45 /** 46 * Lock information if this page is currently locked. 47 * 48 * @var array 49 */ 50 protected $_lock = null; 51 52 /** 53 * Constructs a standard page class to represent a wiki page. 54 * 55 * @param string $pagename The name of the page to represent. 56 */ 57 public function __construct($pagename) 58 { 59 if (is_array($pagename)) { 60 $this->_page = $pagename; 61 return; 62 } 63 64 $page = null; 65 try { 66 $page = $GLOBALS['wicked']->retrieveByName($pagename); 67 } catch (Wicked_Exception $e) { 68 // If we can't load $pagename, see if there's default data for it. 69 // Protect against directory traversion. 70 $pagepath = realpath(WICKED_BASE . '/data/' 71 . $GLOBALS['conf']['wicked']['format']); 72 $pagefile = realpath($pagepath . '/' . $pagename); 73 if ($pagefile && 74 Horde_String::common($pagefile, $pagepath) == $pagepath && 75 substr($pagename, 0, 1) != '.' && 76 file_exists($pagefile) && 77 ($text = file_get_contents($pagefile))) { 78 try { 79 $GLOBALS['wicked']->newPage($pagename, $text); 80 try { 81 $page = $GLOBALS['wicked']->retrieveByName($pagename); 82 } catch (Wicked_Exception $e) { 83 $GLOBALS['notification']->push(sprintf(_("Unable to create %s"), $pagename), 'horde.error'); 84 } 85 } catch (Wicked_Exception $e) {} 86 } 87 } 88 89 if ($page) { 90 $this->_page = $page; 91 } else { 92 if ($pagename == 'Wiki/Home') { 93 $GLOBALS['notification']->push(_("Unable to create Wiki/Home. The wiki is not configured."), 'horde.error'); 94 } 95 $this->_page = array(); 96 } 97 98 // Make sure 'wicked' permission exists. Set reasonable defaults if 99 // necessary. 100 $perms = $GLOBALS['injector']->getInstance('Horde_Perms'); 101 $corePerms = $GLOBALS['injector']->getInstance('Horde_Core_Perms'); 102 if (!$perms->exists('wicked')) { 103 $perm = $corePerms->newPermission('wicked'); 104 $perm->addGuestPermission(Horde_Perms::SHOW | Horde_Perms::READ, false); 105 $perm->addDefaultPermission(Horde_Perms::SHOW | Horde_Perms::READ | Horde_Perms::EDIT | Horde_Perms::DELETE, false); 106 $perms->addPermission($perm); 107 } 108 109 // Make sure 'wicked:pages' exists. Copy from 'wicked' if it does not 110 // exist. 111 if (!$perms->exists('wicked:pages')) { 112 $perm = $corePerms->newPermission('wicked:pages'); 113 $copyFrom = $perms->getPermission('wicked'); 114 $perm->addGuestPermission($copyFrom->getGuestPermissions(), false); 115 $perm->addDefaultPermission($copyFrom->getDefaultPermissions(), false); 116 $perm->addCreatorPermission($copyFrom->getCreatorPermissions(), false); 117 foreach ($copyFrom->getUserPermissions() as $user => $uperm) { 118 $perm->addUserPermission($user, $uperm, false); 119 } 120 foreach ($copyFrom->getGroupPermissions() as $group => $gperm) { 121 $perm->addGroupPermission($group, $gperm, false); 122 } 123 $perms->addPermission($perm); 124 } 125 126 if ($GLOBALS['conf']['lock']['driver'] != 'none') { 127 $this->supportedModes[Wicked::MODE_LOCKING] = $this->supportedModes[Wicked::MODE_UNLOCKING] = true; 128 $this->_locks = $GLOBALS['injector']->getInstance('Horde_Lock'); 129 $locks = $this->_locks->getLocks('wicked', $pagename, Horde_Lock::TYPE_EXCLUSIVE); 130 if ($locks) { 131 $this->_lock = reset($locks); 132 } 133 } 134 } 135 136 /** 137 * Returns if the page allows a mode. Access rights and user state 138 * are taken into consideration. 139 * 140 * @see $supportedModes 141 * 142 * @param integer $mode The mode to check for. 143 * 144 * @return boolean True if the mode is allowed. 145 */ 146 public function allows($mode) 147 { 148 switch ($mode) { 149 case Wicked::MODE_EDIT: 150 if ($this->isLocked()) { 151 return Wicked::lockUser() == $this->_lock['lock_owner']; 152 } 153 break; 154 155 case Wicked::MODE_LOCKING: 156 if ($GLOBALS['browser']->isRobot()) { 157 return false; 158 } 159 if ($GLOBALS['registry']->isAdmin()) { 160 return true; 161 } 162 if (($this->getPermissions() & Horde_Perms::EDIT) == 0) { 163 return false; 164 } 165 break; 166 167 case Wicked::MODE_UNLOCKING: 168 if ($GLOBALS['registry']->isAdmin()) { 169 return true; 170 } 171 if ($this->_lock) { 172 return Wicked::lockUser() == $this->_lock['lock_owner']; 173 } 174 return false; 175 } 176 return parent::allows($mode); 177 } 178 179 /** 180 * @throws Wicked_Exception 181 */ 182 public function displayContents($isBlock) 183 { 184 $view = $GLOBALS['injector']->createInstance('Horde_View'); 185 $view->text = $this->getProcessor()->transform($this->getText()); 186 if ($isBlock) { 187 return $view->render('display/standard'); 188 } 189 190 $view->showTools = true; 191 if ($this->allows(Wicked::MODE_EDIT) && 192 !$this->isLocked(Wicked::lockUser())) { 193 $view->edit = Horde::widget(array( 194 'url' => Wicked::url('EditPage') 195 ->add('referrer', $this->pageName()), 196 'title' => _("_Edit"), 197 'class' => 'wicked-edit', 198 )); 199 } 200 if ($this->isLocked()) { 201 if ($this->allows(Wicked::MODE_UNLOCKING)) { 202 $view->unlock = Horde::widget(array( 203 'url' => $this->pageUrl(null, 'unlock')->remove('version'), 204 'title' => _("Un_lock"), 205 'class' => 'wicked-unlock', 206 )); 207 } 208 } else { 209 if ($this->allows(Wicked::MODE_LOCKING)) { 210 $view->lock = Horde::widget(array( 211 'url' => $this->pageUrl(null, 'lock')->remove('version'), 212 'title' => _("_Lock"), 213 'class' => 'wicked-lock', 214 )); 215 } 216 } 217 if ($this->allows(Wicked::MODE_REMOVE)) { 218 $params = array('referrer' => $this->pageName()); 219 if ($this->isOld()) { 220 $params['version'] = $this->version(); 221 } 222 $view->remove = Horde::widget(array( 223 'url' => Wicked::url('DeletePage')->add($params), 224 'title' => _("_Delete"), 225 'class' => 'wicked-delete', 226 )); 227 } 228 if ($this->allows(Wicked::MODE_REMOVE) && 229 !$this->isLocked(Wicked::lockUser())) { 230 $view->rename = Horde::widget(array( 231 'url' => Wicked::url('MergeOrRename') 232 ->add('referrer', $this->pageName()), 233 'title' => _("_Merge/Rename") 234 )); 235 } 236 $view->backLinks = Horde::widget(array( 237 'url' => Wicked::url('BackLinks') 238 ->add('referrer', $this->pageName()), 239 'title' => _("_Backlinks") 240 )); 241 $view->likePages = Horde::widget(array( 242 'url' => Wicked::url('LikePages') 243 ->add('referrer', $this->pageName()), 244 'title' => _("S_imilar Pages") 245 )); 246 $view->attachedFiles = Horde::widget(array( 247 'url' => Wicked::url('AttachedFiles') 248 ->add('referrer', $this->pageName()), 249 'title' => _("Attachments") 250 )); 251 if ($this->allows(Wicked::MODE_HISTORY)) { 252 $view->changes = Horde::widget(array( 253 'url' => $this->pageUrl('history.php')->remove('version'), 254 'title' => _("Hi_story") 255 )); 256 } 257 if ($GLOBALS['registry']->isAdmin()) { 258 $permsurl = Horde::url($GLOBALS['registry']->get('webroot', 'horde') . '/admin/perms/edit.php') 259 ->add(array( 260 'category' => 'wicked:pages:' . $this->pageId(), 261 'autocreate' => 1, 262 'autocreate_copy' => 'wicked', 263 'autocreate_guest' => Horde_Perms::SHOW | Horde_Perms::READ, 264 'autocreate_default' => Horde_Perms::SHOW | Horde_Perms::READ | Horde_Perms::EDIT | Horde_Perms::DELETE 265 )); 266 $view->perms = Horde::widget(array( 267 'url' => $permsurl, 268 'target' => '_blank', 269 'title' => _("Permissio_ns") 270 )); 271 } 272 if ($histories = $GLOBALS['session']->get('wicked', 'history')) { 273 $view->history = Horde::widget(array( 274 'url' => '#', 275 'onclick' => 'document.location = document.display.history[document.display.history.selectedIndex].value;', 276 'title' => _("Ba_ck to") 277 )); 278 $view->histories = array(); 279 foreach ($histories as $history) { 280 if (!strlen($history)) { 281 continue; 282 } 283 $view->histories[(string)Wicked::url($history)] = $history; 284 } 285 } 286 $pageId = $GLOBALS['wicked']->getPageId($this->pageName()); 287 $attachments = $GLOBALS['wicked']->getAttachedFiles($pageId); 288 if (count($attachments)) { 289 $view->attachments = array(); 290 foreach ($attachments as $attachment) { 291 $url = $GLOBALS['registry'] 292 ->downloadUrl( 293 $attachment['attachment_name'], 294 array( 295 'page' => $this->pageName(), 296 'file' => $attachment['attachment_name'], 297 'version' => $attachment['attachment_version'] 298 ) 299 ); 300 $icon = $GLOBALS['injector'] 301 ->getInstance('Horde_Core_Factory_MimeViewer') 302 ->getIcon( 303 Horde_Mime_Magic::filenameToMime( 304 $attachment['attachment_name'] 305 ) 306 ); 307 $view->attachments[] = Horde::link($url) 308 . '<img src="' . $icon . '" width="16" height="16" alt="" /> ' 309 . htmlspecialchars($attachment['attachment_name']) 310 . '</a>'; 311 } 312 } 313 $view->downloadPlain = Wicked::url($this->pageName()) 314 ->add(array('actionID' => 'export', 'format' => 'plain')) 315 ->link() 316 . _("Plain Text") . '</a>'; 317 $view->downloadHtml = Wicked::url($this->pageName()) 318 ->add(array('actionID' => 'export', 'format' => 'html')) 319 ->link() 320 . _("HTML") . '</a>'; 321 $view->downloadLatex = Wicked::url($this->pageName()) 322 ->add(array('actionID' => 'export', 'format' => 'tex')) 323 ->link() 324 . _("Latex") . '</a>'; 325 $view->downloadRest = Wicked::url($this->pageName()) 326 ->add(array('actionID' => 'export', 'format' => 'rst')) 327 ->link() 328 . _("reStructuredText") . '</a>'; 329 330 return $view->render('display/standard'); 331 } 332 333 /** 334 * Renders this page in History mode. 335 * 336 * @return string The content. 337 * @throws Wicked_Exception 338 */ 339 public function history() 340 { 341 global $injector, $page_output; 342 343 $page_output->addScriptFile('history.js'); 344 345 $view = $injector->createInstance('Horde_View'); 346 347 // Header. 348 $view->formInput = Horde_Util::formInput(); 349 $view->name = $this->pageName(); 350 $view->pageLink = $this->pageUrl()->link() 351 . htmlspecialchars($this->pageName()) . '</a>'; 352 $view->refreshLink = $this->pageUrl('history.php')->link() 353 . Horde::img('reload.png', _("Reload History")) . '</a>'; 354 if ($this->allows(Wicked::MODE_REMOVE)) { 355 $view->remove = Horde::img('delete.png', _("Delete Version")); 356 } 357 if ($this->allows(Wicked::MODE_EDIT) && 358 !$this->isLocked(Wicked::lockUser())) { 359 $view->edit = Horde::img('edit.png', _("Edit Version")); 360 $view->restore = Horde::img('restore.png', _("Restore Version")); 361 } 362 $content = $view->render('history/header'); 363 364 // First item is this page. 365 $view->showRestore = false; 366 $this->_setViewProperties($view, $this); 367 $content .= $view->render('history/summary'); 368 369 // Now the rest of the histories. 370 $view->showRestore = true; 371 foreach ($GLOBALS['wicked']->getHistory($this->pageName()) as $page) { 372 $page = new Wicked_Page_StandardHistoryPage($page); 373 $this->_setViewProperties($view, $page); 374 $view->pversion = $page->version(); 375 $content .= $view->render('history/summary'); 376 } 377 378 // Footer. 379 return $content . $view->render('history/footer'); 380 } 381 382 protected function _setViewProperties($view, $page) 383 { 384 $view->displayLink = $page->pageUrl() 385 ->link(array( 386 'title' => sprintf(_("Display Version %s"), $page->version()) 387 )) 388 . htmlspecialchars($page->version()) . '</a>'; 389 390 $text = sprintf(_("Delete Version %s"), $page->version()); 391 $view->deleteLink = Wicked::url('DeletePage') 392 ->add(array( 393 'referrer' => $page->pageName(), 394 'version' => $page->version() 395 )) 396 ->link(array('title' => $text)) 397 . Horde::img('delete.png', $text) . '</a>'; 398 399 $text = sprintf(_("Edit Version %s"), $page->version()); 400 $view->editLink = Wicked::url('EditPage') 401 ->add(array('referrer' => $page->pageName())) 402 ->link(array('title' => $text)) 403 . Horde::img('edit.png', $text) . '</a>'; 404 405 $text = sprintf(_("Revert to version %s"), $page->version()); 406 $view->restoreLink = Wicked::url('RevertPage') 407 ->add(array( 408 'referrer' => $page->pageName(), 409 'version' => $page->version() 410 )) 411 ->link(array('title' => $text)) 412 . Horde::img('restore.png', $text) . '</a>'; 413 414 $view->author = $page->author(); 415 $view->date = $page->formatVersionCreated(); 416 $view->version = $page->version(); 417 $view->changelog = $page->changeLog(); 418 } 419 420 public function isLocked($owner = null) 421 { 422 if (empty($this->_lock)) { 423 return false; 424 } 425 if (is_null($owner)) { 426 return true; 427 } 428 return $owner != $this->_lock['lock_owner']; 429 } 430 431 /** 432 * @throws Wicked_Exception 433 */ 434 public function lock() 435 { 436 if ($this->_locks) { 437 $id = $this->_locks->setLock(Wicked::lockUser(), 'wicked', $this->pageName(), $GLOBALS['conf']['wicked']['lock']['time'] * 60, Horde_Lock::TYPE_EXCLUSIVE); 438 if ($id) { 439 $this->_lock = $this->_locks->getLockInfo($id); 440 } else { 441 throw new Wicked_Exception(_("The page is already locked.")); 442 } 443 } 444 } 445 446 public function unlock() 447 { 448 if ($this->_locks && $this->_lock) { 449 $this->_locks->clearLock($this->_lock['lock_id']); 450 unset($this->_lock); 451 } 452 } 453 454 public function getLockRequestor() 455 { 456 $requestor = $this->_lock['lock_owner']; 457 if ($requestor) { 458 $name = $GLOBALS['injector'] 459 ->getInstance('Horde_Core_Factory_Identity') 460 ->create($requestor) 461 ->getValue('fullname'); 462 if (!strlen($name)) { 463 $name = $requestor; 464 } 465 return $name; 466 } 467 return _("a guest"); 468 } 469 470 public function getLockTime() 471 { 472 $time = ceil(($this->_lock['lock_expiry_timestamp'] - time()) / 60); 473 return sprintf(ngettext("%d minute", "%d minutes", $time), $time); 474 } 475 476 /** 477 * @throws Wicked_Exception 478 */ 479 public function updateText($newtext, $changelog) 480 { 481 $version = $this->version(); 482 $result = $GLOBALS['wicked']->updateText($this->pageName(), $newtext, 483 $changelog); 484 485 $url = Wicked::url($this->pageName(), true, -1); 486 $new_page = $this->getPage($this->pageName()); 487 488 $message = "Modified page: $url\n" 489 . 'New Revision: ' . $new_page->version() . "\n" 490 . ($changelog ? 'Change log: ' . $changelog . "\n" : '') 491 . "\n" 492 . $new_page->getDiff($version); 493 Wicked::mail($message, 494 array('Subject' => '[' . $GLOBALS['registry']->get('name') 495 . '] changed: ' . $this->pageName())); 496 497 $this->_page['page_text'] = $newtext; 498 } 499 500 public function pageID() 501 { 502 return isset($this->_page['page_id']) ? $this->_page['page_id'] : ''; 503 } 504 505 public function pageName() 506 { 507 return isset($this->_page['page_name']) 508 ? $this->_page['page_name'] 509 : ''; 510 } 511 512 public function getText() 513 { 514 return isset($this->_page['page_text']) 515 ? $this->_page['page_text'] 516 : ''; 517 } 518 519 public function versionCreated() 520 { 521 return isset($this->_page['version_created']) 522 ? $this->_page['version_created'] 523 : ''; 524 } 525 526 public function hits() 527 { 528 return !empty($this->_page['page_hits']) 529 ? $this->_page['page_hits'] 530 : 0; 531 } 532 533 public function changeLog() 534 { 535 return $this->_page['change_log']; 536 } 537 538 public function version() 539 { 540 if (isset($this->_page['page_version'])) { 541 return $this->_page['page_version']; 542 } else { 543 return ''; 544 } 545 } 546 547 /** 548 * Renders this page in diff mode. 549 * 550 * @param string $version The version to diff this page against. 551 */ 552 public function diff($version) 553 { 554 $view = $GLOBALS['injector']->createInstance('Horde_View'); 555 $view->link = $this->pageUrl()->link() 556 . htmlspecialchars($this->pageName()) 557 . '</a>'; 558 $view->version1 = $version; 559 $view->version2 = $this->version(); 560 $view->diff = $this->getDiff($version, 'inline'); 561 echo $view->render('diff/diff'); 562 } 563 564 /** 565 * Produces a diff for this page. 566 * 567 * @param string $version Previous version, or null if diffing with 568 * `before the beginning' (empty). 569 * @param string $renderer The diff renderer. 570 */ 571 public function getDiff($version, $renderer = 'unified') 572 { 573 if (is_null($version)) { 574 $old_page_text = ''; 575 } else { 576 $old_page = $this->getPage($this->pageName(), $version); 577 $old_page_text = $old_page->getText(); 578 } 579 $diff = new Horde_Text_Diff('auto', 580 array(explode("\n", $old_page_text), 581 explode("\n", $this->getText()))); 582 $class = 'Horde_Text_Diff_Renderer_' . Horde_String::ucfirst($renderer); 583 $renderer = new $class(); 584 return $renderer->render($diff); 585 } 586 587} 588