2 rcs_id('$Id: PagePerm.php,v 1.16 2004-05-16 22:32:53 rurban Exp $');
4 Copyright 2004 $ThePhpWikiProgrammingTeam
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
19 along with PhpWiki; if not, write to the Free Software
20 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
24 Permissions per page and action based on current user,
25 ownership and group membership implemented with ACL's (Access Control Lists),
26 opposed to the simplier unix like ugo:rwx system.
27 The previous system was only based on action and current user. (lib/main.php)
29 Permissions maybe inherited its parent pages, and ultimativly the
30 optional master page (".")
31 Pagenames starting with "." have special default permissions.
32 For Authentication see WikiUserNew.php, WikiGroup.php and main.php
33 Page Permssions are in PhpWiki since v1.3.9 and enabled since v1.4.0
35 This file might replace the following functions from main.php:
36 Request::_notAuthorized($require_level)
37 display the denied message and optionally a login form
38 to gain higher privileges
39 Request::getActionDescription($action)
40 helper to localize the _notAuthorized message per action,
42 Request::getDisallowedActionDescription($action)
43 helper to localize the _notAuthorized message per action,
45 Request::requiredAuthority($action)
46 returns the needed user level
47 has a hook for plugins on POST
48 Request::requiredAuthorityForAction($action)
49 just returns the level per action, will be replaced with the
52 The defined main.php actions map to simplier access types:
55 create => edit or create
59 list in PageList => list
62 /* Symbolic special ACL groups. Untranslated to be stored in page metadata*/
63 define('ACL_EVERY', '_EVERY');
64 define('ACL_ANONYMOUS', '_ANONYMOUS');
65 define('ACL_BOGOUSER', '_BOGOUSER');
66 define('ACL_HASHOMEPAGE', '_HASHOMEPAGE');
67 define('ACL_SIGNED', '_SIGNED');
68 define('ACL_AUTHENTICATED','_AUTHENTICATED');
69 define('ACL_ADMIN', '_ADMIN');
70 define('ACL_OWNER', '_OWNER');
71 define('ACL_CREATOR', '_CREATOR');
73 // Return an page permissions array for this page.
74 // To provide ui helpers to view and change page permissions:
75 // <tr><th>Group</th><th>Access</th><th>Allow or Forbid</th></tr>
76 // <tr><td>$group</td><td>_($access)</td><td> [ ] </td></tr>
77 function pagePermissions($pagename) {
79 $page = $request->getPage($pagename);
80 // Page not found (new page); returned inherited permissions, to be displayed in gray
81 if (! $page->exists() ) {
82 if ($pagename == '.') // stop recursion
83 return array('default',new PagePermission());
85 return array('inherited',pagePermissions(getParentPage($pagename)));
87 } elseif ($perm = getPagePermissions($page)) {
88 return array('page',$perm);
89 // or no permissions defined; returned inherited permissions, to be displayed in gray
91 return array('inherited',pagePermissions(getParentPage($pagename)));
95 function pagePermissionsSimpleFormat($perm_tree,$owner,$group=false) {
96 list($type,$perm) = pagePermissionsAcl($perm_tree[0], $perm_tree);
98 $type = $perm_tree[0];
99 $perm = pagePermissionsAcl($perm_tree);
100 if (is_object($perm_tree[1]))
101 $perm = $perm_tree[1];
102 elseif (is_array($perm_tree[1])) {
103 $perm_tree = pagePermissionsSimpleFormat($perm_tree[1],$owner,$group);
104 if (isa($perm_tree[1],'pagepermission'))
105 $perm = $perm_tree[1];
106 elseif (isa($perm_tree,'htmlelement'))
111 return HTML::tt(HTML::bold($perm->asRwxString($owner,$group).'+'));
112 elseif ($type == 'default')
113 return HTML::tt($perm->asRwxString($owner,$group));
114 elseif ($type == 'inherited') {
115 return HTML::tt(array('class'=>'inherited','style'=>'color:#aaa;'),
116 $perm->asRwxString($owner,$group));
120 function pagePermissionsAcl($type,$perm_tree) {
121 $perm = $perm_tree[1];
122 while (!is_object($perm)) {
123 $perm_tree = pagePermissionsAcl($type, $perm);
124 $perm = $perm_tree[1];
126 return array($type,$perm);
131 function pagePermissionsAclFormat($perm_tree, $editable=false) {
132 list($type,$perm) = pagePermissionsAcl($perm_tree[0], $perm_tree);
134 return $perm->asEditableTable($type);
136 return $perm->asTable($type);
140 * Check permission per page.
141 * Returns true or false.
143 function mayAccessPage ($access, $pagename) {
144 return _requiredAuthorityForPagename($access, $pagename);
147 /** Check the permissions for the current action.
148 * Walk down the inheritance tree. Collect all permissions until
149 * the minimum required level is gained, which is not
150 * overruled by more specific forbid rules.
151 * Todo: cache result per access and page in session?
153 function requiredAuthorityForPage ($action) {
154 $auth = _requiredAuthorityForPagename(action2access($action),
155 $GLOBALS['request']->getArg('pagename'));
156 assert($auth !== -1);
158 return $GLOBALS['request']->_user->_level;
160 return WIKIAUTH_UNOBTAINABLE;
163 // Translate action or plugin to the simplier access types:
164 function action2access ($action) {
183 $page = $request->getPage();
184 $current = $page->getCurrentRevision();
185 if ($current->hasDefaultContents())
192 // probably create/edit but we cannot check all page permissions, can we?
199 //Todo: Plugins should be able to override its access type
200 if (isWikiWord($action))
208 // Recursive helper to do the real work
209 function _requiredAuthorityForPagename($access, $pagename) {
211 $page = $request->getPage($pagename);
212 // Page not found; check against default permissions
213 if (! $page->exists() ) {
214 $perm = new PagePermission();
215 return ($perm->isAuthorized($access,$request->_user) === true);
217 // no ACL defined; check for special dotfile or walk down
218 if (! ($perm = getPagePermissions($page))) {
219 if ($pagename[0] == '.') {
220 $perm = new PagePermission(PagePermission::dotPerms());
221 return ($perm->isAuthorized($access,$request->_user) === true);
223 return _requiredAuthorityForPagename($access,getParentPage($pagename));
225 // ACL defined; check if isAuthorized returns true or false or undecided
226 $authorized = $perm->isAuthorized($access,$request->_user);
227 if ($authorized != -1) // -1 for undecided
230 return _requiredAuthorityForPagename($access,getParentPage($pagename));
234 * @param string $pagename page from which the parent page is searched.
235 * @return string parent pagename or the (possibly pseudo) dot-pagename.
237 function getParentPage($pagename) {
238 if (isSubPage($pagename)) {
239 return subPageSlice($pagename,0);
245 // Read the ACL from the page
246 // Done: Not existing pages should NOT be queried.
247 // Check the parent page instead and don't take the default ACL's
248 function getPagePermissions ($page) {
249 if ($hash = $page->get('perm')) // hash => object
250 return new PagePermission(unserialize($hash));
255 // Store the ACL in the page
256 function setPagePermissions ($page,$perm) {
260 function getAccessDescription($access) {
261 static $accessDescriptions;
262 if (! $accessDescriptions) {
263 $accessDescriptions = array(
264 'list' => _("List this page and all subpages"),
265 'view' => _("View this page and all subpages"),
266 'edit' => _("Edit this page and all subpages"),
267 'create' => _("Create a new (sub)page"),
268 'dump' => _("Download the page contents"),
269 'change' => _("Change page attributes"),
270 'remove' => _("Remove this page"),
273 if (in_array($access, array_keys($accessDescriptions)))
274 return $accessDescriptions[$access];
280 function array_diff_assoc_recursive($array1, $array2) {
281 foreach ($array1 as $key => $value) {
282 if (is_array($value)) {
283 if (!is_array($array2[$key])) {
284 $difference[$key] = $value;
286 $new_diff = array_diff_assoc_recursive($value, $array2[$key]);
287 if ($new_diff != false) {
288 $difference[$key] = $new_diff;
291 } elseif(!isset($array2[$key]) || $array2[$key] != $value) {
292 $difference[$key] = $value;
295 return !isset($difference) ? 0 : $difference;
299 * The ACL object per page. It is stored in a page, but can also
300 * be merged with ACL's from other pages or taken from the master (pseudo) dot-file.
302 * A hash of "access" => "requires" pairs.
303 * "access" is a shortcut for common actions, which map to main.php actions
304 * "requires" required username or groupname or any special group => true or false
306 * Define any special rules here, like don't list dot-pages.
308 class PagePermission {
311 function PagePermission($hash = array()) {
312 $this->_group = &WikiGroup::getGroup($GLOBALS['request']);
313 if (is_array($hash) and !empty($hash)) {
314 $accessTypes = $this->accessTypes();
315 foreach ($hash as $access => $requires) {
316 if (in_array($access,$accessTypes))
317 $this->perm[$access] = $requires;
319 trigger_error(sprintf(_("Unsupported ACL access type %s ignored."),$access),
323 // set default permissions, the so called dot-file acl's
324 $this->perm = $this->defaultPerms();
330 * The workhorse to check the user against the current ACL pairs.
331 * Must translate the various special groups to the actual users settings
332 * (userid, group membership).
334 function isAuthorized($access,$user) {
335 if (!empty($this->perm{$access})) {
336 foreach ($this->perm[$access] as $group => $bool) {
337 if ($this->isMember($user,$group)) {
342 return -1; // undecided
346 * Translate the various special groups to the actual users settings
347 * (userid, group membership).
349 function isMember($user, $group) {
351 if ($group === ACL_EVERY) return true;
352 if (!isset($this->_group)) $member =& WikiGroup::getGroup($request);
353 else $member =& $this->_group;
354 //$user = & $request->_user;
355 if ($group === ACL_ADMIN) // WIKI_ADMIN or member of _("Administrators")
356 return $user->isAdmin() or
357 ($user->isAuthenticated() and
358 $member->isMember(GROUP_ADMIN));
359 if ($group === ACL_ANONYMOUS)
360 return ! $user->isSignedIn();
361 if ($group === ACL_BOGOUSER)
363 return isa($user,'_BogoUser') or
364 (isWikiWord($user->_userid) and $user->_level >= WIKIAUTH_BOGO);
365 else return isWikiWord($user->UserName());
366 if ($group === ACL_HASHOMEPAGE)
367 return $user->hasHomePage();
368 if ($group === ACL_SIGNED)
369 return $user->isSignedIn();
370 if ($group === ACL_AUTHENTICATED)
371 return $user->isAuthenticated();
372 if ($group === ACL_OWNER) {
373 $page = $request->getPage();
374 return ($user->isAuthenticated() and
375 $page->getOwner() === $user->UserName());
377 if ($group === ACL_CREATOR) {
378 $page = $request->getPage();
379 return ($user->isAuthenticated() and
380 $page->getCreator() === $user->UserName());
382 /* Or named groups or usernames.
383 Note: We don't seperate groups and users here.
384 Users overrides groups with the same name.
386 return $user->UserName() === $group or
387 $member->isMember($group);
391 * returns hash of default permissions.
392 * check if the page '.' exists and returns this instead.
394 function defaultPerms() {
395 //Todo: check for the existance of '.' and take this instead.
396 //Todo: honor more config.ini auth settings here
397 $perm = array('view' => array(ACL_EVERY => true),
398 'edit' => array(ACL_EVERY => true),
399 'create' => array(ACL_EVERY => true),
400 'list' => array(ACL_EVERY => true),
401 'remove' => array(ACL_ADMIN => true,
403 'change' => array(ACL_ADMIN => true,
406 $perm['dump'] = array(ACL_ADMIN => true,
409 $perm['dump'] = array(ACL_EVERY => true);
411 if (!ALLOW_ANON_USER) {
412 if (!ALLOW_USER_PASSWORDS)
413 $perm['view'] = array(ACL_SIGNED => true);
415 $perm['view'] = array(ACL_AUTHENTICATED => true);
416 $perm['view'][ACL_BOGOUSER] = ALLOW_BOGO_LOGIN ? true : false;
419 if (!ALLOW_ANON_EDIT) {
420 if (!ALLOW_USER_PASSWORDS)
421 $perm['edit'] = array(ACL_SIGNED => true);
423 $perm['edit'] = array(ACL_AUTHENTICATED => true);
424 $perm['edit'][ACL_BOGOUSER] = ALLOW_BOGO_LOGIN ? true : false;
425 $perm['create'] = $perm['edit'];
431 foreach ($this->perm as $access => $groups) {
432 foreach ($groups as $group => $bool) {
433 $this->perm[$access][$group] = (boolean) $bool;
439 * do a recursive comparison
441 function equal($otherperm) {
442 $diff = array_diff_assoc_recursive($this->perm, $otherperm);
447 * returns list of all supported access types.
449 function accessTypes() {
450 return array_keys($this->defaultPerms());
454 * special permissions for dot-files, beginning with '.'
455 * maybe also for '_' files?
457 function dotPerms() {
458 $def = array(ACL_ADMIN => true,
461 foreach ($this->accessTypes() as $access) {
462 $perm[$access] = $def;
468 * dead code. not needed inside the object. see getPagePermissions($page)
470 function retrieve($page) {
471 $hash = $page->get('perm');
472 if ($hash) // hash => object
473 $perm = new PagePermission(unserialize($hash));
475 $perm = new PagePermission();
480 function store($page) {
483 return $page->set('perm',serialize($this->perm));
486 function groupName ($group) {
487 if ($group[0] == '_') return constant("GROUP".$group);
491 /* type: page, default, inherited */
492 function asTable($type) {
493 $table = HTML::table();
494 foreach ($this->perm as $access => $perms) {
495 $td = HTML::table(array('class' => 'cal','valign' => 'top'));
496 foreach ($perms as $group => $bool) {
497 $td->pushContent(HTML::tr(HTML::td(array('align'=>'right'),$group),
498 HTML::td($bool ? '[X]' : '[ ]')));
500 $table->pushContent(HTML::tr(array('valign' => 'top'),
501 HTML::td($access),HTML::td($td)));
503 if ($type == 'default')
504 $table->setAttr('style','border: dotted thin black; background-color:#eee;');
505 elseif ($type == 'inherited')
506 $table->setAttr('style','border: dotted thin black; background-color:#ddd;');
507 elseif ($type == 'page')
508 $table->setAttr('style','border: solid thin black; font-weight: bold;');
512 /* type: page, default, inherited */
513 function asEditableTable($type) {
515 if (!isset($this->_group)) {
516 $this->_group =& WikiGroup::getGroup($GLOBALS['request']);
518 $table = HTML::table();
519 $table->pushContent(HTML::tr(
520 HTML::th(array('align' => 'left'),
522 HTML::th(array('align'=>'right'),
524 HTML::th(_("Grant")),
525 HTML::th(_("Del/+")),
526 HTML::th(_("Description"))));
528 $allGroups = $this->_group->_specialGroups();
529 foreach ($this->_group->getAllGroupsIn() as $group) {
530 if (!in_array($group,$this->_group->specialGroups()))
531 $allGroups[] = $group;
533 //array_unique(array_merge($this->_group->getAllGroupsIn(),
534 $deletesrc = $Theme->_findData('images/delete.png');
535 $addsrc = $Theme->_findData('images/add.png');
536 $nbsp = HTML::raw(' ');
537 foreach ($this->perm as $access => $groups) {
538 //$permlist = HTML::table(array('class' => 'cal','valign' => 'top'));
540 $newperm = HTML::input(array('type' => 'checkbox',
541 'name' => "acl[_new_perm][$access]",
543 $addbutton = HTML::input(array('type' => 'checkbox',
544 'name' => "acl[_add_group][$access]",
547 'title' => _("Add this ACL"),
549 $newgroup = HTML::select(array('name' => "acl[_new_group][$access]",
550 'style'=> 'text-align: right;',
552 foreach ($allGroups as $groupname) {
553 if (!isset($groups[$groupname]))
554 $newgroup->pushContent(HTML::option(array('value' => $groupname),
555 $this->groupName($groupname)));
557 if (empty($groups)) {
558 $addbutton->setAttr('checked','checked');
559 $newperm->setAttr('checked','checked');
561 HTML::tr(array('valign' => 'top'),
562 HTML::td(HTML::strong($access.":")),
564 HTML::td($nbsp,$newperm),
565 HTML::td($nbsp,$addbutton),
566 HTML::td(HTML::em(getAccessDescription($access)))));
568 foreach ($groups as $group => $bool) {
569 $checkbox = HTML::input(array('type' => 'checkbox',
570 'name' => "acl[$access][$group]",
571 'title' => _("Allow / Deny"),
573 if ($bool) $checkbox->setAttr('checked','checked');
574 $checkbox = HTML(HTML::input(array('type' => 'hidden',
575 'name' => "acl[$access][$group]",
578 $deletebutton = HTML::input(array('type' => 'checkbox',
579 'name' => "acl[_del_group][$access][$group]",
580 'style' => 'background: #aaa url('.$deletesrc.')',
581 //'src' => $deletesrc,
583 'title' => _("Delete this ACL"),
588 HTML::td(HTML::strong($access.":")),
589 HTML::td(array('class' => 'cal-today','align'=>'right'),
590 HTML::strong($this->groupName($group))),
591 HTML::td(array('align'=>'center'),$nbsp,$checkbox),
592 HTML::td(array('align'=>'right','style' => 'background: #aaa url('.$deletesrc.') no-repeat'),$deletebutton),
593 HTML::td(HTML::em(getAccessDescription($access)))));
599 HTML::td(array('class' => 'cal-today','align'=>'right'),
600 HTML::strong($this->groupName($group))),
601 HTML::td(array('align'=>'center'),$nbsp,$checkbox),
602 HTML::td(array('align'=>'right','style' => 'background: #aaa url('.$deletesrc.') no-repeat'),$deletebutton),
608 HTML::tr(array('valign' => 'top'),
609 HTML::td(array('align'=>'right'),_("add ")),
611 HTML::td(array('align'=>'center'),$nbsp,$newperm),
612 HTML::td(array('align'=>'right','style' => 'background: #ccc url('.$addsrc.') no-repeat'),$addbutton),
613 HTML::td(HTML::small(_("Check to add this Acl")))));
615 if ($type == 'default')
616 $table->setAttr('style','border: dotted thin black; background-color:#eee;');
617 elseif ($type == 'inherited')
618 $table->setAttr('style','border: dotted thin black; background-color:#ddd;');
619 elseif ($type == 'page')
620 $table->setAttr('style','border: solid thin black; font-weight: bold;');
624 // this is just a bad hack for testing
625 // simplify the ACL to a unix-like "rwx------" string
626 function asRwxString($owner,$group=false) {
628 // simplify object => rwxrw---x+ string as in cygwin (+ denotes additional ACLs)
629 $perm =& $this->perm;
630 // get effective user and group
632 if (isset($perm['view'][$owner]) or
633 (isset($perm['view'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
635 if (isset($perm['edit'][$owner]) or
636 (isset($perm['edit'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
638 if (isset($perm['change'][$owner]) or
639 (isset($perm['change'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
641 if (!empty($group)) {
642 if (isset($perm['view'][$group]) or
643 (isset($perm['view'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
645 if (isset($perm['edit'][$group]) or
646 (isset($perm['edit'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
648 if (isset($perm['change'][$group]) or
649 (isset($perm['change'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
652 if (isset($perm['view'][ACL_EVERY]) or
653 (isset($perm['view'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
655 if (isset($perm['edit'][ACL_EVERY]) or
656 (isset($perm['edit'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
658 if (isset($perm['change'][ACL_EVERY]) or
659 (isset($perm['change'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
665 // $Log: not supported by cvs2svn $
666 // Revision 1.15 2004/05/16 22:07:35 rurban
667 // check more config-default and predefined constants
668 // various PagePerm fixes:
669 // fix default PagePerms, esp. edit and view for Bogo and Password users
670 // implemented Creator and Owner
671 // BOGOUSERS renamed to BOGOUSER
672 // fixed syntax errors in signin.tmpl
674 // Revision 1.14 2004/05/15 22:54:49 rurban
675 // fixed important WikiDB bug with DEBUG > 0: wrong assertion
676 // improved SetAcl (works) and PagePerms, some WikiGroup helpers.
678 // Revision 1.13 2004/05/15 19:48:33 rurban
679 // fix some too loose PagePerms for signed, but not authenticated users
680 // (admin, owner, creator)
681 // no double login page header, better login msg.
682 // moved action_pdf to lib/pdf.php
684 // Revision 1.12 2004/05/04 22:34:25 rurban
687 // Revision 1.11 2004/05/02 21:26:38 rurban
688 // limit user session data (HomePageHandle and auth_dbi have to invalidated anyway)
689 // because they will not survive db sessions, if too large.
690 // extended action=upgrade
691 // some WikiTranslation button work
692 // revert WIKIAUTH_UNOBTAINABLE (need it for main.php)
693 // some temp. session debug statements
695 // Revision 1.10 2004/04/29 22:32:56 zorloc
696 // Slightly more elegant fix. Instead of WIKIAUTH_FORBIDDEN, the current user's level + 1 is returned on a false.
698 // Revision 1.9 2004/04/29 17:18:19 zorloc
699 // Fixes permission failure issues. With PagePermissions and Disabled Actions when user did not have permission WIKIAUTH_FORBIDDEN was returned. In WikiUser this was ok because WIKIAUTH_FORBIDDEN had a value of 11 -- thus no user could perform that action. But WikiUserNew has a WIKIAUTH_FORBIDDEN value of -1 -- thus a user without sufficent permission to do anything. The solution is a new high value permission level (WIKIAUTH_UNOBTAINABLE) to be the default level for access failure.
701 // Revision 1.8 2004/03/14 16:24:35 rurban
702 // authenti(fi)cation spelling
704 // Revision 1.7 2004/02/28 22:25:07 rurban
705 // First PagePerm implementation:
707 // $Theme->setAnonEditUnknownLinks(false);
709 // Layout improvement with dangling links for mostly closed wiki's:
710 // If false, only users with edit permissions will be presented the
711 // special wikiunknown class with "?" and Tooltip.
712 // If true (default), any user will see the ?, but will be presented
713 // the PrintLoginForm on a click.
715 // Revision 1.6 2004/02/24 15:20:05 rurban
716 // fixed minor warnings: unchecked args, POST => Get urls for sortby e.g.
718 // Revision 1.5 2004/02/23 21:30:25 rurban
719 // more PagePerm stuff: (working against 1.4.0)
720 // ACL editing and simplification of ACL's to simple rwx------ string
723 // Revision 1.4 2004/02/12 13:05:36 rurban
724 // Rename functional for PearDB backend
725 // some other minor changes
726 // SiteMap comes with a not yet functional feature request: includepages (tbd)
728 // Revision 1.3 2004/02/09 03:58:12 rurban
729 // for now default DB_SESSION to false
731 // * not existing perms will now query the parent, and not
732 // return the default perm
733 // * added pagePermissions func which returns the object per page
734 // * added getAccessDescription
736 // * added global ->prepare (not yet used) with smart user/pref/member table prefixing.
737 // * force init of authdbh in the 2 db classes
739 // * fixed session handling (not triple auth request anymore)
740 // * don't store cookie prefs with sessions
741 // stdlib: global obj2hash helper from _AuthInfo, also needed for PagePerm
743 // Revision 1.2 2004/02/08 13:17:48 rurban
744 // This should be the functionality. Needs testing and some minor todos.
746 // Revision 1.1 2004/02/08 12:29:30 rurban
747 // initial version, not yet hooked into lib/main.php
755 // c-hanging-comment-ender-p: nil
756 // indent-tabs-mode: nil