]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/PagePerm.php
limit user session data (HomePageHandle and auth_dbi have to invalidated anyway)
[SourceForge/phpwiki.git] / lib / PagePerm.php
1 <?php // -*-php-*-
2 rcs_id('$Id: PagePerm.php,v 1.11 2004-05-02 21:26:38 rurban Exp $');
3 /*
4  Copyright 2004 $ThePhpWikiProgrammingTeam
5
6  This file is part of PhpWiki.
7
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.
12
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.
17
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
21  */
22
23 /* 
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)
28
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
34
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, 
41        when login is tried.
42      Request::getDisallowedActionDescription($action)
43        helper to localize the _notAuthorized message per action, 
44        when it aborts
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 
50        action + page pair
51
52      The defined main.php actions map to simplier access types:
53        browse => view
54        edit   => edit
55        create => edit or create
56        remove => remove
57        rename => change
58        store prefs => change
59        list in PageList => list
60 */
61
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_BOGOUSERS',    '_BOGOUSERS');
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');
72
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) {
78     global $request;
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());
84         else {
85             return array('inherited',pagePermissions(getParentPage($pagename)));
86         }
87     } elseif ($perm = getPagePermissions($page)) {
88         return array('page',$perm);
89     // or no permissions defined; returned inherited permissions, to be displayed in gray
90     } else {
91         return array('inherited',pagePermissions(getParentPage($pagename)));
92     }
93 }
94
95 function pagePermissionsSimpleFormat($perm_tree,$owner,$group=false) {
96     list($type,$perm) = pagePermissionsAcl($perm_tree[0], $perm_tree);
97     /*
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'))
107             return $perm_tree;
108     }
109     */
110     if ($type == 'page')
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));
117     }
118 }
119
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];
125     }
126     return array($type,$perm);
127 }
128
129 // view => who
130 // edit => who
131 function pagePermissionsAclFormat($perm_tree,$editable=false) {
132     list($type,$perm) = pagePermissionsAcl($perm_tree[0], $perm_tree);
133     if ($editable)
134         return $perm->asEditableTable($type);
135     else
136         return $perm->asTable($type);
137 }
138
139 /**
140  * Check permission per page.
141  * Returns true or false.
142  */
143 function mayAccessPage ($access,$pagename) {
144     return _requiredAuthorityForPagename($access,$pagename);
145 }
146
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?
152  */
153 function requiredAuthorityForPage ($action) {
154     if (_requiredAuthorityForPagename(action2access($action),
155                                       $GLOBALS['request']->getArg('pagename')))
156         return $GLOBALS['request']->_user->_level;
157     else
158         return WIKIAUTH_UNOBTAINABLE;
159 }
160
161 // Translate action or plugin to the simplier access types:
162 function action2access ($action) {
163     global $request;
164     switch ($action) {
165     case 'browse':
166     case 'viewsource':
167     case 'diff':
168     case 'select':
169     case 'xmlrpc':
170     case 'search':
171         return 'view';
172     case 'zip':
173     case 'ziphtml':
174     case 'dumpserial':
175     case 'dumphtml':
176         return 'dump';
177     case 'edit':
178         return 'edit';
179     case 'create':
180         $page = $request->getPage();
181         $current = $page->getCurrentRevision();
182         if ($current->hasDefaultContents())
183             return 'edit';
184         else
185             return 'view'; 
186         break;
187     case 'upload':
188     case 'loadfile': 
189         // probably create/edit but we cannot check all page permissions, can we?
190     case 'remove':
191     case 'lock':
192     case 'unlock':
193     case 'upgrade':
194             return 'change';
195     default:
196         //Todo: Plugins should be able to override its access type
197         if (isWikiWord($action))
198             return 'view';
199         else
200             return 'change';
201         break;
202     }
203 }
204
205 // Recursive helper to do the real work
206 function _requiredAuthorityForPagename($access, $pagename) {
207     global $request;
208     $page = $request->getPage($pagename);
209     // Page not found; check against default permissions
210     if (! $page->exists() ) {
211         $perm = new PagePermission();
212         return ($perm->isAuthorized($access,$request->_user) === true);
213     }
214     // no ACL defined; check for special dotfile or walk down
215     if (! ($perm = getPagePermissions($page))) { 
216         if ($pagename[0] == '.') {
217             $perm = new PagePermission(PagePermission::dotPerms());
218             return ($perm->isAuthorized($access,$request->_user) === true);
219         }
220         return _requiredAuthorityForPagename($access,getParentPage($pagename));
221     }
222     // ACL defined; check if isAuthorized returns true or false or undecided
223     $authorized = $perm->isAuthorized($access,$request->_user);
224     if ($authorized != -1) // -1 for undecided
225         return $authorized;
226     else
227         return _requiredAuthorityForPagename($access,getParentPage($pagename));
228 }
229
230 /**
231  * @param  string $pagename   page from which the parent page is searched.
232  * @return string parent      pagename or the (possibly pseudo) dot-pagename.
233  */
234 function getParentPage($pagename) {
235     if (isSubPage($pagename)) {
236         return subPageSlice($pagename,0);
237     } else {
238         return '.';
239     }
240 }
241
242 // Read the ACL from the page
243 // Done: Not existing pages should NOT be queried. 
244 // Check the parent page instead and don't take the default ACL's
245 function getPagePermissions ($page) {
246     if ($hash = $page->get('perm'))  // hash => object
247         return new PagePermission(unserialize($hash));
248     else 
249         return false;
250 }
251
252 // Store the ACL in the page
253 function setPagePermissions ($page,$perm) {
254     $perm->store($page);
255 }
256
257 function getAccessDescription($access) {
258     static $accessDescriptions;
259     if (! $accessDescriptions) {
260         $accessDescriptions = array(
261                                     'list'     => _("List this page and all subpages"),
262                                     'view'     => _("View this page and all subpages"),
263                                     'edit'     => _("Edit this page and all subpages"),
264                                     'create'   => _("Create a new (sub)page"),
265                                     'dump'     => _("Download the page contents"),
266                                     'change'   => _("Change page attributes"),
267                                     'remove'   => _("Remove this page"),
268                                     );
269     }
270     if (in_array($access, array_keys($accessDescriptions)))
271         return $accessDescriptions[$access];
272     else
273         return $access;
274 }
275
276 /**
277  * The ACL object per page. It is stored in a page, but can also 
278  * be merged with ACL's from other pages or taken from the master (pseudo) dot-file.
279  *
280  * A hash of "access" => "requires" pairs.
281  *   "access"   is a shortcut for common actions, which map to main.php actions
282  *   "requires" required username or groupname or any special group => true or false
283  *
284  * Define any special rules here, like don't list dot-pages.
285  */ 
286 class PagePermission {
287     var $perm;
288
289     function PagePermission($hash = array()) {
290         if (is_array($hash) and !empty($hash)) {
291             $accessTypes = $this->accessTypes();
292             foreach ($hash as $access => $requires) {
293                 if (in_array($access,$accessTypes))
294                     $this->perm[$access] = $requires;
295                 else
296                     trigger_error(sprintf(_("Unsupported ACL access type %s ignored."),$access),
297                                   E_USER_WARNING);
298             }
299         } else {
300             // set default permissions, the so called dot-file acl's
301             $this->perm = $this->defaultPerms();
302         }
303         return $this;
304     }
305
306     /**
307      * The workhorse to check the user against the current ACL pairs.
308      * Must translate the various special groups to the actual users settings 
309      * (userid, group membership).
310      */
311     function isAuthorized($access,$user) {
312         if (!empty($this->perm{$access})) {
313             foreach ($this->perm[$access] as $group => $bool) {
314                 if ($this->isMember($user,$group)) {
315                     return $bool;
316                 }
317             }
318         }
319         return -1; // undecided
320     }
321
322     /**
323      * Translate the various special groups to the actual users settings 
324      * (userid, group membership).
325      */
326     function isMember($user,$group) {
327         global $request;
328         if ($group === ACL_EVERY) return true;
329         $member = &WikiGroup::getGroup($request);
330         //$user = & $request->_user;
331         if ($group === ACL_ADMIN)   // WIKI_ADMIN or member of _("Administrators")
332             return $user->isAdmin() or
333                    $member->isMember(GROUP_ADMIN);
334         if ($group === ACL_ANONYMOUS) 
335             return ! $user->isSignedIn();
336         if ($group === ACL_BOGOUSERS)
337             if (ENABLE_USER_NEW) return isa($user,'_BogoUser');
338             else return isWikiWord($user->UserName());
339         if ($group === ACL_HASHOMEPAGE)
340             return $user->hasHomePage();
341         if ($group === ACL_SIGNED)
342             return $user->isSignedIn();
343         if ($group === ACL_AUTHENTICATED)
344             return $user->isAuthenticated();
345         if ($group === ACL_OWNER) {
346             $page = $request->getPage();
347             return $page->get('author') === $user->UserName();
348         }
349         if ($group === ACL_CREATOR) {
350             $page = $request->getPage();
351             $rev = $page->getRevision(1);
352             return $rev->get('author') === $user->UserName();
353         }
354         /* Or named groups or usernames.
355          Note: We don't seperate groups and users here. 
356          Users overrides groups with the same name. */
357         return $user->UserName() === $group or
358                $member->isMember($group);
359     }
360
361     /**
362      * returns hash of default permissions.
363      * check if the page '.' exists and returns this instead.
364      */
365     function defaultPerms() {
366         //Todo: check for the existance of '.' and take this instead.
367         //Todo: honor more index.php auth settings here
368         $perm = array('view'   => array(ACL_EVERY => true),
369                       'edit'   => array(ACL_EVERY => true),
370                       'create' => array(ACL_EVERY => true),
371                       'list'   => array(ACL_EVERY => true),
372                       'remove' => array(ACL_ADMIN => true,
373                                         ACL_OWNER => true),
374                       'change' => array(ACL_ADMIN => true,
375                                         ACL_OWNER => true));
376         if (defined('ZIPDUMP_AUTH') && ZIPDUMP_AUTH)
377             $perm['dump'] = array(ACL_ADMIN => true,
378                                   ACL_OWNER => true);
379         else
380             $perm['dump'] = array(ACL_EVERY => true);
381         if (defined('REQUIRE_SIGNIN_BEFORE_EDIT') && REQUIRE_SIGNIN_BEFORE_EDIT)
382             $perm['edit'] = array(ACL_SIGNED => true);
383         if (defined('ALLOW_ANON_USER') && ! ALLOW_ANON_USER) {
384             if (defined('ALLOW_BOGO_USER') && ALLOW_BOGO_USER) {
385                 $perm['view'] = array(ACL_BOGOUSER => true);
386                 $perm['edit'] = array(ACL_BOGOUSER => true);
387             } elseif (defined('ALLOW_USER_PASSWORDS') && ALLOW_USER_PASSWORDS) {
388                 $perm['view'] = array(ACL_AUTHENTICATED => true);
389                 $perm['edit'] = array(ACL_AUTHENTICATED => true);
390             } else {
391                 $perm['view'] = array(ACL_SIGNED => true);
392                 $perm['edit'] = array(ACL_SIGNED => true);
393             }
394         }
395         return $perm;
396     }
397
398     /**
399      * returns list of all supported access types.
400      */
401     function accessTypes() {
402         return array_keys($this->defaultPerms());
403     }
404
405     /**
406      * special permissions for dot-files, beginning with '.'
407      * maybe also for '_' files?
408      */
409     function dotPerms() {
410         $def = array(ACL_ADMIN => true,
411                      ACL_OWNER => true);
412         $perm = array();
413         foreach ($this->accessTypes() as $access) {
414             $perm[$access] = $def;
415         }
416         return $perm;
417     }
418
419     /**
420      *  dead code. not needed inside the object. see getPagePermissions($page)
421      */
422     function retrieve($page) {
423         $hash = $page->get('perm');
424         if ($hash)  // hash => object
425             return new PagePermission(unserialize($hash));
426         else 
427             return new PagePermission();
428     }
429
430     function store($page) {
431         // object => hash
432         return $page->set('perm',serialize(obj2hash($this->perm)));
433     }
434
435     /* type: page, default, inherited */
436     function asTable($type) {
437         $table = HTML::table();
438         foreach ($this->perm as $access => $perms) {
439             $td = HTML::table(array('class' => 'cal','valign' => 'top'));
440             foreach ($perms as $group => $bool) {
441                 $td->pushContent(HTML::tr(HTML::td(array('align'=>'right'),$group),
442                                                    HTML::td($bool ? '[X]' : '[ ]')));
443             }
444             $table->pushContent(HTML::tr(array('valign' => 'top'),
445                                          HTML::td($access),HTML::td($td)));
446         }
447         if ($type == 'default')
448             $table->setAttr('style','border: dotted thin black; background-color:#eee;');
449         elseif ($type == 'inherited')
450             $table->setAttr('style','border: dotted thin black; background-color:#ddd;');
451         elseif ($type == 'page')
452             $table->setAttr('style','border: solid thin black; font-weight: bold;');
453         return $table;
454     }
455
456     /* type: page, default, inherited */
457     function asEditableTable($type) {
458         $table = HTML::table();
459         $table->pushContent(HTML::tr(array('valign' => 'top'),
460                                      HTML::th(array('align' => 'left'),'access'),
461                                      HTML::th(array('align'=>'right'),
462                                               'Group/User'),
463                                      HTML::th(),
464                                      HTML::th('Description')));
465         foreach ($this->perm as $access => $perms) {
466             //$permlist = HTML::table(array('class' => 'cal','valign' => 'top'));
467             $first_only = true;
468             foreach ($perms as $group => $bool) {
469                 $checkbox = HTML::input(array('type' => 'checkbox',
470                                               'name' => "acl[$access][$group]",
471                                               'value' => true));
472                 if ($bool) $checkbox->setAttr('checked','checked');
473                 if ($first_only) {
474                     $table->pushContent(HTML::tr(array('valign' => 'top'),
475                                                  HTML::td(HTML::strong($access.":")),
476                                                  HTML::td(array('class' => 'cal-today','align'=>'right'),
477                                                           constant("GROUP".$group)),
478                                                  HTML::td($checkbox),
479                                                  HTML::td(HTML::em(getAccessDescription($access)))));
480                     $first_only = false;
481                 } else {
482                     $table->pushContent(HTML::tr(array('valign' => 'top'),
483                                                  HTML::td(),
484                                                  HTML::td(array('class' => 'cal-today','align'=>'right'),
485                                                           constant("GROUP".$group)),
486                                                  HTML::td($checkbox),
487                                                  HTML::td()));
488                 }
489             }
490             /*
491             $table->pushContent(HTML::tr(array('valign' => 'top'),
492                                          HTML::td(HTML::strong($access)),
493                                          HTML::td($permlist),
494                                          HTML::td(HTML::em(getAccessDescription($access)))));
495             */
496         }
497         if ($type == 'default')
498             $table->setAttr('style','border: dotted thin black; background-color:#eee;');
499         elseif ($type == 'inherited')
500             $table->setAttr('style','border: dotted thin black; background-color:#ddd;');
501         elseif ($type == 'page')
502             $table->setAttr('style','border: solid thin black; font-weight: bold;');
503         return $table;
504     }
505
506     // this is just a bad hack for testing
507     // simplify the ACL to a unix-like "rwx------" string
508     function asRwxString($owner,$group=false) {
509         global $request;
510         // simplify object => rwxrw---x+ string as in cygwin (+ denotes additional ACLs)
511         $perm =& $this->perm;
512         // get effective user and group
513         $s = '---------';
514         if (isset($perm['view'][$owner]) or 
515             (isset($perm['view'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
516             $s[0] = 'r';
517         if (isset($perm['edit'][$owner]) or 
518             (isset($perm['edit'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
519             $s[1] = 'w';
520         if (isset($perm['change'][$owner]) or 
521             (isset($perm['change'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
522             $s[2] = 'x';
523         if (!empty($group)) {
524             if (isset($perm['view'][$group]) or 
525                 (isset($perm['view'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
526                 $s[3] = 'r';
527             if (isset($perm['edit'][$group]) or 
528                 (isset($perm['edit'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
529                 $s[4] = 'w';
530             if (isset($perm['change'][$group]) or 
531                 (isset($perm['change'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
532                 $s[5] = 'x';
533         }
534         if (isset($perm['view'][ACL_EVERY]) or 
535             (isset($perm['view'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
536             $s[6] = 'r';
537         if (isset($perm['edit'][ACL_EVERY]) or 
538             (isset($perm['edit'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
539             $s[7] = 'w';
540         if (isset($perm['change'][ACL_EVERY]) or 
541             (isset($perm['change'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
542             $s[8] = 'x';
543         return $s;
544     }
545 }
546
547 // $Log: not supported by cvs2svn $
548 // Revision 1.10  2004/04/29 22:32:56  zorloc
549 // Slightly more elegant fix.  Instead of WIKIAUTH_FORBIDDEN, the current user's level + 1 is returned on a false.
550 //
551 // Revision 1.9  2004/04/29 17:18:19  zorloc
552 // 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.
553 //
554 // Revision 1.8  2004/03/14 16:24:35  rurban
555 // authenti(fi)cation spelling
556 //
557 // Revision 1.7  2004/02/28 22:25:07  rurban
558 // First PagePerm implementation:
559 //
560 // $Theme->setAnonEditUnknownLinks(false);
561 //
562 // Layout improvement with dangling links for mostly closed wiki's:
563 // If false, only users with edit permissions will be presented the
564 // special wikiunknown class with "?" and Tooltip.
565 // If true (default), any user will see the ?, but will be presented
566 // the PrintLoginForm on a click.
567 //
568 // Revision 1.6  2004/02/24 15:20:05  rurban
569 // fixed minor warnings: unchecked args, POST => Get urls for sortby e.g.
570 //
571 // Revision 1.5  2004/02/23 21:30:25  rurban
572 // more PagePerm stuff: (working against 1.4.0)
573 //   ACL editing and simplification of ACL's to simple rwx------ string
574 //   not yet working.
575 //
576 // Revision 1.4  2004/02/12 13:05:36  rurban
577 // Rename functional for PearDB backend
578 // some other minor changes
579 // SiteMap comes with a not yet functional feature request: includepages (tbd)
580 //
581 // Revision 1.3  2004/02/09 03:58:12  rurban
582 // for now default DB_SESSION to false
583 // PagePerm:
584 //   * not existing perms will now query the parent, and not
585 //     return the default perm
586 //   * added pagePermissions func which returns the object per page
587 //   * added getAccessDescription
588 // WikiUserNew:
589 //   * added global ->prepare (not yet used) with smart user/pref/member table prefixing.
590 //   * force init of authdbh in the 2 db classes
591 // main:
592 //   * fixed session handling (not triple auth request anymore)
593 //   * don't store cookie prefs with sessions
594 // stdlib: global obj2hash helper from _AuthInfo, also needed for PagePerm
595 //
596 // Revision 1.2  2004/02/08 13:17:48  rurban
597 // This should be the functionality. Needs testing and some minor todos.
598 //
599 // Revision 1.1  2004/02/08 12:29:30  rurban
600 // initial version, not yet hooked into lib/main.php
601 //
602 //
603
604 // Local Variables:
605 // mode: php
606 // tab-width: 8
607 // c-basic-offset: 4
608 // c-hanging-comment-ender-p: nil
609 // indent-tabs-mode: nil
610 // End:
611 ?>