3 * Copyright © 2004,2007 $ThePhpWikiProgrammingTeam
4 * Copyright © 2009-2010 Marc-Etienne Vargenau, Alcatel-Lucent
6 * This file is part of PhpWiki.
8 * PhpWiki is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
13 * PhpWiki is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License along
19 * with PhpWiki; if not, write to the Free Software Foundation, Inc.,
20 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 * SPDX-License-Identifier: GPL-2.0-or-later
27 * Permissions per page and action based on current user,
28 * ownership and group membership implemented with ACL's (Access Control Lists),
29 * opposed to the simplier unix-like ugo:rwx system.
30 * The previous system was only based on action and current user. (lib/main.php)
32 * Permissions may be inherited from its parent pages, a optional the
33 * optional master page ("."), and predefined default permissions, if "."
35 * Pagenames starting with "." have special default permissions.
36 * For Authentication see WikiUser.php, WikiGroup.php and main.php
37 * Page Permissions are in PhpWiki since v1.3.9 and enabled since v1.4.0
39 * This file might replace the following functions from main.php:
40 * Request::_notAuthorized($require_level)
41 * display the denied message and optionally a login form
42 * to gain higher privileges
43 * Request::getActionDescription($action)
44 * helper to localize the _notAuthorized message per action,
45 * when login is tried.
46 * Request::getDisallowedActionDescription($action)
47 * helper to localize the _notAuthorized message per action,
49 * Request::requiredAuthority($action)
50 * returns the needed user level
51 * has a hook for plugins on POST
52 * Request::requiredAuthorityForAction($action)
53 * just returns the level per action, will be replaced with the
56 * The defined main.php actions map to simplier access types:
59 * create => edit or create
62 * store prefs => change
63 * list in PageList => list
66 /* Symbolic special ACL groups. Untranslated to be stored in page metadata*/
67 define('ACL_EVERY', '_EVERY');
68 define('ACL_ANONYMOUS', '_ANONYMOUS');
69 define('ACL_BOGOUSER', '_BOGOUSER');
70 define('ACL_HASHOMEPAGE', '_HASHOMEPAGE');
71 define('ACL_SIGNED', '_SIGNED');
72 define('ACL_AUTHENTICATED', '_AUTHENTICATED');
73 define('ACL_ADMIN', '_ADMIN');
74 define('ACL_OWNER', '_OWNER');
75 define('ACL_CREATOR', '_CREATOR');
77 // Return an page permissions array for this page.
78 // To provide ui helpers to view and change page permissions:
79 // <tr><th>Group</th><th>Access</th><th>Allow or Forbid</th></tr>
80 // <tr><td>$group</td><td>_($access)</td><td> [ ] </td></tr>
81 function pagePermissions($pagename)
84 $page = $request->getPage($pagename);
85 // Page not found (new page); returned inherited permissions, to be displayed in gray
86 if (!$page->exists()) {
87 if ($pagename == '.') // stop recursion
88 return array('default', new PagePermission());
90 return array('inherited', pagePermissions(getParentPage($pagename)));
92 } elseif ($perm = getPagePermissions($page)) {
93 return array('page', $perm);
94 // or no permissions defined; returned inherited permissions, to be displayed in gray
95 } elseif ($pagename == '.') { // stop recursion in pathological case.
96 // "." defined, without any acl
97 return array('default', new PagePermission());
99 return array('inherited', pagePermissions(getParentPage($pagename)));
103 function pagePermissionsSimpleFormat($perm_tree, $owner, $group = false)
105 list($type, $perm) = pagePermissionsAcl($perm_tree[0], $perm_tree);
107 $type = $perm_tree[0];
108 $perm = pagePermissionsAcl($perm_tree);
109 if (is_object($perm_tree[1]))
110 $perm = $perm_tree[1];
111 elseif (is_array($perm_tree[1])) {
112 $perm_tree = pagePermissionsSimpleFormat($perm_tree[1],$owner,$group);
113 if (is_a($perm_tree[1],'pagepermission'))
114 $perm = $perm_tree[1];
115 elseif (is_a($perm_tree,'htmlelement'))
120 return HTML::samp(HTML::strong($perm->asRwxString($owner, $group)));
121 elseif ($type == 'default')
122 return HTML::samp($perm->asRwxString($owner, $group));
123 elseif ($type == 'inherited') {
124 return HTML::samp(array('class' => 'inherited', 'style' => 'color:#aaa;'),
125 $perm->asRwxString($owner, $group));
130 function pagePermissionsAcl($type, $perm_tree)
132 $perm = $perm_tree[1];
133 while (!is_object($perm)) {
134 $perm_tree = pagePermissionsAcl($type, $perm);
135 $perm = $perm_tree[1];
137 return array($type, $perm);
142 function pagePermissionsAclFormat($perm_tree, $editable = false)
144 list($type, $perm) = pagePermissionsAcl($perm_tree[0], $perm_tree);
146 return $perm->asEditableTable($type);
148 return $perm->asTable($type);
152 * Check the permissions for the current action.
153 * Walk down the inheritance tree. Collect all permissions until
154 * the minimum required level is gained, which is not
155 * overruled by more specific forbid rules.
156 * Todo: cache result per access and page in session?
158 function requiredAuthorityForPage($action)
161 $auth = _requiredAuthorityForPagename(action2access($action),
162 $request->getArg('pagename'));
163 assert($auth !== -1);
165 return $request->_user->_level;
167 return WIKIAUTH_UNOBTAINABLE;
170 // Translate action or plugin to the simplier access types:
171 function action2access($action)
184 // performance and security relevant
193 // invent a new access-perm massedit? or switch back to change, or keep it at edit?
194 case _("PhpWikiAdministration") . "/" . _("Rename"):
195 case _("PhpWikiAdministration") . "/" . _("SearchReplace"):
202 $page = $request->getPage();
203 if (!$page->exists())
210 // probably create/edit but we cannot check all page permissions, can we?
221 //Todo: Plugins should be able to override its access type
222 if (isWikiWord($action))
230 // Recursive helper to do the real work.
231 // Using a simple perm cache for page-access pairs.
232 // Maybe page-(current+edit+change?)action pairs will help
233 function _requiredAuthorityForPagename($access, $pagename)
235 static $permcache = array();
237 if (array_key_exists($pagename, $permcache)
238 and array_key_exists($access, $permcache[$pagename])
240 return $permcache[$pagename][$access];
243 $page = $request->getPage($pagename);
246 if (defined('FUSIONFORGE') && FUSIONFORGE) {
247 if ($pagename != '.' && isset($request->_user->_is_external) && $request->_user->_is_external && !$page->get('external')) {
248 $permcache[$pagename][$access] = 0;
252 if ((READONLY or $request->_dbi->readonly)
253 and in_array($access, array('edit', 'create', 'change'))
258 // Page not found; check against default permissions
259 if (!$page->exists()) {
260 $perm = new PagePermission();
261 $result = ($perm->isAuthorized($access, $request->_user) === true);
262 $permcache[$pagename][$access] = $result;
265 // no ACL defined; check for special dotfile or walk down
266 if (!($perm = getPagePermissions($page))) {
267 if ($pagename == '.') {
268 $perm = new PagePermission();
269 if ($perm->isAuthorized('change', $request->_user)) {
270 // warn the user to set ACL of ".", if he has permissions to do so.
271 trigger_error(". (dotpage == rootpage for inheriting pageperm ACLs) exists without any ACL!\n" .
272 "Please do ?action=setacl&pagename=.", E_USER_WARNING);
274 $result = ($perm->isAuthorized($access, $request->_user) === true);
275 $permcache[$pagename][$access] = $result;
277 } elseif ($pagename[0] == '.') {
278 $perm = new PagePermission(PagePermission::dotPerms());
279 $result = ($perm->isAuthorized($access, $request->_user) === true);
280 $permcache[$pagename][$access] = $result;
283 return _requiredAuthorityForPagename($access, getParentPage($pagename));
285 // ACL defined; check if isAuthorized returns true or false or undecided
286 $authorized = $perm->isAuthorized($access, $request->_user);
287 if ($authorized !== -1) { // interestingly true is also -1
288 $permcache[$pagename][$access] = $authorized;
290 } elseif ($pagename == '.') {
293 return _requiredAuthorityForPagename($access, getParentPage($pagename));
298 * @param string $pagename page from which the parent page is searched.
299 * @return string parent pagename or the (possibly pseudo) dot-pagename.
301 function getParentPage($pagename)
303 if (isSubPage($pagename)) {
304 return subPageSlice($pagename, 0);
310 // Read the ACL from the page
311 // Done: Not existing pages should NOT be queried.
312 // Check the parent page instead and don't take the default ACL's
313 function getPagePermissions($page)
315 if ($hash = $page->get('perm')) // hash => object
316 return new PagePermission(unserialize($hash));
321 // Store the ACL in the page
322 function setPagePermissions($page, $perm)
327 function getAccessDescription($access)
329 static $accessDescriptions;
330 if (!$accessDescriptions) {
331 $accessDescriptions = array(
332 'list' => _("List this page and all subpages"),
333 'view' => _("View this page and all subpages"),
334 'edit' => _("Edit this page and all subpages"),
335 'create' => _("Create a new (sub)page"),
336 'dump' => _("Download page contents"),
337 'change' => _("Change page attributes"),
338 'remove' => _("Remove this page"),
339 'purge' => _("Purge this page"),
342 if (in_array($access, array_keys($accessDescriptions)))
343 return $accessDescriptions[$access];
349 * The ACL object per page. It is stored in a page, but can also
350 * be merged with ACL's from other pages or taken from the master (pseudo) dot-file.
352 * A hash of "access" => "requires" pairs.
353 * "access" is a shortcut for common actions, which map to main.php actions
354 * "requires" required username or groupname or any special group => true or false
356 * Define any special rules here, like don't list dot-pages.
362 function __construct($hash = array())
365 * @var WikiRequest $request
369 $this->_group = &$request->getGroup();
370 if (is_array($hash) and !empty($hash)) {
371 $accessTypes = $this->accessTypes();
372 foreach ($hash as $access => $requires) {
373 if (in_array($access, $accessTypes))
374 $this->perm[$access] = $requires;
376 trigger_error(sprintf(_("Unsupported ACL access type %s ignored."), $access),
380 // set default permissions, the so called dot-file acl's
381 $this->perm = $this->defaultPerms();
387 * The workhorse to check the user against the current ACL pairs.
388 * Must translate the various special groups to the actual users settings
389 * (userid, group membership).
391 function isAuthorized($access, $user)
393 // Admin can see all pages, regardless of access rights
394 if ($user->isAdmin()) {
398 if (!empty($this->perm[$access])) {
399 foreach ($this->perm[$access] as $group => $bool) {
400 if ($this->isMember($user, $group)) {
402 } elseif ($allow == -1) { // not a member and undecided: check other groups
407 return $allow; // undecided
411 * Translate the various special groups to the actual users settings
412 * (userid, group membership).
414 function isMember($user, $group)
417 if ($group === ACL_EVERY) return true;
418 if (!isset($this->_group)) $member =& $request->getGroup();
419 else $member =& $this->_group;
420 //$user = & $request->_user;
421 if ($group === ACL_ADMIN) // WIKI_ADMIN or member of _("Administrators")
422 return $user->isAdmin() or
423 ($user->isAuthenticated() and
424 $member->isMember(GROUP_ADMIN));
425 if ($group === ACL_ANONYMOUS)
426 return !$user->isSignedIn();
427 if ($group === ACL_BOGOUSER)
428 return is_a($user, '_BogoUser') or
429 (isWikiWord($user->_userid) and $user->_level >= WIKIAUTH_BOGO);
430 if ($group === ACL_HASHOMEPAGE)
431 return $user->hasHomePage();
432 if ($group === ACL_SIGNED)
433 return $user->isSignedIn();
434 if ($group === ACL_AUTHENTICATED)
435 return $user->isAuthenticated();
436 if ($group === ACL_OWNER) {
437 if (!$user->isAuthenticated()) return false;
438 $page = $request->getPage();
439 $owner = $page->getOwner();
440 return ($owner === $user->UserName()
441 or $member->isMember($owner));
443 if ($group === ACL_CREATOR) {
444 if (!$user->isAuthenticated()) return false;
445 $page = $request->getPage();
446 $creator = $page->getCreator();
447 return ($creator === $user->UserName()
448 or $member->isMember($creator));
450 /* Or named groups or usernames.
451 Note: We don't separate groups and users here.
452 Users overrides groups with the same name.
454 return $user->UserName() === $group or
455 $member->isMember($group);
459 * returns hash of default permissions.
460 * check if the page '.' exists and returns this instead.
462 function defaultPerms()
464 //Todo: check for the existance of '.' and take this instead.
465 //Todo: honor more config.ini auth settings here
466 $perm = array('view' => array(ACL_EVERY => true),
467 'edit' => array(ACL_EVERY => true),
468 'create' => array(ACL_EVERY => true),
469 'list' => array(ACL_EVERY => true),
470 'remove' => array(ACL_ADMIN => true,
472 'purge' => array(ACL_ADMIN => true,
474 'dump' => array(ACL_ADMIN => true,
476 'change' => array(ACL_ADMIN => true,
478 if (defined('ZIPDUMP_AUTH') and ZIPDUMP_AUTH)
479 $perm['dump'] = array(ACL_ADMIN => true,
481 elseif (defined('INSECURE_ACTIONS_LOCALHOST_ONLY') and INSECURE_ACTIONS_LOCALHOST_ONLY) {
483 $perm['dump'] = array(ACL_EVERY => true);
485 $perm['dump'] = array(ACL_ADMIN => true);
487 $perm['dump'] = array(ACL_EVERY => true);
488 if (defined('REQUIRE_SIGNIN_BEFORE_EDIT') && REQUIRE_SIGNIN_BEFORE_EDIT)
489 $perm['edit'] = array(ACL_SIGNED => true);
491 if (!ALLOW_ANON_USER) {
492 if (!ALLOW_USER_PASSWORDS)
493 $perm['view'] = array(ACL_SIGNED => true);
495 $perm['view'] = array(ACL_AUTHENTICATED => true);
496 $perm['view'][ACL_BOGOUSER] = ALLOW_BOGO_LOGIN ? true : false;
499 if (!ALLOW_ANON_EDIT) {
500 if (!ALLOW_USER_PASSWORDS)
501 $perm['edit'] = array(ACL_SIGNED => true);
503 $perm['edit'] = array(ACL_AUTHENTICATED => true);
504 $perm['edit'][ACL_BOGOUSER] = ALLOW_BOGO_LOGIN ? true : false;
505 $perm['create'] = $perm['edit'];
511 * FIXME: check valid groups and access
515 foreach ($this->perm as $access => $groups) {
516 foreach ($groups as $group => $bool) {
517 $this->perm[$access][$group] = (boolean)$bool;
523 * do a recursive comparison
525 function equal($otherperm)
527 // The equal function seems to be unable to detect removed perm.
528 // Use case is when a rule is removed.
529 return (print_r($this->perm, true) === print_r($otherperm, true));
533 * returns list of all supported access types.
535 function accessTypes()
537 return array_keys(PagePermission::defaultPerms());
541 * special permissions for dot-files, beginning with '.'
542 * maybe also for '_' files?
544 static function dotPerms()
546 $def = array(ACL_ADMIN => true,
549 foreach (PagePermission::accessTypes() as $access) {
550 $perm[$access] = $def;
556 * dead code. not needed inside the object. see getPagePermissions($page)
558 function retrieve($page)
560 $hash = $page->get('perm');
561 if ($hash) // hash => object
562 $perm = new PagePermission(unserialize($hash));
564 $perm = new PagePermission();
569 function store($page)
573 return $page->set('perm', serialize($this->perm));
576 function groupName($group)
578 if ($group[0] == '_') return constant("GROUP" . $group);
582 /* type: page, default, inherited */
583 function asTable($type)
585 $table = HTML::table();
586 foreach ($this->perm as $access => $perms) {
587 $td = HTML::table(array('class' => 'cal'));
588 foreach ($perms as $group => $bool) {
589 $td->pushContent(HTML::tr(HTML::td(array('class' => 'align-right'), $group),
590 HTML::td($bool ? '[X]' : '[ ]')));
592 $table->pushContent(HTML::tr(array('class' => 'top'),
593 HTML::td($access), HTML::td($td)));
595 if ($type == 'default')
596 $table->setAttr('style', 'border: dotted thin black; background-color:#eee;');
597 elseif ($type == 'inherited')
598 $table->setAttr('style', 'border: dotted thin black; background-color:#ddd;'); elseif ($type == 'page')
599 $table->setAttr('style', 'border: solid thin black; font-weight: bold;');
603 /* type: page, default, inherited */
604 function asEditableTable($type)
608 * @var WikiRequest $request
612 if (!isset($this->_group)) {
613 $this->_group =& $request->getGroup();
615 $table = HTML::table();
616 $table->pushContent(HTML::tr(
617 HTML::th(array('class' => 'align-left'),
619 HTML::th(array('class' => 'align-right'),
621 HTML::th(_("Grant")),
622 HTML::th(_("Del/+")),
623 HTML::th(_("Description"))));
625 $allGroups = $this->_group->_specialGroups();
626 foreach ($this->_group->getAllGroupsIn() as $group) {
627 if (!in_array($group, $this->_group->specialGroups()))
628 $allGroups[] = $group;
630 //array_unique(array_merge($this->_group->getAllGroupsIn(),
631 $deletesrc = $WikiTheme->_findData('images/delete.png');
632 $addsrc = $WikiTheme->_findData('images/add.png');
633 $nbsp = HTML::raw(' ');
634 foreach ($this->perm as $access => $groups) {
635 //$permlist = HTML::table(array('class' => 'cal'));
637 $newperm = HTML::input(array('type' => 'checkbox',
638 'name' => "acl[_new_perm][$access]",
640 $addbutton = HTML::input(array('type' => 'checkbox',
641 'name' => "acl[_add_group][$access]",
644 'title' => _("Add this ACL"),
646 $newgroup = HTML::select(array('name' => "acl[_new_group][$access]",
647 'style' => 'text-align: right;',
649 foreach ($allGroups as $groupname) {
650 if (!isset($groups[$groupname]))
651 $newgroup->pushContent(HTML::option(array('value' => $groupname),
652 $this->groupName($groupname)));
654 if (empty($groups)) {
655 $addbutton->setAttr('checked', 'checked');
656 $newperm->setAttr('checked', 'checked');
658 HTML::tr(array('class' => 'top'),
659 HTML::td(HTML::strong($access . ":")),
661 HTML::td($nbsp, $newperm),
662 HTML::td($nbsp, $addbutton),
663 HTML::td(HTML::em(getAccessDescription($access)))));
665 foreach ($groups as $group => $bool) {
666 $checkbox = HTML::input(array('type' => 'checkbox',
667 'name' => "acl[$access][$group]",
668 'title' => _("Allow / Deny"),
670 if ($bool) $checkbox->setAttr('checked', 'checked');
671 $checkbox = HTML(HTML::input(array('type' => 'hidden',
672 'name' => "acl[$access][$group]",
675 $deletebutton = HTML::input(array('type' => 'checkbox',
676 'name' => "acl[_del_group][$access][$group]",
677 'style' => 'background: #aaa url(' . $deletesrc . ')',
678 //'src' => $deletesrc,
680 'title' => _("Delete this ACL"),
685 HTML::td(HTML::strong($access . ":")),
686 HTML::td(array('class' => 'cal-today align-right'),
687 HTML::strong($this->groupName($group))),
688 HTML::td(array('class' => 'align-center'), $nbsp, $checkbox),
689 HTML::td(array('class' => 'align-right', 'style' => 'background: #aaa url(' . $deletesrc . ') no-repeat'), $deletebutton),
690 HTML::td(HTML::em(getAccessDescription($access)))));
696 HTML::td(array('class' => 'cal-today align-right'),
697 HTML::strong($this->groupName($group))),
698 HTML::td(array('class' => 'align-center'), $nbsp, $checkbox),
699 HTML::td(array('class' => 'align-right', 'style' => 'background: #aaa url(' . $deletesrc . ') no-repeat'), $deletebutton),
705 HTML::tr(array('class' => 'top'),
706 HTML::td(array('class' => 'align-right'), _("add ")),
708 HTML::td(array('class' => 'align-center'), $nbsp, $newperm),
709 HTML::td(array('class' => 'align-right', 'style' => 'background: #ccc url(' . $addsrc . ') no-repeat'), $addbutton),
710 HTML::td(HTML::small(_("Check to add this ACL")))));
712 if ($type == 'default')
713 $table->setAttr('style', 'border: dotted thin black; background-color:#eee;');
714 elseif ($type == 'inherited')
715 $table->setAttr('style', 'border: dotted thin black; background-color:#ddd;'); elseif ($type == 'page')
716 $table->setAttr('style', 'border: solid thin black; font-weight: bold;');
720 // Print ACL as lines of [+-]user[,group,...]:access[,access...]
721 // Seperate acl's by "; " or whitespace
722 // See http://opag.ca/wiki/HelpOnAccessControlLists
723 // As used by WikiAdminSetAclSimple
724 function asAclLines()
728 foreach ($this->perm as $access => $groups) {
729 // unify groups for same access+bool
730 // view:CREATOR,-OWNER,
731 $line = $access . ':';
732 foreach ($groups as $group => $bool) {
733 $line .= ($bool ? '' : '-') . $group . ",";
735 if (substr($line, -1) == ',')
736 $s .= substr($line, 0, -1) . "; ";
738 if (substr($s, -2) == '; ')
739 $s = substr($s, 0, -2);
743 // This is just a bad hack for testing.
744 // Simplify the ACL to a unix-like "rwx------+" string
746 function asRwxString($owner, $group = false)
749 // simplify object => rwxrw---x+ string as in cygwin (+ denotes additional ACLs)
750 $perm =& $this->perm;
751 // get effective user and group
753 if (isset($perm['view'][$owner]) or
754 (isset($perm['view'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated())
757 if (isset($perm['edit'][$owner]) or
758 (isset($perm['edit'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated())
761 if (isset($perm['change'][$owner]) or
762 (isset($perm['change'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated())
765 if (!empty($group)) {
766 if (isset($perm['view'][$group]) or
767 (isset($perm['view'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated())
770 if (isset($perm['edit'][$group]) or
771 (isset($perm['edit'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated())
774 if (isset($perm['change'][$group]) or
775 (isset($perm['change'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated())
779 if (isset($perm['view'][ACL_EVERY]) or
780 (isset($perm['view'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated())
783 if (isset($perm['edit'][ACL_EVERY]) or
784 (isset($perm['edit'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated())
787 if (isset($perm['change'][ACL_EVERY]) or
788 (isset($perm['change'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated())