-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathAdminRestrictBranch.module.php
More file actions
512 lines (426 loc) · 22.7 KB
/
AdminRestrictBranch.module.php
File metadata and controls
512 lines (426 loc) · 22.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
<?php
/**
* Processwire module to restrict site editors to a single branch of the tree.
* by Adrian Jones
*
* Copyright (C) 2021 by Adrian Jones
* Licensed under GNU/GPL v2, see LICENSE.TXT
*
*/
class AdminRestrictBranch extends WireData implements Module, ConfigurableModule {
/**
* Basic information about module
*/
public static function getModuleInfo() {
return array(
'title' => 'Admin Restrict Branch',
'summary' => 'Restrict site editors to a single branch of the tree.',
'author' => 'Adrian Jones',
'href' => 'https://processwire.com/talk/topic/11499-admin-restrict-branch/',
'version' => '1.0.12',
'autoload' => true,
'singular' => true,
'icon' => 'key',
'requires' => 'ProcessWire>=2.5.14',
);
}
/**
* Data as used by the get/set functions
*
*/
protected $data = array();
protected $restrictionEnabled = false;
protected $branchRootParentId = '';
protected $noMatch = false;
/**
* Default configuration for module
*
*/
static public function getDefaultData() {
return array(
"matchType" => 'disabled',
"branchesParent" => null,
"allOrNone" => 'all',
"phpCode" => '',
"branchExclusions" => null,
"restrictType" => 'editing_and_view',
"restrictFromSearch" => null,
"modifyBreadcrumbs" => null
);
}
/**
* Populate the default config data
*
*/
public function __construct() {
foreach(self::getDefaultData() as $key => $value) {
$this->$key = $value;
}
}
/**
* Initialize the module and setup hooks
*/
public function init() {
// early exit if match type not set yet or superuser or guest user (we don't need to restrict them further)
if($this->data['matchType'] == 'disabled' || $this->data['matchType'] == '' || $this->wire('user')->isSuperuser() || $this->wire('user')->isGuest()) return;
// get the branch root parent page ID for the matched page
$this->branchRootParentId = $this->getBranchRootParentId();
// if the matched branch parent is home (this can actually also mean there was no match) and non-matching users
// are allowed to view 'all' then exit because we don't need to do anything else with this module
if($this->branchRootParentId === 1 && $this->data['allOrNone'] == 'all') return;
// modify $page->editable() and $page->addable() based on branch restrictions - works in admin and front-end (for FREDI, FEEL, etc)
$this->wire()->addHookAfter('Page::editable', $this, 'hookPageEditable');
$this->wire()->addHookAfter('Page::addable', $this, 'hookPageAddable');
// restrict from search results, like for pages returned from autocomplete when inserting a link to a page in a CKEditor field
if($this->data['restrictFromSearch']) $this->wire()->addHookAfter('ProcessPageSearch::executeFor', $this, 'restrictFromSearch');
$this->restrictionEnabled = true;
}
public function ready() {
// exit if enabled not true from init()
if(!$this->restrictionEnabled) return;
// only set up page list restriction hook if in admin and restrict type allows it
if($this->wire('page')->template == 'admin' && $this->data['restrictType'] == 'editing_and_view') {
if(!isset($_GET['id'])) {
$this->wire()->addHookBefore('ProcessPageList::execute', $this, 'setBranchRoot', array('priority'=>2));
}
elseif(is_numeric($_GET['id'])) {
$this->wire()->addHookBefore('ProcessPageList::execute', $this, 'resetId', array('priority'=>1));
}
// modify breadcrumbs to remove pages outside restricted branch
if($this->data['modifyBreadcrumbs']) $this->wire()->addHookAfter('Process::breadcrumb', $this, 'modifyBreadcrumb');
// make Add New button respect branch restrictions
$this->wire()->addHookAfter('ProcessPageAdd::executeNavJSON', $this, 'restrictPageAdd');
$this->wire()->addHookBefore('ProcessPageList::executeNavJSON', $this, 'restrictPageList');
// make sure results from Lister are limited to restricted branch
$this->wire()->addHookAfter('ProcessPageLister::getSelector', $this, 'limitLister');
}
}
protected function limitLister ($event) {
if($this->wire('page')->process == 'ProcessUser') return;
$event->return = $event->return . ', has_parent='.$this->branchRootParentId;
}
protected function modifyBreadcrumb($event) {
$href = $event->arguments[0];
if (strpos($href, '=') !== false) {
$string_parts = explode("=", $href);
$pid = $string_parts[1];
if(!$this->onAllowedBranches($this->wire('pages')->get((int)$pid))) {
$this->wire('breadcrumbs')->pop();
// add a "Pages" breadcrumb so it is easy to get back to the main PageList view
if($this->wire('breadcrumbs')->count() == 0) {
$this->wire('breadcrumbs')->add(new Breadcrumb($this->wire('config')->urls->admin . 'page/', 'Pages'));
}
}
}
}
protected function restrictPageAdd($event) {
$responseArray = is_array($event->return) ? $event->return : json_decode($event->return, true);
$matches = $responseArray['list'];
$i=0;
foreach($matches as $match) {
if(!is_array($match)) continue;
if(isset($match['parent_id']) && $match['parent_id'] == 0) {
$allowedPages = $this->wire('pages')->find("has_parent=".$this->branchRootParentId.",template=".$match['template_id']);
if($allowedPages->count() > 0) {
$matches[$i]['parent_id'] = $allowedPages->first()->parent->id;
$matches[$i]['url'] = $match['url'] . '&parent_id='.$matches[$i]['parent_id'];
}
}
$i++;
}
$responseArray['list'] = $matches;
$event->return = is_array($event->return) ? $responseArray : json_encode($responseArray);
}
protected function restrictPageList($event) {
if(!$this->wire('input')->get->parent_id) $this->wire('input')->get->parent_id = $this->branchRootParentId;
}
protected function restrictFromSearch($event) {
$response = $event->return;
$responseArray = json_decode($response, true);
$matches = $responseArray['matches'];
$i=0;
foreach($matches as $match) {
if(!$this->onAllowedBranches($this->wire('pages')->get($match['id']))) {
unset($matches[$i]);
}
$i++;
}
$responseArray['matches'] = $matches;
$event->return = json_encode($responseArray);
}
// in case the user tries to manually pass an id in the url to access another branch
protected function resetId($event) {
$access = $this->accessCheck();
if($access != 'all') {
$this->error($access);
$event->replace = true;
$event->return = false;
}
if(isset($_GET['id']) && !$this->onAllowedBranches($this->wire('pages')->get((int)$_GET['id']))) {
// get the restricted branch root
$_GET['id'] = $this->branchRootParentId;
$this->wire('input')->get->id = $_GET['id'];
}
}
// set pagelist root to the correct parent based on the "How to match user to branch settings"
protected function setBranchRoot($event) {
$access = $this->accessCheck();
if($access != 'all') {
$this->error($access);
$event->replace = true;
$event->return = false;
}
// set parent page of branch
$_GET['id'] = $this->branchRootParentId;
$this->wire('input')->get->id = $_GET['id'];
// if pagelist_open cookie is set
// then need this to prevent doubling of page branch
if(isset($this->wire('input')->cookie->pagelist_open) && $this->branchRootParentId !== 1) {
$this->wire('input')->cookie->pagelist_open = str_replace('"1-0",', '', $this->wire('input')->cookie->pagelist_open);
}
// if open get variable is set and it matches the defined parent,
// then need this to prevent doubling of page branch
if(isset($_GET['open']) && $_GET['open'] == $this->branchRootParentId) $this->wire('input')->get->open = null;
}
private function accessCheck() {
if(($this->data['allOrNone'] == 'none' && $this->noMatch === true) || $this->branchRootParentId === false) {
return __("You don't have permission to view this branch of the page tree.");
}
else {
return 'all';
}
}
protected function ___getBranchRootParentId() {
if(isset($this->data['branchesParent']) && $this->data['branchesParent']) {
$branchesParentSelector = "has_parent=".$this->data['branchesParent'].", ";
}
else {
$branchesParentSelector = '';
}
if($this->data['matchType'] == 'role_name') {
foreach($this->wire('user')->roles as $role) {
$p = $this->wire('pages')->get($branchesParentSelector . "has_parent!=2, has_parent!=7, templates_id!=2, name={$role->name}, include=all");
if($p->id) break;
}
}
elseif($this->data['matchType'] == 'custom_php_code') {
$user = $this->wire('user');
$pages = $this->wire('pages');
$evaldName = eval($this->data['phpCode']);
// if false is return, then exit now and return false
// this results in an empty PageTreeList
if($evaldName === false) return false;
// if it includes a forward slash then it's a path match, not a name match
if(strpos($evaldName, '/') !== false) {
$p = $this->wire('pages')->get($evaldName);
}
else {
$p = $this->wire('pages')->get($branchesParentSelector . "has_parent!=2, name=".$this->wire('sanitizer')->pageNameTranslate($evaldName));
}
}
elseif($this->data['matchType'] == 'specified_parent_role') {
foreach($this->wire('user')->roles as $r) {
if($r->branch_parent->id) {
$p = $r->branch_parent;
break;
}
}
}
else {
// option to match branch parent defined in user profile
$p = $this->wire('user')->branch_parent;
}
// if no match, default to the homepage: id = 1, but set noMatch variable
// so it can be used in conjunction with the allOrNone setting to determine what they have access to
if(isset($p) && $p->id) {
return $p->id;
}
else {
$this->noMatch = true;
return 1;
}
}
/**
* Check if this page, or any ancestor pages, are the defined branch root parent or in the excluded branches
*
* From Netcarver
*/
private function onAllowedBranches($page) {
$page_on_my_branch = $this->branchCheck($page);
if(!$page_on_my_branch) {
$parents = $page->parents();
while(!$page_on_my_branch && count($parents)) {
$p = $parents->pop();
$page_on_my_branch = $this->branchCheck($p);
}
}
return $page_on_my_branch;
}
private function branchCheck($page) {
$access = $this->accessCheck();
$repeaterParent = $this->wire('pages')->get("path=".$this->wire('config')->urls->admin . "repeaters/");
array_push($this->data['branchExclusions'], $repeaterParent->id);
if(in_array($page->id, $this->data['branchExclusions'])) {
return true;
}
elseif($access != 'all') {
return false;
}
else {
return $page->id == $this->branchRootParentId ? true : false;
}
}
/**
* Page::editable hook
*
*/
protected function hookPageEditable($event) {
// in case there is already a defined exclusion for this user's role for this page
if(!$event->return) return;
if($this->wire('user')->hasPermission('page-edit')) {
$event->return = $this->onAllowedBranches($event->object);
} else {
$event->return = false;
}
}
/**
* Page::addable hook
*
*/
protected function hookPageAddable($event) {
// in case there is already a defined exclusion for this user's role for this page
if(!$event->return) return;
if($this->wire('user')->hasPermission('page-add')) {
$event->return = $this->onAllowedBranches($event->object);
} else {
$event->return = false;
}
}
/**
* Return an InputfieldsWrapper of Inputfields used to configure the class
*
* @param array $data Array of config values indexed by field name
* @return InputfieldsWrapper
*
*/
public function getModuleConfigInputfields(array $data) {
$data = array_merge(self::getDefaultData(), $data);
$wrapper = new InputfieldWrapper();
$f = $this->wire('modules')->get("InputfieldRadios");
$f->attr('name', 'matchType');
$f->label = __('How to match user to branch', __FILE__);
$f->description = "• " . __("'User Specified Branch Parent' is specifically set on each user's profile page using the 'Branch parent to restrict access to' field.", __FILE__) . PHP_EOL . "• " . __("'Role Specified Branch Parent' is specifically set on each role page using the 'Branch parent to restrict access to' field.", __FILE__) . PHP_EOL . "• " . __("'Role Name' limits users to the branch whose parent page name matches the name of one of their roles, eg. 'branch-one' role will be restricted to the 'Branch One' branch.", __FILE__) . PHP_EOL . "• " . __("'Custom PHP Code' allows you to build up a page name based on user fields.", __FILE__);
$f->notes = __('WARNING!! If you use the "Role Name" or the "Custom PHP code (returning a page name or path)", you must be sure to select "No Access" for the "If no match, give all access or no access", because an editor could change the name of the branch parent resulting in no match, and therefore gain full access.', __FILE__);
$f->required = true;
$f->addOption('disabled', __('Disabled', __FILE__));
$f->addOption('specified_parent', __('User Specified Branch Parent', __FILE__));
$f->addOption('specified_parent_role', __('Role Specified Branch Parent', __FILE__));
$f->addOption('role_name', __('Role Name', __FILE__));
$f->addOption('custom_php_code', __('Custom PHP code', __FILE__));
$f->value = $data['matchType'];
$wrapper->add($f);
$f = $this->wire('modules')->get("InputfieldPageListSelect");
$f->attr('name', 'branchesParent');
$f->label = __('Parent to restrict Role Name and Custom PHP code matches to.', __FILE__);
$f->description = __('The Role Name option matches a page name (not a path), so this option limits matching to branches under the selected parent. This can also be used for the Custom PHP code option if you choose to return a name, rather than a path. Note: a path is recommended.', __FILE__);
$f->notes = __('This setting is optional but recommended if trying to match a page name (rather than a path) - if no page is chosen, the selector will search the entire page tree for matching page names. Note that it is checked using "has_parent" so the selected page can be further up the tree than a direct parent.', __FILE__);
$f->showIf="matchType!='specified_parent'";
$f->value = $data['branchesParent'];
$wrapper->add($f);
$f = $this->wire('modules')->get("InputfieldText");
$f->attr('name', 'phpCode');
$f->label = __('Custom PHP code', __FILE__);
$f->description = __('This can return a name or a path. A path is recommended.', __FILE__);
$f->notes = __('Has access to $user and $pages variable.', __FILE__) . PHP_EOL . PHP_EOL . __("This example allows automatic restriction of a user to a branch named to match their first and last names.", __FILE__) . PHP_EOL . 'return strtolower($user->first_name . "-" . $user->last_name);' . PHP_EOL . PHP_EOL . __("This example shows how to restrict the editor user role to a page path. If your conditional returns 'false' (without quotes) instead of '/', non-matching users will be denied access to the entire page tree.", __FILE__) . PHP_EOL . 'return ($user->hasRole("editor")) ? "/restricted-branches/editor/" : "/";';
$f->required = true;
$f->showIf="matchType='custom_php_code'";
$f->requiredIf="matchType='custom_php_code'";
$f->value = $data['phpCode'];
$wrapper->add($f);
$f = $this->wire('modules')->get("InputfieldRadios");
$f->attr('name', 'allOrNone');
$f->label = __('If no match, give all access or no access?', __FILE__);
$f->description = __("If the user doesn't match based on the defined rule, do you want the user to have access to the entire page tree or have no access?", __FILE__);
$f->notes = __('WARNING!! If you use the "Role Name" or the "Custom PHP code (returning a page name or path)", you must be sure to select "No Access", because an editor could change the name of the branch parent resulting in no match, and therefore gain full access.', __FILE__);
$f->required = true;
$f->addOption('all', __('Entire Page Tree', __FILE__));
$f->addOption('none', __('No Access', __FILE__));
$f->value = $data['allOrNone'];
$wrapper->add($f);
$f = $this->wire('modules')->get("InputfieldRadios");
$f->attr('name', 'restrictType');
$f->label = __('Restrict editing and list viewing, or just editing.', __FILE__);
$f->description = __("With 'Editing Only' selected, users will still see all branches in the page tree, but their editing access will still be restricted.", __FILE__);
$f->required = true;
$f->addOption('editing_and_view', __('Editing and List Viewing', __FILE__));
$f->addOption('editing_only', __('Editing Only', __FILE__));
$f->value = $data['restrictType'];
$wrapper->add($f);
$f = $this->wire('modules')->get("InputfieldPageListSelectMultiple");
$f->attr('name', 'branchExclusions');
$f->label = __('Branch edit exclusions', __FILE__);
$f->description = __("Selected branches will be excluded from branch edit restrictions. They still won't show in the page list, but they will remain editable, which is useful for external PageTable branches etc.", __FILE__);
$f->value = $data['branchExclusions'];
$wrapper->add($f);
$f = $this->wire('modules')->get("InputfieldCheckbox");
$f->attr('name', 'restrictFromSearch');
$f->label = __('Restrict from search results', __FILE__);
$f->description = __('Check this to prevent search results in the admin from finding pages outside of the restricted branch. This is primarily for the autocomplete response when inserting a link to a URL from a CKEditor field.', __FILE__);
$f->attr('checked', $data['restrictFromSearch'] == '1' ? 'checked' : '' );
$wrapper->add($f);
$f = $this->wire('modules')->get("InputfieldCheckbox");
$f->attr('name', 'modifyBreadcrumbs');
$f->label = __('Modify Breadcrumbs', __FILE__);
$f->description = __('Check this to remove pages from the breadcrumbs that are outside of the restricted branch.', __FILE__);
$f->attr('checked', $data['modifyBreadcrumbs'] == '1' ? 'checked' : '' );
$wrapper->add($f);
return $wrapper;
}
public function ___install() {
// create branch_parent field on user template
if(!$this->wire('fields')->branch_parent) {
$f = new Field();
$f->type = "FieldtypePage";
$f->derefAsPage = 2;
$f->allowUnpub = 1;
$f->inputfield = "InputfieldPageListSelect";
$f->name = "branch_parent";
$f->label = "Branch parent to restrict access to";
$f->description = "This is used by the Admin Restrict Branch module to limit this user to only see the branch starting with this parent when viewing the page list in the admin. It also restricts editing access to just this branch.";
$f->notes = __("This only works if this Admin Restrict Branch module config option is set to 'Specified Branch Parent'.", __FILE__);
$f->collapsed = Inputfield::collapsedBlank;
$f->save();
foreach($this->wire('config')->userTemplateIDs as $userTemplateId) {
$userTemplate = $this->wire('templates')->get($userTemplateId);
$userTemplate->fields->add($f);
$userTemplate->fields->save();
}
$roleTemplate = $this->wire('templates')->get('role');
$roleTemplate->fields->add($f);
$roleTemplate->fields->save();
}
}
public function ___uninstall() {
// remove branch_parent field
if($this->wire('fields')->branch_parent) {
$f = $this->wire('fields')->branch_parent;
foreach($this->wire('config')->userTemplateIDs as $userTemplateId) {
$userTemplate = $this->wire('templates')->get($userTemplateId);
$userTemplate->fields->remove($f);
$userTemplate->fields->save();
}
$roleTemplate = $this->wire('templates')->get('role');
$roleTemplate->fields->remove($f);
$roleTemplate->fields->save();
$this->wire('fields')->delete($f);
}
}
public function ___upgrade($fromVersion, $toVersion) {
$roleTemplate = $this->wire('templates')->get('role');
$branchParentField = $this->wire('fields')->get('branch_parent');
if(!$roleTemplate->hasField($branchParentField)) {
$roleTemplate->fields->add($branchParentField);
$roleTemplate->fields->save();
}
}
}