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->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 + "$checkboxes
" . + ($note ? "$note
" : "") . + "