diff --git a/wire/core/ProcessWire.php b/wire/core/ProcessWire.php index 5bb1fcfb..6b93b2ed 100644 --- a/wire/core/ProcessWire.php +++ b/wire/core/ProcessWire.php @@ -45,7 +45,7 @@ class ProcessWire extends Wire { * Reversion revision number * */ - const versionRevision = 80; + const versionRevision = 81; /** * Version suffix string (when applicable) diff --git a/wire/core/Role.php b/wire/core/Role.php index 2e6573e2..806f26f0 100644 --- a/wire/core/Role.php +++ b/wire/core/Role.php @@ -24,6 +24,8 @@ class Role extends Page { /** * Create a new Role page in memory. + * + * @param Template $tpl * */ public function __construct(Template $tpl = null) { diff --git a/wire/core/Template.php b/wire/core/Template.php index d7a25ac5..ab4ff8e7 100644 --- a/wire/core/Template.php +++ b/wire/core/Template.php @@ -362,7 +362,7 @@ class Template extends WireData implements Saveable, Exportable { * - `edit` * - `create` * - `add` - * - Or a `Permission` object + * - Or a `Permission` object of `page-view` or `page-edit` * @return bool True if template has the role, false if not * */ @@ -402,12 +402,12 @@ class Template extends WireData implements Saveable, Exportable { * #pw-group-manipulation * * @param array|PageArray $value Role objects or array or Role IDs. - * @param string Specify one of the following: + * @param string $type Specify one of the following: * - `view` (default) * - `edit` * - `create` * - `add` - * - Or a `Permission` object + * - Or a `Permission` object of `page-view` or `page-edit` * */ public function setRoles($value, $type = 'view') { @@ -433,6 +433,40 @@ class Template extends WireData implements Saveable, Exportable { } } + /** + * Add a permission that applies to users having a specific role with pages using this template + * + * Note that the change is not committed until you save() the template. + * + * @param Permission|int|string $permission Permission object, name, or id + * @param Role|int|string $role Role object, name or id + * @param bool $test Specify true to only test if an update would be made, without changing anything + * @return bool Returns true if an update was made (or would be made), false if not + * + */ + public function addPermissionByRole($permission, $role, $test = false) { + return $this->wire('templates')->setTemplatePermissionByRole($this, $permission, $role, false, $test); + } + + /** + * Revoke a permission that applies to users having a specific role with pages using this template + * + * Note that the change is not committed until you save() the template. + * + * @param Permission|int|string $permission Permission object, name, or id + * @param Role|int|string $role Role object, name or id + * @param bool $test Specify true to only test if an update would be made, without changing anything + * @return bool Returns true if an update was made (or would be made), false if not + * + */ + public function revokePermissionByRole($permission, $role, $test = false) { + return $this->wire('templates')->setTemplatePermissionByRole($this, $permission, $role, true, $test); + } + + public function hasPermissionByRole($permission, $role) { + + } + /** * Does this template have the given Field? * @@ -748,7 +782,7 @@ class Template extends WireData implements Saveable, Exportable { * * #pw-group-manipulation * - * @return $this|bool Returns Template if successful, or false if not + * @return Template|bool Returns Template if successful, or false if not * */ public function save() { @@ -809,7 +843,7 @@ class Template extends WireData implements Saveable, Exportable { * */ public function hookFinished(HookEvent $e) { - foreach($this->wire('templates') as $template) { + foreach($e->wire('templates') as $template) { if($template->isChanged('modified') || $template->isChanged('ns')) $template->save(); } } @@ -1041,7 +1075,7 @@ class Template extends WireData implements Saveable, Exportable { * * #pw-group-identification * - * @param $icon Font-awesome icon name + * @param string $icon Font-awesome icon name * @return $this * */ diff --git a/wire/core/Templates.php b/wire/core/Templates.php index 799ebf61..f8c4e419 100644 --- a/wire/core/Templates.php +++ b/wire/core/Templates.php @@ -555,6 +555,88 @@ class Templates extends WireSaveableItems { public function getParentPages(Template $template, $checkAccess = false) { return $this->getParentPage($template, $checkAccess, true); } + + /** + * Set a Permission for a Template for and specific Role + * + * Note: you must also save() the template to commit the change. + * + * #pw-internal + * + * @param Template $template + * @param Permission|string|int $permission + * @param Role|string|int $role + * @param bool $revoke Specify true to revoke the permission, or omit to add the permission + * @param bool $test When true, no changes are made but return value still applicable + * @return bool True if an update was made (or would be made), false if not + * @throws WireException If given unknown Role or Permission + * + */ + public function setTemplatePermissionByRole(Template $template, $permission, $role, $revoke = false, $test = false) { + + if(!$template->useRoles) throw new WireException("Template $template does not have access control enabled"); + + $defaultPermissions = array('page-view', 'page-edit', 'page-create', 'page-add'); + $updated = false; + + if(is_string($role) || is_int($role)) $role = $this->wire('roles')->get($role); + if(!$role instanceof Role) throw new WireException("Unknown role for Template::setPermissionByRole"); + + if(is_string($permission) && in_array($permission, $defaultPermissions)) { + $permissionName = $permission; + } else if($permission instanceof Permission) { + $permissionName = $permission->name; + } else { + $permission = $this->wire('permissions')->get($permission); + $permissionName = $permission ? $permission->name : ''; + } + + if(in_array($permissionName, $defaultPermissions)) { + // use pre-defined view/edit/create/add roles + $roles = $template->getRoles($permissionName); + $has = $roles->has($role); + if($revoke) { + if($has) { + if($test) return true; + $roles->remove($role); + $template->setRoles($roles, $permissionName); + $updated = true; + } + } else if(!$has) { + if($test) return true; + $roles->add($role); + $template->setRoles($roles, $permissionName); + $updated = true; + } + + } else if($permission instanceof Permission) { + $rolesPermissions = $template->get('rolesPermissions'); + if(!is_array($rolesPermissions)) $rolesPermissions = array(); + $rolePermissions = isset($rolesPermissions["$role->id"]) ? $rolesPermissions["$role->id"] : array(); + $_rolePermissions = $rolePermissions; + if($revoke) { + $key = array_search("$permission->id", $rolePermissions); + if($key !== false) unset($rolePermissions[$key]); + if(!in_array("-$permission->id", $rolePermissions)) $rolePermissions[] = "-$permission->id"; + } else { + $key = array_search("-$permission->id", $rolePermissions); + if($key !== false) unset($rolePermissions[$key]); + if(!in_array("$permission->id", $rolePermissions)) $rolePermissions[] = "$permission->id"; + } + if($rolePermissions !== $_rolePermissions) { + if($test) return true; + $rolesPermissions["$role->id"] = $rolePermissions; + $template->set('rolesPermissions', $rolesPermissions); + $updated = true; + } + + } else { + throw new WireException("Unknown permission for Templates::setPermissionByRole"); + } + + return $updated; + } + /** * FUTURE USE: Is the parent/child relationship allowed? diff --git a/wire/modules/Inputfield/InputfieldCheckboxes/InputfieldCheckboxes.module b/wire/modules/Inputfield/InputfieldCheckboxes/InputfieldCheckboxes.module index a082617c..477f138f 100644 --- a/wire/modules/Inputfield/InputfieldCheckboxes/InputfieldCheckboxes.module +++ b/wire/modules/Inputfield/InputfieldCheckboxes/InputfieldCheckboxes.module @@ -44,6 +44,7 @@ class InputfieldCheckboxes extends InputfieldSelectMultiple implements Inputfiel /** @var MarkupAdminDataTable $table */ $table = $this->modules->get("MarkupAdminDataTable"); $table->setEncodeEntities(false); + $table->setSortable(false); $table->addClass('pw-no-select'); if($this->thead) $table->headerRow(explode('|', htmlspecialchars($this->thead, ENT_QUOTES, 'UTF-8'))); diff --git a/wire/modules/Process/ProcessRole/ProcessRole.css b/wire/modules/Process/ProcessRole/ProcessRole.css index 9c9d17e3..df34666d 100644 --- a/wire/modules/Process/ProcessRole/ProcessRole.css +++ b/wire/modules/Process/ProcessRole/ProcessRole.css @@ -6,15 +6,12 @@ label.level2 { padding-left: 2.25em; } -#wrap_Inputfield_permissions td i.fa { - opacity: 0.6; -} #wrap_Inputfield_permissions table td { - width: 70%; + width: 75%; } #wrap_Inputfield_permissions table > tbody > tr > td:first-child { - width: 30%; + width: 25%; } tr:not(.permission32):not(.permission0-page-add):not(.permission0-page-create).permission-checked .permission-added { @@ -58,3 +55,56 @@ p.description + p.description { margin: 0.5em 0; } +.template-permissions { + margin-top: 0.5em; +} +.template-permissions:not(.template-permissions-open) { + display: none; +} +.template-permissions > p { + margin: 0.5em 0; +} +.template-permissions label { + display: block; +} + +.toggle-template-permissions { + display: block; + padding-left: 5px; + padding-right: 5px; +} + +.permission-title { + font-weight: bold; +} + +.page-edit-templates .permission-title { + cursor: pointer; +} + +tr.permission-page-edit:not(.permission-checked) .toggle-template-permissions { + display: none; +} +tr.permission-page-edit:not(.permission-checked) .template-permissions, +tr.permission-page-edit:not(.permission-checked) .template-permissions-open { + display: none !important; +} +tr.permission-checked .template-permissions label.template-permission-add { + display: none; +} +tr.permission-checked .description-not-checked { + display: none; +} +tr:not(.permission-checked) .description-checked { + display: none; +} +tr:not(.permission-checked) .template-permissions label.template-permission-revoke { + display: none; +} +#ProcessPageEdit table.AdminDataTable tr > th, +#ProcessPageEdit table.AdminDataTable tr > td { + /* AdminThemeDefault and AdminThemeReno */ + border-left: none; + border-right: none; +} + diff --git a/wire/modules/Process/ProcessRole/ProcessRole.js b/wire/modules/Process/ProcessRole/ProcessRole.js index 895a6ab4..1c778b19 100644 --- a/wire/modules/Process/ProcessRole/ProcessRole.js +++ b/wire/modules/Process/ProcessRole/ProcessRole.js @@ -2,7 +2,7 @@ function ProcessRoleUpdatePermissions(init, $checkbox) { var $inputfield = $("#wrap_Inputfield_permissions"); - var $checkboxes = $checkbox == null ? $inputfield.find(".permission > input[type=checkbox]") : $checkbox; + var $checkboxes = $checkbox == null ? $inputfield.find("input.global-permission") : $checkbox; if(init) { // update row classes to be the same as the label classes @@ -30,11 +30,21 @@ function ProcessRoleUpdatePermissions(init, $checkbox) { $children = $children.filter(".level" + (level+1)); init ? $children.show() : $children.fadeIn(); $row.addClass('permission-checked'); + if($row.hasClass('permission-page-edit')) { + if(!$row.find('.template-permissions-open').length) { + $row.find('.toggle-template-permissions').click(); + } + } } else { - $children.find("input:not(:disabled)").removeAttr('checked'); + $children.find("input.global-permission:not(:disabled)").removeAttr('checked'); init ? $children.hide() : $children.fadeOut(); $row.removeClass('permission-checked'); + if($row.hasClass('permission-page-edit')) { + if($row.find('.template-permissions-open').length) { + $row.find('.toggle-template-permissions').click(); + } + } } }); } @@ -46,7 +56,7 @@ $(document).ready(function() { ProcessRoleUpdatePermissions(true, null); - $("#wrap_Inputfield_permissions").on("click", "input[type=checkbox], label.checkbox-disabled", function(e) { + $("#wrap_Inputfield_permissions").on("click", "input.global-permission, label.checkbox-disabled", function(e) { if($(this).is("label")) { var $label = $(this); @@ -76,4 +86,36 @@ $(document).ready(function() { } }); + $(".toggle-template-permissions").click(function() { + var $div = $(this).closest('tr').find('.template-permissions'); + if($div.hasClass('template-permissions-open')) { + $div.fadeOut('fast', function() { + $div.removeClass('template-permissions-open'); + }); + } else { + $div.fadeIn('fast', function() { + $div.addClass('template-permissions-open'); + }); + } + var $icon = $(this).find('i'); + $icon.toggleClass($icon.attr('data-toggle')); + return false; + }); + + // make some of the open when page loads + $('.template-permissions-click').each(function() { + $(this).closest('tr').find('.toggle-template-permissions').click(); + $(this).removeClass('template-permissions-click'); + }); + + $('.permission-title').click(function() { + $(this).closest('tr').find('.toggle-template-permissions').click(); + }); + + // ensure checkbox classes are consistent (like for uk-checkbox) + a = $('input.global-permission:eq(0)'); + b = $('
').addClass(a.attr('class')).removeClass('permission permission-checked global-permission'); + c = $('input.template-permission').addClass(b.attr('class')); + }); + diff --git a/wire/modules/Process/ProcessRole/ProcessRole.min.js b/wire/modules/Process/ProcessRole/ProcessRole.min.js index 29215ddc..891ce9f8 100644 --- a/wire/modules/Process/ProcessRole/ProcessRole.min.js +++ b/wire/modules/Process/ProcessRole/ProcessRole.min.js @@ -1 +1 @@ -function ProcessRoleUpdatePermissions(d,b){var a=$("#wrap_Inputfield_permissions");var c=b==null?a.find(".permission > input[type=checkbox]"):b;if(d){c.each(function(){var g=$(this);var f=g.closest("label");var e=g.closest("tr");e.addClass(f.attr("class"))})}c.each(function(){var k=$(this);var f=k.closest("label");var e=k.closest("tr");var i=$("#"+f.attr("data-parent"));var h=f.text();var j=parseInt(f.attr("data-level"));var g=e.nextAll(".parent-"+f.attr("id"));e.addClass(f.attr("id"));if(k.is(":checked")){g=g.filter(".level"+(j+1));d?g.show():g.fadeIn();e.addClass("permission-checked")}else{g.find("input:not(:disabled)").removeAttr("checked");d?g.hide():g.fadeOut();e.removeClass("permission-checked")}})}$(document).ready(function(){var a=$("#Inputfield_permissions_36");if(!a.is(":checked")){a.attr("checked","checked")}ProcessRoleUpdatePermissions(true,null);$("#wrap_Inputfield_permissions").on("click","input[type=checkbox], label.checkbox-disabled",function(f){if($(this).is("label")){var b=$(this);var c=b.children("input")}else{var c=$(this);var b=c.parent()}var g=b.attr("data-alert");var d=b.attr("data-confirm");if(typeof g!="undefined"&&g.length){ProcessWire.alert(g);return false}else{if(typeof d!="undefined"&&d.length){if(c.is(":checked")){if(!confirm(d)){return false}}}}if($(this).is("input")){var c=$(this);setTimeout(function(){ProcessRoleUpdatePermissions(false,c)},100)}})}); \ No newline at end of file +function ProcessRoleUpdatePermissions(g,e){var d=$("#wrap_Inputfield_permissions");var f=e==null?d.find("input.global-permission"):e;if(g){f.each(function(){var j=$(this);var i=j.closest("label");var h=j.closest("tr");h.addClass(i.attr("class"))})}f.each(function(){var n=$(this);var i=n.closest("label");var h=n.closest("tr");var l=$("#"+i.attr("data-parent"));var k=i.text();var m=parseInt(i.attr("data-level"));var j=h.nextAll(".parent-"+i.attr("id"));h.addClass(i.attr("id"));if(n.is(":checked")){j=j.filter(".level"+(m+1));g?j.show():j.fadeIn();h.addClass("permission-checked");if(h.hasClass("permission-page-edit")){if(!h.find(".template-permissions-open").length){h.find(".toggle-template-permissions").click()}}}else{j.find("input.global-permission:not(:disabled)").removeAttr("checked");g?j.hide():j.fadeOut();h.removeClass("permission-checked");if(h.hasClass("permission-page-edit")){if(h.find(".template-permissions-open").length){h.find(".toggle-template-permissions").click()}}}})}$(document).ready(function(){var d=$("#Inputfield_permissions_36");if(!d.is(":checked")){d.attr("checked","checked")}ProcessRoleUpdatePermissions(true,null);$("#wrap_Inputfield_permissions").on("click","input.global-permission, label.checkbox-disabled",function(i){if($(this).is("label")){var f=$(this);var g=f.children("input")}else{var g=$(this);var f=g.parent()}var j=f.attr("data-alert");var h=f.attr("data-confirm");if(typeof j!="undefined"&&j.length){ProcessWire.alert(j);return false}else{if(typeof h!="undefined"&&h.length){if(g.is(":checked")){if(!confirm(h)){return false}}}}if($(this).is("input")){var g=$(this);setTimeout(function(){ProcessRoleUpdatePermissions(false,g)},100)}});$(".toggle-template-permissions").click(function(){var f=$(this).closest("tr").find(".template-permissions");if(f.hasClass("template-permissions-open")){f.fadeOut("fast",function(){f.removeClass("template-permissions-open")})}else{f.fadeIn("fast",function(){f.addClass("template-permissions-open")})}var e=$(this).find("i");e.toggleClass(e.attr("data-toggle"));return false});$(".template-permissions-click").each(function(){$(this).closest("tr").find(".toggle-template-permissions").click();$(this).removeClass("template-permissions-click")});$(".permission-title").click(function(){$(this).closest("tr").find(".toggle-template-permissions").click()});a=$("input.global-permission:eq(0)");b=$("
").addClass(a.attr("class")).removeClass("permission permission-checked global-permission");c=$("input.template-permission").addClass(b.attr("class"))}); \ No newline at end of file diff --git a/wire/modules/Process/ProcessRole/ProcessRole.module b/wire/modules/Process/ProcessRole/ProcessRole.module index 99f94c58..3eb2c342 100644 --- a/wire/modules/Process/ProcessRole/ProcessRole.module +++ b/wire/modules/Process/ProcessRole/ProcessRole.module @@ -16,7 +16,7 @@ class ProcessRole extends ProcessPageType { static public function getModuleInfo() { return array( 'title' => __('Roles', __FILE__), // getModuleInfo title - 'version' => 103, + 'version' => 104, 'summary' => __('Manage user roles and what permissions are attached', __FILE__), // getModuleInfo summary 'permanent' => true, 'permission' => 'role-admin', // add this permission if you want this Process available for roles other than Superuser @@ -26,10 +26,28 @@ class ProcessRole extends ProcessPageType { } protected $icons = array(); - + protected $templatePermissionNotes = array(); + protected $templatePermissionDescriptions = array(); + + /** + * @var Role + * + */ + protected $guestRole; + + /** + * Init and attach hooks + * + */ public function init() { + parent::init(); - $this->addHookBefore('InputfieldForm::render', $this, 'hookFormRender'); + + $this->wire('modules')->get('JqueryUI')->use('vex'); + $this->guestRole = $this->wire('roles')->get($this->wire('config')->guestUserRolePageID); + $this->addHookBefore('InputfieldForm::render', $this, 'hookFormRender'); + $this->addHookBefore('ProcessPageEdit::processInput', $this, 'hookProcessInput'); + $this->icons = array( 'edit' => wireIconMarkup('certificate', 'fw'), 'page' => wireIconMarkup('gear', 'fw'), @@ -38,55 +56,80 @@ class ProcessRole extends ProcessPageType { 'revoke' => wireIconMarkup('minus-circle', 'fw'), 'help' => wireIconMarkup('question-circle'), ); - } + $this->templatePermissionDescriptions = array( + 'page-view' => $this->_('Which types of pages may this role view?'), + 'page-edit' => $this->_('Which types of pages may this role edit?'), + 'page-add' => $this->_('Which types of pages may this role add children to?'), + 'page-create' => $this->_('Which types of pages may this role create?'), + 'default-add' => $this->_('If you want to add {permission} only to specific templates, check the boxes below for the templates you want to add it to, and leave the {permission} permission unchecked.'), + 'default-revoke' => $this->_('The {permission} permission is checked, making it apply to all templates that are editable to the role. To revoke {permission} permission from specific templates, check the boxes below. To add this permission to only specific templates, un-check the {permission} permission first.'), + ); + + $pageEditRequired = $this->_('Note that role must also have page-edit permission for any checked templates above.'); + + $this->templatePermissionNotes = array( + 'default' => $this->_('Most permissions that can be assigned by template also require that the user have page-edit permission to the template. If a template you need is not listed, you must enable access control for it first (see “Access” tab when editing a template).'), + 'page-create' => $pageEditRequired, + 'page-publish' => $pageEditRequired, + 'page-add' => $this->_('Unlike most other permissions, page-edit permission to a template is not a pre-requisite for this permission.'), + 'page-edit' => '', + 'page-view' => '', + ); + + } + + /** + * Hook ProcessPageEdit::processInput to save permission options + * + * @param HookEvent $event + * + */ + public function hookProcessInput(HookEvent $event) { + if($event->wire('input')->post('_pw_page_name')) { + $this->savePermissionOptions(); + } + } + + /** + * Hook before InputfieldForm::render to manipulate output of permissions field + * + * @param HookEvent $event + * + */ public function hookFormRender(HookEvent $event) { + + /** @var Inputfieldform $form */ $form = $event->object; + /** @var InputfieldPage $f */ $f = $form->getChildByName('permissions'); if(!$f) return; + if($this->getPage()->id == $this->wire('config')->superUserRolePageID) { $f->wrapAttr('style', 'display:none'); $fn = $form->getChildByName('_pw_page_name'); if($fn) $fn->notes = $this->_('Note: superuser role always has all permissions, so permissions field is not shown.'); } + $f->entityEncodeText = false; + $f->addClass('global-permission'); $f->label = $this->_('Permissions'); - $f->description = $f->entityEncode(sprintf($this->_('For detailed descriptions of these permissions, please see the [permissions reference](%s).'), 'https://processwire.com/api/user-access/permissions/'), true); // Permissions documentation info - $strikethrough = '' . $this->_('strikethrough') . ''; - $f->appendMarkup = - ""; - + $f->description = $f->entityEncode( + sprintf( + $this->_('For detailed descriptions of these permissions, please see the [permissions reference](%s).'), // Permissions documentation info + 'https://processwire.com/api/user-access/permissions/' + ), true + ); + $f = $f->getInputfield(); - + /** @var InputfieldCheckboxes $f */ $f->table = true; - $f->thead = $this->_('name|description'); // Table head with each column title separated by a pipe "|" + $f->thead = $this->_('name|description| '); // Table head with each column title separated by a pipe "|" $value = $f->attr('value'); $options = $f->getOptions(); $pageViewID = 0; foreach($options as $name => $label) $f->removeOption($name); - $permissions = array(); // establish root permission containers foreach($this->wire('permissions') as $permission) { @@ -98,9 +141,15 @@ class ProcessRole extends ProcessPageType { $permissions[$permission->name]['page-create'] = array(); } } + ksort($permissions); + $pageView = $permissions['page-view']; + $pageEdit = $permissions['page-edit']; + $permissions = array_merge(array('page-view' => $pageView, 'page-edit' => $pageEdit), $permissions); foreach($this->wire('permissions') as $permission) { + /** @var Permission $permission */ + /** @var Permission $parent */ $parent = $permission->getParentPermission(); if(!$parent->id) continue; if(isset($permissions[$parent->name])) { @@ -125,9 +174,20 @@ class ProcessRole extends ProcessPageType { $f->attr('value', $value); } - + + /** + * Add permission options to checkboxes Inputfield + * + * @param array $permissions + * @param Inputfield $f + * @param int $level + * @param $inputfieldValue + * + */ protected function addPermissionOptions(array $permissions, Inputfield $f, $level = 0, &$inputfieldValue) { + /** @var InputfieldCheckboxes $f */ + /** @var Role $role */ $role = $this->getPage(); foreach($permissions as $name => $children) { @@ -138,14 +198,21 @@ class ProcessRole extends ProcessPageType { $revokedTemplates = array(); $disabled = false; $checked = false; + $appliesAllEditable = false; + $templateCheckboxes = array(); + $pageEditTemplates = array(); if($name == 'page-add' || $name == 'page-create') { $parent = $this->wire('permissions')->get('page-edit'); $rootParent = $parent; - $permission = $this->wire('pages')->newNullPage(); - if($name == 'page-add') $title = $this->_('Add children to pages using template'); - else $title = $this->_('Create pages using template'); - $alert = $this->_('This permission can only be assigned by template access settings.'); + $permission = new Permission(); + $permission->set('name', $name); + if($name == 'page-add') { + $title = $this->_('Add children to pages using template'); + } else { + $title = $this->_('Create pages using template'); + } + $alert = $this->_('This permission can only be assigned by template.'); } else { $permission = $this->wire('permissions')->get($name); if(!$permission->id) continue; @@ -155,31 +222,39 @@ class ProcessRole extends ProcessPageType { $checked = in_array($permission->id, $inputfieldValue); } + $title = "$title"; + if($name == 'page-view') { - $title .= " " . $this->_('(required)') . ""; + $title .= $this->renderDetail($this->_('(required)')); $alert = $this->_('This permission is required for all roles.'); } else if($name == 'page-edit') { - $title = $this->icons['edit'] . $title; } if(($parent->name == 'page-edit' || $rootParent->name == 'page-edit') && strpos($name, 'page-') === 0) { if($name == 'page-add' || $name == 'page-create') { - $title = $this->icons['info'] . $title; + $title = $title; } else { - $title = $this->icons['page'] . $title . ' ' . - "(" . $this->_('applies to all editable templates') . ')'; + $appliesAllEditable = true; + $title .= $this->renderDetail('(' . $this->_('applies to all editable templates') . ')', 'permission-all-templates'); } } foreach($this->wire('templates') as $template) { + /** @var Template $template */ + if(!$template->useRoles) continue; + $rolesPermissions = $template->rolesPermissions; + $templateEditURL = "../../../setup/template/edit?id=$template->id#tab_access"; - $templateEditLink = - "" . - $this->icons['add'] . "$template->name"; + $templateEditLink = $this->renderLink($templateEditURL, $this->icons['add'] . $template->name, array( + 'class' => 'tooltip', + 'target' => '_blank', + 'title' => '{tooltip}', + )); if($name == 'page-edit') { if(in_array($role->id, $template->editRoles)) { $addedTemplates[$template->name] = $templateEditLink; + $pageEditTemplates[$template->name] = $template; } } else if($name == 'page-create') { if(in_array($role->id, $template->createRoles)) { @@ -191,27 +266,38 @@ class ProcessRole extends ProcessPageType { $checked = true; $addedTemplates[$template->name] = $templateEditLink; } - } else { - $rolesPermissions = $template->rolesPermissions; - if(!isset($rolesPermissions[$role->id])) continue; + } else if(isset($rolesPermissions[$role->id])) { + // custom added or revoked permission if(in_array($permission->id, $rolesPermissions[$role->id])) { $addedTemplates[$template->name] = $templateEditLink; } else if(in_array($permission->id * -1, $rolesPermissions[$role->id])) { $revokedTemplates[$template->name] = str_replace($this->icons['add'], $this->icons['revoke'], $templateEditLink); } } - } + // if a system template, then do nothing further + if($template->flags & Template::flagSystem) continue; + + if(isset($this->templatePermissionDescriptions[$name]) || $appliesAllEditable) { + // base permission: page-view, page-edit, page-create, page-add + $checked = isset($addedTemplates[$template->name]); + $templateCheckboxes[] = $this->renderTemplatePermissionCheckbox($template, $permission, $checked); + } + } // foreach(templates) + if(count($addedTemplates) || count($revokedTemplates)) { - + // permission was added or revoked from specific templates + + /* foreach($addedTemplates as $templateName => $link) { $tooltip = sprintf($this->_('%1$s added by template %2$s, click to edit'), $name, $templateName); $addedTemplates[$templateName] = str_replace('{tooltip}', $tooltip, $link); } foreach($revokedTemplates as $templateName => $link) { $tooltip = sprintf($this->_('%1$s revoked by template %2$s, click to edit'), $name, $templateName); - $addedTemplates[$templateName] = str_replace('{tooltip}', $tooltip, $link); + $revokedTemplates[$templateName] = str_replace('{tooltip}', $tooltip, $link); } + */ if($name != 'page-edit' && $permission->id) { if(!in_array($permission->id, $inputfieldValue)) { @@ -219,62 +305,394 @@ class ProcessRole extends ProcessPageType { } } + /* if(count($addedTemplates)) { $label = implode(' ', $addedTemplates); - $title .= " $label "; + $title .= $this->renderDetail($label, 'permission-added'); } if(count($revokedTemplates)) { $label = implode(' ', $revokedTemplates); - $title .= " $label"; + $title .= $this->renderDetail($label, 'permission-revoked'); } - - } else if($name == 'page-edit') { - $title .= " (" . $this->_('not currently applied to any templates') . ")"; + */ } + + $classes = array( + "permission", + "permission-$name", + "level$level", + ); - $class = "permission level$level"; $p = $parent; + while($p->id) { - $class .= " parent-permission$p->id"; - $p = $p->getParentPermission($p); + $classes[] = "parent-permission$p->id"; + $classes[] = "parent-permission-$p->name"; + $p = $p->getParentPermission(); } if($permission->id) { $value = $permission->id; $id = "permission$permission->id"; + if($appliesAllEditable) $classes[] = "page-edit-templates"; } else { $value = "0-$name"; $id = "permission0-$name"; $disabled = true; } + if($disabled) $classes[] = 'checkbox-disabled'; + $attributes = array( "id" => $id, - "class" => $class, + "class" => implode(' ', $classes), "data-parent" => "permission$parent->id", "data-level" => $level ); - if(!$permission->id && $checked) $inputfieldValue[] = $value; - if($disabled) { - $attributes['disabled'] = 'disabled'; - $attributes['class'] .= ' checkbox-disabled'; - } + if($disabled) $attributes['disabled'] = 'disabled'; if($alert) $attributes['data-alert'] = $alert; if($confirm) $attributes['data-confirm'] = $confirm; + + if(!$permission->id && $checked) $inputfieldValue[] = $value; /* $title = "" . $this->icons['help'] . "" . $title; */ - - $f->addOption($value, "$name|$title", $attributes); + if(count($templateCheckboxes)) { + $checkboxes = $this->renderTemplatePermissionCheckboxes($permission, $templateCheckboxes); + $toggle = $this->renderTemplatePermissionToggle(); + $f->addOption($value, "$name|$title$checkboxes|$toggle", $attributes); + } else { + $f->addOption($value, "$name|$title| ", $attributes); + } + if(count($children)) { $this->addPermissionOptions($children, $f, $level+1, $inputfieldValue); } + } // foreach(permissions) + } + + /** + * Render a div containing template permission checkboxes + * + * @param Permission $permission + * @param array $checkboxes Array of individually rendered checkboxes for each template + * @return string + * + */ + protected function renderTemplatePermissionCheckboxes(Permission $permission, array $checkboxes) { + + $name = $permission->name; + if(isset($this->templatePermissionNotes[$name])) { + $note = $this->templatePermissionNotes[$name]; + } else { + $note = $this->templatePermissionNotes['default']; + } + + if(isset($this->templatePermissionDescriptions[$name])) { + $desc = + "

" . $this->templatePermissionDescriptions[$name] . "

"; + } else { + $desc = + "

" . + str_replace('{permission}', $name, $this->templatePermissionDescriptions['default-add']) . + "

" . + "

" . + str_replace('{permission}', $name, $this->templatePermissionDescriptions['default-revoke']) . + "

"; + + } + + $class = 'template-permissions'; + $checkboxes = implode('', $checkboxes); + + if(strpos($checkboxes, ' checked ') || in_array($name, array('page-edit', 'page-view', 'page-add', 'page-create'))) { + $class .= ' template-permissions-click'; + } + + return + "
" . + $desc . + "

$checkboxes

" . + ($note ? "

$note

" : "") . + "
"; + } + + /** + * Render a single template permission checkbox + * + * @param Template $template + * @param Permission $permission + * @param bool $checked + * @return string + * + */ + protected function renderTemplatePermissionCheckbox(Template $template, Permission $permission, $checked) { + + $disabled = false; + $note = ''; + + if($permission->name == 'page-view' && $this->guestRole->hasPermission('page-view', $template)) { + $checked = true; + $disabled = true; + $note = $this->_('(inherited from guest role)'); + } + + $checked = $checked ? 'checked' : ''; + $disabled = $disabled ? 'disabled' : ''; + $class = "template-permission template{$template->id}-permission$permission->id"; + + // note: pt=permission+template, tp=template+permission + + if($permission->name == 'page-add') { + $name = "pt_add_$template->id"; + } else if($permission->name == 'page-create') { + $name = "pt_create_$template->id"; + } else if(in_array($permission->name, array('page-edit', 'page-view'))) { + $name = "pt_{$permission->id}_{$template->id}"; + } else { + $name = ''; + } + + if($name) { + // checkbox + $out = + ""; + } else { + // select add or revoke + /** @var Role $role */ + $role = $this->getPage(); + $name = "tp_{$template->id}[]"; + $rolesPermissions = $template->rolesPermissions; + $rolePermissions = isset($rolesPermissions["$role->id"]) ? $rolesPermissions["$role->id"] : array(); + $addChecked = in_array("$permission->id", $rolePermissions) ? 'checked' : ''; + $revokeChecked = in_array("-$permission->id", $rolePermissions) ? 'checked' : ''; + $out = + "" . + ""; + } + + return $out; + } + + /** + * Render the toggle that can trigger the template permission checkboxes + * + * @return string + * + */ + protected function renderTemplatePermissionToggle() { + return + "" . + "" . + ""; + } + + /** + * Render an link + * + * @param string $href + * @param string $text + * @param array $attr + * @return string + * + */ + protected function renderLink($href, $text, array $attr = array()) { + $attr['href'] = $href; + $out = " $value) { + $out .= " $key='" . $this->wire('sanitizer')->entities($value) . "'"; + } + $out .= ">$text"; + return $out; + } + + /** + * Render a detail + * + * @param string $text Markup to render in the detail + * @param string $class May be omitted if not needed + * @param string $tag Default is span + * @return string + * + */ + protected function renderDetail($text, $class = '', $tag = 'span') { + $class = $class ? "detail $class" : "detail"; + return ' ' . $this->renderText($text, $class, $tag); + } + + /** + * Render paragraph of text (or other tag as specified) + * + * @param string $text + * @param string $class Default is blank + * @param string $tag Default is p + * @return string + * + */ + protected function renderText($text, $class = '', $tag = 'p') { + $class = $class ? " class='$class'" : ""; + return "<$tag$class>$text"; + } + + /** + * Save posted permission options to templates + * + */ + protected function savePermissionOptions() { + + $role = $this->getPage(); + if(!$role->id) return; + $isGuestRole = $role->id == $this->guestRole->id; + + /** @var WireInput $input */ + $input = $this->wire('input'); + + $viewPermission = $this->wire('permissions')->get('page-view'); + $editPermission = $this->wire('permissions')->get('page-edit'); + + foreach($this->wire('templates') as $template) { + + /** @var Template $template */ + if(!$template->useRoles) continue; + + $updates = array(); + $createRoles = $template->createRoles; + $addRoles = $template->addRoles; + $editRoles = $template->editRoles; + $guestHasView = $this->guestRole->hasPermission($viewPermission, $template); + $rolesPermissions = $template->rolesPermissions; + $rolePermissions = isset($rolesPermissions["$role->id"]) ? $rolesPermissions["$role->id"] : array(); + + $view = $input->post("pt_{$viewPermission->id}_{$template->id}"); + $edit = $input->post("pt_{$editPermission->id}_{$template->id}"); + $add = $input->post("pt_add_{$template->id}"); + $create = $input->post("pt_create_{$template->id}"); + + // page-view + if($view) { + if(!$template->roles->has($role)) { + $template->roles->add($role); + $updates[] = "Added page-view to template $template->name"; + } + } else { + if($template->roles->has($role)) { + if($isGuestRole || !$guestHasView) { + $template->roles->remove($role); + $updates[] = "Removed page-view from template $template->name"; + } + } + } + + if(!$isGuestRole) { + + // page-edit + if($edit) { + if(!in_array($role->id, $editRoles)) { + $editRoles[] = $role->id; + $updates[] = "Added page-edit to template $template->name"; + } + } else { + $key = array_search($role->id, $editRoles); + if($key !== false) { + unset($editRoles[$key]); + $updates[] = "Removed page-edit from template $template->name"; + } + } + + // page-add + if($add) { + if(!in_array($role->id, $addRoles)) { + $addRoles[] = $role->id; + $updates[] = "Added page-add to template $template->name"; + } + } else { + $key = array_search($role->id, $addRoles); + if($key !== false) { + unset($addRoles[$key]); + $updates[] = "Removed page-add from template $template->name"; + } + } + + // page-create + if($create) { + if(!in_array($role->id, $createRoles)) { + $createRoles[] = $role->id; + $updates[] = "Added page-create to template $template->name"; + } + } else { + $key = array_search($role->id, $createRoles); + if($key !== false) { + unset($createRoles[$key]); + $updates[] = "Removed page-create from template $template->name"; + } + } + } // if(!isGuestRole) + + // rolesPermissions + $adds = $input->post->intArray("add_tp_$template->id"); + $revokes = $input->post->intArray("revoke_tp_$template->id"); + + foreach($adds as $key => $permissionID) { + // force as strings + $adds[$key] = "$permissionID"; + } + foreach($revokes as $key => $permissionID) { + // force as negative integer strings + $revokes[$key] = (string) (-1 * $permissionID); + } + $rolePermissionsNew = array_merge($adds, $revokes); + sort($rolePermissionsNew); + sort($rolePermissions); + if($rolePermissionsNew != $rolePermissions) { + if($this->wire('config')->debug) { + $removedPermissions = array_diff($rolePermissions, $rolePermissionsNew); + $addedPermissions = array_diff($rolePermissionsNew, $rolePermissions); + foreach($removedPermissions as $permissionID) { + $permissionID = (int) $permissionID; + $permission = $this->wire('permissions')->get(abs($permissionID)); + $updates[] = ($permissionID < 0 ? "Removed revoke" : "Removed add") . " " . + "$permission->name for template $template->name" ; + } + foreach($addedPermissions as $permissionID) { + $permissionID = (int) $permissionID; + $permission = $this->wire('permissions')->get(abs($permissionID)); + $updates[] = ($permissionID < 0 ? "Added revoke" : "Added add") . " " . + "$permission->name for template $template->name" ; + } + } + $updates[] = "Updated rolesPermissions for template $template->name"; + $rolesPermissions["$role->id"] = $rolePermissionsNew; + $template->rolesPermissions = $rolesPermissions; + } + + // save changes + if(count($updates)) { + + if($editRoles != $template->editRoles) $template->editRoles = $editRoles; + if($addRoles != $template->addRoles) $template->addRoles = $addRoles; + if($createRoles != $template->createRoles) $template->createRoles = $createRoles; + + if($this->wire('config')->debug) { + foreach($updates as $update) $this->message($update); + } + + $template->save(); + } } } } diff --git a/wire/modules/Process/ProcessTemplate/ProcessTemplate.module b/wire/modules/Process/ProcessTemplate/ProcessTemplate.module index 2248b4f7..4a3b4489 100644 --- a/wire/modules/Process/ProcessTemplate/ProcessTemplate.module +++ b/wire/modules/Process/ProcessTemplate/ProcessTemplate.module @@ -608,6 +608,10 @@ class ProcessTemplate extends Process { */ public function ___executeEdit() { + if(substr($this->wire('input')->url(), -1) === '/') { + // we require non-trailing slash for edit requests + $this->wire('session')->redirect("../edit?id={$this->template->id}"); + } $this->wire('breadcrumbs')->add(new Breadcrumb('./', $this->moduleInfo['title'])); $min = $this->wire('config')->debug ? '' : '.min'; $this->wire('config')->scripts->add($this->wire('config')->urls->ProcessTemplate . "ProcessTemplateFieldCreator$min.js?v=1");