2 rcs_id('$Id: PagePerm.php,v 1.17 2004-05-16 23:10:44 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 may be inherited from its parent pages, a optional the
30 optional master page ("."), and predefined default permissions, if "."
32 Pagenames starting with "." have special default permissions.
33 For Authentication see WikiUserNew.php, WikiGroup.php and main.php
34 Page Permssions are in PhpWiki since v1.3.9 and enabled since v1.4.0
36 This file might replace the following functions from main.php:
37 Request::_notAuthorized($require_level)
38 display the denied message and optionally a login form
39 to gain higher privileges
40 Request::getActionDescription($action)
41 helper to localize the _notAuthorized message per action,
43 Request::getDisallowedActionDescription($action)
44 helper to localize the _notAuthorized message per action,
46 Request::requiredAuthority($action)
47 returns the needed user level
48 has a hook for plugins on POST
49 Request::requiredAuthorityForAction($action)
50 just returns the level per action, will be replaced with the
53 The defined main.php actions map to simplier access types:
56 create => edit or create
60 list in PageList => list
63 /* Symbolic special ACL groups. Untranslated to be stored in page metadata*/
64 define('ACL_EVERY', '_EVERY');
65 define('ACL_ANONYMOUS', '_ANONYMOUS');
66 define('ACL_BOGOUSER', '_BOGOUSER');
67 define('ACL_HASHOMEPAGE', '_HASHOMEPAGE');
68 define('ACL_SIGNED', '_SIGNED');
69 define('ACL_AUTHENTICATED','_AUTHENTICATED');
70 define('ACL_ADMIN', '_ADMIN');
71 define('ACL_OWNER', '_OWNER');
72 define('ACL_CREATOR', '_CREATOR');
74 // Return an page permissions array for this page.
75 // To provide ui helpers to view and change page permissions:
76 // <tr><th>Group</th><th>Access</th><th>Allow or Forbid</th></tr>
77 // <tr><td>$group</td><td>_($access)</td><td> [ ] </td></tr>
78 function pagePermissions($pagename) {
80 $page = $request->getPage($pagename);
81 // Page not found (new page); returned inherited permissions, to be displayed in gray
82 if (! $page->exists() ) {
83 if ($pagename == '.') // stop recursion
84 return array('default',new PagePermission());
86 return array('inherited',pagePermissions(getParentPage($pagename)));
88 } elseif ($perm = getPagePermissions($page)) {
89 return array('page',$perm);
90 // or no permissions defined; returned inherited permissions, to be displayed in gray
92 return array('inherited',pagePermissions(getParentPage($pagename)));
96 function pagePermissionsSimpleFormat($perm_tree,$owner,$group=false) {
97 list($type,$perm) = pagePermissionsAcl($perm_tree[0], $perm_tree);
99 $type = $perm_tree[0];
100 $perm = pagePermissionsAcl($perm_tree);
101 if (is_object($perm_tree[1]))
102 $perm = $perm_tree[1];
103 elseif (is_array($perm_tree[1])) {
104 $perm_tree = pagePermissionsSimpleFormat($perm_tree[1],$owner,$group);
105 if (isa($perm_tree[1],'pagepermission'))
106 $perm = $perm_tree[1];
107 elseif (isa($perm_tree,'htmlelement'))
112 return HTML::tt(HTML::bold($perm->asRwxString($owner,$group).'+'));
113 elseif ($type == 'default')
114 return HTML::tt($perm->asRwxString($owner,$group));
115 elseif ($type == 'inherited') {
116 return HTML::tt(array('class'=>'inherited','style'=>'color:#aaa;'),
117 $perm->asRwxString($owner,$group));
121 function pagePermissionsAcl($type,$perm_tree) {
122 $perm = $perm_tree[1];
123 while (!is_object($perm)) {
124 $perm_tree = pagePermissionsAcl($type, $perm);
125 $perm = $perm_tree[1];
127 return array($type,$perm);
132 function pagePermissionsAclFormat($perm_tree, $editable=false) {
133 list($type,$perm) = pagePermissionsAcl($perm_tree[0], $perm_tree);
135 return $perm->asEditableTable($type);
137 return $perm->asTable($type);
141 * Check permission per page.
142 * Returns true or false.
144 function mayAccessPage ($access, $pagename) {
145 return _requiredAuthorityForPagename($access, $pagename);
148 /** Check the permissions for the current action.
149 * Walk down the inheritance tree. Collect all permissions until
150 * the minimum required level is gained, which is not
151 * overruled by more specific forbid rules.
152 * Todo: cache result per access and page in session?
154 function requiredAuthorityForPage ($action) {
155 $auth = _requiredAuthorityForPagename(action2access($action),
156 $GLOBALS['request']->getArg('pagename'));
157 assert($auth !== -1);
159 return $GLOBALS['request']->_user->_level;
161 return WIKIAUTH_UNOBTAINABLE;
164 // Translate action or plugin to the simplier access types:
165 function action2access ($action) {
184 $page = $request->getPage();
185 $current = $page->getCurrentRevision();
186 if ($current->hasDefaultContents())
193 // probably create/edit but we cannot check all page permissions, can we?
200 //Todo: Plugins should be able to override its access type
201 if (isWikiWord($action))
209 // Recursive helper to do the real work
210 function _requiredAuthorityForPagename($access, $pagename) {
212 $page = $request->getPage($pagename);
213 // Page not found; check against default permissions
214 if (! $page->exists() ) {
215 $perm = new PagePermission();
216 return ($perm->isAuthorized($access,$request->_user) === true);
218 // no ACL defined; check for special dotfile or walk down
219 if (! ($perm = getPagePermissions($page))) {
220 if ($pagename[0] == '.') {
221 $perm = new PagePermission(PagePermission::dotPerms());
222 return ($perm->isAuthorized($access,$request->_user) === true);
224 return _requiredAuthorityForPagename($access,getParentPage($pagename));
226 // ACL defined; check if isAuthorized returns true or false or undecided
227 $authorized = $perm->isAuthorized($access,$request->_user);
228 if ($authorized != -1) // -1 for undecided
231 return _requiredAuthorityForPagename($access,getParentPage($pagename));
235 * @param string $pagename page from which the parent page is searched.
236 * @return string parent pagename or the (possibly pseudo) dot-pagename.
238 function getParentPage($pagename) {
239 if (isSubPage($pagename)) {
240 return subPageSlice($pagename,0);
246 // Read the ACL from the page
247 // Done: Not existing pages should NOT be queried.
248 // Check the parent page instead and don't take the default ACL's
249 function getPagePermissions ($page) {
250 if ($hash = $page->get('perm')) // hash => object
251 return new PagePermission(unserialize($hash));
256 // Store the ACL in the page
257 function setPagePermissions ($page,$perm) {
261 function getAccessDescription($access) {
262 static $accessDescriptions;
263 if (! $accessDescriptions) {
264 $accessDescriptions = array(
265 'list' => _("List this page and all subpages"),
266 'view' => _("View this page and all subpages"),
267 'edit' => _("Edit this page and all subpages"),
268 'create' => _("Create a new (sub)page"),
269 'dump' => _("Download the page contents"),
270 'change' => _("Change page attributes"),
271 'remove' => _("Remove this page"),
274 if (in_array($access, array_keys($accessDescriptions)))
275 return $accessDescriptions[$access];
281 function array_diff_assoc_recursive($array1, $array2) {
282 foreach ($array1 as $key => $value) {
283 if (is_array($value)) {
284 if (!is_array($array2[$key])) {
285 $difference[$key] = $value;
287 $new_diff = array_diff_assoc_recursive($value, $array2[$key]);
288 if ($new_diff != false) {
289 $difference[$key] = $new_diff;
292 } elseif(!isset($array2[$key]) || $array2[$key] != $value) {
293 $difference[$key] = $value;
296 return !isset($difference) ? 0 : $difference;
300 * The ACL object per page. It is stored in a page, but can also
301 * be merged with ACL's from other pages or taken from the master (pseudo) dot-file.
303 * A hash of "access" => "requires" pairs.
304 * "access" is a shortcut for common actions, which map to main.php actions
305 * "requires" required username or groupname or any special group => true or false
307 * Define any special rules here, like don't list dot-pages.
309 class PagePermission {
312 function PagePermission($hash = array()) {
313 $this->_group = &WikiGroup::getGroup($GLOBALS['request']);
314 if (is_array($hash) and !empty($hash)) {
315 $accessTypes = $this->accessTypes();
316 foreach ($hash as $access => $requires) {
317 if (in_array($access,$accessTypes))
318 $this->perm[$access] = $requires;
320 trigger_error(sprintf(_("Unsupported ACL access type %s ignored."),$access),
324 // set default permissions, the so called dot-file acl's
325 $this->perm = $this->defaultPerms();
331 * The workhorse to check the user against the current ACL pairs.
332 * Must translate the various special groups to the actual users settings
333 * (userid, group membership).
335 function isAuthorized($access,$user) {
336 if (!empty($this->perm{$access})) {
337 foreach ($this->perm[$access] as $group => $bool) {
338 if ($this->isMember($user,$group)) {
343 return -1; // undecided
347 * Translate the various special groups to the actual users settings
348 * (userid, group membership).
350 function isMember($user, $group) {
352 if ($group === ACL_EVERY) return true;
353 if (!isset($this->_group)) $member =& WikiGroup::getGroup($request);
354 else $member =& $this->_group;
355 //$user = & $request->_user;
356 if ($group === ACL_ADMIN) // WIKI_ADMIN or member of _("Administrators")
357 return $user->isAdmin() or
358 ($user->isAuthenticated() and
359 $member->isMember(GROUP_ADMIN));
360 if ($group === ACL_ANONYMOUS)
361 return ! $user->isSignedIn();
362 if ($group === ACL_BOGOUSER)
364 return isa($user,'_BogoUser') or
365 (isWikiWord($user->_userid) and $user->_level >= WIKIAUTH_BOGO);
366 else return isWikiWord($user->UserName());
367 if ($group === ACL_HASHOMEPAGE)
368 return $user->hasHomePage();
369 if ($group === ACL_SIGNED)
370 return $user->isSignedIn();
371 if ($group === ACL_AUTHENTICATED)
372 return $user->isAuthenticated();
373 if ($group === ACL_OWNER) {
374 $page = $request->getPage();
375 return ($user->isAuthenticated() and
376 $page->getOwner() === $user->UserName());
378 if ($group === ACL_CREATOR) {
379 $page = $request->getPage();
380 return ($user->isAuthenticated() and
381 $page->getCreator() === $user->UserName());
383 /* Or named groups or usernames.
384 Note: We don't seperate groups and users here.
385 Users overrides groups with the same name.
387 return $user->UserName() === $group or
388 $member->isMember($group);
392 * returns hash of default permissions.
393 * check if the page '.' exists and returns this instead.
395 function defaultPerms() {
396 //Todo: check for the existance of '.' and take this instead.
397 //Todo: honor more config.ini auth settings here
398 $perm = array('view' => array(ACL_EVERY => true),
399 'edit' => array(ACL_EVERY => true),
400 'create' => array(ACL_EVERY => true),
401 'list' => array(ACL_EVERY => true),
402 'remove' => array(ACL_ADMIN => true,
404 'change' => array(ACL_ADMIN => true,
407 $perm['dump'] = array(ACL_ADMIN => true,
410 $perm['dump'] = array(ACL_EVERY => true);
412 if (!ALLOW_ANON_USER) {
413 if (!ALLOW_USER_PASSWORDS)
414 $perm['view'] = array(ACL_SIGNED => true);
416 $perm['view'] = array(ACL_AUTHENTICATED => true);
417 $perm['view'][ACL_BOGOUSER] = ALLOW_BOGO_LOGIN ? true : false;
420 if (!ALLOW_ANON_EDIT) {
421 if (!ALLOW_USER_PASSWORDS)
422 $perm['edit'] = array(ACL_SIGNED => true);
424 $perm['edit'] = array(ACL_AUTHENTICATED => true);
425 $perm['edit'][ACL_BOGOUSER] = ALLOW_BOGO_LOGIN ? true : false;
426 $perm['create'] = $perm['edit'];
432 foreach ($this->perm as $access => $groups) {
433 foreach ($groups as $group => $bool) {
434 $this->perm[$access][$group] = (boolean) $bool;
440 * do a recursive comparison
442 function equal($otherperm) {
443 $diff = array_diff_assoc_recursive($this->perm, $otherperm);
448 * returns list of all supported access types.
450 function accessTypes() {
451 return array_keys($this->defaultPerms());
455 * special permissions for dot-files, beginning with '.'
456 * maybe also for '_' files?
458 function dotPerms() {
459 $def = array(ACL_ADMIN => true,
462 foreach ($this->accessTypes() as $access) {
463 $perm[$access] = $def;
469 * dead code. not needed inside the object. see getPagePermissions($page)
471 function retrieve($page) {
472 $hash = $page->get('perm');
473 if ($hash) // hash => object
474 $perm = new PagePermission(unserialize($hash));
476 $perm = new PagePermission();
481 function store($page) {
484 return $page->set('perm',serialize($this->perm));
487 function groupName ($group) {
488 if ($group[0] == '_') return constant("GROUP".$group);
492 /* type: page, default, inherited */
493 function asTable($type) {
494 $table = HTML::table();
495 foreach ($this->perm as $access => $perms) {
496 $td = HTML::table(array('class' => 'cal','valign' => 'top'));
497 foreach ($perms as $group => $bool) {
498 $td->pushContent(HTML::tr(HTML::td(array('align'=>'right'),$group),
499 HTML::td($bool ? '[X]' : '[ ]')));
501 $table->pushContent(HTML::tr(array('valign' => 'top'),
502 HTML::td($access),HTML::td($td)));
504 if ($type == 'default')
505 $table->setAttr('style','border: dotted thin black; background-color:#eee;');
506 elseif ($type == 'inherited')
507 $table->setAttr('style','border: dotted thin black; background-color:#ddd;');
508 elseif ($type == 'page')
509 $table->setAttr('style','border: solid thin black; font-weight: bold;');
513 /* type: page, default, inherited */
514 function asEditableTable($type) {
516 if (!isset($this->_group)) {
517 $this->_group =& WikiGroup::getGroup($GLOBALS['request']);
519 $table = HTML::table();
520 $table->pushContent(HTML::tr(
521 HTML::th(array('align' => 'left'),
523 HTML::th(array('align'=>'right'),
525 HTML::th(_("Grant")),
526 HTML::th(_("Del/+")),
527 HTML::th(_("Description"))));
529 $allGroups = $this->_group->_specialGroups();
530 foreach ($this->_group->getAllGroupsIn() as $group) {
531 if (!in_array($group,$this->_group->specialGroups()))
532 $allGroups[] = $group;
534 //array_unique(array_merge($this->_group->getAllGroupsIn(),
535 $deletesrc = $Theme->_findData('images/delete.png');
536 $addsrc = $Theme->_findData('images/add.png');
537 $nbsp = HTML::raw(' ');
538 foreach ($this->perm as $access => $groups) {
539 //$permlist = HTML::table(array('class' => 'cal','valign' => 'top'));
541 $newperm = HTML::input(array('type' => 'checkbox',
542 'name' => "acl[_new_perm][$access]",
544 $addbutton = HTML::input(array('type' => 'checkbox',
545 'name' => "acl[_add_group][$access]",
548 'title' => _("Add this ACL"),
550 $newgroup = HTML::select(array('name' => "acl[_new_group][$access]",
551 'style'=> 'text-align: right;',
553 foreach ($allGroups as $groupname) {
554 if (!isset($groups[$groupname]))
555 $newgroup->pushContent(HTML::option(array('value' => $groupname),
556 $this->groupName($groupname)));
558 if (empty($groups)) {
559 $addbutton->setAttr('checked','checked');
560 $newperm->setAttr('checked','checked');
562 HTML::tr(array('valign' => 'top'),
563 HTML::td(HTML::strong($access.":")),
565 HTML::td($nbsp,$newperm),
566 HTML::td($nbsp,$addbutton),
567 HTML::td(HTML::em(getAccessDescription($access)))));
569 foreach ($groups as $group => $bool) {
570 $checkbox = HTML::input(array('type' => 'checkbox',
571 'name' => "acl[$access][$group]",
572 'title' => _("Allow / Deny"),
574 if ($bool) $checkbox->setAttr('checked','checked');
575 $checkbox = HTML(HTML::input(array('type' => 'hidden',
576 'name' => "acl[$access][$group]",
579 $deletebutton = HTML::input(array('type' => 'checkbox',
580 'name' => "acl[_del_group][$access][$group]",
581 'style' => 'background: #aaa url('.$deletesrc.')',
582 //'src' => $deletesrc,
584 'title' => _("Delete this ACL"),
589 HTML::td(HTML::strong($access.":")),
590 HTML::td(array('class' => 'cal-today','align'=>'right'),
591 HTML::strong($this->groupName($group))),
592 HTML::td(array('align'=>'center'),$nbsp,$checkbox),
593 HTML::td(array('align'=>'right','style' => 'background: #aaa url('.$deletesrc.') no-repeat'),$deletebutton),
594 HTML::td(HTML::em(getAccessDescription($access)))));
600 HTML::td(array('class' => 'cal-today','align'=>'right'),
601 HTML::strong($this->groupName($group))),
602 HTML::td(array('align'=>'center'),$nbsp,$checkbox),
603 HTML::td(array('align'=>'right','style' => 'background: #aaa url('.$deletesrc.') no-repeat'),$deletebutton),
609 HTML::tr(array('valign' => 'top'),
610 HTML::td(array('align'=>'right'),_("add ")),
612 HTML::td(array('align'=>'center'),$nbsp,$newperm),
613 HTML::td(array('align'=>'right','style' => 'background: #ccc url('.$addsrc.') no-repeat'),$addbutton),
614 HTML::td(HTML::small(_("Check to add this Acl")))));
616 if ($type == 'default')
617 $table->setAttr('style','border: dotted thin black; background-color:#eee;');
618 elseif ($type == 'inherited')
619 $table->setAttr('style','border: dotted thin black; background-color:#ddd;');
620 elseif ($type == 'page')
621 $table->setAttr('style','border: solid thin black; font-weight: bold;');
625 // this is just a bad hack for testing
626 // simplify the ACL to a unix-like "rwx------" string
627 function asRwxString($owner,$group=false) {
629 // simplify object => rwxrw---x+ string as in cygwin (+ denotes additional ACLs)
630 $perm =& $this->perm;
631 // get effective user and group
633 if (isset($perm['view'][$owner]) or
634 (isset($perm['view'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
636 if (isset($perm['edit'][$owner]) or
637 (isset($perm['edit'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
639 if (isset($perm['change'][$owner]) or
640 (isset($perm['change'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
642 if (!empty($group)) {
643 if (isset($perm['view'][$group]) or
644 (isset($perm['view'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
646 if (isset($perm['edit'][$group]) or
647 (isset($perm['edit'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
649 if (isset($perm['change'][$group]) or
650 (isset($perm['change'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
653 if (isset($perm['view'][ACL_EVERY]) or
654 (isset($perm['view'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
656 if (isset($perm['edit'][ACL_EVERY]) or
657 (isset($perm['edit'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
659 if (isset($perm['change'][ACL_EVERY]) or
660 (isset($perm['change'][ACL_AUTHENTICATED]) and $request->_user->isAuthenticated()))
666 // $Log: not supported by cvs2svn $
667 // Revision 1.16 2004/05/16 22:32:53 rurban
670 // Revision 1.15 2004/05/16 22:07:35 rurban
671 // check more config-default and predefined constants
672 // various PagePerm fixes:
673 // fix default PagePerms, esp. edit and view for Bogo and Password users
674 // implemented Creator and Owner
675 // BOGOUSERS renamed to BOGOUSER
676 // fixed syntax errors in signin.tmpl
678 // Revision 1.14 2004/05/15 22:54:49 rurban
679 // fixed important WikiDB bug with DEBUG > 0: wrong assertion
680 // improved SetAcl (works) and PagePerms, some WikiGroup helpers.
682 // Revision 1.13 2004/05/15 19:48:33 rurban
683 // fix some too loose PagePerms for signed, but not authenticated users
684 // (admin, owner, creator)
685 // no double login page header, better login msg.
686 // moved action_pdf to lib/pdf.php
688 // Revision 1.12 2004/05/04 22:34:25 rurban
691 // Revision 1.11 2004/05/02 21:26:38 rurban
692 // limit user session data (HomePageHandle and auth_dbi have to invalidated anyway)
693 // because they will not survive db sessions, if too large.
694 // extended action=upgrade
695 // some WikiTranslation button work
696 // revert WIKIAUTH_UNOBTAINABLE (need it for main.php)
697 // some temp. session debug statements
699 // Revision 1.10 2004/04/29 22:32:56 zorloc
700 // Slightly more elegant fix. Instead of WIKIAUTH_FORBIDDEN, the current user's level + 1 is returned on a false.
702 // Revision 1.9 2004/04/29 17:18:19 zorloc
703 // 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.
705 // Revision 1.8 2004/03/14 16:24:35 rurban
706 // authenti(fi)cation spelling
708 // Revision 1.7 2004/02/28 22:25:07 rurban
709 // First PagePerm implementation:
711 // $Theme->setAnonEditUnknownLinks(false);
713 // Layout improvement with dangling links for mostly closed wiki's:
714 // If false, only users with edit permissions will be presented the
715 // special wikiunknown class with "?" and Tooltip.
716 // If true (default), any user will see the ?, but will be presented
717 // the PrintLoginForm on a click.
719 // Revision 1.6 2004/02/24 15:20:05 rurban
720 // fixed minor warnings: unchecked args, POST => Get urls for sortby e.g.
722 // Revision 1.5 2004/02/23 21:30:25 rurban
723 // more PagePerm stuff: (working against 1.4.0)
724 // ACL editing and simplification of ACL's to simple rwx------ string
727 // Revision 1.4 2004/02/12 13:05:36 rurban
728 // Rename functional for PearDB backend
729 // some other minor changes
730 // SiteMap comes with a not yet functional feature request: includepages (tbd)
732 // Revision 1.3 2004/02/09 03:58:12 rurban
733 // for now default DB_SESSION to false
735 // * not existing perms will now query the parent, and not
736 // return the default perm
737 // * added pagePermissions func which returns the object per page
738 // * added getAccessDescription
740 // * added global ->prepare (not yet used) with smart user/pref/member table prefixing.
741 // * force init of authdbh in the 2 db classes
743 // * fixed session handling (not triple auth request anymore)
744 // * don't store cookie prefs with sessions
745 // stdlib: global obj2hash helper from _AuthInfo, also needed for PagePerm
747 // Revision 1.2 2004/02/08 13:17:48 rurban
748 // This should be the functionality. Needs testing and some minor todos.
750 // Revision 1.1 2004/02/08 12:29:30 rurban
751 // initial version, not yet hooked into lib/main.php
759 // c-hanging-comment-ender-p: nil
760 // indent-tabs-mode: nil