4 * Copyright 2004,2007 $ThePhpWikiProgrammingTeam
5 * Copyright 2009 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
20 * along with PhpWiki; if not, write to the Free Software
21 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 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,
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
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) {
81 $page = $request->getPage($pagename);
82 // Page not found (new page); returned inherited permissions, to be displayed in gray
83 if (! $page->exists() ) {
84 if ($pagename == '.') // stop recursion
85 return array('default', new PagePermission());
87 return array('inherited', pagePermissions(getParentPage($pagename)));
89 } elseif ($perm = getPagePermissions($page)) {
90 return array('page', $perm);
91 // or no permissions defined; returned inherited permissions, to be displayed in gray
92 } elseif ($pagename == '.') { // stop recursion in pathological case.
93 // "." defined, without any acl
94 return array('default', new PagePermission());
96 return array('inherited', pagePermissions(getParentPage($pagename)));
100 function pagePermissionsSimpleFormat($perm_tree, $owner, $group=false) {
101 list($type,$perm) = pagePermissionsAcl($perm_tree[0], $perm_tree);
103 $type = $perm_tree[0];
104 $perm = pagePermissionsAcl($perm_tree);
105 if (is_object($perm_tree[1]))
106 $perm = $perm_tree[1];
107 elseif (is_array($perm_tree[1])) {
108 $perm_tree = pagePermissionsSimpleFormat($perm_tree[1],$owner,$group);
109 if (isa($perm_tree[1],'pagepermission'))
110 $perm = $perm_tree[1];
111 elseif (isa($perm_tree,'htmlelement'))
116 return HTML::tt(HTML::strong($perm->asRwxString($owner, $group)));
117 elseif ($type == 'default')
118 return HTML::tt($perm->asRwxString($owner, $group));
119 elseif ($type == 'inherited') {
120 return HTML::tt(array('class'=>'inherited', 'style'=>'color:#aaa;'),
121 $perm->asRwxString($owner, $group));
125 function pagePermissionsAcl($type,$perm_tree) {
126 $perm = $perm_tree[1];
127 while (!is_object($perm)) {
128 $perm_tree = pagePermissionsAcl($type, $perm);
129 $perm = $perm_tree[1];
131 return array($type,$perm);
136 function pagePermissionsAclFormat($perm_tree, $editable=false) {
137 list($type,$perm) = pagePermissionsAcl($perm_tree[0], $perm_tree);
139 return $perm->asEditableTable($type);
141 return $perm->asTable($type);
145 * Check the permissions for the current action.
146 * Walk down the inheritance tree. Collect all permissions until
147 * the minimum required level is gained, which is not
148 * overruled by more specific forbid rules.
149 * Todo: cache result per access and page in session?
151 function requiredAuthorityForPage ($action) {
153 $auth = _requiredAuthorityForPagename(action2access($action),
154 $request->getArg('pagename'));
155 assert($auth !== -1);
157 return $request->_user->_level;
159 return WIKIAUTH_UNOBTAINABLE;
162 // Translate action or plugin to the simplier access types:
163 function action2access ($action) {
177 if (INSECURE_ACTIONS_LOCALHOST_ONLY and is_localhost())
182 // 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")."/"._("Replace"):
198 $page = $request->getPage();
199 if (!$page->exists())
206 // probably create/edit but we cannot check all page permissions, can we?
216 //Todo: Plugins should be able to override its access type
217 if (isWikiWord($action))
225 // Recursive helper to do the real work.
226 // Using a simple perm cache for page-access pairs.
227 // Maybe page-(current+edit+change?)action pairs will help
228 function _requiredAuthorityForPagename($access, $pagename) {
229 static $permcache = array();
231 if (array_key_exists($pagename, $permcache)
232 and array_key_exists($access, $permcache[$pagename]))
233 return $permcache[$pagename][$access];
236 $page = $request->getPage($pagename);
239 if (defined('GFORGE') and GFORGE) {
240 if ($pagename != '.' && isset($request->_user->_is_external) && $request->_user->_is_external && ! $page->get('external')) {
241 $permcache[$pagename][$access] = 0;
245 if ((READONLY or $request->_dbi->readonly)
246 and in_array($access, array('edit','create','change')))
251 // Page not found; check against default permissions
252 if (! $page->exists() ) {
253 $perm = new PagePermission();
254 $result = ($perm->isAuthorized($access, $request->_user) === true);
255 $permcache[$pagename][$access] = $result;
258 // no ACL defined; check for special dotfile or walk down
259 if (! ($perm = getPagePermissions($page))) {
260 if ($pagename == '.') {
261 $perm = new PagePermission();
262 if ($perm->isAuthorized('change', $request->_user)) {
263 // warn the user to set ACL of ".", if he has permissions to do so.
264 trigger_error(". (dotpage == rootpage for inheriting pageperm ACLs) exists without any ACL!\n".
265 "Please do ?action=setacl&pagename=.", E_USER_WARNING);
267 $result = ($perm->isAuthorized($access, $request->_user) === true);
268 $permcache[$pagename][$access] = $result;
270 } elseif ($pagename[0] == '.') {
271 $perm = new PagePermission(PagePermission::dotPerms());
272 $result = ($perm->isAuthorized($access, $request->_user) === true);
273 $permcache[$pagename][$access] = $result;
276 return _requiredAuthorityForPagename($access, getParentPage($pagename));
278 // ACL defined; check if isAuthorized returns true or false or undecided
279 $authorized = $perm->isAuthorized($access, $request->_user);
280 if ($authorized !== -1) { // interestingly true is also -1
281 $permcache[$pagename][$access] = $authorized;
283 } elseif ($pagename == '.') {
286 return _requiredAuthorityForPagename($access, getParentPage($pagename));
291 * @param string $pagename page from which the parent page is searched.
292 * @return string parent pagename or the (possibly pseudo) dot-pagename.
294 function getParentPage($pagename) {
295 if (isSubPage($pagename)) {
296 return subPageSlice($pagename, 0);
302 // Read the ACL from the page
303 // Done: Not existing pages should NOT be queried.
304 // Check the parent page instead and don't take the default ACL's
305 function getPagePermissions ($page) {
306 if ($hash = $page->get('perm')) // hash => object
307 return new PagePermission(unserialize($hash));
312 // Store the ACL in the page
313 function setPagePermissions ($page,$perm) {
317 function getAccessDescription($access) {
318 static $accessDescriptions;
319 if (! $accessDescriptions) {
320 $accessDescriptions = array(
321 'list' => _("List this page and all subpages"),
322 'view' => _("View this page and all subpages"),
323 'edit' => _("Edit this page and all subpages"),
324 'create' => _("Create a new (sub)page"),
325 'dump' => _("Download page contents"),
326 'change' => _("Change page attributes"),
327 'remove' => _("Remove this page"),
328 'purge' => _("Purge this page"),
331 if (in_array($access, array_keys($accessDescriptions)))
332 return $accessDescriptions[$access];
338 function array_diff_assoc_recursive($array1, $array2) {
339 foreach ($array1 as $key => $value) {
340 if (is_array($value)) {
341 if (!is_array($array2[$key])) {
342 $difference[$key] = $value;
344 $new_diff = array_diff_assoc_recursive($value, $array2[$key]);
345 if ($new_diff != false) {
346 $difference[$key] = $new_diff;
349 } elseif(!isset($array2[$key]) || $array2[$key] != $value) {
350 $difference[$key] = $value;
353 return !isset($difference) ? 0 : $difference;
357 * The ACL object per page. It is stored in a page, but can also
358 * be merged with ACL's from other pages or taken from the master (pseudo) dot-file.
360 * A hash of "access" => "requires" pairs.
361 * "access" is a shortcut for common actions, which map to main.php actions
362 * "requires" required username or groupname or any special group => true or false
364 * Define any special rules here, like don't list dot-pages.
366 class PagePermission {
369 function PagePermission($hash = array()) {
370 $this->_group = &$GLOBALS['request']->getGroup();
371 if (is_array($hash) and !empty($hash)) {
372 $accessTypes = $this->accessTypes();
373 foreach ($hash as $access => $requires) {
374 if (in_array($access, $accessTypes))
375 $this->perm[$access] = $requires;
377 trigger_error(sprintf(_("Unsupported ACL access type %s ignored."), $access),
381 // set default permissions, the so called dot-file acl's
382 $this->perm = $this->defaultPerms();
388 * The workhorse to check the user against the current ACL pairs.
389 * Must translate the various special groups to the actual users settings
390 * (userid, group membership).
392 function isAuthorized($access, $user) {
394 if (!empty($this->perm{$access})) {
395 foreach ($this->perm[$access] as $group => $bool) {
396 if ($this->isMember($user, $group)) {
398 } elseif ($allow == -1) { // not a member and undecided: check other groups
403 return $allow; // undecided
407 * Translate the various special groups to the actual users settings
408 * (userid, group membership).
410 function isMember($user, $group) {
412 if ($group === ACL_EVERY) return true;
413 if (!isset($this->_group)) $member =& $request->getGroup();
414 else $member =& $this->_group;
415 //$user = & $request->_user;
416 if ($group === ACL_ADMIN) // WIKI_ADMIN or member of _("Administrators")
417 return $user->isAdmin() or
418 ($user->isAuthenticated() and
419 $member->isMember(GROUP_ADMIN));
420 if ($group === ACL_ANONYMOUS)
421 return ! $user->isSignedIn();
422 if ($group === ACL_BOGOUSER)
424 return isa($user,'_BogoUser') or
425 (isWikiWord($user->_userid) and $user->_level >= WIKIAUTH_BOGO);
426 else return isWikiWord($user->UserName());
427 if ($group === ACL_HASHOMEPAGE)
428 return $user->hasHomePage();
429 if ($group === ACL_SIGNED)
430 return $user->isSignedIn();
431 if ($group === ACL_AUTHENTICATED)
432 return $user->isAuthenticated();
433 if ($group === ACL_OWNER) {
434 if (!$user->isAuthenticated()) return false;
435 $page = $request->getPage();
436 $owner = $page->getOwner();
437 return ($owner === $user->UserName()
438 or $member->isMember($owner));
440 if ($group === ACL_CREATOR) {
441 if (!$user->isAuthenticated()) return false;
442 $page = $request->getPage();
443 $creator = $page->getCreator();
444 return ($creator === $user->UserName()
445 or $member->isMember($creator));
447 /* Or named groups or usernames.
448 Note: We don't seperate groups and users here.
449 Users overrides groups with the same name.
451 return $user->UserName() === $group or
452 $member->isMember($group);
456 * returns hash of default permissions.
457 * check if the page '.' exists and returns this instead.
459 function defaultPerms() {
460 //Todo: check for the existance of '.' and take this instead.
461 //Todo: honor more config.ini auth settings here
462 $perm = array('view' => array(ACL_EVERY => true),
463 'edit' => array(ACL_EVERY => true),
464 'create' => array(ACL_EVERY => true),
465 'list' => array(ACL_EVERY => true),
466 'remove' => array(ACL_ADMIN => true,
468 'purge' => array(ACL_ADMIN => true,
470 'dump' => array(ACL_ADMIN => true,
472 'change' => array(ACL_ADMIN => true,
475 $perm['dump'] = array(ACL_ADMIN => true,
477 elseif (INSECURE_ACTIONS_LOCALHOST_ONLY) {
479 $perm['dump'] = array(ACL_EVERY => true);
481 $perm['dump'] = array(ACL_ADMIN => true);
484 $perm['dump'] = array(ACL_EVERY => true);
485 if (defined('REQUIRE_SIGNIN_BEFORE_EDIT') && REQUIRE_SIGNIN_BEFORE_EDIT)
486 $perm['edit'] = array(ACL_SIGNED => true);
488 if (!ALLOW_ANON_USER) {
489 if (!ALLOW_USER_PASSWORDS)
490 $perm['view'] = array(ACL_SIGNED => true);
492 $perm['view'] = array(ACL_AUTHENTICATED => true);
493 $perm['view'][ACL_BOGOUSER] = ALLOW_BOGO_LOGIN ? true : false;
496 if (!ALLOW_ANON_EDIT) {
497 if (!ALLOW_USER_PASSWORDS)
498 $perm['edit'] = array(ACL_SIGNED => true);
500 $perm['edit'] = array(ACL_AUTHENTICATED => true);
501 $perm['edit'][ACL_BOGOUSER] = ALLOW_BOGO_LOGIN ? true : false;
502 $perm['create'] = $perm['edit'];
508 * FIXME: check valid groups and access
511 foreach ($this->perm as $access => $groups) {
512 foreach ($groups as $group => $bool) {
513 $this->perm[$access][$group] = (boolean) $bool;
519 * do a recursive comparison
521 function equal($otherperm) {
522 // The equal function seems to be unable to detect removed perm.
523 // Use case is when a rule is removed.
524 return (print_r($this->perm, true) === print_r($otherperm, true));
526 // $diff = array_diff_assoc_recursive($this->perm, $otherperm);
527 // return empty($diff);
531 * returns list of all supported access types.
533 function accessTypes() {
534 return array_keys(PagePermission::defaultPerms());
538 * special permissions for dot-files, beginning with '.'
539 * maybe also for '_' files?
541 function dotPerms() {
542 $def = array(ACL_ADMIN => true,
545 foreach (PagePermission::accessTypes() as $access) {
546 $perm[$access] = $def;
552 * dead code. not needed inside the object. see getPagePermissions($page)
554 function retrieve($page) {
555 $hash = $page->get('perm');
556 if ($hash) // hash => object
557 $perm = new PagePermission(unserialize($hash));
559 $perm = new PagePermission();
564 function store($page) {
567 return $page->set('perm',serialize($this->perm));
570 function groupName ($group) {
571 if ($group[0] == '_') return constant("GROUP".$group);
575 /* type: page, default, inherited */
576 function asTable($type) {
577 $table = HTML::table();
578 foreach ($this->perm as $access => $perms) {
579 $td = HTML::table(array('class' => 'cal'));
580 foreach ($perms as $group => $bool) {
581 $td->pushContent(HTML::tr(HTML::td(array('align'=>'right'),$group),
582 HTML::td($bool ? '[X]' : '[ ]')));
584 $table->pushContent(HTML::tr(array('valign' => 'top'),
585 HTML::td($access),HTML::td($td)));
587 if ($type == 'default')
588 $table->setAttr('style','border: dotted thin black; background-color:#eee;');
589 elseif ($type == 'inherited')
590 $table->setAttr('style','border: dotted thin black; background-color:#ddd;');
591 elseif ($type == 'page')
592 $table->setAttr('style','border: solid thin black; font-weight: bold;');
596 /* type: page, default, inherited */
597 function asEditableTable($type) {
599 if (!isset($this->_group)) {
600 $this->_group =& $GLOBALS['request']->getGroup();
602 $table = HTML::table();
603 $table->pushContent(HTML::tr(
604 HTML::th(array('align' => 'left'),
606 HTML::th(array('align'=>'right'),
608 HTML::th(_("Grant")),
609 HTML::th(_("Del/+")),
610 HTML::th(_("Description"))));
612 $allGroups = $this->_group->_specialGroups();
613 foreach ($this->_group->getAllGroupsIn() as $group) {
614 if (!in_array($group,$this->_group->specialGroups()))
615 $allGroups[] = $group;
617 //array_unique(array_merge($this->_group->getAllGroupsIn(),
618 $deletesrc = $WikiTheme->_findData('images/delete.png');
619 $addsrc = $WikiTheme->_findData('images/add.png');
620 $nbsp = HTML::raw(' ');
621 foreach ($this->perm as $access => $groups) {
622 //$permlist = HTML::table(array('class' => 'cal'));
624 $newperm = HTML::input(array('type' => 'checkbox',
625 'name' => "acl[_new_perm][$access]",
627 $addbutton = HTML::input(array('type' => 'checkbox',
628 'name' => "acl[_add_group][$access]",
631 'title' => _("Add this ACL"),
633 $newgroup = HTML::select(array('name' => "acl[_new_group][$access]",
634 'style'=> 'text-align: right;',
636 foreach ($allGroups as $groupname) {
637 if (!isset($groups[$groupname]))
638 $newgroup->pushContent(HTML::option(array('value' => $groupname),
639 $this->groupName($groupname)));
641 if (empty($groups)) {
642 $addbutton->setAttr('checked','checked');
643 $newperm->setAttr('checked','checked');
645 HTML::tr(array('valign' => 'top'),
646 HTML::td(HTML::strong($access.":")),
648 HTML::td($nbsp,$newperm),
649 HTML::td($nbsp,$addbutton),
650 HTML::td(HTML::em(getAccessDescription($access)))));
652 foreach ($groups as $group => $bool) {
653 $checkbox = HTML::input(array('type' => 'checkbox',
654 'name' => "acl[$access][$group]",
655 'title' => _("Allow / Deny"),
657 if ($bool) $checkbox->setAttr('checked','checked');
658 $checkbox = HTML(HTML::input(array('type' => 'hidden',
659 'name' => "acl[$access][$group]",
662 $deletebutton = HTML::input(array('type' => 'checkbox',
663 'name' => "acl[_del_group][$access][$group]",
664 'style' => 'background: #aaa url('.$deletesrc.')',
665 //'src' => $deletesrc,
667 'title' => _("Delete this ACL"),
672 HTML::td(HTML::strong($access.":")),
673 HTML::td(array('class' => 'cal-today','align'=>'right'),
674 HTML::strong($this->groupName($group))),
675 HTML::td(array('align'=>'center'),$nbsp,$checkbox),
676 HTML::td(array('align'=>'right','style' => 'background: #aaa url('.$deletesrc.') no-repeat'),$deletebutton),
677 HTML::td(HTML::em(getAccessDescription($access)))));
683 HTML::td(array('class' => 'cal-today','align'=>'right'),
684 HTML::strong($this->groupName($group))),
685 HTML::td(array('align'=>'center'),$nbsp,$checkbox),
686 HTML::td(array('align'=>'right','style' => 'background: #aaa url('.$deletesrc.') no-repeat'),$deletebutton),
692 HTML::tr(array('valign' => 'top'),
693 HTML::td(array('align'=>'right'),_("add ")),
695 HTML::td(array('align'=>'center'),$nbsp,$newperm),
696 HTML::td(array('align'=>'right','style' => 'background: #ccc url('.$addsrc.') no-repeat'),$addbutton),
697 HTML::td(HTML::small(_("Check to add this ACL")))));
699 if ($type == 'default')
700 $table->setAttr('style','border: dotted thin black; background-color:#eee;');
701 elseif ($type == 'inherited')
702 $table->setAttr('style','border: dotted thin black; background-color:#ddd;');
703 elseif ($type == 'page')
704 $table->setAttr('style','border: solid thin black; font-weight: bold;');
708 // Print ACL as lines of [+-]user[,group,...]:access[,access...]
709 // Seperate acl's by "; " or whitespace
710 // See http://opag.ca/wiki/HelpOnAccessControlLists
711 // As used by WikiAdminSetAclSimple
712 function asAclLines() {
715 foreach ($this->perm as $access => $groups) {
716 // unify groups for same access+bool
717 // view:CREATOR,-OWNER,
719 foreach ($groups as $group => $bool) {
720 $line .= ($bool?'':'-').$group.",";
722 if (substr($line,-1) == ',')
723 $s .= substr($line,0,-1)."; ";
725 if (substr($s,-2) == '; ')
726 $s = substr($s,0,-2);
730 // Print ACL as group followed by actions allowed for the group
731 function asAclGroupLines() {
734 $perm =& $this->perm;
735 $actions = array("view", "edit", "create", "list", "remove", "purge", "dump", "change");
736 $groups = array(ACL_EVERY, ACL_ANONYMOUS, ACL_BOGOUSER, ACL_HASHOMEPAGE, ACL_SIGNED, ACL_AUTHENTICATED, ACL_ADMIN, ACL_OWNER, ACL_CREATOR);
738 foreach ($groups as $group) {
740 foreach ($actions as $action) {
741 if (isset($perm[$action][$group])) {
746 $s .= (($perm[$action][$group] ? " " : " -") . $action);
756 // This is just a bad hack for testing.
757 // Simplify the ACL to a unix-like "rwx------+" string
759 function asRwxString($owner,$group=false) {
761 // simplify object => rwxrw---x+ string as in cygwin (+ denotes additional ACLs)
762 $perm =& $this->perm;
763 // get effective user and group
765 if (isset($perm['view'][$owner]) or
766 (isset($perm['view'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
768 if (isset($perm['edit'][$owner]) or
769 (isset($perm['edit'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
771 if (isset($perm['change'][$owner]) or
772 (isset($perm['change'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
774 if (!empty($group)) {
775 if (isset($perm['view'][$group]) or
776 (isset($perm['view'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
778 if (isset($perm['edit'][$group]) or
779 (isset($perm['edit'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
781 if (isset($perm['change'][$group]) or
782 (isset($perm['change'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
785 if (isset($perm['view'][ACL_EVERY]) or
786 (isset($perm['view'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
788 if (isset($perm['edit'][ACL_EVERY]) or
789 (isset($perm['edit'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
791 if (isset($perm['change'][ACL_EVERY]) or
792 (isset($perm['change'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
802 // c-hanging-comment-ender-p: nil
803 // indent-tabs-mode: nil