4 * Copyright 2004,2007 $ThePhpWikiProgrammingTeam
5 * Copyright 2009-2010 Marc-Etienne Vargenau, Alcatel-Lucent
7 * This file is part of PhpWiki.
9 * PhpWiki is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
14 * PhpWiki is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
19 * You should have received a copy of the GNU General Public License along
20 * with PhpWiki; if not, write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
25 * Permissions per page and action based on current user,
26 * ownership and group membership implemented with ACL's (Access Control Lists),
27 * opposed to the simplier unix-like ugo:rwx system.
28 * The previous system was only based on action and current user. (lib/main.php)
30 * Permissions may be inherited from its parent pages, a optional the
31 * optional master page ("."), and predefined default permissions, if "."
33 * Pagenames starting with "." have special default permissions.
34 * For Authentication see WikiUserNew.php, WikiGroup.php and main.php
35 * Page Permissions are in PhpWiki since v1.3.9 and enabled since v1.4.0
37 * This file might replace the following functions from main.php:
38 * Request::_notAuthorized($require_level)
39 * display the denied message and optionally a login form
40 * to gain higher privileges
41 * Request::getActionDescription($action)
42 * helper to localize the _notAuthorized message per action,
43 * when login is tried.
44 * Request::getDisallowedActionDescription($action)
45 * helper to localize the _notAuthorized message per action,
47 * Request::requiredAuthority($action)
48 * returns the needed user level
49 * has a hook for plugins on POST
50 * Request::requiredAuthorityForAction($action)
51 * just returns the level per action, will be replaced with the
54 * The defined main.php actions map to simplier access types:
57 * create => edit or create
60 * store prefs => change
61 * list in PageList => list
64 /* Symbolic special ACL groups. Untranslated to be stored in page metadata*/
65 define('ACL_EVERY', '_EVERY');
66 define('ACL_ANONYMOUS', '_ANONYMOUS');
67 define('ACL_BOGOUSER', '_BOGOUSER');
68 define('ACL_HASHOMEPAGE', '_HASHOMEPAGE');
69 define('ACL_SIGNED', '_SIGNED');
70 define('ACL_AUTHENTICATED', '_AUTHENTICATED');
71 define('ACL_ADMIN', '_ADMIN');
72 define('ACL_OWNER', '_OWNER');
73 define('ACL_CREATOR', '_CREATOR');
75 // Return an page permissions array for this page.
76 // To provide ui helpers to view and change page permissions:
77 // <tr><th>Group</th><th>Access</th><th>Allow or Forbid</th></tr>
78 // <tr><td>$group</td><td>_($access)</td><td> [ ] </td></tr>
79 function pagePermissions($pagename)
82 $page = $request->getPage($pagename);
83 // Page not found (new page); returned inherited permissions, to be displayed in gray
84 if (!$page->exists()) {
85 if ($pagename == '.') // stop recursion
86 return array('default', new PagePermission());
88 return array('inherited', pagePermissions(getParentPage($pagename)));
90 } elseif ($perm = getPagePermissions($page)) {
91 return array('page', $perm);
92 // or no permissions defined; returned inherited permissions, to be displayed in gray
93 } elseif ($pagename == '.') { // stop recursion in pathological case.
94 // "." defined, without any acl
95 return array('default', new PagePermission());
97 return array('inherited', pagePermissions(getParentPage($pagename)));
101 function pagePermissionsSimpleFormat($perm_tree, $owner, $group = false)
103 list($type, $perm) = pagePermissionsAcl($perm_tree[0], $perm_tree);
105 $type = $perm_tree[0];
106 $perm = pagePermissionsAcl($perm_tree);
107 if (is_object($perm_tree[1]))
108 $perm = $perm_tree[1];
109 elseif (is_array($perm_tree[1])) {
110 $perm_tree = pagePermissionsSimpleFormat($perm_tree[1],$owner,$group);
111 if (isa($perm_tree[1],'pagepermission'))
112 $perm = $perm_tree[1];
113 elseif (isa($perm_tree,'htmlelement'))
118 return HTML::tt(HTML::strong($perm->asRwxString($owner, $group)));
119 elseif ($type == 'default')
120 return HTML::tt($perm->asRwxString($owner, $group)); elseif ($type == 'inherited') {
121 return HTML::tt(array('class' => 'inherited', 'style' => 'color:#aaa;'),
122 $perm->asRwxString($owner, $group));
126 function pagePermissionsAcl($type, $perm_tree)
128 $perm = $perm_tree[1];
129 while (!is_object($perm)) {
130 $perm_tree = pagePermissionsAcl($type, $perm);
131 $perm = $perm_tree[1];
133 return array($type, $perm);
138 function pagePermissionsAclFormat($perm_tree, $editable = false)
140 list($type, $perm) = pagePermissionsAcl($perm_tree[0], $perm_tree);
142 return $perm->asEditableTable($type);
144 return $perm->asTable($type);
148 * Check the permissions for the current action.
149 * Walk down the inheritance tree. Collect all permissions until
150 * the minimum required level is gained, which is not
151 * overruled by more specific forbid rules.
152 * Todo: cache result per access and page in session?
154 function requiredAuthorityForPage($action)
157 $auth = _requiredAuthorityForPagename(action2access($action),
158 $request->getArg('pagename'));
159 assert($auth !== -1);
161 return $request->_user->_level;
163 return WIKIAUTH_UNOBTAINABLE;
166 // Translate action or plugin to the simplier access types:
167 function action2access($action)
180 // performance and security relevant
189 // invent a new access-perm massedit? or switch back to change, or keep it at edit?
190 case _("PhpWikiAdministration") . "/" . _("Rename"):
191 case _("PhpWikiAdministration") . "/" . _("SearchReplace"):
198 $page = $request->getPage();
199 if (!$page->exists())
206 // probably create/edit but we cannot check all page permissions, can we?
217 //Todo: Plugins should be able to override its access type
218 if (isWikiWord($action))
226 // Recursive helper to do the real work.
227 // Using a simple perm cache for page-access pairs.
228 // Maybe page-(current+edit+change?)action pairs will help
229 function _requiredAuthorityForPagename($access, $pagename)
231 static $permcache = array();
233 if (array_key_exists($pagename, $permcache)
234 and array_key_exists($access, $permcache[$pagename])
236 return $permcache[$pagename][$access];
239 $page = $request->getPage($pagename);
242 if (defined('FUSIONFORGE') and FUSIONFORGE) {
243 if ($pagename != '.' && isset($request->_user->_is_external) && $request->_user->_is_external && !$page->get('external')) {
244 $permcache[$pagename][$access] = 0;
248 if ((READONLY or $request->_dbi->readonly)
249 and in_array($access, array('edit', 'create', 'change'))
254 // Page not found; check against default permissions
255 if (!$page->exists()) {
256 $perm = new PagePermission();
257 $result = ($perm->isAuthorized($access, $request->_user) === true);
258 $permcache[$pagename][$access] = $result;
261 // no ACL defined; check for special dotfile or walk down
262 if (!($perm = getPagePermissions($page))) {
263 if ($pagename == '.') {
264 $perm = new PagePermission();
265 if ($perm->isAuthorized('change', $request->_user)) {
266 // warn the user to set ACL of ".", if he has permissions to do so.
267 trigger_error(". (dotpage == rootpage for inheriting pageperm ACLs) exists without any ACL!\n" .
268 "Please do ?action=setacl&pagename=.", E_USER_WARNING);
270 $result = ($perm->isAuthorized($access, $request->_user) === true);
271 $permcache[$pagename][$access] = $result;
273 } elseif ($pagename[0] == '.') {
274 $perm = new PagePermission(PagePermission::dotPerms());
275 $result = ($perm->isAuthorized($access, $request->_user) === true);
276 $permcache[$pagename][$access] = $result;
279 return _requiredAuthorityForPagename($access, getParentPage($pagename));
281 // ACL defined; check if isAuthorized returns true or false or undecided
282 $authorized = $perm->isAuthorized($access, $request->_user);
283 if ($authorized !== -1) { // interestingly true is also -1
284 $permcache[$pagename][$access] = $authorized;
286 } elseif ($pagename == '.') {
289 return _requiredAuthorityForPagename($access, getParentPage($pagename));
294 * @param string $pagename page from which the parent page is searched.
295 * @return string parent pagename or the (possibly pseudo) dot-pagename.
297 function getParentPage($pagename)
299 if (isSubPage($pagename)) {
300 return subPageSlice($pagename, 0);
306 // Read the ACL from the page
307 // Done: Not existing pages should NOT be queried.
308 // Check the parent page instead and don't take the default ACL's
309 function getPagePermissions($page)
311 if ($hash = $page->get('perm')) // hash => object
312 return new PagePermission(unserialize($hash));
317 // Store the ACL in the page
318 function setPagePermissions($page, $perm)
323 function getAccessDescription($access)
325 static $accessDescriptions;
326 if (!$accessDescriptions) {
327 $accessDescriptions = array(
328 'list' => _("List this page and all subpages"),
329 'view' => _("View this page and all subpages"),
330 'edit' => _("Edit this page and all subpages"),
331 'create' => _("Create a new (sub)page"),
332 'dump' => _("Download page contents"),
333 'change' => _("Change page attributes"),
334 'remove' => _("Remove this page"),
335 'purge' => _("Purge this page"),
338 if (in_array($access, array_keys($accessDescriptions)))
339 return $accessDescriptions[$access];
345 * The ACL object per page. It is stored in a page, but can also
346 * be merged with ACL's from other pages or taken from the master (pseudo) dot-file.
348 * A hash of "access" => "requires" pairs.
349 * "access" is a shortcut for common actions, which map to main.php actions
350 * "requires" required username or groupname or any special group => true or false
352 * Define any special rules here, like don't list dot-pages.
358 function PagePermission($hash = array())
360 $this->_group = &$GLOBALS['request']->getGroup();
361 if (is_array($hash) and !empty($hash)) {
362 $accessTypes = $this->accessTypes();
363 foreach ($hash as $access => $requires) {
364 if (in_array($access, $accessTypes))
365 $this->perm[$access] = $requires;
367 trigger_error(sprintf(_("Unsupported ACL access type %s ignored."), $access),
371 // set default permissions, the so called dot-file acl's
372 $this->perm = $this->defaultPerms();
378 * The workhorse to check the user against the current ACL pairs.
379 * Must translate the various special groups to the actual users settings
380 * (userid, group membership).
382 function isAuthorized($access, $user)
385 if (!empty($this->perm{$access})) {
386 foreach ($this->perm[$access] as $group => $bool) {
387 if ($this->isMember($user, $group)) {
389 } elseif ($allow == -1) { // not a member and undecided: check other groups
394 return $allow; // undecided
398 * Translate the various special groups to the actual users settings
399 * (userid, group membership).
401 function isMember($user, $group)
404 if ($group === ACL_EVERY) return true;
405 if (!isset($this->_group)) $member =& $request->getGroup();
406 else $member =& $this->_group;
407 //$user = & $request->_user;
408 if ($group === ACL_ADMIN) // WIKI_ADMIN or member of _("Administrators")
409 return $user->isAdmin() or
410 ($user->isAuthenticated() and
411 $member->isMember(GROUP_ADMIN));
412 if ($group === ACL_ANONYMOUS)
413 return !$user->isSignedIn();
414 if ($group === ACL_BOGOUSER)
416 return isa($user, '_BogoUser') or
417 (isWikiWord($user->_userid) and $user->_level >= WIKIAUTH_BOGO);
418 else return isWikiWord($user->UserName());
419 if ($group === ACL_HASHOMEPAGE)
420 return $user->hasHomePage();
421 if ($group === ACL_SIGNED)
422 return $user->isSignedIn();
423 if ($group === ACL_AUTHENTICATED)
424 return $user->isAuthenticated();
425 if ($group === ACL_OWNER) {
426 if (!$user->isAuthenticated()) return false;
427 $page = $request->getPage();
428 $owner = $page->getOwner();
429 return ($owner === $user->UserName()
430 or $member->isMember($owner));
432 if ($group === ACL_CREATOR) {
433 if (!$user->isAuthenticated()) return false;
434 $page = $request->getPage();
435 $creator = $page->getCreator();
436 return ($creator === $user->UserName()
437 or $member->isMember($creator));
439 /* Or named groups or usernames.
440 Note: We don't separate groups and users here.
441 Users overrides groups with the same name.
443 return $user->UserName() === $group or
444 $member->isMember($group);
448 * returns hash of default permissions.
449 * check if the page '.' exists and returns this instead.
451 function defaultPerms()
453 //Todo: check for the existance of '.' and take this instead.
454 //Todo: honor more config.ini auth settings here
455 $perm = array('view' => array(ACL_EVERY => true),
456 'edit' => array(ACL_EVERY => true),
457 'create' => array(ACL_EVERY => true),
458 'list' => array(ACL_EVERY => true),
459 'remove' => array(ACL_ADMIN => true,
461 'purge' => array(ACL_ADMIN => true,
463 'dump' => array(ACL_ADMIN => true,
465 'change' => array(ACL_ADMIN => true,
468 $perm['dump'] = array(ACL_ADMIN => true,
470 elseif (INSECURE_ACTIONS_LOCALHOST_ONLY) {
472 $perm['dump'] = array(ACL_EVERY => true);
474 $perm['dump'] = array(ACL_ADMIN => true);
476 $perm['dump'] = array(ACL_EVERY => true);
477 if (defined('REQUIRE_SIGNIN_BEFORE_EDIT') && REQUIRE_SIGNIN_BEFORE_EDIT)
478 $perm['edit'] = array(ACL_SIGNED => true);
480 if (!ALLOW_ANON_USER) {
481 if (!ALLOW_USER_PASSWORDS)
482 $perm['view'] = array(ACL_SIGNED => true);
484 $perm['view'] = array(ACL_AUTHENTICATED => true);
485 $perm['view'][ACL_BOGOUSER] = ALLOW_BOGO_LOGIN ? true : false;
488 if (!ALLOW_ANON_EDIT) {
489 if (!ALLOW_USER_PASSWORDS)
490 $perm['edit'] = array(ACL_SIGNED => true);
492 $perm['edit'] = array(ACL_AUTHENTICATED => true);
493 $perm['edit'][ACL_BOGOUSER] = ALLOW_BOGO_LOGIN ? true : false;
494 $perm['create'] = $perm['edit'];
500 * FIXME: check valid groups and access
504 foreach ($this->perm as $access => $groups) {
505 foreach ($groups as $group => $bool) {
506 $this->perm[$access][$group] = (boolean)$bool;
512 * do a recursive comparison
514 function equal($otherperm)
516 // The equal function seems to be unable to detect removed perm.
517 // Use case is when a rule is removed.
518 return (print_r($this->perm, true) === print_r($otherperm, true));
522 * returns list of all supported access types.
524 function accessTypes()
526 return array_keys(PagePermission::defaultPerms());
530 * special permissions for dot-files, beginning with '.'
531 * maybe also for '_' files?
535 $def = array(ACL_ADMIN => true,
538 foreach (PagePermission::accessTypes() as $access) {
539 $perm[$access] = $def;
545 * dead code. not needed inside the object. see getPagePermissions($page)
547 function retrieve($page)
549 $hash = $page->get('perm');
550 if ($hash) // hash => object
551 $perm = new PagePermission(unserialize($hash));
553 $perm = new PagePermission();
558 function store($page)
562 return $page->set('perm', serialize($this->perm));
565 function groupName($group)
567 if ($group[0] == '_') return constant("GROUP" . $group);
571 /* type: page, default, inherited */
572 function asTable($type)
574 $table = HTML::table();
575 foreach ($this->perm as $access => $perms) {
576 $td = HTML::table(array('class' => 'cal'));
577 foreach ($perms as $group => $bool) {
578 $td->pushContent(HTML::tr(HTML::td(array('align' => 'right'), $group),
579 HTML::td($bool ? '[X]' : '[ ]')));
581 $table->pushContent(HTML::tr(array('class' => 'top'),
582 HTML::td($access), HTML::td($td)));
584 if ($type == 'default')
585 $table->setAttr('style', 'border: dotted thin black; background-color:#eee;');
586 elseif ($type == 'inherited')
587 $table->setAttr('style', 'border: dotted thin black; background-color:#ddd;'); elseif ($type == 'page')
588 $table->setAttr('style', 'border: solid thin black; font-weight: bold;');
592 /* type: page, default, inherited */
593 function asEditableTable($type)
596 if (!isset($this->_group)) {
597 $this->_group =& $GLOBALS['request']->getGroup();
599 $table = HTML::table();
600 $table->pushContent(HTML::tr(
601 HTML::th(array('align' => 'left'),
603 HTML::th(array('align' => 'right'),
605 HTML::th(_("Grant")),
606 HTML::th(_("Del/+")),
607 HTML::th(_("Description"))));
609 $allGroups = $this->_group->_specialGroups();
610 foreach ($this->_group->getAllGroupsIn() as $group) {
611 if (!in_array($group, $this->_group->specialGroups()))
612 $allGroups[] = $group;
614 //array_unique(array_merge($this->_group->getAllGroupsIn(),
615 $deletesrc = $WikiTheme->_findData('images/delete.png');
616 $addsrc = $WikiTheme->_findData('images/add.png');
617 $nbsp = HTML::raw(' ');
618 foreach ($this->perm as $access => $groups) {
619 //$permlist = HTML::table(array('class' => 'cal'));
621 $newperm = HTML::input(array('type' => 'checkbox',
622 'name' => "acl[_new_perm][$access]",
624 $addbutton = HTML::input(array('type' => 'checkbox',
625 'name' => "acl[_add_group][$access]",
628 'title' => _("Add this ACL"),
630 $newgroup = HTML::select(array('name' => "acl[_new_group][$access]",
631 'style' => 'text-align: right;',
633 foreach ($allGroups as $groupname) {
634 if (!isset($groups[$groupname]))
635 $newgroup->pushContent(HTML::option(array('value' => $groupname),
636 $this->groupName($groupname)));
638 if (empty($groups)) {
639 $addbutton->setAttr('checked', 'checked');
640 $newperm->setAttr('checked', 'checked');
642 HTML::tr(array('class' => 'top'),
643 HTML::td(HTML::strong($access . ":")),
645 HTML::td($nbsp, $newperm),
646 HTML::td($nbsp, $addbutton),
647 HTML::td(HTML::em(getAccessDescription($access)))));
649 foreach ($groups as $group => $bool) {
650 $checkbox = HTML::input(array('type' => 'checkbox',
651 'name' => "acl[$access][$group]",
652 'title' => _("Allow / Deny"),
654 if ($bool) $checkbox->setAttr('checked', 'checked');
655 $checkbox = HTML(HTML::input(array('type' => 'hidden',
656 'name' => "acl[$access][$group]",
659 $deletebutton = HTML::input(array('type' => 'checkbox',
660 'name' => "acl[_del_group][$access][$group]",
661 'style' => 'background: #aaa url(' . $deletesrc . ')',
662 //'src' => $deletesrc,
664 'title' => _("Delete this ACL"),
669 HTML::td(HTML::strong($access . ":")),
670 HTML::td(array('class' => 'cal-today', 'align' => 'right'),
671 HTML::strong($this->groupName($group))),
672 HTML::td(array('align' => 'center'), $nbsp, $checkbox),
673 HTML::td(array('align' => 'right', 'style' => 'background: #aaa url(' . $deletesrc . ') no-repeat'), $deletebutton),
674 HTML::td(HTML::em(getAccessDescription($access)))));
680 HTML::td(array('class' => 'cal-today', 'align' => 'right'),
681 HTML::strong($this->groupName($group))),
682 HTML::td(array('align' => 'center'), $nbsp, $checkbox),
683 HTML::td(array('align' => 'right', 'style' => 'background: #aaa url(' . $deletesrc . ') no-repeat'), $deletebutton),
689 HTML::tr(array('class' => 'top'),
690 HTML::td(array('align' => 'right'), _("add ")),
692 HTML::td(array('align' => 'center'), $nbsp, $newperm),
693 HTML::td(array('align' => 'right', 'style' => 'background: #ccc url(' . $addsrc . ') no-repeat'), $addbutton),
694 HTML::td(HTML::small(_("Check to add this ACL")))));
696 if ($type == 'default')
697 $table->setAttr('style', 'border: dotted thin black; background-color:#eee;');
698 elseif ($type == 'inherited')
699 $table->setAttr('style', 'border: dotted thin black; background-color:#ddd;'); elseif ($type == 'page')
700 $table->setAttr('style', 'border: solid thin black; font-weight: bold;');
704 // Print ACL as lines of [+-]user[,group,...]:access[,access...]
705 // Seperate acl's by "; " or whitespace
706 // See http://opag.ca/wiki/HelpOnAccessControlLists
707 // As used by WikiAdminSetAclSimple
708 function asAclLines()
713 foreach ($this->perm as $access => $groups) {
714 // unify groups for same access+bool
715 // view:CREATOR,-OWNER,
716 $line = $access . ':';
717 foreach ($groups as $group => $bool) {
718 $line .= ($bool ? '' : '-') . $group . ",";
720 if (substr($line, -1) == ',')
721 $s .= substr($line, 0, -1) . "; ";
723 if (substr($s, -2) == '; ')
724 $s = substr($s, 0, -2);
728 // This is just a bad hack for testing.
729 // Simplify the ACL to a unix-like "rwx------+" string
731 function asRwxString($owner, $group = false)
734 // simplify object => rwxrw---x+ string as in cygwin (+ denotes additional ACLs)
735 $perm =& $this->perm;
736 // get effective user and group
738 if (isset($perm['view'][$owner]) or
739 (isset($perm['view'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated())
742 if (isset($perm['edit'][$owner]) or
743 (isset($perm['edit'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated())
746 if (isset($perm['change'][$owner]) or
747 (isset($perm['change'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated())
750 if (!empty($group)) {
751 if (isset($perm['view'][$group]) or
752 (isset($perm['view'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated())
755 if (isset($perm['edit'][$group]) or
756 (isset($perm['edit'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated())
759 if (isset($perm['change'][$group]) or
760 (isset($perm['change'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated())
764 if (isset($perm['view'][ACL_EVERY]) or
765 (isset($perm['view'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated())
768 if (isset($perm['edit'][ACL_EVERY]) or
769 (isset($perm['edit'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated())
772 if (isset($perm['change'][ACL_EVERY]) or
773 (isset($perm['change'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated())
784 // c-hanging-comment-ender-p: nil
785 // indent-tabs-mode: nil