view * edit => edit * create => edit or create * remove => remove * rename => change * store prefs => change * list in PageList => list */ /* Symbolic special ACL groups. Untranslated to be stored in page metadata*/ define('ACL_EVERY', '_EVERY'); define('ACL_ANONYMOUS', '_ANONYMOUS'); define('ACL_BOGOUSER', '_BOGOUSER'); define('ACL_HASHOMEPAGE', '_HASHOMEPAGE'); define('ACL_SIGNED', '_SIGNED'); define('ACL_AUTHENTICATED', '_AUTHENTICATED'); define('ACL_ADMIN', '_ADMIN'); define('ACL_OWNER', '_OWNER'); define('ACL_CREATOR', '_CREATOR'); // Return an page permissions array for this page. // To provide ui helpers to view and change page permissions: // GroupAccessAllow or Forbid // $group_($access) [ ] function pagePermissions($pagename) { global $request; $page = $request->getPage($pagename); // Page not found (new page); returned inherited permissions, to be displayed in gray if (!$page->exists()) { if ($pagename == '.') // stop recursion return array('default', new PagePermission()); else { return array('inherited', pagePermissions(getParentPage($pagename))); } } elseif ($perm = getPagePermissions($page)) { return array('page', $perm); // or no permissions defined; returned inherited permissions, to be displayed in gray } elseif ($pagename == '.') { // stop recursion in pathological case. // "." defined, without any acl return array('default', new PagePermission()); } else { return array('inherited', pagePermissions(getParentPage($pagename))); } } function pagePermissionsSimpleFormat($perm_tree, $owner, $group = false) { list($type, $perm) = pagePermissionsAcl($perm_tree[0], $perm_tree); /* $type = $perm_tree[0]; $perm = pagePermissionsAcl($perm_tree); if (is_object($perm_tree[1])) $perm = $perm_tree[1]; elseif (is_array($perm_tree[1])) { $perm_tree = pagePermissionsSimpleFormat($perm_tree[1],$owner,$group); if (isa($perm_tree[1],'pagepermission')) $perm = $perm_tree[1]; elseif (isa($perm_tree,'htmlelement')) return $perm_tree; } */ if ($type == 'page') return HTML::tt(HTML::strong($perm->asRwxString($owner, $group))); elseif ($type == 'default') return HTML::tt($perm->asRwxString($owner, $group)); elseif ($type == 'inherited') { return HTML::tt(array('class' => 'inherited', 'style' => 'color:#aaa;'), $perm->asRwxString($owner, $group)); } } function pagePermissionsAcl($type, $perm_tree) { $perm = $perm_tree[1]; while (!is_object($perm)) { $perm_tree = pagePermissionsAcl($type, $perm); $perm = $perm_tree[1]; } return array($type, $perm); } // view => who // edit => who function pagePermissionsAclFormat($perm_tree, $editable = false) { list($type, $perm) = pagePermissionsAcl($perm_tree[0], $perm_tree); if ($editable) return $perm->asEditableTable($type); else return $perm->asTable($type); } /** * Check the permissions for the current action. * Walk down the inheritance tree. Collect all permissions until * the minimum required level is gained, which is not * overruled by more specific forbid rules. * Todo: cache result per access and page in session? */ function requiredAuthorityForPage($action) { global $request; $auth = _requiredAuthorityForPagename(action2access($action), $request->getArg('pagename')); assert($auth !== -1); if ($auth) return $request->_user->_level; else return WIKIAUTH_UNOBTAINABLE; } // Translate action or plugin to the simplier access types: function action2access($action) { global $request; switch ($action) { case 'browse': case 'viewsource': case 'diff': case 'select': case 'search': case 'pdf': case 'captcha': return 'view'; // performance and security relevant case 'xmlrpc': case 'soap': case 'zip': case 'ziphtml': case 'dumphtml': case 'dumpserial': return 'dump'; // invent a new access-perm massedit? or switch back to change, or keep it at edit? case _("PhpWikiAdministration") . "/" . _("Rename"): case _("PhpWikiAdministration") . "/" . _("SearchReplace"): case 'replace': case 'rename': case 'revert': case 'edit': return 'edit'; case 'create': $page = $request->getPage(); if (!$page->exists()) return 'create'; else return 'view'; break; case 'upload': case 'loadfile': // probably create/edit but we cannot check all page permissions, can we? case 'remove': case 'purge': case 'lock': case 'unlock': case 'upgrade': case 'chown': case 'setacl': case 'deleteacl': return 'change'; default: //Todo: Plugins should be able to override its access type if (isWikiWord($action)) return 'view'; else return 'change'; break; } } // Recursive helper to do the real work. // Using a simple perm cache for page-access pairs. // Maybe page-(current+edit+change?)action pairs will help function _requiredAuthorityForPagename($access, $pagename) { static $permcache = array(); if (array_key_exists($pagename, $permcache) and array_key_exists($access, $permcache[$pagename]) ) return $permcache[$pagename][$access]; global $request; $page = $request->getPage($pagename); // Exceptions: if (defined('FUSIONFORGE') and FUSIONFORGE) { if ($pagename != '.' && isset($request->_user->_is_external) && $request->_user->_is_external && !$page->get('external')) { $permcache[$pagename][$access] = 0; return 0; } } if ((READONLY or $request->_dbi->readonly) and in_array($access, array('edit', 'create', 'change')) ) { return 0; } // Page not found; check against default permissions if (!$page->exists()) { $perm = new PagePermission(); $result = ($perm->isAuthorized($access, $request->_user) === true); $permcache[$pagename][$access] = $result; return $result; } // no ACL defined; check for special dotfile or walk down if (!($perm = getPagePermissions($page))) { if ($pagename == '.') { $perm = new PagePermission(); if ($perm->isAuthorized('change', $request->_user)) { // warn the user to set ACL of ".", if he has permissions to do so. trigger_error(". (dotpage == rootpage for inheriting pageperm ACLs) exists without any ACL!\n" . "Please do ?action=setacl&pagename=.", E_USER_WARNING); } $result = ($perm->isAuthorized($access, $request->_user) === true); $permcache[$pagename][$access] = $result; return $result; } elseif ($pagename[0] == '.') { $perm = new PagePermission(PagePermission::dotPerms()); $result = ($perm->isAuthorized($access, $request->_user) === true); $permcache[$pagename][$access] = $result; return $result; } return _requiredAuthorityForPagename($access, getParentPage($pagename)); } // ACL defined; check if isAuthorized returns true or false or undecided $authorized = $perm->isAuthorized($access, $request->_user); if ($authorized !== -1) { // interestingly true is also -1 $permcache[$pagename][$access] = $authorized; return $authorized; } elseif ($pagename == '.') { return false; } else { return _requiredAuthorityForPagename($access, getParentPage($pagename)); } } /** * @param string $pagename page from which the parent page is searched. * @return string parent pagename or the (possibly pseudo) dot-pagename. */ function getParentPage($pagename) { if (isSubPage($pagename)) { return subPageSlice($pagename, 0); } else { return '.'; } } // Read the ACL from the page // Done: Not existing pages should NOT be queried. // Check the parent page instead and don't take the default ACL's function getPagePermissions($page) { if ($hash = $page->get('perm')) // hash => object return new PagePermission(unserialize($hash)); else return false; } // Store the ACL in the page function setPagePermissions($page, $perm) { $perm->store($page); } function getAccessDescription($access) { static $accessDescriptions; if (!$accessDescriptions) { $accessDescriptions = array( 'list' => _("List this page and all subpages"), 'view' => _("View this page and all subpages"), 'edit' => _("Edit this page and all subpages"), 'create' => _("Create a new (sub)page"), 'dump' => _("Download page contents"), 'change' => _("Change page attributes"), 'remove' => _("Remove this page"), 'purge' => _("Purge this page"), ); } if (in_array($access, array_keys($accessDescriptions))) return $accessDescriptions[$access]; else return $access; } /** * The ACL object per page. It is stored in a page, but can also * be merged with ACL's from other pages or taken from the master (pseudo) dot-file. * * A hash of "access" => "requires" pairs. * "access" is a shortcut for common actions, which map to main.php actions * "requires" required username or groupname or any special group => true or false * * Define any special rules here, like don't list dot-pages. */ class PagePermission { public $perm; function PagePermission($hash = array()) { $this->_group = &$GLOBALS['request']->getGroup(); if (is_array($hash) and !empty($hash)) { $accessTypes = $this->accessTypes(); foreach ($hash as $access => $requires) { if (in_array($access, $accessTypes)) $this->perm[$access] = $requires; else trigger_error(sprintf(_("Unsupported ACL access type %s ignored."), $access), E_USER_WARNING); } } else { // set default permissions, the so called dot-file acl's $this->perm = $this->defaultPerms(); } return $this; } /** * The workhorse to check the user against the current ACL pairs. * Must translate the various special groups to the actual users settings * (userid, group membership). */ function isAuthorized($access, $user) { $allow = -1; if (!empty($this->perm{$access})) { foreach ($this->perm[$access] as $group => $bool) { if ($this->isMember($user, $group)) { return $bool; } elseif ($allow == -1) { // not a member and undecided: check other groups $allow = !$bool; } } } return $allow; // undecided } /** * Translate the various special groups to the actual users settings * (userid, group membership). */ function isMember($user, $group) { global $request; if ($group === ACL_EVERY) return true; if (!isset($this->_group)) $member =& $request->getGroup(); else $member =& $this->_group; //$user = & $request->_user; if ($group === ACL_ADMIN) // WIKI_ADMIN or member of _("Administrators") return $user->isAdmin() or ($user->isAuthenticated() and $member->isMember(GROUP_ADMIN)); if ($group === ACL_ANONYMOUS) return !$user->isSignedIn(); if ($group === ACL_BOGOUSER) if (ENABLE_USER_NEW) return isa($user, '_BogoUser') or (isWikiWord($user->_userid) and $user->_level >= WIKIAUTH_BOGO); else return isWikiWord($user->UserName()); if ($group === ACL_HASHOMEPAGE) return $user->hasHomePage(); if ($group === ACL_SIGNED) return $user->isSignedIn(); if ($group === ACL_AUTHENTICATED) return $user->isAuthenticated(); if ($group === ACL_OWNER) { if (!$user->isAuthenticated()) return false; $page = $request->getPage(); $owner = $page->getOwner(); return ($owner === $user->UserName() or $member->isMember($owner)); } if ($group === ACL_CREATOR) { if (!$user->isAuthenticated()) return false; $page = $request->getPage(); $creator = $page->getCreator(); return ($creator === $user->UserName() or $member->isMember($creator)); } /* Or named groups or usernames. Note: We don't separate groups and users here. Users overrides groups with the same name. */ return $user->UserName() === $group or $member->isMember($group); } /** * returns hash of default permissions. * check if the page '.' exists and returns this instead. */ function defaultPerms() { //Todo: check for the existance of '.' and take this instead. //Todo: honor more config.ini auth settings here $perm = array('view' => array(ACL_EVERY => true), 'edit' => array(ACL_EVERY => true), 'create' => array(ACL_EVERY => true), 'list' => array(ACL_EVERY => true), 'remove' => array(ACL_ADMIN => true, ACL_OWNER => true), 'purge' => array(ACL_ADMIN => true, ACL_OWNER => true), 'dump' => array(ACL_ADMIN => true, ACL_OWNER => true), 'change' => array(ACL_ADMIN => true, ACL_OWNER => true)); if (ZIPDUMP_AUTH) $perm['dump'] = array(ACL_ADMIN => true, ACL_OWNER => true); elseif (INSECURE_ACTIONS_LOCALHOST_ONLY) { if (is_localhost()) $perm['dump'] = array(ACL_EVERY => true); else $perm['dump'] = array(ACL_ADMIN => true); } else $perm['dump'] = array(ACL_EVERY => true); if (defined('REQUIRE_SIGNIN_BEFORE_EDIT') && REQUIRE_SIGNIN_BEFORE_EDIT) $perm['edit'] = array(ACL_SIGNED => true); // view: if (!ALLOW_ANON_USER) { if (!ALLOW_USER_PASSWORDS) $perm['view'] = array(ACL_SIGNED => true); else $perm['view'] = array(ACL_AUTHENTICATED => true); $perm['view'][ACL_BOGOUSER] = ALLOW_BOGO_LOGIN ? true : false; } // edit: if (!ALLOW_ANON_EDIT) { if (!ALLOW_USER_PASSWORDS) $perm['edit'] = array(ACL_SIGNED => true); else $perm['edit'] = array(ACL_AUTHENTICATED => true); $perm['edit'][ACL_BOGOUSER] = ALLOW_BOGO_LOGIN ? true : false; $perm['create'] = $perm['edit']; } return $perm; } /** * FIXME: check valid groups and access */ function sanify() { foreach ($this->perm as $access => $groups) { foreach ($groups as $group => $bool) { $this->perm[$access][$group] = (boolean)$bool; } } } /** * do a recursive comparison */ function equal($otherperm) { // The equal function seems to be unable to detect removed perm. // Use case is when a rule is removed. return (print_r($this->perm, true) === print_r($otherperm, true)); } /** * returns list of all supported access types. */ function accessTypes() { return array_keys(PagePermission::defaultPerms()); } /** * special permissions for dot-files, beginning with '.' * maybe also for '_' files? */ function dotPerms() { $def = array(ACL_ADMIN => true, ACL_OWNER => true); $perm = array(); foreach (PagePermission::accessTypes() as $access) { $perm[$access] = $def; } return $perm; } /** * dead code. not needed inside the object. see getPagePermissions($page) */ function retrieve($page) { $hash = $page->get('perm'); if ($hash) // hash => object $perm = new PagePermission(unserialize($hash)); else $perm = new PagePermission(); $perm->sanify(); return $perm; } function store($page) { // object => hash $this->sanify(); return $page->set('perm', serialize($this->perm)); } function groupName($group) { if ($group[0] == '_') return constant("GROUP" . $group); else return $group; } /* type: page, default, inherited */ function asTable($type) { $table = HTML::table(); foreach ($this->perm as $access => $perms) { $td = HTML::table(array('class' => 'cal')); foreach ($perms as $group => $bool) { $td->pushContent(HTML::tr(HTML::td(array('align' => 'right'), $group), HTML::td($bool ? '[X]' : '[ ]'))); } $table->pushContent(HTML::tr(array('class' => 'top'), HTML::td($access), HTML::td($td))); } if ($type == 'default') $table->setAttr('style', 'border: dotted thin black; background-color:#eee;'); elseif ($type == 'inherited') $table->setAttr('style', 'border: dotted thin black; background-color:#ddd;'); elseif ($type == 'page') $table->setAttr('style', 'border: solid thin black; font-weight: bold;'); return $table; } /* type: page, default, inherited */ function asEditableTable($type) { global $WikiTheme; if (!isset($this->_group)) { $this->_group =& $GLOBALS['request']->getGroup(); } $table = HTML::table(); $table->pushContent(HTML::tr( HTML::th(array('align' => 'left'), _("Access")), HTML::th(array('align' => 'right'), _("Group/User")), HTML::th(_("Grant")), HTML::th(_("Del/+")), HTML::th(_("Description")))); $allGroups = $this->_group->_specialGroups(); foreach ($this->_group->getAllGroupsIn() as $group) { if (!in_array($group, $this->_group->specialGroups())) $allGroups[] = $group; } //array_unique(array_merge($this->_group->getAllGroupsIn(), $deletesrc = $WikiTheme->_findData('images/delete.png'); $addsrc = $WikiTheme->_findData('images/add.png'); $nbsp = HTML::raw(' '); foreach ($this->perm as $access => $groups) { //$permlist = HTML::table(array('class' => 'cal')); $first_only = true; $newperm = HTML::input(array('type' => 'checkbox', 'name' => "acl[_new_perm][$access]", 'value' => 1)); $addbutton = HTML::input(array('type' => 'checkbox', 'name' => "acl[_add_group][$access]", //'src' => $addsrc, //'alt' => "Add", 'title' => _("Add this ACL"), 'value' => 1)); $newgroup = HTML::select(array('name' => "acl[_new_group][$access]", 'style' => 'text-align: right;', 'size' => 1)); foreach ($allGroups as $groupname) { if (!isset($groups[$groupname])) $newgroup->pushContent(HTML::option(array('value' => $groupname), $this->groupName($groupname))); } if (empty($groups)) { $addbutton->setAttr('checked', 'checked'); $newperm->setAttr('checked', 'checked'); $table->pushContent( HTML::tr(array('class' => 'top'), HTML::td(HTML::strong($access . ":")), HTML::td($newgroup), HTML::td($nbsp, $newperm), HTML::td($nbsp, $addbutton), HTML::td(HTML::em(getAccessDescription($access))))); } foreach ($groups as $group => $bool) { $checkbox = HTML::input(array('type' => 'checkbox', 'name' => "acl[$access][$group]", 'title' => _("Allow / Deny"), 'value' => 1)); if ($bool) $checkbox->setAttr('checked', 'checked'); $checkbox = HTML(HTML::input(array('type' => 'hidden', 'name' => "acl[$access][$group]", 'value' => 0)), $checkbox); $deletebutton = HTML::input(array('type' => 'checkbox', 'name' => "acl[_del_group][$access][$group]", 'style' => 'background: #aaa url(' . $deletesrc . ')', //'src' => $deletesrc, //'alt' => "Del", 'title' => _("Delete this ACL"), 'value' => 1)); if ($first_only) { $table->pushContent( HTML::tr( HTML::td(HTML::strong($access . ":")), HTML::td(array('class' => 'cal-today', 'align' => 'right'), HTML::strong($this->groupName($group))), HTML::td(array('align' => 'center'), $nbsp, $checkbox), HTML::td(array('align' => 'right', 'style' => 'background: #aaa url(' . $deletesrc . ') no-repeat'), $deletebutton), HTML::td(HTML::em(getAccessDescription($access))))); $first_only = false; } else { $table->pushContent( HTML::tr( HTML::td(), HTML::td(array('class' => 'cal-today', 'align' => 'right'), HTML::strong($this->groupName($group))), HTML::td(array('align' => 'center'), $nbsp, $checkbox), HTML::td(array('align' => 'right', 'style' => 'background: #aaa url(' . $deletesrc . ') no-repeat'), $deletebutton), HTML::td())); } } if (!empty($groups)) $table->pushContent( HTML::tr(array('class' => 'top'), HTML::td(array('align' => 'right'), _("add ")), HTML::td($newgroup), HTML::td(array('align' => 'center'), $nbsp, $newperm), HTML::td(array('align' => 'right', 'style' => 'background: #ccc url(' . $addsrc . ') no-repeat'), $addbutton), HTML::td(HTML::small(_("Check to add this ACL"))))); } if ($type == 'default') $table->setAttr('style', 'border: dotted thin black; background-color:#eee;'); elseif ($type == 'inherited') $table->setAttr('style', 'border: dotted thin black; background-color:#ddd;'); elseif ($type == 'page') $table->setAttr('style', 'border: solid thin black; font-weight: bold;'); return $table; } // Print ACL as lines of [+-]user[,group,...]:access[,access...] // Seperate acl's by "; " or whitespace // See http://opag.ca/wiki/HelpOnAccessControlLists // As used by WikiAdminSetAclSimple function asAclLines() { $s = ''; $line = ''; $this->sanify(); foreach ($this->perm as $access => $groups) { // unify groups for same access+bool // view:CREATOR,-OWNER, $line = $access . ':'; foreach ($groups as $group => $bool) { $line .= ($bool ? '' : '-') . $group . ","; } if (substr($line, -1) == ',') $s .= substr($line, 0, -1) . "; "; } if (substr($s, -2) == '; ') $s = substr($s, 0, -2); return $s; } // This is just a bad hack for testing. // Simplify the ACL to a unix-like "rwx------+" string // See getfacl(8) function asRwxString($owner, $group = false) { global $request; // simplify object => rwxrw---x+ string as in cygwin (+ denotes additional ACLs) $perm =& $this->perm; // get effective user and group $s = '---------+'; if (isset($perm['view'][$owner]) or (isset($perm['view'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()) ) $s[0] = 'r'; if (isset($perm['edit'][$owner]) or (isset($perm['edit'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()) ) $s[1] = 'w'; if (isset($perm['change'][$owner]) or (isset($perm['change'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()) ) $s[2] = 'x'; if (!empty($group)) { if (isset($perm['view'][$group]) or (isset($perm['view'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()) ) $s[3] = 'r'; if (isset($perm['edit'][$group]) or (isset($perm['edit'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()) ) $s[4] = 'w'; if (isset($perm['change'][$group]) or (isset($perm['change'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()) ) $s[5] = 'x'; } if (isset($perm['view'][ACL_EVERY]) or (isset($perm['view'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()) ) $s[6] = 'r'; if (isset($perm['edit'][ACL_EVERY]) or (isset($perm['edit'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()) ) $s[7] = 'w'; if (isset($perm['change'][ACL_EVERY]) or (isset($perm['change'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()) ) $s[8] = 'x'; return $s; } } // Local Variables: // mode: php // tab-width: 8 // c-basic-offset: 4 // c-hanging-comment-ender-p: nil // indent-tabs-mode: nil // End: