├── LICENSE.txt ├── README.md ├── extensions ├── file-methods.php ├── files-methods.php ├── page-methods.php └── pages-methods.php ├── fields ├── firewall │ ├── assets │ │ ├── css │ │ │ └── style.css │ │ └── js │ │ │ └── script.js │ ├── firewall.php │ └── languages │ │ ├── de.php │ │ └── en.php ├── roles │ └── roles.php └── users │ └── users.php ├── firewall.php ├── package.json └── routes.php /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright © 2016 Daniel Weidner 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kirby Firewall Plugin 2 | 3 | ![Version](https://img.shields.io/badge/version-1.1.1-orange.svg) ![License](https://img.shields.io/badge/license-MIT-green.svg) ![Kirby Version](https://img.shields.io/badge/Kirby-2.4%2B-red.svg) 4 | 5 | Protect your pages and files from unauthorized access. Simply select the users and roles that should be able to view your content via a custom field. 6 | 7 | ![Firewall Field](http://dweidner.github.io/kirby-firewall/images/firewall-field.gif) 8 | 9 | ## Requirements 10 | 11 | - [**Kirby**](https://getkirby.com/) 2.4+ 12 | - PHP 5.4.0+ 13 | 14 | ## Installation 15 | 16 | Use one of the alternatives below. 17 | 18 | ### 1. Kirby CLI 19 | 20 | If you are using the [Kirby CLI](https://github.com/getkirby/cli) you can install this plugin by running the following commands in your shell: 21 | 22 | ``` 23 | $ cd path/to/project 24 | $ kirby plugin:install dweidner/kirby-firewall 25 | ``` 26 | 27 | ### 2. Clone or download 28 | 29 | 1. [Clone](https://github.com/dweidner/kirby-firewall.git) or [download](https://github.com/dweidner/kirby-firewall/archive/master.zip) this repository. 30 | 2. Unzip the archive if needed and rename the folder to `firewall`. 31 | 32 | **Make sure that the plugin folder structure looks like this:** 33 | 34 | ``` 35 | site/plugins/firewall/ 36 | ``` 37 | 38 | ### 3. Git Submodule 39 | 40 | If you know your way around Git, you can download this plugin as a submodule: 41 | 42 | ``` 43 | $ cd path/to/project 44 | $ git submodule add https://github.com/dweidner/kirby-firewall site/plugins/firewall 45 | ``` 46 | 47 | ## Setup 48 | 49 | ### 1. Firewall Field (optional) 50 | 51 | To use the access control field within your blueprint use the following: 52 | 53 | ``` 54 | fields: 55 | firewall: 56 | label: Access Control 57 | type: firewall 58 | ``` 59 | 60 | You can exclude both users as well as roles from the corresponding input list: 61 | 62 | ``` 63 | fields: 64 | firewall: 65 | label: Access Control 66 | type: firewall 67 | exclude: 68 | role: 69 | - guest 70 | ``` 71 | 72 | Have a lot of users? You might want to increase the number of columns: 73 | 74 | ``` 75 | fields: 76 | firewall: 77 | label: Access Control 78 | type: firewall 79 | columns: 3 80 | ``` 81 | 82 | Want default values other than public (roles or users): 83 | 84 | ``` 85 | fields: 86 | firewall: 87 | label: Access Control 88 | type: firewall 89 | default: 90 | roles: 91 | - editor 92 | - admin 93 | users: 94 | - root 95 | 96 | ``` 97 | 98 | ### 2. Asset Firewall (optional) 99 | 100 | In order for the asset firewall to work, you have to customize your `.htaccess` file in the project root. Change the following line: 101 | 102 | ``` 103 | RewriteRule ^content/(.*)\.(txt|md|mdown)$ index.php [L] 104 | ``` 105 | 106 | to 107 | 108 | ``` 109 | RewriteRule ^content/(.*)$ index.php [L] 110 | ``` 111 | 112 | It allows our custom route to control the access to all your files within the content folder. 113 | 114 | ## Usage 115 | 116 | Once you have completed the setup you can limit access to a page and its contents via a custom field. In order to only allow users of the role `Editor` to access the page `http://example.com/submissions` you need to edit the corresponding content file `content/05-submissions/submissions.md` as follows: 117 | 118 | ``` 119 | Title: Downloads 120 | 121 | ---- 122 | 123 | Firewall: 124 | roles: 125 | - editor 126 | ``` 127 | 128 | You can also combine role ids with usernames: 129 | 130 | ``` 131 | Title: Downloads 132 | 133 | ---- 134 | 135 | Firewall: 136 | roles: 137 | - editor 138 | users: 139 | - dweidner 140 | ``` 141 | 142 | If you don't like to edit your content files by hand you can install the [Kirby Panel](https://github.com/getkirby/panel). Once the Panel is running on your server our custom field will help you out with that process. Hava a look into the section [Firewall Field](#1-firewall-field-optional) for further setup instructions. 143 | 144 | ## Options 145 | 146 | The following options can be set in your `/site/config/config.php` file: 147 | 148 | ```php 149 | c::set('plugin.firewall.fieldname', 'firewall'); 150 | c::set('plugin.firewall.redirect', false); 151 | c::set('plugin.firewall.pages', '(.*)'); 152 | c::set('plugin.firewall.content', 'content/(.*)'); 153 | 154 | c::set('field.users.template', '{username} ({role})'); 155 | c::set('field.roles.template', '{id} ({name})'); 156 | ``` 157 | 158 | ### plugin.firewall.fieldname 159 | 160 | Name of the field that is controlling the access to your pages or asset files (default: `firewall`). 161 | 162 | ### plugin.firewall.redirect 163 | 164 | Set a custom redirect uri for users with insufficient user privileges. By default a simple "Access denied" page with corresponding "403 Forbidden" response header is returned. If you prefer to redirect the user to a specific page (e.g. `http://yourdomain.com/auth/login`) simply set this option to the desired uri (e.g. `auth/login`). 165 | 166 | ### plugin.firewall.pages 167 | 168 | Allows you to customize the uri pattern of the route which is protecting access to your pages. By default all of your pages which use the Firewall field are protected. You can change the uri pattern if you want to protect a specific subdirectory of your site only (e.g. `/staff/(.*)`). Addionally you can disable the route entirely by setting the option to `false`. 169 | 170 | ### plugin.firewall.content 171 | 172 | Allows you to customize the uri pattern of the route which is protecting access to your content files. By default all of your files are protected which belong to a page using the Firewall field. You can change the uri pattern if you want to protect access to specific files of your site only (e.g. `content/downloads/(.*)`). Addionally you can disable protection of content files entirely by setting the option to `false`. 173 | 174 | ### field.users.template 175 | 176 | This option allows you to customize the way a user is displayed in the panel (default: `{username} ({role})`). 177 | 178 | Available placeholders: 179 | 180 | - username 181 | - email 182 | - role 183 | - language 184 | - avatar 185 | - gravatar 186 | 187 | ### field.roles.template 188 | 189 | This option allows you to customize the way a role is displayed in the panel (default: `{id} ({name})`). 190 | 191 | Available placeholders: 192 | 193 | - id 194 | - name 195 | 196 | ## Credits 197 | 198 | - [Kirby Cookbook](https://getkirby.com/docs/cookbook/asset-firewall) The core of this plugin is heavily based on the suggestions made in this recipe. 199 | -------------------------------------------------------------------------------- /extensions/file-methods.php: -------------------------------------------------------------------------------- 1 | 8 | * @package Kirby\Plugin\Firewall 9 | */ 10 | 11 | /** 12 | * Check whether acces to the given file is restricted to specific users or 13 | * roles only. 14 | * 15 | * @since 1.0.0 16 | * 17 | * @param \File $file File to test. 18 | * @return bool 19 | */ 20 | $kirby->set('file::method', 'isAccessRestricted', function($file) { 21 | return $file->page()->isAccessRestricted(); 22 | }); 23 | 24 | /** 25 | * Check whether the file is accessible by a certain user or role. 26 | * 27 | * @since 1.0.0 28 | * 29 | * @param \File $file File to test. 30 | * @param \User|\Role $obj User or role object. 31 | * @return bool 32 | */ 33 | $kirby->set('file::method', 'isAccessibleBy', function($file, $obj) { 34 | return $file->page()->isAccessibleBy($obj); 35 | }); 36 | 37 | /** 38 | * Check whether the file is accessible by the currently logged-in user. 39 | * 40 | * @since 1.1.0 41 | * 42 | * @param \File $file File to test. 43 | * @return bool 44 | */ 45 | $kirby->set('file::method', 'isAccessibleByCurrentUser', function($file) { 46 | $page = $file->page(); 47 | return $page->isAccessibleBy($page->site()->user()); 48 | }); 49 | -------------------------------------------------------------------------------- /extensions/files-methods.php: -------------------------------------------------------------------------------- 1 | 8 | * @package Kirby\Plugin\Firewall 9 | */ 10 | 11 | /** 12 | * Return all files from the collection that are accessible by the given user. 13 | * 14 | * @since 1.0.0 15 | * 16 | * @param \Files $files Collection of files. 17 | * @param \User|\Role $obj User or role object. 18 | * @return \Files 19 | */ 20 | $kirby->set('files::method', 'accessibleBy', function($files, $obj) { 21 | return $files->filter(function($file) use ($obj) { 22 | return $file->isAccessibleBy($obj); 23 | }); 24 | }); 25 | 26 | /** 27 | * Return all files from the collection that are accessible by the currently 28 | * logged-in user. 29 | * 30 | * @since 1.0.0 31 | * 32 | * @param \Files $files Collection of files. 33 | * @return \Files 34 | */ 35 | $kirby->set('files::method', 'accessibleByCurrentUser', function($files) { 36 | $user = site()->user(); 37 | return $files->filter(function($file) use ($user) { 38 | return $file->isAccessibleBy($user); 39 | }); 40 | }); 41 | 42 | /** 43 | * Return all files from the collection that the given user is not allowed 44 | * to access. 45 | * 46 | * @since 1.0.0 47 | * 48 | * @param \Files $files Collection of files. 49 | * @param \User|\Role $obj User or role object. 50 | * @return \Files 51 | */ 52 | $kirby->set('files::method', 'inaccessibleBy', function($files, $obj) { 53 | return $files->filter(function($file) use ($obj) { 54 | return !$file->isAccessibleBy($obj); 55 | }); 56 | }); 57 | 58 | /** 59 | * Return all files from the collection that the currently logged-in user is 60 | * not allowed to access. 61 | * 62 | * @since 1.1.0 63 | * 64 | * @param \Files $files Collection of files. 65 | * @return \Files 66 | */ 67 | $kirby->set('files::method', 'inaccessibleByCurrentUser', function($files) { 68 | $user = site()->user(); 69 | return $files->filter(function($file) use ($user) { 70 | return !$file->isAccessibleBy($user); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /extensions/page-methods.php: -------------------------------------------------------------------------------- 1 | 8 | * @package Kirby\Plugin\Firewall 9 | */ 10 | 11 | /** 12 | * Check whether the given page has restricted access to specific users or 13 | * roles only. 14 | * 15 | * @since 1.0.0 16 | * 17 | * @param \Page $page Page to test. 18 | * @return bool 19 | */ 20 | $kirby->set('page::method', 'isAccessRestricted', function($page) { 21 | $field = $page->content()->get(c::get('plugin.firewall.fieldname', 'firewall')); 22 | 23 | if (!$field->exists() || v::accepted($field->value())) { 24 | return false; 25 | } 26 | 27 | if (v::denied($field->value())) { 28 | return true; 29 | } 30 | 31 | $value = $field->yaml(); 32 | $value = array_filter($value); 33 | 34 | return is_array($value) && !empty($value); 35 | }); 36 | 37 | /** 38 | * Check whether the page is accessible by a certain user or role. 39 | * 40 | * @since 1.0.0 41 | * 42 | * @param \Page $page Page to test. 43 | * @param \User|\Role $obj User or role object. 44 | * @return bool 45 | */ 46 | $kirby->set('page::method', 'isAccessibleBy', function($page, $obj) { 47 | if (!$page->isAccessRestricted()) { 48 | return true; 49 | } 50 | 51 | $field = $page->content()->get(c::get('plugin.firewall.fieldname', 'firewall')); 52 | 53 | if (!$obj || v::denied($field->value())) { 54 | return false; 55 | } 56 | 57 | $rules = $field->yaml(); 58 | $users = a::get($rules, 'users'); 59 | $roles = a::get($rules, 'roles'); 60 | 61 | if ($obj instanceof \Role) { 62 | return !empty($roles) && in_array($obj->id(), $roles); 63 | } 64 | 65 | return ( !empty($users) && in_array($obj->username(), $users) ) || ( !empty($roles) && in_array($obj->role()->id(), $roles) ); 66 | }); 67 | 68 | /** 69 | * Check whether the page is accessible by the currently logged-in user. 70 | * 71 | * @since 1.1.0 72 | * 73 | * @param \Page $page Page to test. 74 | * @return bool 75 | */ 76 | $kirby->set('page::method', 'isAccessibleByCurrentUser', function($page) { 77 | return $page->isAccessibleBy($page->site()->user()); 78 | }); 79 | -------------------------------------------------------------------------------- /extensions/pages-methods.php: -------------------------------------------------------------------------------- 1 | 8 | * @package Kirby\Plugin\Firewall 9 | */ 10 | 11 | /** 12 | * Return all pages from the collection that are accessible by the given 13 | * user/role. 14 | * 15 | * @since 1.0.0 16 | * 17 | * @param \Pages $pages Collection of pages. 18 | * @param \User|\Role $obj User or role object. 19 | * @return \Pages 20 | */ 21 | $kirby->set('pages::method', 'accessibleBy', function($pages, $obj) { 22 | return $pages->filter(function($page) use ($obj) { 23 | return $page->isAccessibleBy($obj); 24 | }); 25 | }); 26 | 27 | /** 28 | * Return all pages from the collection that the currently logged-in user 29 | * is allowed to access. 30 | * 31 | * @since 1.1.0 32 | * 33 | * @param \Pages $pages Collection of pages. 34 | * @return \Pages 35 | */ 36 | $kirby->set('pages::method', 'accessibleByCurrentUser', function($pages) { 37 | $user = site()->user(); 38 | return $pages->filter(function($page) use ($user) { 39 | return $page->isAccessibleBy($user); 40 | }); 41 | }); 42 | 43 | /** 44 | * Return all pages from the collection that the given user/role 45 | * is not allowed to access. 46 | * 47 | * @since 1.0.0 48 | * 49 | * @param \Pages $pages Collection of pages. 50 | * @param \User|\Role $obj User or role object. 51 | * @return \Pages 52 | */ 53 | $kirby->set('pages::method', 'inaccessibleBy', function($pages, $obj) { 54 | return $pages->filter(function($page) use ($obj) { 55 | return !$page->isAccessibleBy($obj); 56 | }); 57 | }); 58 | 59 | /** 60 | * Return all pages from the collection the currently logged-in user 61 | * is not allowed to access. 62 | * 63 | * @since 1.0.0 64 | * 65 | * @param \Pages $pages Collection of pages. 66 | * @return \Pages 67 | */ 68 | $kirby->set('pages::method', 'inaccessibleByCurrentUser', function($pages) { 69 | $user = site()->user(); 70 | return $pages->filter(function($page) use ($user) { 71 | return !$page->isAccessibleBy($user); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /fields/firewall/assets/css/style.css: -------------------------------------------------------------------------------- 1 | /*! FirewallField - Daniel Weidner */ 2 | 3 | /* ===== 1 Components ===================================================== */ 4 | 5 | /* ----- 1.1 Bubble Component --------------------------------------------- */ 6 | 7 | .ff-bubble { 8 | position: relative; 9 | padding: 1em 1.5em; 10 | border: 2px solid #ddd; 11 | } 12 | 13 | .ff-bubble-up:before, 14 | .ff-bubble-up:after { 15 | content: ""; 16 | position: absolute; 17 | right: 0.75em; 18 | bottom: 100%; 19 | width: 0; 20 | height: 0; 21 | border: 0.75em solid transparent; 22 | border-bottom-color: #ddd; 23 | } 24 | 25 | .ff-bubble-up:after { 26 | margin-bottom: -3px; 27 | border-bottom-color: #efefef; 28 | } 29 | 30 | /* ===== 2 Layout ========================================================= */ 31 | 32 | /* ----- 1.1 Field Footer ------------------------------------------------- */ 33 | 34 | .ff .field-panel { 35 | position: relative; 36 | overflow: hidden; 37 | padding-top: 0.75em; 38 | margin-top: 1em; 39 | max-height: 25em; 40 | transition: max-height 0.2s linear, padding 0.2s linear; 41 | } 42 | .ff .field-panel[aria-hidden="true"] { 43 | padding-top: 0 !important; 44 | max-height: 0 !important; 45 | } 46 | .ff .field-help + .field-panel { 47 | margin-top: 0; 48 | } 49 | .ff .field-panel :last-child { 50 | margin-bottom: 0; 51 | } 52 | -------------------------------------------------------------------------------- /fields/firewall/assets/js/script.js: -------------------------------------------------------------------------------- 1 | /*! FirewallField - Daniel Weidner */ 2 | 3 | var FirewallField = (function($, $field) { 4 | 5 | "use strict"; 6 | 7 | /** 8 | * A reference to the current function scope. Can be used within callback 9 | * functions. 10 | * 11 | * @type {Function} 12 | */ 13 | var self = this; 14 | 15 | /** 16 | * Element that toggles the visibility of the panel. 17 | * 18 | * @type {jQuery} 19 | */ 20 | self.$toggle = $field.find('.js-toggle-panel'); 21 | 22 | /** 23 | * Panel element containing additional fields. 24 | * 25 | * @type {jQuery} 26 | */ 27 | self.$panel = $field.find('#' + self.$toggle.attr('aria-controls')); 28 | 29 | /** 30 | * Initialize the field instance. 31 | * 32 | * @returns {self} 33 | */ 34 | self.init = function () { 35 | 36 | self.$panel.css('max-height', self.$panel.outerHeight() + 16); 37 | 38 | if (self.isExpanded()) { 39 | self.expand(); 40 | } else { 41 | self.collapse(); 42 | } 43 | 44 | console.log(self); 45 | 46 | return self.bind(); 47 | }; 48 | 49 | /** 50 | * Check whether the panel is currently expanded. 51 | * 52 | * @return {Boolean} 53 | */ 54 | self.isExpanded = function() { 55 | return self.$toggle.attr('aria-expanded') === 'true'; 56 | }; 57 | 58 | /** 59 | * Registers event handlers. 60 | * 61 | * @returns {self} 62 | */ 63 | self.bind = function() { 64 | 65 | self.$toggle.on('change', $.proxy(self.toggle, self)); 66 | 67 | return self; 68 | 69 | }; 70 | 71 | /** 72 | * Expand the panel element. 73 | */ 74 | self.expand = function() { 75 | 76 | self.$toggle.attr('aria-expanded', true); 77 | self.$panel.attr('aria-hidden', false); 78 | 79 | return self; 80 | 81 | }; 82 | 83 | /** 84 | * Collapse the panel element. 85 | */ 86 | self.collapse = function() { 87 | 88 | self.$toggle.attr('aria-expanded', false); 89 | self.$panel.attr('aria-hidden', true); 90 | 91 | return self; 92 | 93 | }; 94 | 95 | /** 96 | * Toggle the visibility of the panel element. 97 | */ 98 | self.toggle = function() { 99 | return self.isExpanded() ? self.collapse() : self.expand(); 100 | }; 101 | 102 | return self.init(); 103 | 104 | }); 105 | 106 | (function($) { 107 | 108 | /** 109 | * Create a new instance of the FirewallField. 110 | * 111 | * @returns {FirewallField} 112 | */ 113 | $.fn.firewall = function() { 114 | 115 | var field = this.data('firewall-field'); 116 | 117 | if (!field) { 118 | field = new FirewallField($, this); 119 | this.data('firewall-field', field); 120 | } 121 | 122 | return field; 123 | 124 | }; 125 | 126 | })(jQuery); 127 | -------------------------------------------------------------------------------- /fields/firewall/firewall.php: -------------------------------------------------------------------------------- 1 | 9 | * @package Kirby\Plugin\Firewall 10 | * @subpackage FirewallField 11 | * @since 1.0.0 12 | */ 13 | class FirewallField extends BaseField { 14 | 15 | /** 16 | * Version of the field. 17 | * 18 | * @var string 19 | */ 20 | const VERSION = '1.1.1'; 21 | 22 | /** 23 | * Name of the custom field. Represents the identifier users have to use 24 | * within their blueprints. 25 | * 26 | * @var string 27 | */ 28 | const FIELDNAME = 'firewall'; 29 | 30 | /** 31 | * Assets to load by the Kirby Panel. 32 | * 33 | * @var array 34 | */ 35 | public static $assets = [ 36 | 'js' => [ 37 | 'script.js', 38 | ], 39 | 'css' => [ 40 | 'style.css', 41 | ], 42 | ]; 43 | 44 | /** 45 | * A list of possible entity types the user can select from. 46 | * 47 | * @var array 48 | */ 49 | public static $fields = [ 50 | 'roles', 51 | 'users', 52 | ]; 53 | 54 | /** 55 | * Allow to display the list items of the child controls in multiple columns. 56 | * 57 | * @var int 58 | */ 59 | public $columns = 2; 60 | 61 | /** 62 | * Name of users or roles that should be excluded from display. 63 | * 64 | * @var array 65 | */ 66 | public $exclude = [ 67 | 'roles' => null, 68 | 'users' => null, 69 | ]; 70 | 71 | /** 72 | * Collection of fields maintained by the current instance. 73 | * 74 | * @var array 75 | */ 76 | protected $children = null; 77 | 78 | /** 79 | * Load field localization. 80 | */ 81 | public static function setup() { 82 | 83 | $base = __DIR__ . DS . 'languages' . DS; 84 | $lang = panel()->translation()->code(); 85 | 86 | if (file_exists($base . $lang . '.php')) { 87 | require $base . $lang . '.php'; 88 | } else { 89 | require $base . 'en.php'; 90 | } 91 | 92 | } 93 | 94 | /** 95 | * Create a new instance of the FirewallField class. 96 | */ 97 | public function __construct() { 98 | 99 | $this->text = $this->l('label'); 100 | $this->default = true; 101 | 102 | } 103 | 104 | /** 105 | * Fetch a language variable for multi-language sites. Takes into account the 106 | * text domain of the current field. 107 | * 108 | * @param string $value 109 | * @return string 110 | */ 111 | public function l($value) { 112 | return l('fields.' . self::FIELDNAME . '.' . $value); 113 | } 114 | 115 | /** 116 | * Get the id of the current field instance. 117 | * 118 | * @return string 119 | */ 120 | public function id() { 121 | 122 | $prefix = ''; 123 | 124 | if (is_a($this->parentField, 'BaseField')) { 125 | $prefix .= $this->parentField->id() . '-'; 126 | } 127 | 128 | return $prefix . parent::id(); 129 | 130 | } 131 | 132 | /** 133 | * Get the name of the current field instance. 134 | * 135 | * @return string 136 | */ 137 | public function name() { 138 | 139 | $prefix = ''; 140 | 141 | if (is_a($this->parentField, 'BaseField')) { 142 | $prefix .= $this->parentField->name() . '-'; 143 | } 144 | 145 | return $prefix . parent::name(); 146 | 147 | } 148 | 149 | /** 150 | * Get the default value to use when no value is given by the user. 151 | * 152 | * @return mixed 153 | */ 154 | public function defaultValue() { 155 | return ($this->default === '') ? '1' : $this->default; 156 | } 157 | 158 | /** 159 | * Synchronize the value of the current field with those of the child 160 | * elements. We need to assign the value manually as the child elements 161 | * are detached from the actual form. 162 | * 163 | * @param array? $data Optional data store to synchronize. 164 | */ 165 | public function sync($data = null) { 166 | 167 | if (is_null($data)) { 168 | $data = $this->value(); 169 | } else { 170 | $this->value = $data; 171 | } 172 | 173 | foreach ($this->fields() as $name => $field) { 174 | $field->value = a::get($data, $name); 175 | } 176 | 177 | } 178 | 179 | /** 180 | * Collect request data for all fields that belong to the current instance. 181 | * 182 | * @return array 183 | */ 184 | public function data() { 185 | 186 | $data = []; 187 | 188 | foreach ($this->fields() as $name => $field) { 189 | $data[$name] = get($field->name()); 190 | } 191 | 192 | return $data; 193 | 194 | } 195 | 196 | /** 197 | * Get the current value of the field instance. 198 | * 199 | * @return mixed 200 | */ 201 | public function value() { 202 | 203 | if ($this->value === '') { 204 | $this->value = $this->defaultValue(); 205 | } else if (is_string($this->value) && !v::accepted($this->value) && !v::denied($this->value)) { 206 | $this->value = yaml::decode($this->value); 207 | } 208 | 209 | return $this->value; 210 | 211 | } 212 | 213 | /** 214 | * Validate the selected field values. 215 | * 216 | * @return bool 217 | */ 218 | public function validate() { 219 | 220 | // Validate the value of the checkbox 221 | $value = get($this->name()); 222 | 223 | if (!(is_null($value) || v::accepted($value) || v::denied($value))) { 224 | return false; 225 | } 226 | 227 | // Validate the value of all fields attached to the current instance 228 | $this->sync($this->data()); 229 | 230 | foreach ($this->fields() as $name => $field) { 231 | if (!$field->validate()) { 232 | return false; 233 | } 234 | } 235 | 236 | // All test have passed. Data can be saved to file. 237 | return true; 238 | } 239 | 240 | /** 241 | * Generate the field value that can be saved within the corresponding content 242 | * file. Converts the current value to a yaml formatted string. 243 | * 244 | * @param bool $encode Whether to yaml encode the result. 245 | * @return string 246 | */ 247 | public function result($encode = true) { 248 | 249 | $checked = get($this->name()); 250 | 251 | // Access control is disabled. Page is public. 252 | if (v::accepted($checked)) { 253 | return $encode ? '1' : true; 254 | } 255 | 256 | // Collect user and role names selected by the user. 257 | $data = $this->data(); 258 | $data = array_filter($data); 259 | 260 | // Access control is enabled. Page is hidden for all. 261 | if (empty($data)) { 262 | return $encode ? '0' : false; 263 | } 264 | 265 | // Access control is enabled. Page is visile for specific users/roles only. 266 | return $encode ? yaml::encode($data) : $result; 267 | 268 | } 269 | 270 | /** 271 | * Test whether the user has enabled access control for the current page. 272 | * 273 | * @return bool 274 | */ 275 | public function checked() { 276 | 277 | $value = $this->value(); 278 | return !is_array($value) && v::accepted($value); 279 | 280 | } 281 | 282 | /** 283 | * Get the markup for the input element of the field. 284 | * 285 | * @return \Brick 286 | */ 287 | public function input() { 288 | 289 | $container = brick('label') 290 | ->text($this->i18n($this->text())) 291 | ->attr('for', $this->id()) 292 | ->addClass('input') 293 | ->addClass('input-with-checkbox'); 294 | 295 | $input = brick('input') 296 | ->addClass('checkbox') 297 | ->addClass('js-toggle-panel') 298 | ->attr([ 299 | 'type' => 'checkbox', 300 | 'id' => $this->id(), 301 | 'name' => $this->name(), 302 | 'required' => $this->required(), 303 | 'checked' => $this->checked(), 304 | 'readonly' => $this->readonly(), 305 | 'autofocus' => $this->autofocus(), 306 | 'autocomplete' => $this->autocomplete(), 307 | 'aria-controls' => $this->id() . '-panel', 308 | 'aria-expanded' => $this->checked() ? 'false' : 'true', 309 | ]); 310 | 311 | if($this->readonly()) { 312 | $input->attr('tabindex', '-1'); 313 | $container->addClass('input-is-readonly'); 314 | } 315 | 316 | return $container->prepend($input); 317 | 318 | } 319 | 320 | /** 321 | * Generate the markup for the field container. 322 | * 323 | * @see https://forum.getkirby.com/t/panel-field-javascript-click-does-not-work-after-save/3474/7 Panel field javascript click does not work after save 324 | * @return \Brick 325 | */ 326 | public function element() { 327 | 328 | return parent::element() 329 | ->data('field', self::FIELDNAME) 330 | ->addClass('ff'); 331 | 332 | } 333 | 334 | /** 335 | * Get all fields that belong to the current field instance. If the `fields` 336 | * paramater is given new fields are added to the footer of the field. 337 | * 338 | * @return array 339 | */ 340 | public function fields() { 341 | 342 | if (!is_null($this->children)) { 343 | return $this->children; 344 | } 345 | 346 | $fields = []; 347 | $state = $this->value(); 348 | $user = panel()->user(); 349 | 350 | $defaults = [ 351 | 'page' => $this->page, 352 | 'model' => $this->model, 353 | 'parentField' => $this, 354 | 'columns' => $this->columns(), 355 | ]; 356 | 357 | foreach (self::$fields as $name) { 358 | 359 | // Ensure the field exists and the user has access to the corresponding 360 | // content type 361 | $classname = ucfirst($name) . 'Field'; 362 | 363 | if (!class_exists($classname) || !$user->can("panel.{$name}.read")) { 364 | continue; 365 | } 366 | 367 | // Create a new field instance with sensitive defaults 368 | $instance = new $classname; 369 | 370 | $options = array_merge($defaults, [ 371 | 'name' => $name, 372 | 'label' => $this->l($name), 373 | 'value' => a::get($state, $name), 374 | 'exclude' => a::get($this->exclude, $name, []), 375 | ]); 376 | 377 | foreach ($options as $key => $value) { 378 | if (property_exists($instance, $key)) { 379 | $instance->{$key} = $value; 380 | } 381 | } 382 | 383 | // Add the new field to the registry 384 | $fields[$name] = $instance; 385 | 386 | } 387 | 388 | return $this->children = $fields; 389 | 390 | } 391 | 392 | /** 393 | * Generate the markup for the entire field element with all its input 394 | * elements and accompanying contents. 395 | * 396 | * @return \Brick 397 | */ 398 | public function template() { 399 | 400 | // Append the markup for all child elements to the container 401 | $container = brick('div') 402 | ->addClass('field-panel') 403 | ->attr([ 404 | 'id' => $this->id() . '-panel', 405 | ]); 406 | 407 | // Ensure that the values of all fields attached to the current instance 408 | // are synchronized with the root 409 | $this->sync(); 410 | 411 | // Generate the markup of each field attached 412 | foreach ($this->fields() as $name => $field) { 413 | $type = str_replace('field', '', strtolower(get_class($field))); 414 | $child = $field->template(); 415 | $child 416 | ->removeClass('field-grid-item') 417 | ->addClass('ff-bubble') 418 | ->addClass('ff-bubble-up') 419 | ->addClass('js-ff-' . str::slug($type)); 420 | $container->append($child); 421 | } 422 | 423 | // Generate the markup for the select field and append the child elements. 424 | return parent::template()->append($container); 425 | 426 | } 427 | 428 | } 429 | -------------------------------------------------------------------------------- /fields/firewall/languages/de.php: -------------------------------------------------------------------------------- 1 | 'Öffentlich', 7 | $domain . 'users' => 'Benutzer', 8 | $domain . 'roles' => 'Rollen', 9 | 10 | $domain . 'type.public' => 'Öffentlich', 11 | $domain . 'type.users' => 'Benutzer', 12 | $domain . 'type.roles' => 'Rollen', 13 | ]); 14 | -------------------------------------------------------------------------------- /fields/firewall/languages/en.php: -------------------------------------------------------------------------------- 1 | 'Public', 7 | $domain . 'users' => 'Users', 8 | $domain . 'roles' => 'Roles', 9 | 10 | $domain . 'type.public' => 'Public', 11 | $domain . 'type.users' => 'Users', 12 | $domain . 'type.roles' => 'Roles', 13 | ]); 14 | -------------------------------------------------------------------------------- /fields/roles/roles.php: -------------------------------------------------------------------------------- 1 | 10 | * @package Kirby\Plugin\Firewall 11 | * @subpackage RolesField 12 | * @since 1.0.0 13 | */ 14 | class RolesField extends CheckboxesField { 15 | 16 | /** 17 | * Version of the field. 18 | * 19 | * @var string 20 | */ 21 | const VERSION = '1.1.1'; 22 | 23 | /** 24 | * Name of the custom field. Represents the identifier users have to use 25 | * within their blueprints. 26 | * 27 | * @var string 28 | */ 29 | const FIELDNAME = 'roles'; 30 | 31 | /** 32 | * Name of roles to exclude from display. 33 | * 34 | * @var array 35 | */ 36 | public $exclude = []; 37 | 38 | /** 39 | * Get the id of the current field instance. 40 | * 41 | * @return string 42 | */ 43 | public function id() { 44 | 45 | $prefix = ''; 46 | 47 | if (is_a($this->parentField, 'BaseField')) { 48 | $prefix .= $this->parentField->id() . '-'; 49 | } 50 | 51 | return $prefix . parent::id(); 52 | 53 | } 54 | 55 | /** 56 | * Get the name of the current field instance. 57 | * 58 | * @return string 59 | */ 60 | public function name() { 61 | 62 | $prefix = ''; 63 | 64 | if (is_a($this->parentField, 'BaseField')) { 65 | $prefix .= $this->parentField->name() . '-'; 66 | } 67 | 68 | return $prefix . parent::name(); 69 | 70 | } 71 | 72 | /** 73 | * Generate an option element for each registered user. 74 | * 75 | * @return array 76 | */ 77 | public function options() { 78 | 79 | $template = c::get('field.' . self::FIELDNAME . '.template', '{id} ({name})'); 80 | $options = []; 81 | 82 | foreach ($this->roles() as $role) { 83 | $options[$role->id()] = str::template($template, $role->toArray()); 84 | } 85 | 86 | return $options; 87 | 88 | } 89 | 90 | /** 91 | * Retrieve a list of user roles. Exclude those roles listed in the 92 | * blacklist. 93 | * 94 | * @return \Roles 95 | */ 96 | protected function roles() { 97 | 98 | $roles = site()->roles(); 99 | 100 | if (!empty($this->exclude)) { 101 | $roles = call([ $roles, 'not' ], $this->exclude); 102 | } 103 | 104 | return $roles; 105 | 106 | } 107 | 108 | } 109 | 110 | -------------------------------------------------------------------------------- /fields/users/users.php: -------------------------------------------------------------------------------- 1 | 10 | * @package Kirby\Plugin\Firewall 11 | * @subpackage UsersField 12 | * @since 1.0.0 13 | */ 14 | class UsersField extends CheckboxesField { 15 | 16 | /** 17 | * Version of the field. 18 | * 19 | * @var string 20 | */ 21 | const VERSION = '1.1.1'; 22 | 23 | /** 24 | * Name of the custom field. Represents the identifier users have to use 25 | * within their blueprints. 26 | * 27 | * @var string 28 | */ 29 | const FIELDNAME = 'users'; 30 | 31 | /** 32 | * Name of users to exclude from display. 33 | * 34 | * @var array 35 | */ 36 | public $exclude = []; 37 | 38 | /** 39 | * Get the id of the current field instance. 40 | * 41 | * @return string 42 | */ 43 | public function id() { 44 | 45 | $prefix = ''; 46 | 47 | if (is_a($this->parentField, 'BaseField')) { 48 | $prefix .= $this->parentField->id() . '-'; 49 | } 50 | 51 | return $prefix . parent::id(); 52 | 53 | } 54 | 55 | /** 56 | * Get the name of the current field instance. 57 | * 58 | * @return string 59 | */ 60 | public function name() { 61 | 62 | $prefix = ''; 63 | 64 | if (is_a($this->parentField, 'BaseField')) { 65 | $prefix .= $this->parentField->name() . '-'; 66 | } 67 | 68 | return $prefix . parent::name(); 69 | 70 | } 71 | 72 | /** 73 | * Customize the label and add a direct link to the user overview. 74 | * 75 | * @return \Brick 76 | */ 77 | public function label() { 78 | 79 | $label = parent::label(); 80 | 81 | if (panel()->user()->can('panel.user.create')) { 82 | $button = brick('a') 83 | ->addClass('structure-add-button label-option') 84 | ->attr('href', purl('users/add')) 85 | ->html('' . l('add')); 86 | $label->append($button); 87 | } 88 | 89 | return $label; 90 | 91 | } 92 | 93 | /** 94 | * Generate an option element for each registered user. 95 | * 96 | * @return array 97 | */ 98 | public function options() { 99 | 100 | $template = c::get('field.' . self::FIELDNAME . '.template', '{username} ({role})'); 101 | $options = []; 102 | 103 | foreach ($this->users() as $user) { 104 | $options[$user->username()] = str::template($template, $user->toArray()); 105 | } 106 | 107 | return $options; 108 | 109 | } 110 | 111 | /** 112 | * Retrieve a list of registered users. Exclude those users listed in the 113 | * blacklist. 114 | * 115 | * @return \Users 116 | */ 117 | protected function users() { 118 | 119 | $users = site()->users(); 120 | 121 | if (!empty($this->exclude)) { 122 | $users = call([ $users, 'not' ], $this->exclude); 123 | } 124 | 125 | return $users; 126 | 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /firewall.php: -------------------------------------------------------------------------------- 1 | 8 | * @package Kirby\Plugin\Firewall 9 | * @since 1.0.0 10 | */ 11 | 12 | /** Extending Kirby’s core objects. */ 13 | include __DIR__ . DS . 'extensions' . DS . 'page-methods.php'; 14 | include __DIR__ . DS . 'extensions' . DS . 'pages-methods.php'; 15 | 16 | /** Register custom request routes. */ 17 | include __DIR__ . DS . 'routes.php'; 18 | 19 | /** Register custom panel fields. */ 20 | $kirby->set('field', 'users', __DIR__ . DS . 'fields' . DS . 'users'); 21 | $kirby->set('field', 'roles', __DIR__ . DS . 'fields' . DS . 'roles'); 22 | $kirby->set('field', 'firewall', __DIR__ . DS . 'fields' . DS . 'firewall'); 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firewall", 3 | "description": "Protect your pages and assets from unauthorized access.", 4 | "author": "Daniel Weidner ", 5 | "version": "1.1.1", 6 | "type": "kirby-plugin", 7 | "license": "MIT" 8 | } 9 | -------------------------------------------------------------------------------- /routes.php: -------------------------------------------------------------------------------- 1 | 8 | * @package Kirby\Plugin\Firewall 9 | * @since 1.0.0 10 | */ 11 | 12 | 13 | 14 | if ($pattern = c::get('plugin.firewall.content', 'content/(.*)')): 15 | 16 | /** 17 | * File Access Control 18 | * 19 | * A custom route that prevents users with insufficient permissions from 20 | * accessing page assets. 21 | * 22 | * @see https://getkirby.com/docs/cookbook/asset-firewall How to build an asset firewall 23 | */ 24 | $kirby->set('route', [ 25 | 'pattern' => $pattern, 26 | 'action' => function($path) { 27 | $directories = str::split($path, '/'); 28 | $filename = array_pop($directories); 29 | 30 | $page = site(); 31 | $user = site()->user(); 32 | 33 | foreach ($directories as $dirname) { 34 | if ($child = $page->children()->findBy('dirname', $dirname)) { 35 | if ($child->isAccessibleBy($user)) { 36 | $page = $child; 37 | } else { 38 | header::forbidden(); 39 | die('Access denied'); 40 | } 41 | } else { 42 | header::notFound(); 43 | die('Page not found'); 44 | } 45 | } 46 | 47 | if ($file = $page->file($filename)) { 48 | $file->show(); 49 | } else { 50 | header::notFound(); 51 | die('File not found'); 52 | } 53 | }, 54 | ]); 55 | 56 | endif; 57 | 58 | 59 | 60 | if ($pattern = c::get('plugin.firewall.pages', '(.*)')): 61 | 62 | /** 63 | * Page Access Control 64 | * 65 | * A custom route that prevents users with insufficient permissions from 66 | * accessing certain pages. 67 | * 68 | * @see https://getkirby.com/docs/developer-guide/advanced/routing Advanced tasks: Routing 69 | */ 70 | $kirby->set('route', [ 71 | 'pattern' => $pattern, 72 | 'action' => function($uid) { 73 | $page = ($uid === '/') ? site()->homePage() : page($uid); 74 | $user = site()->user(); 75 | 76 | if (!$page) { 77 | return site()->visit(site()->errorPage()); 78 | } 79 | 80 | if (!$page->isAccessibleBy($user)) { 81 | if ($redirect = c::get('plugin.firewall.redirect')) { 82 | go($redirect); 83 | } else { 84 | header::forbidden(); 85 | die('Access denied'); 86 | } 87 | } 88 | 89 | return site()->visit($page); 90 | } 91 | ]); 92 | 93 | endif; 94 | --------------------------------------------------------------------------------