Subversion Repositories web.creative

Rev

Details | Last modification | View Log

Rev Author Line No. Line
41 mjordaan 1
<?php namespace ProcessWire;
2
 
3
/**
4
 * ProcessWire Page Edit Process
5
 *
6
 * Provides the UI for editing a page
7
 * 
8
 * For more details about how Process modules work, please see: 
9
 * /wire/core/Process.php 
10
 * 
11
 * ProcessWire 3.x, Copyright 2018 by Ryan Cramer
12
 * https://processwire.com
13
 * 
14
 * @property string $noticeUnknown
15
 * @property string $noticeLocked
16
 * @property string $noticeNoAccess
17
 * @property string $noticeIncomplete
18
 * @property string $viewAction One of 'panel', 'modal', 'new', 'this' (see getViewActions method)
19
 * @property bool $useBookmarks
20
 * 
21
 * @method Page loadPage($id)
22
 * @method string execute()
23
 * @method string executeTemplate()
24
 * @method void executeSaveTemplate($template = null)
25
 * @method string executeBookmarks()
26
 * @method array getViewActions($actions = array(), $configMode = false)
27
 * @method array getSubmitActions()
28
 * @method bool processSubmitAction($value)
29
 * @method void processSaveRedirect($redirectUrl)
30
 * @method InputfieldForm buildForm(InputfieldForm $form)
31
 * @method InputfieldWrapper buildFormContent()
32
 * @method InputfieldWrapper buildFormChildren()
33
 * @method InputfieldWrapper buildFormSettings()
34
 * @method InputfieldWrapper buildFormDelete()
35
 * @method void buildFormView($url)
36
 * @method InputfieldMarkup buildFormRoles()
37
 * @method void processInput(InputfieldWrapper $form, $level = 0, $formRoot = null)
38
 * @method void ajaxSave(Page $page)
39
 * @method bool ajaxEditable(Page $page, $fieldName = '')
40
 * @method array getTabs()
41
 * 
42
 */
43
 
44
class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableModule {
45
 
46
	/**
47
	 * Module information
48
	 *
49
	 * @return array
50
	 *
51
	 */
52
	public static function getModuleInfo() {
53
		return array(
54
			'title' => 'Page Edit',
55
			'summary' => 'Edit a Page',
56
			'version' => 109,
57
			'permanent' => true,
58
			'permission' => 'page-edit',
59
			'icon' => 'edit',
60
			'useNavJSON' => true
61
		);
62
	}
63
 
64
	/**
65
	 * Page edit form
66
	 * 
67
	 * @var InputfieldForm
68
	 * 
69
	 */
70
	protected $form;
71
 
72
	/**
73
	 * Page being edited
74
	 * 
75
	 * @var Page
76
	 * 
77
	 */
78
	protected $page;
79
 
80
	/**
81
	 * Single field to edit (if only 'fields' specified, this contains first field present in 'fields')
82
	 * 
83
	 * @var null|Field
84
	 * 
85
	 */
86
	protected $field = null; 
87
 
88
	/**
89
	 * Array of fields to edit, indexed by field name
90
	 * 
91
	 * @var array|Field[]
92
	 * 
93
	 */	
94
	protected $fields = array();
95
 
96
	/**
97
	 * Field name suffix, applicable only when field or fields (above) is also set, in specific situations like repeaters
98
	 * 
99
	 * @var string
100
	 * 
101
	 */
102
	protected $fnsx = '';
103
 
104
	/**
105
	 * Substituted master page (deprecated)
106
	 * 
107
	 * @var null|Page
108
	 * 
109
	 */
110
	protected $masterPage = null;
111
 
112
	/**
113
	 * Parent of page being edited
114
	 * 
115
	 * @var Page
116
	 * 
117
	 */
118
	protected $parent;
119
 
120
	/**
121
	 * User that is editing
122
	 * 
123
	 * @var User
124
	 * 
125
	 */
126
	protected $user;
127
 
128
	/**
129
	 * @var int ID of page being edited
130
	 * 
131
	 */
132
	protected $id;
133
 
134
	/**
135
	 * URL to redirect to
136
	 * 
137
	 * @var string
138
	 * 
139
	 */
140
	protected $redirectUrl;
141
 
142
	/**
143
	 * @var string PHP class name of Page being edited
144
	 * 
145
	 */
146
	protected $pageClass;
147
 
148
	/**
149
	 * Is the page in the trash?
150
	 * 
151
	 * @var bool
152
	 * 
153
	 */
154
	protected $isTrash;
155
 
156
	/**
157
	 * Cache used by getAllowedTemplates() method
158
	 * 
159
	 * Contains Template objects indexed by template ID.
160
	 * 
161
	 * @var array|Template[]
162
	 * 
163
	 */
164
	protected $allowedTemplates = null; // cache
165
 
166
	/**
167
	 * Is this a POST request to save a page?
168
	 * 
169
	 * @var bool
170
	 * 
171
	 */
172
	protected $isPost = false;
173
 
174
	/**
175
	 * Show the "settings" tab?
176
	 * 
177
	 * @var bool
178
	 * 
179
	 */
180
	protected $useSettings = true;
181
 
182
	/**
183
	 * Show the "children" tab?
184
	 * 
185
	 * @var bool
186
	 * 
187
	 */
188
	protected $useChildren = true;
189
 
190
	/**
191
	 * Show the "view" tab/link?
192
	 * 
193
	 * @var bool
194
	 * 
195
	 */
196
	protected $useView = true;
197
 
198
	/**
199
	 * Identified tabs in the form indexed by tab ID and values are tab labels
200
	 * 
201
	 * @var array
202
	 * 
203
	 */
204
	protected $tabs = array();
205
 
206
	/**
207
	 * Predefined list of parents allowed for edited page (array of Page objects), set by setPredefinedParents() method
208
	 * 
209
	 * @var array|PageArray
210
	 * 
211
	 */
212
	protected $predefinedParents = array();
213
 
214
	/**
215
	 * Predefined list of templates allowed for edited page (array of Template objects), set by setPredefinedTemplates() method
216
	 * 
217
	 * @var array|Template[]
218
	 * 
219
	 */
220
	protected $predefinedTemplates = array();
221
 
222
	/**
223
	 * Primary editor process, if not $this
224
	 * 
225
	 * @var null|WirePageEditor
226
	 * 
227
	 */
228
	protected $editor = null;
229
 
230
	/**
231
	 * Tell the Page what Process is being used to edit it?
232
	 * 
233
	 */
234
	protected $setEditor = true;
235
 
236
	/**
237
	 * Names of changed fields
238
	 * 
239
	 * @var array
240
	 * 
241
	 */
242
	protected $changes = array();
243
 
244
	/**
245
	 * @var Modules
246
	 * 
247
	 */
248
	protected $modules;
249
 
250
	/**
251
	 * @var WireInput
252
	 * 
253
	 */
254
	protected $input;
255
 
256
	/**
257
	 * @var Config
258
	 * 
259
	 */
260
	protected $config;
261
 
262
	/**
263
	 * @var Sanitizer
264
	 * 
265
	 */
266
	protected $sanitizer;
267
 
268
	/**
269
	 * @var Session
270
	 * 
271
	 */
272
	protected $session;
273
 
274
	/**
275
	 * Sanitized contents of get[modal]
276
	 * 
277
	 * @var int|string|bool|null
278
	 * 
279
	 */
280
	protected $requestModal = null;
281
 
282
	/**
283
	 * Sanitized contents of get[context]
284
	 * 
285
	 * @var string
286
	 * 
287
	 */
288
	protected $requestContext = '';
289
 
290
	/**
291
	 * Sanitized contents of get[language]
292
	 * 
293
	 * @var Language|null
294
	 * 
295
	 */
296
	protected $requestLanguage = null;
297
 
298
	/**
299
	 * Is the LanguageSupportPageNames module installed?
300
	 * 
301
	 * @var bool
302
	 * 
303
	 */
304
	protected $hasLanguagePageNames = false;
305
 
306
	/**
307
	 * Contents of $config->pageEdit
308
	 * 
309
	 * @var array
310
	 * 
311
	 */
312
	protected $configSettings = array(
313
		'viewNew' => false,
314
		'confirm' => true,
315
		'ajaxChildren' => true,
316
		'ajaxParent' => true,
317
		'editCrumbs' => false,
318
	);
319
 
320
	/**
321
	 * Other core page classes
322
	 * 
323
	 * @var array
324
	 * 
325
	 */
326
	protected $otherCorePageClasses = array(
327
		'User',	
328
		'Role',
329
		'Permission',
330
		'Language'
331
	);
332
 
333
	/***********************************************************************************************************************
334
	 * METHODS
335
	 * 
336
	 */
337
 
338
	/**
339
	 * Construct
340
	 * 
341
	 */
342
	public function __construct() {
343
		$this->set('useBookmarks', false);
344
		$this->set('viewAction', 'this');
345
		return parent::__construct();
346
	}
347
 
348
	public function wired() {
349
		if($this->wire('process') instanceof WirePageEditor) {
350
			// keep existing process, which may be building on top of this one
351
		} else {
352
			$this->wire('process', $this);
353
		}
354
	}
355
 
356
	/**
357
	 * Initialize the page editor by loading the requested page and any dependencies
358
	 * 
359
	 * @throws WireException|Wire404Exception|WirePermissionException
360
	 *
361
	 */
362
	public function init() {
363
 
364
		$this->modules = $this->wire('modules');
365
		$this->input = $this->wire('input');
366
		$this->config = $this->wire('config');
367
		$this->user = $this->wire('user');
368
		$this->sanitizer = $this->wire('sanitizer');
369
		$this->session = $this->wire('session');
370
 
371
		// predefined messages that maybe used in multiple places
372
		$this->set('noticeUnknown', $this->_("Unknown page")); // Init error: Unknown page
373
		$this->set('noticeLocked', $this->_("This page is locked for edits")); // Init error: Page is locked
374
		$this->set('noticeNoAccess', $this->_("You don't have access to edit")); // Init error: User doesn't have access
375
		$this->set('noticeIncomplete', $this->_("This page might have one or more incomplete fields (attempt to save or publish for more info)"));
376
 
377
		$settings = $this->config->pageEdit;
378
		if(is_array($settings)) $this->configSettings = array_merge($this->configSettings, $settings); 
379
 
380
		if(in_array($this->input->urlSegment1, array('navJSON', 'bookmarks'))) return;
381
 
382
		$getID = $this->input->get('id');
383
		if($getID === 'bookmark') {
384
			$this->session->redirect('./bookmarks/');
385
			return;
386
		}
387
		$getID = (int) $getID;
388
		$postID = (int) $this->input->post('id');
389
		$id = abs($postID ? $postID : $getID); 
390
 
391
		if(!$id) {
392
			$this->session->redirect('./bookmarks/');
393
			throw new Wire404Exception($this->noticeUnknown, Wire404Exception::codeSecondary); // Init error: no page provided
394
		}
395
 
396
		$this->page = $this->loadPage($id); 
397
		$this->id = $this->page->id; 
398
		$this->pageClass = $this->page->className();
399
		$this->page->setOutputFormatting(false);
400
		$this->parent = $this->pages->get($this->page->parent_id);
401
		$this->isTrash = $this->page->isTrash();
402
 
403
		// check if editing specific field or fieldset only
404
		if($this->page) {
405
			$field = $this->input->get('field');
406
			$fields = $this->input->get('fields'); 
407
			if($this->input->get('fnsx') !== null) $this->fnsx = $this->input->get->fieldName('fnsx');
408
			if($field && !$fields) $fields = $field;
409
			if($fields) {
410
				$fields = explode(',', $fields); 
411
				foreach($fields as $fieldName) {
412
					$fieldName = $this->sanitizer->fieldName($fieldName);
413
					if(!$fieldName) throw new WireException("Invalid field name specified");
414
					$field = $this->page->template->fieldgroup->getField($fieldName, true); // get in context
415
					if(!$field) throw new WireException("Field '$fieldName' is not applicable to this page");
416
					$this->fields[$field->name] = $field; 
417
				}
418
				$this->field = reset($this->fields);
419
				$this->useChildren = false;
420
				$this->useSettings = false;
421
				$this->useView = false;
422
			}
423
		}
424
 
425
		// determine if we're going to be dealing with a save/post request
426
		$this->isPost = ($postID > 0 && ($postID === $this->page->id)) 
427
			|| ($this->config->ajax && (count($_POST) || isset($_SERVER['HTTP_X_FIELDNAME']))); 
428
 
429
		if(!$this->isPost) { 
430
			$this->setupHeadline();
431
			$this->setupBreadcrumbs();
432
		}
433
 
434
		// optional context GET var
435
		$context = $this->input->get('context');
436
		if($context) $this->requestContext = $this->sanitizer->name($context);
437
 
438
		// optional language GET var
439
		$languages = $this->wire('languages');
440
		if($languages) {
441
			$this->hasLanguagePageNames = $this->modules->isInstalled('LanguageSupportPageNames');
442
			if($this->hasLanguagePageNames) {
443
				$languageID = (int) $this->input->get('language');
444
				if($languageID > 0) {
445
					$language = $languages->get($languageID);
446
					if($language->id && $language->id != $this->user->language->id) $this->requestLanguage = $language;
447
				}
448
			}
449
		}
450
 
451
		// optional modal setting
452
		if($this->config->modal) {
453
			$this->requestModal = $this->sanitizer->name($this->config->modal);	
454
		}
455
 
456
		parent::init();
457
 
458
		if(!$this->isPost) {
459
			$this->modules->get('JqueryWireTabs');
460
			/** @var JqueryUI $jQueryUI */
461
			$jQueryUI = $this->modules->get('JqueryUI');
462
			$jQueryUI->use('modal');
463
		}
464
 
465
	}
466
 
467
	/**
468
	 * Given a page ID, return the Page object
469
	 *
470
	 * @param int $id
471
	 * @return Page
472
	 * @throws WireException|WirePermissionException
473
	 *
474
	 */
475
	protected function ___loadPage($id) {
476
 
477
		/** @var Page|NullPage $page */
478
		$page = $this->wire('pages')->get((int) $id); 
479
 
480
		if($page instanceof NullPage) {
481
			throw new WireException($this->noticeUnknown); // page doesn't exist
482
		}
483
 
484
		$editable = $page->editable();
485
 
486
		/** @var User $user */
487
		$user = $this->user;
488
 
489
		/** @var Config $config */
490
		$config = $this->config;
491
 
492
		/** @var Config $config */
493
		$input = $this->input; 
494
 
495
		if($page instanceof User) {
496
			// special case when page is a User
497
 
498
			$userAdmin = $user->hasPermission('user-admin');
499
			$field = $input->get('field') ? $this->wire('fields')->get($input->get->fieldName('field')) : null;
500
 
501
			if($userAdmin && $this->wire('process') != 'ProcessUser') {
502
				// only allow user pages to be edited from the access section (at least for non-superusers)
503
				$this->session->redirect($config->urls->admin . 'access/users/edit/?id=' . $page->id);
504
 
505
			} else if(!$userAdmin && $page->id === $user->id && $field && $config->ajax) {
506
				// user is editing themself and we're responding to an ajax request for a field
507
				/** @var PagePermissions $pagePermissions */
508
				$pagePermissions = $this->modules->get('PagePermissions');
509
				$editable = $pagePermissions->userFieldEditable($field); 
510
				// prevent a later potential redirect to user editor
511
				if($editable) $this->setEditor = false;
512
			}
513
		}
514
 
515
		if(!$editable) {
516
			throw new WirePermissionException($this->noticeNoAccess);
517
		}
518
 
519
		return $page;
520
	}
521
 
522
	/**
523
	 * Execute the Page Edit process by building the form and checking if it was submitted
524
	 * 
525
	 * @return string
526
	 * @throws WireException
527
	 *
528
	 */
529
	public function ___execute() {
530
 
531
		if(!$this->page) throw new WireException("No page found");
532
 
533
		if($this->setEditor) {
534
			// note that setting the editor can force a redirect to a ProcessPageType editor
535
			$this->page->setEditor($this->editor ? $this->editor : $this);
536
		}
537
 
538
		if($this->config->ajax && (isset($_SERVER['HTTP_X_FIELDNAME']) || count($_POST))) {
539
			$this->ajaxSave($this->page);
540
			return '';
541
		}
542
 
543
		if($this->page->hasStatus(Page::statusTemp) && $this->page->parent->template->childNameFormat == 'title') {
544
			// make it set page name from page title
545
			$this->page->name = '';
546
		}
547
 
548
		$adminTheme = $this->wire('adminTheme');
549
		if($adminTheme) {
550
			$className = $this->className();
551
			$adminTheme->addBodyClass("$className-id-{$this->page->id}");
552
			$adminTheme->addBodyClass("$className-template-{$this->page->template->name}");
553
		}
554
 
555
		$this->form = $this->modules->get('InputfieldForm');
556
		$this->form = $this->buildForm($this->form);
557
		$this->form->setTrackChanges();
558
 
559
		if($this->isPost && count($_POST)) $this->processSave();
560
 
561
		if($this->page->hasStatus(Page::statusLocked)) {
562
			if($this->user->hasPermission('page-lock', $this->page)) {
563
				$this->warning($this->noticeLocked); // Page locked message
564
			} else {
565
				$this->error($this->noticeLocked); // Page locked error
566
			}
567
		} else if(!$this->isPost && $this->page->hasStatus(Page::statusFlagged) && !$this->input->get('s')) {
568
			$this->warning($this->noticeIncomplete); 
569
		}
570
 
571
		return $this->renderEdit();
572
	}
573
 
574
 
575
	/*********************************************************************************************************************
576
	 * EDITOR FORM BUILDING
577
	 *
578
	 */
579
 
580
	/**
581
	 * Render the Page Edit form
582
	 *
583
	 * @return string
584
	 * 
585
	 */
586
	protected function renderEdit() {
587
 
588
		$class = '';
589
		$numFields = count($this->fields);
590
		$out = "<p id='PageIDIndicator' class='$class'>" . ($this->page->id ? $this->page->id : "New") . "</p>";
591
 
592
		$description = $this->form->getSetting('description');
593
		if($description) { 
594
			$out .= "<h2>" . $this->form->entityEncode($description, Inputfield::textFormatBasic) . "</h2>";
595
			$this->form->set('description', '');
596
		}
597
 
598
		if(!$numFields) { 
599
			/** @var JqueryWireTabs $tabs */
600
			$tabs = $this->modules->get('JqueryWireTabs');
601
			$this->form->value = $tabs->renderTabList($this->getTabs(), array('id' => 'PageEditTabs')); 
602
		}
603
 
604
		$out .= $this->form->render();
605
 
606
		// buttons with dropdowns
607
		if(!$numFields) {
608
			$submitActions = $this->getSubmitActions();
609
			if(count($submitActions)) {
610
				$config = $this->config;
611
				$file = $config->debug ? 'dropdown.js' : 'dropdown.min.js';
612
				$config->scripts->add($config->urls('InputfieldSubmit') . $file);
613
				$input = "<input type='hidden' id='after-submit-action' name='_after_submit_action' value='' />";
614
				$out = str_replace('</form>', "$input</form>", $out);
615
				$out .= "<ul class='pw-button-dropdown' data-pw-dropdown-input='#after-submit-action' data-my='right top' data-at='right bottom+1'>";
616
				foreach($submitActions as $action) {
617
					$icon = empty($action['icon']) ? "" : "<i class='fa fa-fw fa-$action[icon]'></i>";
618
					$class = empty($action['class']) ? "after-submit-$action[value]" : $action['class'];
619
					$out .= "<li><a class='$class' data-pw-dropdown-value='$action[value]' href='#'>$icon $action[label]</a></li>";
620
				}
621
				$out .= "</ul>";
622
			}
623
		}
624
 
625
		if(!$numFields && !$this->requestModal && $this->page->viewable()) {
626
			// this supports code in the buildFormView() method
627
			$out .= "<ul id='_ProcessPageEditViewDropdown' class='pw-dropdown-menu pw-dropdown-menu-rounded' data-my='left top' data-at='left top-9'>";
628
			foreach($this->getViewActions() as $name => $action) {
629
				$out .= "<li class='page-view-action-$name'>$action</li>";
630
			}
631
			$out .= "</ul>";
632
		}
633
 
634
		$out .= "<scr" . "ipt>initPageEditForm();</script>"; // ends up being slightly faster than ready() (or at least appears that way)
635
 
636
		return $out; 
637
	}
638
 
639
	/**
640
	 * Get actions for submit button(s)
641
	 * 
642
	 * Should return array where each item in the array is itself an array like this:
643
	 * ~~~~~
644
	 * [ 
645
	 *   'value' => 'value of action, i.e. view, edit, add, etc.', 
646
	 *   'icon' => 'icon name excluding the “fa-” part', 
647
	 *   'label' => 'text label where %s is replaced with submit button label',
648
	 *   'class' => 'optional class attribute',
649
	 * ]
650
	 * ~~~~~~
651
	 * Array returned by this method is indexed by the 'value', though this is not required for hooks.
652
	 * 
653
	 * #pw-hooker
654
	 * 
655
	 * @return array
656
	 * @throws WireException
657
	 * @since 3.0.142
658
	 * @see ___processSubmitAction()
659
	 * 
660
	 */
661
	protected function ___getSubmitActions() {
662
 
663
		if($this->requestModal) return array();
664
 
665
		$viewable = $this->page->viewable();
666
		$actions = array();
667
 
668
		$actions['exit'] = array(
669
			'value' => 'exit',
670
			'icon' => 'close',
671
			'label' => $this->_('%s + Exit'),
672
			'class' => '',
673
		);
674
 
675
		if($viewable) $actions['view'] = array(
676
			'value' => 'view',
677
			'icon' => 'eye',
678
			'label' => $this->_('%s + View'),
679
			'class' => '',
680
		);
681
 
682
		if($this->wire('process') == $this && $this->page->id > 1) {
683
 
684
			$parent = $this->page->parent();
685
			if($parent->addable()) $actions['add'] = array(
686
				'value' => 'add',
687
				'icon' => 'plus-circle',
688
				'label' => $this->_('%s + Add New'),
689
				'class' => '',
690
			);
691
 
692
			if($parent->numChildren > 1) $actions['next'] = array(
693
				'value' => 'next',
694
				'icon' => 'edit',
695
				'label' => $this->_('%s + Next'),
696
				'class' => '',
697
			);
698
		}
699
 
700
		return $actions;
701
	}
702
 
703
	/**
704
	 * Get URL to view this page
705
	 * 
706
	 * @param Language|int|string|null $language
707
	 * @return string
708
	 * @throws WireException
709
	 * @since 3.0.142 Was protected in previous versions
710
	 * 
711
	 */
712
	public function getViewUrl($language = null) {
713
		$url = '';
714
		if(!$this->page) throw new WireException('No page yet');
715
		if($this->hasLanguagePageNames) {
716
			/** @var Languages $languages */
717
			$languages = $this->wire('languages');
718
			if($language) {
719
				if(is_string($language) || is_int($language)) $language = $languages->get($language);
720
				$userLanguage = $language;
721
			} else if($this->requestLanguage) {
722
				$userLanguage = $this->requestLanguage;
723
			} else {
724
				$userLanguage = $this->user->language;
725
			}
726
			if($userLanguage && $userLanguage->id) {
727
				$url = $this->page->localHttpUrl($userLanguage);
728
			}
729
		}
730
		if(!$url) $url = $this->page->httpUrl();
731
		return $url;
732
	}
733
 
734
	/**
735
	 * Get actions for the "View" dropdown
736
	 * 
737
	 * #pw-hooker
738
	 * 
739
	 * @param array $actions Actions in case hook wants to populate them
740
	 * @param bool $configMode Specify true if retrieving for configuration purposes rather than runtime purposes.
741
	 * @return array of <a> tags or array of labels if $configMode == true
742
	 * 
743
	 */
744
	protected function ___getViewActions($actions = array(), $configMode = false) {
745
 
746
		$labels = array(
747
			'view' => $this->_x('Page View', 'panel-title'),
748
			'panel' => $this->_x('Panel', 'view-label'),
749
			'modal' => $this->_x('Modal Popup', 'view-label'),
750
			'new' => $this->_x('New Window/Tab', 'view-label'),
751
			'this' => $this->_x('Exit + View', 'view-label'),
752
		);
753
 
754
		$icons = array(
755
			'panel' => 'columns',
756
			'modal' => 'picture-o',
757
			'new' => 'external-link-square',
758
			'this' => 'eye',
759
		);
760
 
761
		if($configMode) {
762
			unset($labels['view']);
763
			return $labels;
764
		}
765
 
766
		$url = $this->getViewUrl();
767
		if($this->page->hasStatus(Page::statusDraft) && strpos($url, '?') === false) $url .= '?draft=1';
768
		$languages = $this->hasLanguagePageNames ? $this->page->template->getLanguages() : null;
769
 
770
		foreach($icons as $name => $icon) {
771
			$labels[$name] = "<i class='fa fa-fw fa-$icon'></i>&nbsp;" . $labels[$name];
772
		}
773
 
774
		$class = '';
775
		$languageUrls = array();
776
		if($languages) {
777
			$class .= ' pw-has-items';
778
			foreach($languages as $language) {
779
				if(!$this->page->viewable($language)) continue;
780
				$localUrl = $this->page->localHttpUrl($language);
781
				if($this->page->hasStatus(Page::statusDraft) && strpos($localUrl, '?') === false) $localUrl .= '?draft=1';
782
				$languageUrls[$language->id] = $localUrl;
783
			}
784
		}
785
 
786
		$actions = array_merge(array(
787
			"panel" => "<a class='pw-panel pw-panel-reload$class' href='$url' data-tab-text='$labels[view]' data-tab-icon='eye'>$labels[panel]</a>",
788
			"modal" => "<a class='pw-modal pw-modal-large$class' href='$url'>$labels[modal]</a>",
789
			"new" => "<a class='$class' target='_blank' href='$url'>$labels[new]</a>",
790
			"this" => "<a class='$class' target='_top' href='$url'>$labels[this]</a>",
791
		), $actions);
792
 
793
		foreach($actions as $name => $action) {
794
			if(count($languageUrls) > 1) {
795
				$ul = "<ul class=''>";
796
				foreach($languages as $language) {
797
					/** @var Language $language */
798
					if(!isset($languageUrls[$language->id])) continue;
799
					$localUrl = $languageUrls[$language->id];
800
					$label = $language->get('title|name');
801
					$_action = str_replace(' pw-has-items', '', $action);
802
					$_action = str_replace("'$url'", "'$localUrl'", $_action);
803
					$_action = str_replace(">" . $labels[$name] . "<", ">$label<", $_action);
804
					$_action = str_replace("='$labels[view]'", "='$label'", $_action); // panel language
805
					$ul .= "<li>$_action</li>";
806
				}
807
				$ul .= "</ul>";
808
				$actions[$name] = str_replace('</a>', ' &nbsp;</a>', $actions[$name]) . $ul;
809
			} else {
810
				$actions[$name] = str_replace(' pw-has-items', '', $action);
811
			}
812
		}
813
 
814
		return $actions;
815
	}
816
 
817
	/**
818
	 * Get URL (or form action attribute) for editing this page
819
	 * 
820
	 * @param array $options
821
	 *  - `id` (int): Page ID to edit
822
	 *  - `modal` (int|string): Modal mode, when applicable
823
	 *  - `context` (string): Additional request context string, when applicable
824
	 *  - `language` (int|Language|string): Language for editor, if different from user’s language
825
	 *  - `field` (string): Only edit field with this name
826
	 *  - `fields` (string): CSV string of fields to edit, rather than all fields on apge
827
	 *  - `fnsx` (string): Field name suffix, applicable only when field or fields (above) is also set, in specific situations like repeaters
828
	 *  - `uploadOnlyMode (string|int): Upload only mode (internal use)
829
	 * @return string
830
	 * 
831
	 */
832
	public function getEditUrl($options = array()) {
833
		$defaults = array(
834
			'id' => $this->page->id, 
835
			'modal' => $this->requestModal,
836
			'context' => $this->requestContext,
837
			'language' => $this->requestLanguage,
838
			'field' => '', 
839
			'fields' => '',
840
			'fnsx' => $this->fnsx,
841
			'uploadOnlyMode' => '',
842
		);
843
		if($this->field) {
844
			$numFields = count($this->fields);
845
			if($numFields == 1 && $this->field) {
846
				$defaults['field'] = $this->field->name;
847
			} else if($numFields > 1) {
848
				$defaults['fields'] = implode(',', array_keys($this->fields));
849
			}
850
		}
851
		$uploadOnlyMode = (int) $this->input->get('uploadOnlyMode'); 
852
		if($uploadOnlyMode && !$this->config->ajax) $defaults['uploadOnlyMode'] = $uploadOnlyMode;
853
		$options = array_merge($defaults, $options);
854
		$qs = array();
855
		foreach($options as $name => $value) {
856
			if(!empty($value)) $qs[] = "$name=$value";
857
		}
858
		return './?' . implode('&', $qs);
859
	}
860
 
861
	/**
862
	 * Build the form used for Page Edits
863
	 * 
864
	 * @param InputfieldForm $form
865
	 * @return InputfieldForm
866
	 *
867
	 */
868
	protected function ___buildForm(InputfieldForm $form) {
869
 
870
		$form->attr('id+name', 'ProcessPageEdit');
871
		$form->attr('action', $this->getEditUrl(array('id' => $this->id)));
872
		$form->attr('method', 'post'); 
873
		$form->attr('enctype', 'multipart/form-data'); 
874
		$form->attr('class', 'ui-helper-clearfix template_' . $this->page->template . ' class_' . $this->page->className); 
875
		$form->attr('autocomplete', 'off');
876
		$form->attr('data-uploading', $this->_('Are you sure? An upload is currently in progress and it may be lost if you proceed.'));
877
 
878
		if($this->configSettings['confirm']) $form->addClass('InputfieldFormConfirm');
879
 
880
		// for ProcessPageEditImageSelect support
881
		if($this->input->get('uploadOnlyMode') && !$this->config->ajax) {
882
			// for modal uploading with InputfieldFile or InputfieldImage
883
			if(count($this->fields) && $this->field->type instanceof FieldtypeImage) {
884
				$this->setRedirectUrl("../image/?id=$this->id");
885
			}
886
		}
887
 
888
		$saveName = 'submit_save';
889
		$saveLabel = $this->_("Save"); // Button: save
890
		$submit2 = null; // second submit button, when applicable
891
 
892
		if($this->field) { 
893
			// focus in on a specific field or fields 
894
			$form->addClass('ProcessPageEditSingleField');
895
 
896
 
897
			foreach($this->fields as $field) {
898
				$options = array(
899
					'contextStr' => $this->fnsx,
900
					'fieldName' => $field->name,
901
					'namespace' => '',
902
					'flat' => true,
903
				);
904
				foreach($this->page->getInputfields($options) as $inputfield) {
905
					if(!$this->page->editable($field->name, false)) continue;
906
					$skipCollapsed = array(
907
						Inputfield::collapsedHidden,
908
						Inputfield::collapsedNoLocked,
909
						Inputfield::collapsedYesLocked,
910
					);
911
					$collapsed = $inputfield->getSetting('collapsed');
912
					if($collapsed > 0 && !in_array($collapsed, $skipCollapsed)) {
913
						$inputfield->collapsed = Inputfield::collapsedNo;
914
					}
915
					$form->add($inputfield);
916
				}
917
			}
918
 
919
		} else {
920
			// all fields
921
			// determine what content fields should become tabs
922
 
923
			$contentTab = $this->buildFormContent();
924
			$tabs = array();
925
			$tabWrap = null;
926
			$tabOpen = null;
927
			$tabViewable = null;
928
 
929
			foreach($contentTab as $inputfield) {
930
				if(!$tabOpen && $inputfield->className == 'InputfieldFieldsetTabOpen') {
931
					// open new tab
932
					$showable = $this->isTrash ? 'editable' : 'viewable';
933
					$tabViewable = $this->page->$showable($inputfield->attr('name'));
934
					if($this->isPost) {
935
						// only remove non-visible tabs when in post/save mode, for proper processInput()
936
						if(!$tabViewable) $contentTab->remove($inputfield);
937
						// during post requests, this goes no further, as theres no need for visual tab manipulation
938
						continue;
939
					}
940
					$tabOpen = $inputfield; 
941
					$tabWrap = $this->wire(new InputfieldWrapper());
942
					$tabWrap->attr('title', $tabOpen->getSetting('label'));
943
					$tabWrap->id = $tabOpen->attr('id');
944
					$tabWrap->collapsed = $tabOpen->getSetting('collapsed');
945
					// @todo support description in fieldset tab: works but needs styles for each admin theme, so commented out for now
946
					// $tabWrap->description = $inputfield->description;
947
					$tabWrap->notes = $inputfield->notes;
948
					$contentTab->remove($inputfield); 
949
					if(!$tabViewable) continue;
950
 
951
					if($inputfield->modal) {
952
						$href = $this->getEditUrl(array('field' => $inputfield->name, 'modal' => 1)); 
953
						$this->addTab($tabOpen->id, "<a class='pw-modal' " .
954
							"title='" . $this->sanitizer->entities($tabOpen->label) . "' " . 
955
							"data-buttons='#ProcessPageEdit button[type=submit]' " . 
956
							"data-autoclose='1' " . 
957
							"href='$href'>" . 
958
							$this->sanitizer->entities1($tabOpen->label) . "</a>");
959
						/** @var JqueryUI $jqueryUI */
960
						$jqueryUI = $this->modules->get('JqueryUI');
961
						$jqueryUI->use('modal');
962
						$tabOpen = null;
963
					} else {
964
						$this->addTab($tabOpen->id, $this->sanitizer->entities1($tabOpen->label));
965
					}
966
 
967
				} else if($tabOpen && !$this->isPost) {
968
					/** @var Inputfield $tabOpen */
969
					// already have a tab open
970
					if($inputfield->attr('name') == $tabOpen->attr('name') . '_END') {
971
						// close tab
972
						if($tabViewable) $tabs[] = $tabWrap; 
973
						$tabOpen = null;
974
					} else if($tabViewable) {
975
						// add to already open tab
976
						$tabWrap->add($inputfield); 
977
					}
978
					$contentTab->remove($inputfield); 
979
				}
980
			}
981
 
982
			$form->append($contentTab);
983
			if(!$this->isPost) {
984
				foreach($tabs as $tab) $form->append($tab);
985
			}
986
 
987
			if($this->page->addable() || $this->page->numChildren) $form->append($this->buildFormChildren()); 
988
			if(!$this->page->template->noSettings && $this->useSettings) $form->append($this->buildFormSettings()); 
989
			if($this->isTrash && !$this->isPost) {
990
				$this->message($this->_("This page is in the Trash"));
991
				$tabRestore = $this->buildFormRestore();
992
				if($tabRestore) $form->append($tabRestore);
993
			}
994
			$tabDelete = $this->buildFormDelete();
995
			if($tabDelete->children()->count()) $form->append($tabDelete);
996
			if($this->page->viewable() && !$this->requestModal) $this->buildFormView($this->getViewUrl()); 
997
 
998
			if($this->page->hasStatus(Page::statusUnpublished)) {
999
				$pageClassName = wireClassName($this->page, false); 
1000
				$publishable = $this->page->publishable();
1001
				if($publishable && (in_array($pageClassName, $this->otherCorePageClasses) || $this->page->template->noUnpublish)) {
1002
					// Do not show a button allowing page to remain unpublished for User, Permission, Role, Language or
1003
					// if the page's template indicates it cannot be unpublished
1004
				} else {
1005
					/** @var InputfieldSubmit $submit2 */
1006
					$submit2 = $this->modules->get('InputfieldSubmit');
1007
					$submit2->attr('name', 'submit_save');
1008
					$submit2->attr('id', 'submit_save_unpublished');
1009
					$submit2->showInHeader();
1010
					$submit2->setSecondary();
1011
					if($this->session->get('clientWidth') > 900) {
1012
						$submit2->attr('value', $this->_('Save + Keep Unpublished')); // Button: save unpublished
1013
					} else {
1014
						$submit2->attr('value', $saveLabel); // Button: save unpublished
1015
					}
1016
				}
1017
 
1018
				if($publishable) {
1019
					$saveName = 'submit_publish';
1020
					$saveLabel = $this->_("Publish"); // Button: publish
1021
				} else {
1022
					$saveName = '';
1023
				}
1024
			} else {
1025
				// use saveName and saveLabel defined at top of method
1026
			}
1027
		} // !$fieldName
1028
 
1029
		if($saveName) {
1030
			/** @var InputfieldSubmit $submit */
1031
			$submit = $this->modules->get('InputfieldSubmit');
1032
			$submit->attr('id+name', $saveName);
1033
			$submit->attr('value', $saveLabel);
1034
			$submit->showInHeader();
1035
			$form->append($submit);
1036
		}
1037
 
1038
		if($submit2) $form->append($submit2); 
1039
 
1040
		/** @var InputfieldHidden $field */
1041
		$field = $this->modules->get('InputfieldHidden');
1042
		$field->attr('name', 'id');
1043
		$field->attr('value', $this->page->id); 
1044
		$form->append($field);
1045
 
1046
		return $form; 
1047
	}
1048
 
1049
	/**
1050
	 * Build the 'content' tab on the Page Edit form
1051
	 * 
1052
	 * @return InputfieldWrapper
1053
	 *
1054
	 */
1055
	protected function ___buildFormContent() {
1056
 
1057
		$fields = $this->page->getInputfields(array('flat' => !$this->isPost));
1058
		$id = $this->className() . 'Content'; 
1059
		$title = $this->page->template->getTabLabel('content'); 
1060
		if(!$title) $title = $this->_('Content'); // Tab Label: Content
1061
 
1062
		$fields->attr('id', $id); 
1063
		$fields->attr('title', $title); 
1064
		$fields->addClass('WireTab');
1065
		$this->addTab($id, $title);
1066
 
1067
		if($this->page->template->nameContentTab) {
1068
			$fields->prepend($this->buildFormPageName());
1069
		}
1070
 
1071
		return $fields;
1072
	}
1073
 
1074
	/**
1075
	 * Build the 'children' tab on the Page Edit form
1076
	 * 
1077
	 * @return InputfieldWrapper
1078
 	 *
1079
	 */
1080
	protected function ___buildFormChildren() {
1081
 
1082
		$page = $this->masterPage ? $this->masterPage : $this->page; 
1083
		$wrapper = $this->wire(new InputfieldWrapper());
1084
		$id = $this->className() . 'Children';
1085
		$wrapper->attr('id+name', $id);
1086
		if(!empty($this->configSettings['ajaxChildren'])) $wrapper->collapsed = Inputfield::collapsedYesAjax;
1087
		$defaultTitle = $this->_('Children'); // Tab Label: Children
1088
		$title = $this->page->template->getTabLabel('children'); 
1089
		if(!$title) $title = $defaultTitle;
1090
		if($page->numChildren) $wrapper->attr('title', "<em>$title</em>"); 
1091
			else $wrapper->attr('title', $title); 
1092
		$this->addTab($id, $title);
1093
		$templateSortfield = $this->page->template->sortfield;
1094
 
1095
		if(!$this->isPost) { 
1096
 
1097
			$pageListParent = $page ? $page : $this->parent;
1098
			if($pageListParent->numChildren) {
1099
				/** @var ProcessPageList $pageList */
1100
				$pageList = $this->modules->get('ProcessPageList'); 
1101
				$pageList->set('id', $pageListParent->id); 
1102
				$pageList->set('showRootPage', false); 
1103
			} else $pageList = null;
1104
 
1105
			/** @var InputfieldMarkup $field */
1106
			$field = $this->modules->get("InputfieldMarkup"); 
1107
			$field->attr('id+name', 'ChildrenPageList');
1108
			$field->label = $title == $defaultTitle ? $this->_("Children / Subpages") : $title; // Children field label
1109
			if($pageList) {
1110
				$field->value = $pageList->execute();
1111
			} else {
1112
				$field->description = $this->_("There are currently no children/subpages below this page.");
1113
			}
1114
 
1115
			if($templateSortfield && $templateSortfield != 'sort') {
1116
				$field->notes = sprintf($this->_('Children are sorted by "%s", per the template setting.'), $templateSortfield); 
1117
			}
1118
 
1119
			if($page->addable()) { 
1120
				/** @var InputfieldButton $button */
1121
				$button = $this->modules->get("InputfieldButton"); 
1122
				$button->attr('id+name', 'AddPageBtn'); 
1123
				$button->attr('value', $this->_('Add New Page Here')); // Button: add new child page
1124
				$button->icon = 'plus-circle';
1125
				$button->attr('href', "../add/?parent_id={$page->id}" . ($this->requestModal ? "&modal=$this->requestModal" : ''));
1126
				$field->append($button);
1127
			}
1128
			$wrapper->append($field); 
1129
		}
1130
 
1131
		if(empty($this->page->template->sortfield) && $this->user->hasPermission('page-sort', $this->page)) { 		
1132
			$sortfield = $this->page->sortfield && $this->page->sortfield != 'sort' ? $this->page->sortfield : '';
1133
			$fieldset = self::buildFormSortfield($sortfield, $this); 
1134
			$fieldset->attr('id+name', 'ChildrenSortSettings'); 
1135
			$fieldset->label = $this->_('Sort Settings'); // Children sort settings field label
1136
			$fieldset->icon = 'sort';
1137
			$fieldset->description = $this->_("If you want all current and future children to automatically sort by a specific field, select the field below and optionally check the 'reverse' checkbox to make the sort descending. Leave the sort field blank if you want to be able to drag-n-drop to your own order."); // Sort settings description text
1138
			$wrapper->append($fieldset); 
1139
		}
1140
 
1141
		return $wrapper;
1142
	}
1143
 
1144
	/**
1145
	 * Build the sortfield configuration fieldset
1146
	 *
1147
	 * NOTE: This is also used by ProcessTemplate, so it is self contained
1148
	 *
1149
	 * @param string $sortfield Current sortfield value
1150
	 * @param Process $caller The calling process
1151
	 * @return InputfieldFieldset
1152
	 *
1153
	 */
1154
	public static function buildFormSortfield($sortfield, Process $caller) {
1155
 
1156
		$fieldset = $caller->wire('modules')->get("InputfieldFieldset"); 
1157
		if(!$sortfield) $fieldset->collapsed = Inputfield::collapsedYes; 
1158
 
1159
		$field = $caller->wire('modules')->get('InputfieldSelect');
1160
		$field->name = 'sortfield'; 
1161
		$field->value = ltrim($sortfield, '-'); 
1162
		$field->columnWidth = 60; 
1163
		$field->label = __('Children are sorted by', __FILE__); // Children sort field label
1164
 
1165
		// if in ProcessTemplate, give a 'None' option that indicates the Page has control
1166
		if($caller instanceof ProcessTemplate) $field->addOption('', __('None', __FILE__)); 
1167
 
1168
		$field->addOption('sort', __('Manual drag-n-drop', __FILE__));
1169
 
1170
		$options = array(
1171
			'name' => 'name', 
1172
			'status' => 'status', 
1173
			'modified' => 'modified', 
1174
			'created' => 'created',
1175
			'published' => 'published', 
1176
			); 
1177
 
1178
		$field->addOption(__('Native Fields', __FILE__), $options); // Optgroup label for sorting by fields native to ProcessWire
1179
 
1180
		$customOptions = array();
1181
 
1182
		foreach($caller->wire('fields') as $f) {
1183
			//if(!($f->flags & Field::flagAutojoin)) continue; 
1184
			if($f->flags & Field::flagSystem && $f->name != 'title' && $f->name != 'email') continue; 
1185
			if($f->type instanceof FieldtypeFieldsetOpen) continue; 
1186
			$customOptions[$f->name] = $f->name; 
1187
		}
1188
 
1189
		ksort($customOptions); 
1190
		$field->addOption(__('Custom Fields', __FILE__), $customOptions); // Optgroup label for sorting by custom fields
1191
		$fieldset->append($field); 
1192
 
1193
		$f = $caller->wire('modules')->get('InputfieldCheckbox');
1194
		$f->value = 1; 
1195
		$f->attr('id+name', 'sortfield_reverse'); 
1196
		$f->label = __('Reverse sort direction?', __FILE__); // Checkbox labe to reverse the sort direction
1197
		$f->icon = 'rotate-left';
1198
		if(substr($sortfield, 0, 1) == '-') $f->attr('checked', 'checked'); 
1199
		$f->showIf = "sortfield!='', sortfield!=sort";
1200
		$f->columnWidth = 40; 
1201
 
1202
		$fieldset->append($f); 
1203
		return $fieldset; 
1204
	}
1205
 
1206
	/**
1207
	 * Build the 'settings' tab on the Page Edit form
1208
	 * 
1209
	 * @return InputfieldWrapper
1210
	 *
1211
	 */
1212
	protected function ___buildFormSettings() {
1213
 
1214
		$superuser = $this->wire('user')->isSuperuser();
1215
 
1216
		/** @var InputfieldWrapper $wrapper */
1217
		$wrapper = $this->wire(new InputfieldWrapper());
1218
		$id = $this->className() . 'Settings';
1219
		$title = $this->_('Settings'); // Tab Label: Settings
1220
		$wrapper->attr('id', $id); 
1221
		$wrapper->attr('title', $title); 
1222
		$this->addTab($id, $title);
1223
 
1224
		// name
1225
		if(($this->page->id > 1 || $this->hasLanguagePageNames) && !$this->page->template->nameContentTab) {
1226
			$wrapper->prepend($this->buildFormPageName()); 
1227
		}
1228
 
1229
		// template
1230
		$wrapper->add($this->buildFormTemplate()); 
1231
 
1232
		// parent
1233
		if($this->page->id > 1 && $this->page->editable('parent', false)) {
1234
			$wrapper->add($this->buildFormParent()); 
1235
		}
1236
 
1237
		// createdUser
1238
		if($this->page->id && $superuser && $this->page->template->allowChangeUser) {
1239
			$wrapper->add($this->buildFormCreatedUser());
1240
		}
1241
 
1242
		// status
1243
		$wrapper->add($this->buildFormStatus()); 
1244
 
1245
		// roles and references
1246
		if(!$this->isPost) {
1247
			// what users may access this page
1248
			$wrapper->add($this->buildFormRoles());
1249
			// what pages link tot his page
1250
			$wrapper->add($this->buildFormReferences());
1251
		}
1252
 
1253
		// page path history (previous URLs)
1254
		if($superuser) {
1255
			$f = $this->buildFormPrevPaths();
1256
			if($f) $wrapper->add($f);
1257
		}
1258
 
1259
		// information about created and modified user and time
1260
		if(!$this->isPost) {
1261
			$wrapper->add($this->buildFormInfo());
1262
		}
1263
 
1264
		return $wrapper; 
1265
	}
1266
 
1267
	/**
1268
	 * Build the page name input
1269
	 *
1270
	 * @return InputfieldPageName
1271
	 *
1272
	 */
1273
	protected function buildFormPageName() {
1274
 
1275
		/** @var InputfieldPageName $field */
1276
		$field = $this->modules->get('InputfieldPageName');
1277
		$field->attr('name', '_pw_page_name');
1278
		$field->attr('value', $this->page->name);
1279
		$field->slashUrls = $this->page->template->slashUrls;
1280
		$field->required = $this->page->id != 1 && !$this->page->hasStatus(Page::statusTemp);
1281
 
1282
		$label = $this->page->template->getNameLabel();
1283
		if($label) $field->label = $label;
1284
 
1285
		if(!$this->page->editable('name', false)) {
1286
			$field->attr('disabled', 'disabled');
1287
			$field->required = false;
1288
		}
1289
 
1290
		if($this->hasLanguagePageNames) {
1291
			// Using 'hasLanguages' as opposed to 'useLanguages' for different support from LanguageSupportPageNames
1292
			$field->setQuietly('hasLanguages', true);
1293
		}
1294
 
1295
		$field->editPage = $this->page;
1296
		if($this->page->parent) $field->parentPage = $this->page->parent;
1297
 
1298
		return $field;
1299
	}
1300
 
1301
	/**
1302
	 * Build the template selection field
1303
	 *
1304
	 * @return InputfieldMarkup|InputfieldSelect
1305
	 *
1306
	 */
1307
	protected function buildFormTemplate() {
1308
 
1309
		if($this->page->editable('template', false)) {
1310
			/** @var Languages $languages */
1311
			$languages = $this->wire('languages');
1312
			/** @var Language $language */
1313
			$language = $this->user->language;
1314
 
1315
			/** @var InputfieldSelect $field */
1316
			$field = $this->modules->get('InputfieldSelect');
1317
			$field->attr('id+name', 'template');
1318
			$field->attr('value', $this->page->template->id);
1319
			$field->required = true;
1320
 
1321
			foreach($this->getAllowedTemplates() as $template) {
1322
				/** @var Template $template */
1323
				$label = '';
1324
				if($languages && $language) $label = $template->get('label' . $language->id);
1325
				if(!$label) $label = $template->label ? $template->label : $template->name;
1326
				$field->addOption($template->id, $label);
1327
			}
1328
		} else {
1329
			/** @var InputfieldMarkup $field */
1330
			$field = $this->modules->get('InputfieldMarkup');
1331
			$field->attr('value', "<p>" . $this->page->template->getLabel() . "</p>");
1332
		}
1333
 
1334
		$field->label = $this->_('Template'); // Settings: Template field label
1335
		$field->icon = 'cubes';
1336
 
1337
		return $field;
1338
	}
1339
 
1340
	/**
1341
	 * Build the parent selection Inputfield
1342
	 *
1343
	 * @return InputfieldPageListSelect|InputfieldSelect
1344
	 *
1345
	 */
1346
	protected function buildFormParent() {
1347
 
1348
		if(count($this->predefinedParents)) {
1349
			/** @var InputfieldSelect $field */
1350
			$field = $this->modules->get('InputfieldSelect');
1351
			foreach($this->predefinedParents as $p) {
1352
				$field->addOption($p->id, $p->path);
1353
			}
1354
 
1355
		} else {
1356
			/** @var InputfieldPageListSelect $field */
1357
			$field = $this->modules->get('InputfieldPageListSelect');
1358
			$field->set('parent_id', 0);
1359
			if(!empty($this->configSettings['ajaxParent'])) {
1360
				$field->collapsed = Inputfield::collapsedYesAjax;
1361
			}
1362
		}
1363
 
1364
		$field->required = true;
1365
		$field->label = $this->_('Parent'); // Settings: Parent field label
1366
		$field->icon = 'folder-open-o';
1367
		$field->attr('id+name', 'parent_id');
1368
		$field->attr('value', $this->page->parent_id);
1369
 
1370
		return $field;
1371
	}
1372
 
1373
	/**
1374
	 * Build the created user selection
1375
	 *
1376
	 * @return InputfieldPageListSelect
1377
	 *
1378
	 */
1379
	protected function buildFormCreatedUser() {
1380
		/** @var InputfieldPageListSelect $field */
1381
		$field = $this->modules->get('InputfieldPageListSelect');
1382
		$field->label = $this->_('Created by User');
1383
		$field->attr('id+name', 'created_users_id');
1384
		$field->attr('value', $this->page->created_users_id);
1385
		$field->parent_id = $this->config->usersPageID; // @todo support $config->usersPageIDs (array)
1386
		$field->showPath = false;
1387
		$field->required = true;
1388
 
1389
		return $field;
1390
	}
1391
 
1392
	/**
1393
	 * Build the Settings > References fieldset on the Page Edit form
1394
	 *
1395
	 * @return InputfieldMarkup
1396
	 *
1397
	 */
1398
	protected function buildFormReferences() {
1399
 
1400
		/** @var InputfieldMarkup $field */
1401
		$field = $this->modules->get('InputfieldMarkup');
1402
		$field->attr('id', 'ProcessPageEditReferences');
1403
		$field->label = $this->_('What pages link to this page?');
1404
		$field->icon = 'link';
1405
		$field->collapsed = Inputfield::collapsedYesAjax;
1406
 
1407
		if($this->input->get('renderInputfieldAjax') != 'ProcessPageEditReferences') return $field;
1408
 
1409
		$links = $this->page->links("include=all, limit=100");
1410
		$references = $this->page->references("include=all, limit=100");
1411
 
1412
		$numTotal = $references->getTotal() + $links->getTotal();
1413
		$numShown = $references->count() + $links->count();
1414
		$numNotShown = $numTotal - $numShown;
1415
		$labelNotListable = $this->_('Not listable');
1416
 
1417
		if($numTotal) {
1418
			$field->description = sprintf(
1419
				$this->_('Found %d other page(s) linking to this one in Page fields or href links.'),
1420
				$numTotal
1421
			);
1422
			$out = "<ul>";
1423
			$itemsByType = array(
1424
				$this->_('(in page field)') => $references,
1425
				$this->_('(in href link)') => $links
1426
			);
1427
			foreach($itemsByType as $label => $items) {
1428
				$label = "<span class='detail'>$label</span>";
1429
				foreach($items as $item) {
1430
					/** @var Page $item */
1431
					if($item->listable()) {
1432
						$url = $item->editable() ? $item->editUrl() : $item->url();
1433
						$out .= "<li><a href='$url' title='$item->url' target='_blank'>" . $item->get('title|path') . "</a> $label</li>";
1434
					} else {
1435
						$out .= "<li>$item->id $labelNotListable $label</li>";
1436
					}
1437
				}
1438
			}
1439
			$out .= "</ul>";
1440
			if($numNotShown) {
1441
				$out .= "<div class='notes'>" . sprintf($this->_('%d additional pages not shown.'), $numNotShown) . "</div>";
1442
			}
1443
		} else {
1444
			$out = "<p>" . $this->_('Did not find any other pages pointing to this one in page fields or href links.') . "</p>";
1445
		}
1446
 
1447
		$field->value = $out;
1448
 
1449
		return $field;
1450
	}
1451
 
1452
	/**
1453
	 * Build the “Settings > What URLs redirect to this page?” fieldset on the Page Edit form
1454
	 *
1455
	 * @return InputfieldMarkup|null
1456
	 *
1457
	 */
1458
	protected function buildFormPrevPaths() {
1459
 
1460
		/** @var WireInput $input */
1461
		$input = $this->wire('input');
1462
		/** @var Modules $modules */
1463
		$modules = $this->wire('modules');
1464
		/** @var Sanitizer $sanitizer */
1465
		$sanitizer = $this->wire('sanitizer');
1466
		/** @var Languages|null $languages */
1467
		$languages = $this->wire('languages');
1468
 
1469
		if($this->isPost && $input->post('_prevpath_add') === null) return null;
1470
		if(!$modules->isInstalled('PagePathHistory')) return null;
1471
 
1472
		/** @var InputfieldMarkup $field */
1473
		$field = $modules->get('InputfieldMarkup');
1474
		$field->attr('id', 'ProcessPageEditPrevPaths');
1475
		$field->label = $this->_('What other URLs redirect to this page?');
1476
		$field->icon = 'map-signs';
1477
 
1478
		if(!$this->isPost) {				
1479
			$field->collapsed = Inputfield::collapsedYesAjax;
1480
			if($input->get('renderInputfieldAjax') != 'ProcessPageEditPrevPaths') return $field;
1481
		}
1482
 
1483
		$field->description = 
1484
			$this->_('Whenever a page is moved or the name changes, we remember the previous location for redirects.') . ' ' . 
1485
			$this->_('Below is a list of URLs (paths) that automatically redirect to this page (using 301 permanent redirect).') . ' ' . 
1486
			$this->_('You may delete any paths/URLs or manually add new ones.'); 
1487
 
1488
		/** @var PagePathHistory $history */
1489
		$history = $modules->get('PagePathHistory');
1490
		$data = $history->getPathHistory($this->page, array(
1491
			'verbose' => true,
1492
			'virtual' => true
1493
		));
1494
 
1495
		$multilang = $languages && $modules->isInstalled('LanguageSupportPageNames');
1496
		$slashUrls = $this->page->template->slashUrls;
1497
		$deleteIDs = array();
1498
		$rootUrl = $this->wire('config')->urls->root;
1499
 
1500
		/** @var InputfieldCheckbox $delete */
1501
		$delete = $modules->get('InputfieldCheckbox');
1502
		$delete->label = wireIconMarkup('trash-o');
1503
		$delete->attr('name', '_prevpath_delete[]');
1504
		$delete->entityEncodeLabel = false;
1505
		$delete->attr('title', $this->_x('Delete', 'prev-path-delete'));
1506
		$delete->renderReady();
1507
 
1508
		if($this->isPost) {
1509
			$deleteIDs = array_flip($input->post->array('_prevpath_delete'));
1510
		}
1511
 
1512
		/** @var MarkupAdminDataTable $table */
1513
		$table = $modules->get('MarkupAdminDataTable');
1514
		$table->setEncodeEntities(false);
1515
		$table->setSortable(false);
1516
 
1517
		$header = array(
1518
			$this->_x('URL', 'prev-path'),
1519
			$this->_x('When', 'prev-path-date'),
1520
		);
1521
 
1522
		if(count($data)) {
1523
			if($multilang) $header[] = $this->_x('Language', 'prev-path-language');
1524
			$header[] = '&nbsp;';
1525
			if(!$multilang) {
1526
				$row = array(
1527
					$sanitizer->entities($this->page->path),
1528
					$this->_x('Current', 'prev-path-current'),
1529
					'&nbsp;',
1530
				);
1531
				$table->row($row);
1532
			}
1533
		} else {
1534
			$table->row(array(
1535
				$this->_('No redirect paths'),
1536
				$this->_('Not yet')
1537
			));
1538
		}
1539
 
1540
		$table->headerRow($header);
1541
 
1542
		foreach($data as $n => $item) {
1543
 
1544
			$id = md5($item['path'] . $item['date']); 
1545
			$path = $item['path'];
1546
 
1547
			if($this->isPost && isset($deleteIDs[$id])) {
1548
				if($history->deletePathHistory($this->page, $path)) {
1549
					$this->message(sprintf($this->_('Deleted redirect for previous URL: %s'), $path));
1550
					continue;
1551
				}
1552
			}
1553
 
1554
			if($slashUrls) $path .= '/';
1555
 
1556
			$url = $sanitizer->entities(rtrim($rootUrl, '/') . $path);
1557
			$path = $sanitizer->entities($path);
1558
			$row = array(
1559
				"<a href='$url' target='_blank'>$path</a>",
1560
				wireRelativeTimeStr($item['date']),
1561
			);
1562
			if($multilang && isset($item['language'])) {
1563
				/** @var Language $language */
1564
				$language = $item['language'];
1565
				if($language && $language->id) {
1566
					$langLabel = $language->get('title|name');
1567
					if(!$language->isDefault() && !$this->page->get("status$language")) $langLabel = "<s>$langLabel</s>";
1568
					$row[] = $langLabel;
1569
				} else {
1570
					$row[] = '?';
1571
				}
1572
			}
1573
			if(empty($item['virtual'])) {
1574
				$delete->attr('name', '_prevpath_delete[]');
1575
				$delete->attr('value', $id);
1576
				$row[] = "<div class='InputfieldCheckbox'>" . $delete->render() . "</div>";
1577
			} else {
1578
				$parentLabel = $this->_x('Parent', 'prev-path-parent');
1579
				$parent = $this->wire('pages')->get((int) $item['virtual']);
1580
				if($parent->id) $parentLabel = "<a target='_blank' title='$parent->path' href='$parent->editUrl'>$parentLabel</a>";
1581
				$row[] = $parentLabel;
1582
			}
1583
			$table->row($row);
1584
		}
1585
 
1586
		/** @var InputfieldTextarea $add */
1587
		$add = $modules->get('InputfieldTextarea');
1588
		$add->attr('name', '_prevpath_add'); 
1589
		$add->label = $this->_('Add new redirect URLs');
1590
		$add->description = 
1591
			$this->_('Enter additional paths/URLs (one per line) that should redirect to this page.') . ' ' . 
1592
			$this->_('Enter the URL path only (i.e. “/hello/world/”), do NOT include scheme, domain, port, query string or fragments.') . ' ';
1593
		if($rootUrl != '/') {
1594
			$add->description .= sprintf(
1595
				$this->_('Paths are relative to site root so do NOT include the %s subdirectory at the beginning.'), 
1596
				$rootUrl
1597
			);
1598
		}
1599
		$add->collapsed = Inputfield::collapsedYes;
1600
		$add->icon = 'plus';
1601
		$add->addClass('InputfieldIsSecondary', 'wrapClass');
1602
		if($multilang) {
1603
			$add->notes = $this->_('To specify a language for the redirect, enter path/URL on line prefixed with language name:');
1604
			foreach($languages->findNonDefault() as $language) {
1605
				$add->notes .= "\n`$language->name:" . 
1606
					sprintf($this->_('/your/%s/url/'), $language->name) . "` " . // /your/[language-name]/url/
1607
					sprintf($this->_('(for %s)'), $language->get('title|name')); // (for [language-title])
1608
			}
1609
		}
1610
 
1611
		if($this->isPost) {
1612
			$add->processInput($input->post);
1613
			if($add->val()) {
1614
				foreach(explode("\n", $add->val()) as $path) {
1615
					if(strpos($path, ':')) {
1616
						list($langName, $path) = explode(':', $path, 2);
1617
						$language = $languages->get($sanitizer->pageName($langName));
1618
						if(!$language || !$language->id) $language = null;
1619
					} else {
1620
						$language = null;
1621
					}
1622
					$path = $sanitizer->pagePathName($path);
1623
					if(!strlen($path)) continue; 
1624
					if($history->addPathHistory($this->page, $path, $language)) {
1625
						$this->message(sprintf(
1626
							$this->_('Added redirect: %s'), 
1627
							$path
1628
						));
1629
					} else {
1630
						$this->warning(sprintf(
1631
							$this->_('Unable to add redirect %s because it appears to conflict with another path'), 
1632
							$path
1633
						));
1634
					}
1635
				}
1636
			}
1637
		} else {
1638
			$field->val($table->render());
1639
			$field->add($add);
1640
		}
1641
 
1642
		return $field;
1643
	}
1644
 
1645
	/**
1646
	 * Build the Settings > Info fieldset on the Page Edit form
1647
	 * 
1648
	 * @return InputfieldMarkup
1649
	 *
1650
	 */
1651
	protected function buildFormInfo() {
1652
		$page = $this->page; 
1653
		$dateFormat = $this->config->dateFormat;
1654
		$unknown = '[?]';
1655
		/** @var InputfieldMarkup $field */
1656
		$field = $this->modules->get("InputfieldMarkup"); 
1657
		$createdName = $page->createdUser ? $page->createdUser->name : ''; 
1658
		$modifiedName = $page->modifiedUser ? $page->modifiedUser->name : ''; 
1659
		if(empty($createdName)) $createdName = $unknown;
1660
		if(empty($modifiedName)) $modifiedName = $unknown;
1661
		if($this->user->isSuperuser()) {
1662
			$url = $this->config->urls->admin . 'access/users/edit/?id=';
1663
			if($createdName != $unknown && $page->createdUser instanceof User) $createdName = "<a href='$url{$page->createdUser->id}'>$createdName</a>";
1664
			if($modifiedName != $unknown && $page->modifiedUser instanceof User) $modifiedName = "<a href='$url{$page->modifiedUser->id}'>$modifiedName</a>";
1665
		}
1666
		$lowestDate = strtotime('1974-10-10');
1667
		$createdDate = $page->created > $lowestDate ? date($dateFormat, $page->created) . " " . 
1668
			"<span class='detail'>(" . wireRelativeTimeStr($page->created) . ")</span>" : $unknown;
1669
		$modifiedDate = $page->modified > $lowestDate ? date($dateFormat, $page->modified) . " " . 
1670
			"<span class='detail'>(" . wireRelativeTimeStr($page->modified) . ")</span>" : $unknown; 
1671
		$publishedDate = $page->published > $lowestDate ? date($dateFormat, $page->published) . " " . 
1672
			"<span class='detail'>(" . wireRelativeTimeStr($page->published) . ")</span>" : $unknown;
1673
 
1674
		$info =	"\n<p>" . 
1675
				sprintf($this->_('Created by %1$s on %2$s'), $createdName, $createdDate) . "<br />" . // Settings: created user/date information line
1676
				sprintf($this->_('Last modified by %1$s on %2$s'), $modifiedName, $modifiedDate) . "<br />" . // Settings: modified user/date information line
1677
				sprintf($this->_('Published on %s'), $publishedDate) . // Settings: published information line
1678
				"</p>"; 
1679
 
1680
		$field->attr('id+name', 'ProcessPageEditInfo'); 
1681
		$field->label = $this->_('Info'); // Settings: Info field label
1682
		$field->icon = 'info-circle';
1683
		if($this->config->advanced) $field->notes = "Object type: " . $page->className();
1684
		$field->value = $info; 
1685
 
1686
		return $field; 
1687
	}
1688
 
1689
	/**
1690
	 * Build the Settings > Status fieldset on the Page Edit form
1691
	 * 
1692
	 * @return InputfieldCheckboxes
1693
	 *
1694
	 */
1695
	protected function buildFormStatus() {
1696
 
1697
		$status = (int) $this->page->status;
1698
		$statuses = array(); 
1699
		$debug = $this->config->debug;
1700
		$advanced = $this->config->advanced;
1701
 
1702
		/** @var InputfieldCheckboxes $field */
1703
		$field = $this->modules->get('InputfieldCheckboxes');
1704
		$field->attr('name', 'status');
1705
		$field->icon = 'sliders';
1706
 
1707
		if(!$this->page->template->noUnpublish && $this->page->publishable()) {
1708
			$statuses[Page::statusUnpublished] = $this->_('Unpublished: Not visible on site'); // Settings: Unpublished status checkbox label
1709
		}
1710
		if($this->user->hasPermission('page-hide', $this->page)) {
1711
			$statuses[Page::statusHidden] = $this->_('Hidden: Excluded from lists and searches'); // Settings: Hidden status checkbox label
1712
		}
1713
		if($this->user->hasPermission('page-lock', $this->page)) {
1714
			$statuses[Page::statusLocked] = $this->_('Locked: Not editable'); // Settings: Locked status checkbox label
1715
		}
1716
 
1717
		if($this->user->isSuperuser()) {
1718
			$statuses[Page::statusUnique] = sprintf($this->_('Unique: Require page name “%s” to be globally unique'), $this->page->name) . 
1719
				($this->wire('languages') ?  ' ' . $this->_('(in default language only)') : '');
1720
			if($advanced) {
1721
				$statuses[Page::statusSystemID] = "System: Non-deleteable and locked ID (status not removeable via API)";
1722
				$statuses[Page::statusSystem] = "System: Non-deleteable and locked ID, name, template, parent (status not removeable via API)";
1723
			}
1724
		}
1725
 
1726
		$value = array();
1727
 
1728
		foreach($statuses as $s => $label) {
1729
			if($s & $status) $value[] = $s;
1730
			if(strpos($label, ': ')) $label = str_replace(': ', ': [span.detail]', $label) . '[/span]';
1731
			$field->addOption($s, $label);
1732
		}
1733
 
1734
		$field->attr('value', $value); 
1735
		$field->label = $this->_('Status'); // Settings: Status field label
1736
 
1737
		if($debug) $field->notes = $this->page->statusStr;
1738
 
1739
		return $field; 
1740
	}
1741
 
1742
	/**
1743
	 * Build the 'delete' tab on the Page Edit form
1744
	 * 
1745
	 * @return InputfieldWrapper
1746
	 *
1747
	 */
1748
	protected function ___buildFormDelete() {
1749
 
1750
		$wrapper = $this->wire(new InputfieldWrapper());
1751
		$deleteable = $this->page->deleteable();
1752
		$trashable = $deleteable || $this->page->trashable();
1753
		if(!$trashable) return $wrapper;
1754
 
1755
		$id = $this->className() . 'Delete';
1756
		$deleteLabel = $this->_('Delete'); // Tab Label: Delete
1757
		$wrapper->attr('id', $id); 
1758
		$wrapper->attr('title', $deleteLabel); 
1759
		$this->addTab($id, $deleteLabel);
1760
 
1761
		if($trashable) {
1762
 
1763
			/** @var InputfieldCheckbox $field */
1764
			$field = $this->modules->get('InputfieldCheckbox');
1765
			$field->attr('id+name', 'delete_page'); 
1766
			$field->attr('value', $this->page->id); 
1767
 
1768
			if($deleteable && ($this->isTrash || $this->page->template->noTrash)) {
1769
				$deleteLabel = $this->_('Delete Permanently'); // Delete permanently checkbox label
1770
			} else {
1771
				$deleteLabel = $this->_('Move to Trash'); // Move to trash checkbox label
1772
			}
1773
			$field->icon = 'trash-o';
1774
			$field->label = $deleteLabel;
1775
			$field->description = $this->_('Check the box to confirm that you want to do this.'); // Delete page confirmation instruction
1776
			$field->label2 = $this->_('Confirm'); 
1777
			$wrapper->append($field); 
1778
		}
1779
 
1780
		if(count($wrapper->children())) {
1781
			$field = $this->modules->get('InputfieldButton');
1782
			$field->attr('id+name', 'submit_delete'); 
1783
			$field->value = $deleteLabel;
1784
			$wrapper->append($field);
1785
		} else {
1786
			$wrapper->description = $this->_('This page may not be deleted at this time'); // Page can't be deleted message
1787
		}
1788
 
1789
		return $wrapper;
1790
	}
1791
 
1792
	/**
1793
	 * Build the 'restore' tab shown for pages in the trash
1794
	 * 
1795
	 * Returns boolean false if restore not possible. 
1796
	 * 
1797
	 * @return InputfieldWrapper|bool
1798
	 * 
1799
	 */
1800
	protected function buildFormRestore() {
1801
 
1802
		if(!$this->page->isTrash()) return false;
1803
		if(!$this->page->restorable()) return false;
1804
		$info = $this->wire('pages')->trasher()->getRestoreInfo($this->page);
1805
		if(!$info['restorable']) return false;
1806
 
1807
		/** @var InputfieldWrapper $wrapper */
1808
		$wrapper = $this->wire(new InputfieldWrapper());
1809
		$id = $this->className() . 'Restore';
1810
		$restoreLabel = $this->_('Restore'); // Tab Label: Restore
1811
		$restoreLabel2 = $this->_('Move out of trash and restore to original location'); 
1812
		$wrapper->attr('id', $id);
1813
		$wrapper->attr('title', $restoreLabel);
1814
		$this->addTab($id, $restoreLabel);
1815
		/** @var Page $parent */
1816
		$parent = $info['parent'];
1817
		$newPath = $parent->path() . $info['name'] . '/';
1818
 
1819
		/** @var InputfieldCheckbox $field */
1820
		$field = $this->modules->get('InputfieldCheckbox');
1821
		$field->attr('id+name', 'restore_page');
1822
		$field->attr('value', $this->page->id);
1823
 
1824
		$field->icon = 'trash-o';
1825
		$field->label = $restoreLabel2;
1826
		$field->description = $this->_('Check the box to confirm that you want to restore this page.'); // Restore page confirmation instruction
1827
		$field->notes = sprintf($this->_('The page will be restored to: **%s**.'), $newPath);
1828
		if($info['namePrevious']) $field->notes .= ' ' . 
1829
			sprintf($this->_('Original name will be adjusted from **%1$s** to **%2$s** to be unique.'), $info['namePrevious'], $info['name']);
1830
		$field->label2 = $restoreLabel;
1831
		$wrapper->append($field);
1832
 
1833
		return $wrapper;
1834
	}
1835
 
1836
	/**
1837
	 * Build the 'view' tab on the Page Edit form
1838
	 * 
1839
	 * @param string $url
1840
	 *
1841
	 */ 
1842
	protected function ___buildFormView($url) {
1843
 
1844
		$label = $this->_('View'); // Tab Label: View
1845
		$id = $this->className() . 'View';
1846
 
1847
		if((!empty($this->configSettings['viewNew'])) || $this->viewAction == 'new') {
1848
			$target = '_blank';
1849
		} else {
1850
			$target = '_top';
1851
		}
1852
 
1853
		$a = 
1854
			"<a id='_ProcessPageEditView' target='$target' href='$url' data-action='$this->viewAction'>$label" . 
1855
			"<span id='_ProcessPageEditViewDropdownToggle' class='pw-dropdown-toggle' data-pw-dropdown='#_ProcessPageEditViewDropdown'>" . 
1856
			"<i class='fa fa-angle-down'></i></span></a>";
1857
 
1858
		$this->addTab($id, $a);
1859
	}
1860
 
1861
	/**
1862
	 * Build the Settings > Roles fieldset on the Page Edit form 
1863
	 * 
1864
	 * @return InputfieldMarkup
1865
	 *
1866
	 */
1867
	protected function ___buildFormRoles() {
1868
 
1869
		/** @var InputfieldMarkup $field */
1870
		$field = $this->modules->get("InputfieldMarkup"); 
1871
		$field->label = $this->_('Who can access this page?'); // Roles information field label
1872
		$field->icon = 'users';
1873
		$field->attr('id+name', 'ProcessPageEditRoles');
1874
		$field->collapsed = Inputfield::collapsedYesAjax;
1875
 
1876
		/** @var MarkupAdminDataTable $table */
1877
		$table = $this->modules->get("MarkupAdminDataTable"); 
1878
 
1879
		if($this->input->get('renderInputfieldAjax') == 'ProcessPageEditRoles') {
1880
			$roles = $this->page->getAccessRoles();
1881
			$accessTemplate = $this->page->getAccessTemplate('edit');
1882
			if($accessTemplate) {
1883
				$editRoles = $accessTemplate->editRoles;
1884
				$addRoles = $accessTemplate->addRoles;
1885
				$createRoles = $accessTemplate->createRoles;
1886
			} else {
1887
				$editRoles = array();
1888
				$addRoles = array();
1889
				$createRoles = array();
1890
			}
1891
 
1892
			$table->headerRow(array(
1893
				$this->_('Role'), // Roles table column header: Role
1894
				$this->_('What they can do') // Roles table colum header: what they can do
1895
			));
1896
			$table->setEncodeEntities(false);
1897
			$addLabel = 'add';
1898
 
1899
			if(count($roles)) {
1900
 
1901
				$hasPublishPermission = $this->wire('permissions')->has('page-publish');
1902
 
1903
				foreach($roles as $role) {
1904
 
1905
					$permissions = array();
1906
					$roleName = $role->name;
1907
					if($roleName == 'guest') $roleName .= " " . $this->_('(everyone)'); // Identifies who guest is (everyone)
1908
					$permissions["page-view"] = 'view';
1909
 
1910
					$checkEditable = true;
1911
					if($hasPublishPermission && !$this->page->hasStatus(Page::statusUnpublished) 
1912
						&& !$role->hasPermission('page-publish', $this->page)) {
1913
						$checkEditable = false;
1914
					}
1915
 
1916
					$key = array_search($role->id, $addRoles);
1917
					if($key !== false && $role->hasPermission('page-add', $this->page)) {
1918
						$permissions["page-add"] = 'add';
1919
						unset($addRoles[$key]);
1920
					}
1921
 
1922
					$editable = $role->hasPermission('page-edit', $this->page) && in_array($role->id, $editRoles);
1923
 
1924
					if($checkEditable && $editable) {
1925
 
1926
						foreach($role->permissions as $permission) {
1927
							if(strpos($permission->name, 'page-') !== 0) continue;
1928
							if(in_array($permission->name, array('page-view', 'page-publish', 'page-create', 'page-add'))) continue;
1929
							if(!$role->hasPermission($permission, $this->page)) continue;
1930
							$permissions[$permission->name] = str_replace('page-', '', $permission->name); // only page-context permissions
1931
						}
1932
 
1933
						if($hasPublishPermission && $role->hasPermission('page-publish', $this->page)) {
1934
							$permissions["page-publish"] = 'publish';
1935
						}
1936
					}
1937
 
1938
					if(in_array($role->id, $createRoles) && $editable) {
1939
						$permissions["page-create"] = 'create';
1940
					}
1941
 
1942
					$table->row(array($roleName, implode(', ', $permissions)));
1943
				}
1944
 
1945
			}
1946
 
1947
			if(count($addRoles)) {
1948
				foreach($addRoles as $roleID) {
1949
					$role = $this->wire('roles')->get($roleID);
1950
					if(!$role->id) continue;
1951
					if(!$role->hasPermission("page-add", $this->page)) continue;
1952
					$table->row(array($role->name, $addLabel));
1953
				}
1954
			}
1955
 
1956
			$table->row(array('superuser', $this->_x('all', 'all permissions')));
1957
			$field->value = $table->render();
1958
		}
1959
 
1960
		$accessParent = $this->page->getAccessParent();
1961
		if($accessParent === $this->page) {
1962
			$field->notes = sprintf($this->_('Access is defined with this page\'s template: %s'), $accessParent->template);	// Where access is defined: with this page's template
1963
		} else {
1964
			$field->notes = sprintf($this->_('Access is inherited from page "%1$s" and defined with template: %2$s'), $accessParent->path, $accessParent->template); // Where access is defined: inherited from a parent
1965
		}
1966
 
1967
		return $field;
1968
	}
1969
 
1970
	/***********************************************************************************************************************
1971
	 * FORM PROCESSING
1972
	 * 
1973
	 */
1974
 
1975
	/**
1976
	 * Save a submitted Page Edit form
1977
	 *
1978
	 */
1979
	protected function processSave() {
1980
 
1981
		if($this->page->hasStatus(Page::statusLocked)) {
1982
			if(!$this->user->hasPermission('page-lock', $this->page) || (!empty($_POST['status']) && in_array(Page::statusLocked, $_POST['status']))) {
1983
				$this->error($this->noticeLocked);
1984
				$this->processSaveRedirect($this->redirectUrl);
1985
				return;
1986
			}
1987
		}
1988
 
1989
		$formErrors = 0;
1990
 
1991
		// remove temporary status that may have been assigned by ProcessPageAdd quick add mode
1992
		if($this->page->hasStatus(Page::statusTemp)) $this->page->removeStatus(Page::statusTemp);
1993
 
1994
		if($this->input->post('submit_delete')) {
1995
 
1996
			if($this->input->post('delete_page')) $this->deletePage();
1997
 
1998
		} else {
1999
 
2000
			$this->processInput($this->form);
2001
			$changes = array_unique($this->page->getChanges());
2002
			$numChanges = count($changes);
2003
			if($numChanges) {
2004
				$this->changes = $changes;
2005
				$this->message(sprintf($this->_('Change: %s'), implode(', ', $changes)), Notice::debug); // Message shown for each changed field
2006
			}
2007
 
2008
			foreach($this->notices as $notice) {
2009
				if($notice instanceof NoticeError) $formErrors++;
2010
			}
2011
 
2012
			// if any Inputfields threw errors during processing, give the page a 'flagged' status
2013
			// so that it can later be identified the page may be missing something
2014
			if($formErrors && count($this->form->getErrors())) {
2015
				// add flagged status when form had errors
2016
				$this->page->addStatus(Page::statusFlagged);
2017
			} else if($this->page->hasStatus(Page::statusFlagged)) {
2018
				// if no errors, remove incomplete status
2019
				$this->page->removeStatus(Page::statusFlagged);
2020
				$this->message($this->_('Removed flagged status because no errors reported during save'));
2021
			}
2022
 
2023
			$isUnpublished = $this->page->hasStatus(Page::statusUnpublished);
2024
 
2025
			if($this->input->post('submit_publish') || $this->input->post('submit_save')) {
2026
 
2027
				try {
2028
					$options = array();
2029
					$name = '';
2030
 
2031
					if($this->page->isChanged('name')) {
2032
						if(!strlen($this->page->name) && $this->page->namePrevious) {
2033
							// blank page name when there was a previous name, set back the previous
2034
							// example instance: when template.childNameFormat in use and template.noSettings active
2035
							$this->page->name = $this->page->namePrevious;
2036
						} else {
2037
							$name = $this->page->name;
2038
						}
2039
						$options['adjustName'] = true;
2040
					}
2041
 
2042
					$numChanges = $numChanges > 0 ? ' (' . sprintf($this->_n('%d change', '%d changes', $numChanges) . ')', $numChanges) : '';
2043
					if($this->input->post('submit_publish') && $isUnpublished && $this->page->publishable() && !$formErrors) {
2044
						$this->page->removeStatus(Page::statusUnpublished);
2045
						$message = sprintf($this->_('Published Page: %s'), '{path}') . $numChanges; // Message shown when page is published
2046
					} else {
2047
						$message = sprintf($this->_('Saved Page: %s'), '{path}') . $numChanges; // Message shown when page is saved
2048
						if($isUnpublished && $formErrors && $this->input->post('submit_publish')) {
2049
							$message .= ' - ' . $this->_('Cannot be published until errors are corrected');
2050
						}
2051
					}
2052
 
2053
					$restored = false;
2054
					if($this->input->post('restore_page') && $this->page->isTrash() && $this->page->restorable()) {
2055
						if($formErrors) {
2056
							$this->warning($this->_('Page cannot be restored while errors are present'));
2057
						} else if($this->wire('pages')->restore($this->page, false)) {
2058
							$message = sprintf($this->_('Restored Page: %s'), '{path}') . $numChanges; 
2059
							$restored = true;
2060
						} else {
2061
							$this->warning($this->_('Error restoring page'));
2062
						}
2063
					}
2064
 
2065
					$this->wire('pages')->save($this->page, $options);
2066
					if($restored) $this->wire('pages')->restored($this->page);
2067
					$message = str_replace('{path}', $this->page->path, $message);
2068
					$this->message($message);
2069
 
2070
					if($name && $name != $this->page->name) {
2071
						$this->warning(sprintf($this->_('Changed page URL name to "%s" because requested name was already taken.'), $this->page->name));
2072
					}
2073
 
2074
				} catch(\Exception $e) {
2075
					$show = true;
2076
					$message = $e->getMessage();
2077
					foreach($this->errors('all') as $error) {
2078
						if(strpos($error, $message) === false) continue;
2079
						$show = false;
2080
						break;
2081
					}
2082
					if($show) $this->error($message);
2083
				}
2084
			}
2085
		}
2086
 
2087
		if($this->redirectUrl) {
2088
			// non-default redirectUrl overrides after_submit_action
2089
		} else if($formErrors) {
2090
			// if there were errors to attend to, stay where we are
2091
		} else {
2092
			// after submit action
2093
			$submitAction = $this->input->post('_after_submit_action');
2094
			if($submitAction) $this->processSubmitAction($submitAction);
2095
		}
2096
 
2097
		$this->processSaveRedirect($this->getRedirectUrl());
2098
	}
2099
 
2100
	/**
2101
	 * Process the given submit action value
2102
	 * 
2103
	 * #pw-hooker
2104
	 * 
2105
	 * @param string $value Value of selected action, i.e. 'exit', 'view', 'add', next', etc.
2106
	 * @return bool Returns true if value was acted upon or false if not
2107
	 * @since 3.0.142
2108
	 * @see ___getSubmitActions(), setRedirectUrl()
2109
	 * 
2110
	 */
2111
	protected function ___processSubmitAction($value) {
2112
 
2113
		if($value == 'exit') {
2114
			$this->setRedirectUrl('../');
2115
 
2116
		} else if($value == 'view') {
2117
			$this->setRedirectUrl($this->getViewUrl());
2118
 
2119
		} else if($value == 'add') {
2120
			$this->setRedirectUrl("../add/?parent_id={$this->page->parent_id}");
2121
 
2122
		} else if($value == 'next') {
2123
			$nextPage = $this->page->next("include=unpublished");
2124
			if($nextPage->id) {
2125
				if(!$nextPage->editable()) {
2126
					$nextPage = $this->page->next("include=hidden");
2127
					if($nextPage->id && !$nextPage->editable()) {
2128
						$nextPage = $this->page->next();
2129
						if($nextPage->id && !$nextPage->editable()) $nextPage = new NullPage();
2130
					}
2131
				}
2132
			}
2133
			if($nextPage->id) {
2134
				$this->setRedirectUrl($this->getEditUrl(array('id' => $nextPage->id)));
2135
			} else {
2136
				$this->warning($this->_('There is no editable next page to edit.'));
2137
			}
2138
 
2139
		} else {
2140
			return false;
2141
		}
2142
 
2143
		return true;
2144
	}
2145
 
2146
	/**
2147
	 * Perform an after save redirect
2148
	 *
2149
	 * @param string $redirectUrl
2150
	 *
2151
	 */
2152
	protected function ___processSaveRedirect($redirectUrl = '') {
2153
		if($redirectUrl) {
2154
			$c = substr($redirectUrl, 0, 1);
2155
			$admin = $c === '.' || $c === '?' || strpos($redirectUrl, $this->config->urls->admin) === 0; 
2156
			if($admin) {
2157
				$redirectUrl .= (strpos($redirectUrl, '?') === false ? '?' : '&') . 's=1';
2158
			}
2159
		} else {
2160
			$admin = true;
2161
			$redirectUrl = $this->getEditUrl(array('s' => 1)); 
2162
		}
2163
		if($admin) {
2164
			$redirectUrl .= "&c=" . count($this->changes);
2165
			if(count($this->fields) && count($this->changes)) {
2166
				$redirectUrl .= "&changes=" . implode(',', $this->changes);
2167
			}
2168
		}
2169
		$this->setRedirectUrl($redirectUrl);
2170
		$this->session->redirect($this->getRedirectUrl());
2171
	}
2172
 
2173
	/**
2174
	 * Process the input from a submitted Page Edit form, delegating to other methods where appropriate
2175
	 * 
2176
	 * @param InputfieldWrapper $form
2177
	 * @param int $level
2178
	 * @param Inputfield $formRoot
2179
 	 *
2180
	 */
2181
	protected function ___processInput(InputfieldWrapper $form, $level = 0, $formRoot = null) {
2182
 
2183
		static $skipFields = array(
2184
			'sortfield_reverse', 
2185
			'submit_publish', 
2186
			'submit_save',
2187
			'delete_page',
2188
			);
2189
 
2190
		if(!$level) {
2191
			$form->processInput($this->input->post);
2192
			$formRoot = $form;
2193
			$this->page->setQuietly('_forceAddStatus', 0);
2194
		}
2195
 
2196
		$languages = $this->wire('languages'); 
2197
		$errorAction = (int) $this->page->template->errorAction;
2198
 
2199
		foreach($form as $inputfield) {
2200
 
2201
			/** @var Inputfield|InputfieldWrapper $inputfield */
2202
 
2203
			$name = $inputfield->attr('name'); 
2204
			if($name == '_pw_page_name') $name = 'name';
2205
			if(in_array($name, $skipFields)) continue; 
2206
 
2207
			if(!$this->page->editable($name, false)) {
2208
				$this->page->untrackChange($name); // just in case
2209
				continue;
2210
			}
2211
 
2212
			if($name == 'sortfield' && $this->useChildren && $form->isProcessable($inputfield->parent->parent)) {
2213
				$this->processInputSortfield($inputfield) ;
2214
				continue;
2215
			}
2216
 
2217
			if($this->useSettings) { 
2218
 
2219
				if($name == 'template') { 
2220
					$this->processInputTemplate($inputfield); 
2221
					continue; 
2222
 
2223
				} else if($name == 'created_users_id') {
2224
					$this->processInputUser($inputfield);
2225
					continue;
2226
 
2227
				} else if($name == 'parent_id' && count($this->predefinedParents)) {
2228
					if(!$this->predefinedParents->has("id=$inputfield->value")) {
2229
						$this->error("Parent $inputfield->value is not allowed for $this->page"); 
2230
						continue; 
2231
					}
2232
				}
2233
 
2234
				if($name == 'status' && $this->processInputStatus($inputfield)) continue; 
2235
			}
2236
 
2237
			if($this->processInputErrorAction($this->page, $inputfield, $name, $errorAction)) continue;
2238
 
2239
			if($name && $inputfield->isChanged()) {
2240
				if($languages && $inputfield->getSetting('useLanguages')) {
2241
					$v = $this->page->get($name); 
2242
					if(is_object($v)) {
2243
						$v->setFromInputfield($inputfield); 
2244
						$this->page->set($name, $v); 
2245
						$this->page->trackChange($name); 
2246
					} else {
2247
						$this->page->set($name, $inputfield->value); 
2248
					}
2249
				} else { 
2250
					$this->page->set($name, $inputfield->value);
2251
				}
2252
			}
2253
 
2254
			if($inputfield instanceof InputfieldWrapper && count($inputfield->getChildren())) {
2255
				$this->processInput($inputfield, $level + 1, $formRoot);
2256
			}
2257
		}
2258
 
2259
		if(!$level) {
2260
			$forceAddStatus = $this->page->get('_forceAddStatus');
2261
			if($forceAddStatus && !$this->page->hasStatus($forceAddStatus)) {
2262
				$this->page->addStatus($forceAddStatus);
2263
			}
2264
		}
2265
	}
2266
 
2267
	/**
2268
	 * Process required error actions as configured with page’s template
2269
	 * 
2270
	 * @param Page $page
2271
	 * @param Inputfield|InputfieldRepeater $inputfield Inputfield that has already had its processInput() method called.
2272
	 * @param string $name Name of field that we are checking.
2273
	 * @param null|int $errorAction Error action from $page->template->errorAction, or omit to auto-detect. 
2274
	 * @return bool Returns true if field $name should be skipped over during processing, or false if not
2275
	 * 
2276
	 */
2277
	public function processInputErrorAction(Page $page, Inputfield $inputfield, $name, $errorAction = null) {
2278
 
2279
		if(empty($name)) return false;
2280
		if($errorAction === null) $errorAction = (int) $page->template->get('errorAction');
2281
		if(!$errorAction) return false;
2282
		if($page->isUnpublished()) return false;
2283
 
2284
		$isRequired = $inputfield->getSetting('required');
2285
		$isRepeater = strpos($inputfield->className(), 'Repeater') > 0 && wireInstanceOf($inputfield, 'InputfieldRepeater', false);
2286
 
2287
		if(!$isRepeater && !$isRequired) return false;
2288
		if($inputfield->getSetting('requiredSkipped')) return false;
2289
 
2290
		if($isRepeater) {
2291
			if($inputfield->numRequiredEmpty() > 0) {
2292
				// repeater has required fields that are empty
2293
			} else if($isRequired && $inputfield->numPublished() < 1) {
2294
				// repeater is required and has no published items
2295
			} else {
2296
				// repeater is okay for now
2297
				return false;
2298
			}
2299
		} else if(!$inputfield->isEmpty()) {
2300
			return false;
2301
		}
2302
 
2303
		if($errorAction === 1) {
2304
			// restore existing value by skipping processing of empty when required
2305
			$value = $inputfield->attr('value');
2306
			if($value instanceof Wire) $value->resetTrackChanges();
2307
			if($page->getField($name)) $page->remove($name); // force fresh copy to reload
2308
			$previousValue = $page->get($name);
2309
			$page->untrackChange($name);
2310
			if($previousValue) {
2311
				// we should have a previous value to restore
2312
				if(WireArray::iterable($previousValue) && !count($previousValue)) {
2313
					// previous value still empty
2314
				} else {
2315
					// previous value restored by simply not setting new value to $page
2316
					$inputfield->error($this->_('Restored previous value'));
2317
					return true;
2318
				}
2319
			}
2320
 
2321
		} else if($errorAction === 2 && $page->publishable() && $page->id > 1) {
2322
			// unpublish page missing required value
2323
			$page->setQuietly('_forceAddStatus', Page::statusUnpublished);
2324
			$label = $inputfield->getSetting('label');
2325
			if(empty($label)) $label = $inputfield->attr('name');
2326
			$inputfield->error(sprintf($this->_('Page unpublished because field "%s" is required'), $label));
2327
			return false;
2328
		}
2329
 
2330
		return false;
2331
	}
2332
 
2333
	/**
2334
	 * Check to see if the page's created user has changed and make sure it's valid
2335
	 * 
2336
	 * @param Inputfield $inputfield
2337
	 *
2338
	 */
2339
	protected function processInputUser(Inputfield $inputfield) {
2340
		if(!$this->user->isSuperuser() || !$this->page->id || !$this->page->template->allowChangeUser) return;
2341
		$userID = (int) $inputfield->attr('value');
2342
		if(!$userID) return;
2343
		if($userID == $this->page->created_users_id) return; // no change
2344
		$user = $this->pages->get($userID); 
2345
		if(!in_array($user->template->id, $this->config->userTemplateIDs)) return; // invalid user template
2346
		if(!in_array($user->parent_id, $this->config->usersPageIDs)) return; // invalid user parent
2347
		$this->page->created_users_id = $userID; 
2348
		$this->page->trackChange('created_users_id');
2349
	}
2350
 
2351
	/**
2352
	 * Check to see if the page's template has changed and setup a redirect to a confirmation form if it has
2353
	 * 
2354
	 * @param Inputfield $inputfield
2355
	 * @return bool
2356
	 * @throws WireException
2357
	 *
2358
	 */
2359
	protected function processInputTemplate(Inputfield $inputfield) {
2360
		if($this->page->template->noChangeTemplate) return true; 
2361
		$templateID = (int) $inputfield->attr('value');
2362
		if(!$templateID) return true; 
2363
		$template = $this->wire('templates')->get((int) $inputfield->attr('value')); 
2364
		if(!$template) return true; // invalid template
2365
		if($template->id == $this->page->template->id) return true; // no change
2366
		if(!$this->isAllowedTemplate($template)) {
2367
			throw new WireException(sprintf($this->_("Template '%s' is not allowed"), $template)); // Selected template is not allowed
2368
		}
2369
 
2370
		// template has changed, set a redirect URL which will confirm the change
2371
		$this->setRedirectUrl("template?id={$this->page->id}&template={$template->id}");
2372
		return true; 
2373
	}
2374
 
2375
	/**
2376
	 * Process the submitted 'status' field and account for the bitwise logic present
2377
	 * 
2378
	 * @param Inputfield $inputfield
2379
	 * @return bool
2380
	 *
2381
	 */
2382
	protected function processInputStatus(Inputfield $inputfield) {
2383
 
2384
		$status = $inputfield->value; 
2385
		$value = $this->page->status; 
2386
 
2387
		if(!is_array($status)) $status = array();
2388
 
2389
		$statusFlags = array();
2390
		if($this->user->hasPermission('page-hide', $this->page)) $statusFlags[] = Page::statusHidden; 
2391
		if($this->page->publishable()) $statusFlags[] = Page::statusUnpublished; 
2392
		if($this->user->hasPermission('page-lock', $this->page)) $statusFlags[] = Page::statusLocked;
2393
 
2394
		if($this->user->isSuperuser()) {
2395
			$statusFlags[] = Page::statusUnique;
2396
			if($this->config->advanced) {
2397
				$statusFlags[] = Page::statusSystemID;
2398
				$statusFlags[] = Page::statusSystem;
2399
			}
2400
		}
2401
 
2402
		foreach($statusFlags as $flag) {
2403
			if(in_array($flag, $status)) {
2404
				if(!($value & $flag)) $value = $value | $flag; 
2405
 
2406
			} else if($value & $flag) {
2407
				$value = $value & ~$flag; 
2408
			}
2409
		}
2410
 
2411
		$this->page->status = $value; 
2412
		return true; 
2413
	}
2414
 
2415
	/**
2416
	 * Process the Children > Sortfield input
2417
	 * 
2418
	 * @param Inputfield $inputfield
2419
	 * @return bool
2420
	 *
2421
	 * 
2422
	 */
2423
	protected function processInputSortfield(Inputfield $inputfield) {
2424
		if(!$this->user->hasPermission('page-sort', $this->page)) return true; 
2425
		$sortfield = $this->sanitizer->name($inputfield->value); 
2426
		if($sortfield != 'sort' && !empty($_POST['sortfield_reverse'])) $sortfield = '-' . $sortfield; 
2427
		if(empty($sortfield)) $sortfield = 'sort';
2428
		$this->page->sortfield = $sortfield; 
2429
		return true; 
2430
	}
2431
 
2432
	/**
2433
	 * Process a delete page request, moving the page to the trash if applicable
2434
	 * 
2435
	 * @return bool
2436
	 *
2437
	 */
2438
	protected function deletePage() {
2439
 
2440
		if(!$this->page->trashable(true)) {
2441
			$this->error($this->_('This page is not deleteable')); 
2442
			return false; 
2443
		}
2444
 
2445
		$afterDeleteRedirect = $this->config->urls->admin . "page/?open={$this->parent->id}";
2446
		if($this->wire('page')->process != $this->className()) $afterDeleteRedirect = "../";
2447
		$pagePath = $this->page->path();
2448
 
2449
		if(($this->isTrash || $this->page->template->noTrash) && $this->page->deleteable()) {
2450
			$this->session->message(sprintf($this->_('Deleted page: %s'), $pagePath)); // Page deleted message
2451
			$this->pages->delete($this->page, true); 
2452
			$this->session->redirect($afterDeleteRedirect); 
2453
 
2454
		} else if($this->pages->trash($this->page)) {
2455
			$this->session->message(sprintf($this->_('Moved page to trash: %s'), $pagePath)); // Page moved to trash message
2456
			$this->session->redirect($afterDeleteRedirect); 
2457
 
2458
		} else { 
2459
			$this->error($this->_('Unable to move page to trash')); // Page can't be moved to the trash error
2460
			return false;
2461
		}
2462
 
2463
		return true;
2464
	}
2465
 
2466
	/**
2467
	 * Save only the fields posted via ajax
2468
	 *
2469
	 * - Field name must be included in server header HTTP_X_FIELDNAME or directly in the POST vars.
2470
	 * - Note that fields that would be not present in POST vars (like a checkbox) are only supported
2471
	 *   by the HTTP_X_FIELDNAME version.
2472
	 * - Works for custom fields only at present.
2473
	 *
2474
	 * @param Page $page
2475
	 * @throws WireException
2476
	 *
2477
	 */
2478
	protected function ___ajaxSave(Page $page) {
2479
 
2480
		if($this->config->demo) throw new WireException("Ajax save is disabled in demo mode");
2481
		if($page->hasStatus(Page::statusLocked)) throw new WireException($this->noticeLocked);
2482
		if(!$this->ajaxEditable($page)) throw new WirePermissionException($this->noticeNoAccess);
2483
		$this->session->CSRF->validate(); // throws exception when invalid
2484
 
2485
		$form = $this->wire(new InputfieldWrapper());
2486
		$form->useDependencies = false;
2487
		$keys = array();
2488
 
2489
		if(isset($_SERVER['HTTP_X_FIELDNAME'])) {
2490
			$keys[] = $this->sanitizer->fieldName($_SERVER['HTTP_X_FIELDNAME']);
2491
 
2492
		} else {
2493
			foreach($this->input->post as $key => $unused) {
2494
				if($key == 'id') continue;
2495
				$keys[] = $this->sanitizer->fieldName($key);
2496
			}
2497
		}
2498
 
2499
		foreach($keys as $key) {
2500
 
2501
			if(!$field = $page->template->fieldgroup->getFieldContext($key)) continue;
2502
			if(!$this->ajaxEditable($page, $key)) continue;
2503
			if(!$inputfield = $field->getInputfield($page)) continue;
2504
 
2505
			$inputfield->showIf = ''; // cancel showIf dependencies since other fields may not be present
2506
			$inputfield->name = $key;
2507
			$inputfield->value = $page->get($key);
2508
			$form->add($inputfield);
2509
		}
2510
 
2511
		$form->processInput($this->input->post);
2512
		$page->setTrackChanges(true);
2513
		$numFields = 0;
2514
		$lastFieldName = null;
2515
		$languages = $this->wire('languages');
2516
 
2517
		foreach($form->children() as $inputfield) {
2518
			$name = $inputfield->name;
2519
			if($languages && $inputfield->getSetting('useLanguages')) {
2520
				$v = $page->get($name);
2521
				if(is_object($v)) {
2522
					$v->setFromInputfield($inputfield);
2523
					$page->set($name, $v);
2524
					$page->trackChange($name);
2525
				} else {
2526
					$page->set($name, $inputfield->value);
2527
				}
2528
			} else {
2529
				$page->set($name, $inputfield->value);
2530
			}
2531
			$numFields++;
2532
			$lastFieldName = $inputfield->name;
2533
		}
2534
 
2535
		if($page->isChanged()) {
2536
			if($numFields === 1) {
2537
				$page->save((string)$lastFieldName);
2538
				$this->message("AJAX Saved page '{$page->id}' field '$lastFieldName'");
2539
			} else {
2540
				$page->save();
2541
				$this->message("AJAX Saved page '{$page->id}' multiple fields");
2542
			}
2543
		} else {
2544
			$this->message("AJAX Page not saved (no changes)");
2545
		}
2546
	}
2547
 
2548
 
2549
	/***************************************************************************************************************
2550
	 * OTHER ACTIONS
2551
	 * 
2552
	 */
2553
 
2554
	/**
2555
	 * Execute a template change for a page, building an info + confirmation form (handler for /template/ action)
2556
	 * 
2557
	 * @return string
2558
	 * @throws WireException
2559
	 *
2560
	 */
2561
	public function ___executeTemplate() {
2562
 
2563
		if(!$this->useSettings || !$this->user->hasPermission('page-template', $this->page)) {
2564
			throw new WireException("You don't have permission to change the template on this page.");
2565
		}
2566
 
2567
		$templateID = (int) $this->input->get('template');
2568
		if($templateID < 1) throw new WireException("This method requires a 'template' get var"); 
2569
		$template = $this->templates->get($templateID); 
2570
		if(!$template) throw new WireException("Unknown template"); 
2571
 
2572
		if(!$this->isAllowedTemplate($template->id)) {
2573
			throw new WireException("That template is not allowed");
2574
		}
2575
 
2576
		$labelConfirm = $this->_('Confirm template change'); // Change template confirmation subhead
2577
		$labelAction = sprintf($this->_('Change template from "%1$s" to "%2$s"'), $this->page->template, $template); // Change template A to B headline
2578
 
2579
		$this->headline($labelConfirm);
2580
		if($this->requestModal) $this->error("$labelConfirm – $labelAction"); // force modal open
2581
 
2582
		/** @var InputfieldForm $form */
2583
		$form = $this->modules->get("InputfieldForm"); 
2584
		$form->attr('action', 'saveTemplate'); 
2585
		$form->attr('method', 'post'); 
2586
		$form->description = $labelAction;
2587
 
2588
		/** @var InputfieldMarkup $f */
2589
		$f = $this->modules->get("InputfieldMarkup"); 	
2590
		$f->icon = 'cubes';
2591
		$f->label = $labelConfirm;
2592
		$list = array();
2593
		foreach($this->page->template->fieldgroup as $field) {
2594
			if(!$template->fieldgroup->has($field)) {
2595
				$list[] = $this->sanitizer->entities($field->getLabel()) . " ($field->name)";
2596
			}
2597
		}
2598
		if(!$list) $this->executeSaveTemplate($template); 
2599
		$f->description = $this->_('Warning, changing the template will delete the following fields:'); // Headline that precedes list of fields that will be deleted as a result of template change
2600
		$icon = "<i class='fa fa-times-circle'></i> ";
2601
		$f->attr('value', "<p class='ui-state-error-text'>$icon" . implode("<br />$icon", $list) . '</p>');
2602
		$form->append($f); 
2603
 
2604
		/** @var InputfieldCheckbox $f */
2605
		$f = $this->modules->get("InputfieldCheckbox"); 
2606
		$f->attr('name', 'template'); 
2607
		$f->attr('value', $template->id); 
2608
		$f->label = $this->_('Are you sure?'); // Checkbox label to confirm they want to change template
2609
		$f->label2 = $labelAction;
2610
		$f->icon = 'warning';
2611
		$f->description = $this->_('Please confirm that you understand the above by clicking the checkbox below.'); // Checkbox description to confirm they want to change template
2612
		$form->append($f); 
2613
 
2614
		/** @var InputfieldHidden $f */
2615
		$f = $this->modules->get("InputfieldHidden"); 
2616
		$f->attr('name', 'id'); 
2617
		$f->attr('value', $this->page->id); 
2618
		$form->append($f); 
2619
 
2620
		/** @var InputfieldSubmit $f */
2621
		$f = $this->modules->get("InputfieldSubmit"); 
2622
		$form->append($f); 
2623
 
2624
		$page = $this->masterPage ? $this->masterPage : $this->page; 
2625
		$this->wire('breadcrumbs')->add(new Breadcrumb("./?id={$page->id}", $page->get("title|name"))); 
2626
 
2627
		return $form->render();
2628
	}
2629
 
2630
	/**
2631
	 * Save a template change for a page (handler for /saveTemplate/ action)
2632
	 * 
2633
	 * @param Template $template
2634
	 * @throws WireException
2635
	 *
2636
	 */
2637
	public function ___executeSaveTemplate($template = null) {
2638
 
2639
		if(!$this->useSettings || !$this->user->hasPermission('page-template', $this->page)) {
2640
			throw new WireException($this->_("You don't have permission to change the template on this page.")); // Error: user doesn't have permission to change template
2641
		}
2642
 
2643
		if(!$this->page->template->noChangeTemplate) { 
2644
 
2645
			if(!is_null($template) || (isset($_POST['template']) && ($template = $this->templates->get((int) $_POST['template'])))) {
2646
				try { 
2647
					if(!$this->isAllowedTemplate($template)) {
2648
						throw new WireException($this->_('That template is not allowed')); // Error: selected template is not allowed
2649
					}
2650
					$this->page->template = $template; 
2651
					$this->page->save();
2652
					$this->message(sprintf($this->_("Changed template to '%s'"), $template)); // Message: template was changed 
2653
				} catch(\Exception $e) {
2654
					$this->error($e->getMessage()); 
2655
				}
2656
			}
2657
		}
2658
 
2659
		$this->session->redirect("./?id={$this->page->id}"); 
2660
	}
2661
 
2662
	/**
2663
	 * Returns an array of templates that are allowed to be used here
2664
	 * 
2665
	 * @return array|Template[] Array of Template objects
2666
	 *
2667
	 */
2668
	protected function getAllowedTemplates() {
2669
 
2670
		if(is_array($this->allowedTemplates)) return $this->allowedTemplates;
2671
 
2672
		$templates = array();
2673
		$user = $this->user;
2674
		$isSuperuser = $user->isSuperuser();
2675
		$page = $this->masterPage ? $this->masterPage : $this->page;
2676
		$parent = $page->parent; 
2677
		$parentEditable = ($parent->id && $parent->editable());
2678
		/** @var Config $config */
2679
		$config = $this->wire('config');
2680
		$superAdvanced = $isSuperuser && $config->advanced; 
2681
 
2682
		// current page template is assumed, otherwise we wouldn't be here
2683
		$templates[$page->template->id] = $page->template; 
2684
 
2685
		// check if they even have permission to change it
2686
		if(!$user->hasPermission('page-template', $page) || $page->template->noChangeTemplate) {
2687
			$this->allowedTemplates = $templates;
2688
			return $templates;
2689
		}
2690
 
2691
		$allTemplates = count($this->predefinedTemplates) ? $this->predefinedTemplates : $this->wire('templates'); 
2692
 
2693
		foreach($allTemplates as $template) {
2694
			/** @var Template $template */
2695
 
2696
			if(isset($templates[$template->id])) continue; 
2697
 
2698
			if($template->flags & Template::flagSystem) {
2699
				// if($template->name == 'user' && $parent->id != $this->config->usersPageID) continue;
2700
				if(in_array($template->id, $config->userTemplateIDs) && !in_array($parent->id, $config->usersPageIDs)) continue; 
2701
				if($template->name == 'role' && $parent->id != $config->rolesPageID) continue;
2702
				if($template->name == 'permission' && $parent->id != $config->permissionsPageID) continue;
2703
				if(strpos($template->name, 'repeater_') === 0 || strpos($template->name, 'fieldset_') === 0) continue;
2704
			}
2705
 
2706
			if(count($template->parentTemplates) && $parent->id && !in_array($parent->template->id, $template->parentTemplates)) {
2707
				// this template specifies it can only be used with certain parents, and our parent's template isn't one of them
2708
				continue;
2709
			}	
2710
 
2711
			if($parent->id && count($parent->template->childTemplates)) {
2712
				// the page's parent only allows certain templates for it's children
2713
				// if this isn't one of them, then continue; 
2714
				if(!in_array($template->id, $parent->template->childTemplates)) continue; 
2715
			}
2716
 
2717
			if(!$superAdvanced && $template->noParents < 0 && $template->getNumPages() > 0) {
2718
				// only one of these is allowed to exist (noParents=-1)
2719
				continue;
2720
 
2721
			} else if($template->noParents > 0) {
2722
				// user can't change to a template that has been specified as no more instances allowed
2723
				continue;
2724
 
2725
			} else if($isSuperuser) {
2726
				$templates[$template->id] = $template;
2727
 
2728
			} else if((!$template->useRoles && $parentEditable) || $user->hasPermission('page-edit', $template)) {
2729
				// determine if the template's assigned roles match up with the users's roles
2730
				// and that at least one of those roles has page-edit permission
2731
				if($user->hasPermission('page-create', $page)) { 
2732
					// user is allowed to create more pages of this type, so template may be used
2733
					$templates[$template->id] = $template; 
2734
				}
2735
			}
2736
		}
2737
 
2738
		$this->allowedTemplates = $templates;
2739
 
2740
		return $templates; 
2741
	}
2742
 
2743
	/**
2744
	 * Is the given template or template ID allowed here?
2745
	 * 
2746
	 * @param int|Template $id
2747
	 * @return bool
2748
	 *
2749
	 */
2750
	protected function isAllowedTemplate($id) {
2751
 
2752
		// if $id is a template, then convert it to it's numeric ID
2753
		if(is_object($id) && $id instanceof Template) $id = $id->id; 
2754
 
2755
		$id = (int) $id; 
2756
 
2757
		// if the template is the same one already in place, of course it's allowed
2758
		if($id == $this->page->template->id) return true; 
2759
 
2760
		// if we've made it this far, then get a list of templates that are allowed...
2761
		$templates = $this->getAllowedTemplates();
2762
 
2763
		// ...and determine if the supplied template is in that list
2764
		return isset($templates[$id]); 
2765
	}
2766
 
2767
	/**
2768
	 * Returns true if this page may be ajax saved (user has access), or false if not
2769
	 *
2770
	 * @param Page $page
2771
	 * @param string $fieldName Optional field name
2772
	 * @return bool
2773
	 *
2774
	 */
2775
	protected function ___ajaxEditable(Page $page, $fieldName = '') {
2776
		return $page->editable($fieldName);
2777
	}
2778
 
2779
	/**
2780
	 * Return instance of the Page being edited (required by WirePageEditor interface)
2781
	 *
2782
	 * For Inputfields/Fieldtypes to use if they want to retrieve the editing page rather than the viewing page
2783
	 * 
2784
	 * @return Page
2785
	 *
2786
	 */
2787
	public function getPage() {
2788
		return $this->page; 
2789
	}
2790
 
2791
	/**
2792
	 * Set the page being edited
2793
	 * 
2794
	 * @param Page $page
2795
	 * 
2796
	 */
2797
	public function setPage(Page $page) {
2798
		$this->page = $page; 
2799
	}
2800
 
2801
	/**
2802
	 * Set the 'master' page
2803
	 * 
2804
	 * @param Page $page
2805
	 * @deprecated
2806
	 * 
2807
	 */
2808
	public function setMasterPage(Page $page) {
2809
		$this->masterPage = $page; 
2810
	}
2811
 
2812
	/**
2813
	 * Get the 'master' page (if set)
2814
	 * 
2815
	 * @return null|Page
2816
	 * @deprecated
2817
	 * 
2818
	 */
2819
	public function getMasterPage() {
2820
		return $this->masterPage; 
2821
	}
2822
 
2823
	/**
2824
	 * Set whether or not 'settings' tab should show
2825
	 * 
2826
	 * @param bool $useSettings
2827
	 * 
2828
	 */
2829
	public function setUseSettings($useSettings) {
2830
		$this->useSettings = (bool) $useSettings;
2831
	}
2832
 
2833
	/**
2834
	 * Set predefined allowed templates
2835
	 * 
2836
	 * @param array|Template[] $templates
2837
	 * 
2838
	 */
2839
 
2840
	public function setPredefinedTemplates($templates) {
2841
		if(WireArray::iterable($templates)) $this->predefinedTemplates = $templates;
2842
	}
2843
 
2844
	/**
2845
	 * Set predefined allowed parents
2846
	 * 
2847
	 * @param PageArray $parents
2848
	 * 
2849
	 */
2850
	public function setPredefinedParents(PageArray $parents) {
2851
		$this->predefinedParents = $parents; 
2852
	}
2853
 
2854
	/**
2855
	 * Set the primary editor, if not ProcessPageEdit
2856
	 * 
2857
	 * @param WirePageEditor $editor
2858
	 * 
2859
	 */
2860
	public function setEditor(WirePageEditor $editor) {
2861
		$this->editor = $editor; 
2862
	}
2863
 
2864
	/**
2865
	 * Called on save requests, sets the next redirect URL for the next request
2866
	 * 
2867
	 * @param string $url URL to redirect to
2868
	 * @since 3.0.142 Was protected in previous versions
2869
	 * 
2870
	 */
2871
	public function setRedirectUrl($url) {
2872
		$this->redirectUrl = $url;
2873
	}
2874
 
2875
	/**
2876
	 * Get the current redirectUrl
2877
	 * 
2878
	 * @param array $extras Any extra parts you want to add as array of strings like "key=value"
2879
	 * @return string
2880
	 * @since 3.0.142 Was protected in previous versions
2881
	 * 
2882
	 */
2883
	public function getRedirectUrl(array $extras = array()) {
2884
		$url = $this->redirectUrl;
2885
		if(!strlen($url)) $url = "./?id=$this->id";
2886
		if($this->requestModal && strpos($url, 'modal=') === false) {
2887
			$extras[] = "modal=$this->requestModal";
2888
		}
2889
		if(strpos($url, '&field=') === false && strpos($url, '&fields=') === false) {
2890
			if(count($this->fields)) {
2891
				$names = array();
2892
				foreach($this->fields as $field) {
2893
					$names[] = "$field";
2894
				}
2895
				$extras[] = "fields=" . implode(',', $names);
2896
			} else if($this->field) {
2897
				$extras[] = "field=$this->field";
2898
			}
2899
		}
2900
		if(strpos($url, './') === 0 || (strpos($url, '/') !== 0 && strpos($url, '../') !== 0)) {
2901
			if($this->requestLanguage && strpos($url, 'language=') === false) {
2902
				$extras[] = "language=$this->requestLanguage";
2903
			}
2904
			if($this->requestContext && preg_match('/\bid=' . $this->id . '\b/', $url)) {
2905
				$extras[] = "context=$this->requestContext";
2906
			}
2907
		}
2908
		if(count($extras)) {
2909
			$url .= strpos($url, '?') === false ? "?" : "&"; 
2910
			$url .= implode('&', $extras);
2911
		}
2912
		return $url;
2913
	}
2914
 
2915
	/**
2916
	 * Add a tab with HTML id attribute and label
2917
	 * 
2918
	 * Label may contain markup, and thus you should entity encode text labels as appropriate.
2919
	 * 
2920
	 * @param string $id
2921
	 * @param string $label
2922
	 * 
2923
	 */
2924
	public function addTab($id, $label) {
2925
		$this->tabs[$id] = $label; 
2926
	}
2927
 
2928
	/**
2929
	 * Remove the tab with the given id
2930
	 * 
2931
	 * @param string $id
2932
	 * 
2933
	 */
2934
	public function removeTab($id) {
2935
		unset($this->tabs[$id]); 
2936
	}
2937
 
2938
	/**
2939
	 * Returns associative array of tab ID => tab Label
2940
	 * 
2941
	 * @return array
2942
	 * 
2943
	 */
2944
	public function ___getTabs() {
2945
		return $this->tabs; 
2946
	}
2947
 
2948
	/**
2949
	 * Get PageBookmarks array
2950
	 * 
2951
	 * @return PageBookmarks
2952
	 * 
2953
	 */
2954
	protected function getPageBookmarks() {
2955
		static $bookmarks = null;
2956
		if(is_null($bookmarks)) {
2957
			require_once(dirname(__FILE__) . '/PageBookmarks.php');
2958
			$bookmarks = $this->wire(new PageBookmarks($this));
2959
		}
2960
		return $bookmarks;
2961
	}
2962
 
2963
	/**
2964
	 * navJSON action
2965
	 * 
2966
	 * @param array $options
2967
	 * @return string
2968
	 * @throws Wire404Exception
2969
	 * @throws WireException
2970
	 * 
2971
	 */
2972
	public function ___executeNavJSON(array $options = array()) {
2973
		$bookmarks = $this->getPageBookmarks();
2974
		$options['edit'] = $this->wire('config')->urls->admin . 'page/edit/?id={id}';
2975
		$options['defaultIcon'] = 'pencil';
2976
		$options = $bookmarks->initNavJSON($options);
2977
		return parent::___executeNavJSON($options); 
2978
	}
2979
 
2980
	/**
2981
	 * Bookmarks action
2982
	 * 
2983
	 * @return string
2984
	 * 
2985
	 */
2986
	public function ___executeBookmarks() {
2987
		$bookmarks = $this->getPageBookmarks();
2988
		return $bookmarks->editBookmarks();
2989
	}
2990
 
2991
	/**
2992
	 * Set the headline used in the UI
2993
	 *
2994
	 */
2995
	public function setupHeadline() {
2996
 
2997
		$titlePage = null;
2998
		$page = $this->page;
2999
 
3000
		if($page && $page->id) {
3001
			$title = $page->get('title');
3002
			if(is_object($title) && !strlen("$title") && wireInstanceOf($title, 'LanguagesPageFieldValue')) {
3003
				/** @var LanguagesPageFieldValue $title */
3004
				$title = $title->getNonEmptyValue($page->name);
3005
			} else {
3006
				$title = (string) $title;
3007
			}
3008
			if(empty($title)) {
3009
				if($this->wire('pages')->names()->isUntitledPageName($page->name)) {
3010
					$title = $page->template->getLabel();
3011
				} else {
3012
					$title = $page->get('name');
3013
				}
3014
			}
3015
			if(empty($title)) $title = $page->name;
3016
		} else if($this->parent && $this->parent->id) {
3017
			$titlePage = $this->parent;
3018
			$title = rtrim($this->parent->path, '/') . '/[...]';
3019
		} else {
3020
			$titlePage = new NullPage();
3021
			$title = '[...]';
3022
		}
3023
 
3024
		$browserTitle = sprintf($this->_('Edit Page: %s'), $title);
3025
		$headline = '';
3026
 
3027
		if($this->field) {
3028
			if(count($this->fields) == 1) {
3029
				$headline = $this->field->getLabel();
3030
			} else {
3031
				$labels = array();
3032
				foreach($this->fields as $field) {
3033
					$labels[] = $field->getLabel();
3034
				}
3035
				$headline = implode(', ', $labels);
3036
			}
3037
			$browserTitle .= " ($headline)";
3038
 
3039
		} else if($titlePage) {
3040
			$headline = $titlePage->get('title|name');
3041
		}
3042
 
3043
		if(empty($headline)) $headline = $title;
3044
 
3045
		$this->headline($headline);
3046
		$this->browserTitle($browserTitle);
3047
	}
3048
 
3049
	/**
3050
	 * Setup the breadcrumbs used in the UI
3051
	 *
3052
	 */
3053
	public function setupBreadcrumbs() {
3054
		if($this->input->urlSegment1) return;
3055
		if($this->wire('page')->process != $this->className()) return;
3056
		$this->wire('breadcrumbs')->shift(); // shift off the 'Admin' breadcrumb
3057
		if($this->page && $this->page->id != 1) $this->wire('breadcrumbs')->shift(); // shift off the 'Pages' breadcrumb
3058
		$page = $this->page ? $this->page : $this->parent;
3059
		if($this->masterPage) $page = $this->masterPage;
3060
		$lastID = (int) $this->session->get('ProcessPageList', 'lastID');
3061
		$editCrumbs = !empty($this->configSettings['editCrumbs']);
3062
 
3063
		$numParents = $page->parents->count();
3064
		foreach($page->parents() as $cnt => $p) {
3065
			$url = $editCrumbs && $p->editable() ? "./?id=$p->id" : "../?open=$p->id";
3066
			if(!$editCrumbs && $cnt == $numParents-1 && $p->id == $lastID) $url = "../";
3067
			$this->breadcrumb($url, $p->get("title|name"));
3068
		}
3069
 
3070
		if($this->page && $this->field) {
3071
			$this->breadcrumb("./?id={$this->page->id}", $page->get("title|name"));
3072
		}
3073
	}
3074
 
3075
 
3076
	/**
3077
	 * Module config
3078
	 * 
3079
	 * @param array $data
3080
	 * @return InputfieldWrapper
3081
	 * @throws WireException
3082
	 * 
3083
	 */
3084
	public function getModuleConfigInputfields(array $data) {
3085
 
3086
		$inputfields = new InputfieldWrapper();
3087
		$this->wire($inputfields); 
3088
 
3089
		$f = $this->wire('modules')->get('InputfieldRadios');
3090
		$f->name = 'viewAction'; 
3091
		$f->label = $this->_('Default "view" location/action'); 
3092
		$f->description = $this->_('The default type of action used when the "view" tab is clicked on in the page editor.');
3093
		$f->icon = 'eye';
3094
 
3095
		foreach($this->getViewActions(array(), true) as $name => $label) {
3096
			$f->addOption($name, $label);
3097
		}
3098
 
3099
		$configData = $this->wire('config')->pageEdit;
3100
		if(isset($data['viewAction'])) {
3101
			$f->attr('value', $data['viewAction']);
3102
		} else if(is_array($configData) && !empty($configData['viewNew'])) {
3103
			$f->attr('value', 'new');
3104
		} else {
3105
			$f->attr('value', 'this');
3106
		}
3107
 
3108
		$inputfields->add($f);
3109
 
3110
		$bookmarks = $this->getPageBookmarks();
3111
		$bookmarks->addConfigInputfields($inputfields);
3112
		$admin = $this->wire('pages')->get($this->wire('config')->adminRootPageID);
3113
		$page = $this->wire('pages')->get($admin->path . 'page/edit/');
3114
		$bookmarks->checkProcessPage($page);
3115
 
3116
		return $inputfields;
3117
	}
3118
 
3119
}
3120