512 lines
15 KiB
Text
512 lines
15 KiB
Text
|
<?php namespace ProcessWire;
|
||
|
|
||
|
/**
|
||
|
* ProcessWire Page Clone Process
|
||
|
*
|
||
|
* For more details about how Process modules work, please see:
|
||
|
* /wire/core/Process.php
|
||
|
*
|
||
|
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
|
||
|
* https://processwire.com
|
||
|
*
|
||
|
* Optional GET variables:
|
||
|
* - redirect_page (int): Contains ID of page to redirect to after clone.
|
||
|
*
|
||
|
* @method InputfieldForm buildForm()
|
||
|
* @method string render()
|
||
|
* @method void process()
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
class ProcessPageClone extends Process {
|
||
|
|
||
|
public static function getModuleInfo() {
|
||
|
return array(
|
||
|
'title' => __('Page Clone', __FILE__),
|
||
|
'summary' => __('Provides ability to clone/copy/duplicate pages in the admin. Adds a "copy" option to all applicable pages in the PageList.', __FILE__),
|
||
|
'version' => 104,
|
||
|
'autoload' => "template=admin", // Note: most Process modules should not be 'autoload', this is an exception.
|
||
|
'permission' => 'page-clone',
|
||
|
'permissions' => array(
|
||
|
'page-clone' => 'Clone a page',
|
||
|
'page-clone-tree' => 'Clone a tree of pages'
|
||
|
),
|
||
|
'page' => array(
|
||
|
'name' => 'clone',
|
||
|
'title' => 'Clone',
|
||
|
'parent' => 'page',
|
||
|
'status' => Page::statusHidden,
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* The page being cloned
|
||
|
*
|
||
|
* @var Page|null
|
||
|
*
|
||
|
*/
|
||
|
protected $page;
|
||
|
|
||
|
/**
|
||
|
* The action link label used in the PageList
|
||
|
*
|
||
|
* Redefined for mult-language support in the ready method.
|
||
|
*
|
||
|
*/
|
||
|
protected $pageListActionLabel = 'Copy';
|
||
|
|
||
|
/**
|
||
|
* URL to the admin, cached here to avoid repeat $config calls
|
||
|
*
|
||
|
*/
|
||
|
protected $adminUrl = '';
|
||
|
|
||
|
/**
|
||
|
* Called when the API and $page loaded are ready
|
||
|
*
|
||
|
* We use this to determine whether we should add a hook or not.
|
||
|
* If we're in ProcessPageList, we add the hook.
|
||
|
*
|
||
|
*/
|
||
|
public function ready() {
|
||
|
$this->adminUrl = $this->wire()->config->urls->admin;
|
||
|
$this->pageListActionLabel = $this->_('Copy'); // Action label that appears in PageList
|
||
|
$this->addHookAfter("ProcessPageListActions::getExtraActions", $this, 'hookPageListActions');
|
||
|
$this->addHookAfter("ProcessPageListActions::processAction", $this, 'hookProcessExtraAction');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Hook into ProcessPageListActions::getExtraActions to return a 'copy' action when appropriate
|
||
|
*
|
||
|
* @param HookEvent $event
|
||
|
*
|
||
|
*/
|
||
|
public function hookPageListActions(HookEvent $event) {
|
||
|
$page = $event->arguments[0];
|
||
|
$actions = array();
|
||
|
if($this->hasPermission($page)) {
|
||
|
$actions['copy'] = array(
|
||
|
'cn' => 'Copy',
|
||
|
'name' => $this->pageListActionLabel,
|
||
|
'url' => "{$this->adminUrl}page/clone/?id={$page->id}",
|
||
|
);
|
||
|
if(!$page->numChildren) {
|
||
|
$actions['copy']['url'] = "{$this->adminUrl}page/?action=clone&id={$page->id}";
|
||
|
$actions['copy']['ajax'] = true;
|
||
|
}
|
||
|
}
|
||
|
if(count($actions)) $event->return = $actions + $event->return;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Hook to ProcessPageListActions::processAction()
|
||
|
*
|
||
|
* @param HookEvent $event
|
||
|
*
|
||
|
*/
|
||
|
public function hookProcessExtraAction(HookEvent $event) {
|
||
|
$page = $event->arguments(0);
|
||
|
$action = $event->arguments(1);
|
||
|
if($action !== 'clone') return;
|
||
|
$result = $this->processAjax($page, true);
|
||
|
if(!empty($result['page'])) $result['appendItem'] = $result['page'];
|
||
|
$event->return = $result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Main execution: Display Copy Page form or process it
|
||
|
*
|
||
|
*/
|
||
|
public function ___execute() {
|
||
|
$input = $this->wire()->input;
|
||
|
$this->headline($this->_('Copy Page')); // Headline
|
||
|
$error = $this->_("Unable to load page");
|
||
|
$id = (int) $input->get('id');
|
||
|
if($id < 2) throw new WireException($error);
|
||
|
$this->page = $this->wire()->pages->get($id);
|
||
|
if($this->page->id < 2) throw new WireException($error);
|
||
|
if(!$this->hasPermission($this->page)) throw new WirePermissionException($error);
|
||
|
if($input->post('submit_clone')) $this->process();
|
||
|
return $this->render();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check if the current user has permission to clone the given page
|
||
|
*
|
||
|
* @param Page $page
|
||
|
* @return bool
|
||
|
*
|
||
|
*/
|
||
|
public function hasPermission(Page $page) {
|
||
|
$user = $this->user;
|
||
|
$parent = $page->parent();
|
||
|
$parentTemplate = $parent->template;
|
||
|
$pageTemplate = $page->template;
|
||
|
|
||
|
if(!$parentTemplate) return false;
|
||
|
|
||
|
if($page->hasStatus(Page::statusSystem) || $page->hasStatus(Page::statusSystemID)) return false;
|
||
|
if($parentTemplate->noChildren) return false;
|
||
|
if($pageTemplate->noParents) return false;
|
||
|
|
||
|
if(count($parentTemplate->childTemplates) && !in_array($pageTemplate->id, $parentTemplate->childTemplates)) return false;
|
||
|
if(count($pageTemplate->parentTemplates) && !in_array($parentTemplate->id, $pageTemplate->parentTemplates)) return false;
|
||
|
|
||
|
if($user->isSuperuser()) return true;
|
||
|
if($user->hasPermission('page-create', $page) && $user->hasPermission('page-clone', $page) && $parent->addable()) return true;
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return array with suggested 'name' and 'title' elements for given $page
|
||
|
*
|
||
|
* @param Page $page
|
||
|
* @return array
|
||
|
*
|
||
|
*/
|
||
|
protected function getSuggestedNameAndTitle(Page $page) {
|
||
|
|
||
|
$pages = $this->wire()->pages;
|
||
|
|
||
|
$name = $pages->names()->uniquePageName(array(
|
||
|
'name' => $page->name,
|
||
|
'parent' => $page->parent()
|
||
|
));
|
||
|
|
||
|
$copy = $this->_('(copy)');
|
||
|
$copyN = $this->_('(copy %d)');
|
||
|
$title = $page->title;
|
||
|
|
||
|
$n = (int) $pages->names()->hasNumberSuffix($name);
|
||
|
if(strpos($title, $copy) !== false) $title = str_replace(" $copy", '', $title);
|
||
|
$regexCopyN = str_replace('%d', '[0-9]+', $copyN);
|
||
|
$regexCopyN = str_replace(array('(', ')'), array('\\(', '\\)'), $regexCopyN);
|
||
|
$title = preg_replace("/$regexCopyN/", '', $title);
|
||
|
$title .= ' ' . ($n > 1 ? sprintf($copyN, $n) : $copy);
|
||
|
|
||
|
$result = array(
|
||
|
'name' => $name,
|
||
|
'title' => $title,
|
||
|
'n' => $n
|
||
|
);
|
||
|
|
||
|
|
||
|
$languages = $this->wire()->languages;
|
||
|
if($languages && $languages->hasPageNames()) {
|
||
|
foreach($languages as $language) {
|
||
|
/** @var Language $language */
|
||
|
if($language->isDefault()) continue;
|
||
|
$value = $page->get("name$language");
|
||
|
if(!strlen("$value")) continue;
|
||
|
$result["name$language"] = $pages->names()->incrementName($value, $n);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Render a form asking for information to be used for the new cloned page.
|
||
|
*
|
||
|
* @return InputfieldForm
|
||
|
*
|
||
|
*/
|
||
|
protected function ___buildForm() {
|
||
|
|
||
|
$page = $this->page;
|
||
|
$modules = $this->wire()->modules;
|
||
|
|
||
|
/** @var InputfieldForm $form */
|
||
|
$form = $modules->get("InputfieldForm");
|
||
|
$form->attr('action', './?id=' . $page->id);
|
||
|
$form->attr('method', 'post');
|
||
|
$form->description = sprintf($this->_("This will make a copy of %s"), $page->path); // Form description/headline
|
||
|
$form->addClass('InputfieldFormFocusFirst');
|
||
|
|
||
|
$suggested = $this->getSuggestedNameAndTitle($page);
|
||
|
|
||
|
/** @var InputfieldPageTitle $titleField */
|
||
|
$titleField = $modules->get("InputfieldPageTitle");
|
||
|
$titleField->attr('name', 'clone_page_title');
|
||
|
$titleField->attr('value', $suggested['title']);
|
||
|
$titleField->label = $this->_("Title of new page"); // Label for title field
|
||
|
|
||
|
/** @var InputfieldPageName $nameField */
|
||
|
$nameField = $modules->get("InputfieldPageName");
|
||
|
$nameField->attr('name', 'clone_page_name');
|
||
|
$nameField->attr('value', $suggested['name']);
|
||
|
$nameField->parentPage = $page->parent;
|
||
|
|
||
|
$languages = $this->wire()->languages;
|
||
|
$useLanguages = $languages;
|
||
|
if($useLanguages) {
|
||
|
/** @var Field $title */
|
||
|
$title = $this->wire()->fields->get('title');
|
||
|
$titleUseLanguages = $title
|
||
|
&& $page->template->fieldgroup->hasField($title)
|
||
|
&& $title->getInputfield($page)->getSetting('useLanguages');
|
||
|
$nameUseLanguages = $languages->hasPageNames();
|
||
|
if($titleUseLanguages) $titleField->useLanguages = true;
|
||
|
if($nameUseLanguages) $nameField->useLanguages = true;
|
||
|
foreach($languages as $language) {
|
||
|
/** @var Language $language */
|
||
|
if($language->isDefault()) continue;
|
||
|
if($titleUseLanguages) {
|
||
|
/** @var LanguagesPageFieldValue $pageTitle */
|
||
|
$pageTitle = $page->title;
|
||
|
$value = $pageTitle->getLanguageValue($language);
|
||
|
$titleField->set("value$language->id", $value);
|
||
|
}
|
||
|
if($nameUseLanguages) {
|
||
|
$value = $page->get("name$language->id");
|
||
|
if(strlen($value)) {
|
||
|
if(!empty($suggested["name$language"])) {
|
||
|
$nameLang = $suggested["name$language"];
|
||
|
} else {
|
||
|
$nameLang = $value . '-' . $suggested['n'];
|
||
|
}
|
||
|
$nameField->set("value$language->id", $nameLang);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if($page->template->fieldgroup->hasField('title')) $form->add($titleField);
|
||
|
$form->add($nameField);
|
||
|
|
||
|
/** @var InputfieldCheckbox $field */
|
||
|
$field = $modules->get("InputfieldCheckbox");
|
||
|
$field->attr('name', 'clone_page_unpublished');
|
||
|
$field->attr('value', 1);
|
||
|
$field->label = $this->_("Make the new page unpublished?");
|
||
|
$field->collapsed = Inputfield::collapsedYes;
|
||
|
$field->description = $this->_("If checked, the cloned page will be given an unpublished status so that it can't yet be seen on the front-end of your site.");
|
||
|
$form->add($field);
|
||
|
|
||
|
if($page->numChildren && $this->user->hasPermission('page-clone-tree', $page)) {
|
||
|
/** @var InputfieldCheckbox $field */
|
||
|
$field = $modules->get("InputfieldCheckbox");
|
||
|
$field->attr('name', 'clone_page_tree');
|
||
|
$field->attr('value', 1);
|
||
|
$field->label = $this->_("Copy children too?");
|
||
|
$field->description = $this->_("If checked, all children, grandchildren, etc., will also be cloned with this page.");
|
||
|
$field->notes = $this->_("Warning: if there is a very large structure of pages below this, it may be time consuming or impossible to complete.");
|
||
|
$field->collapsed = Inputfield::collapsedYes;
|
||
|
$form->add($field);
|
||
|
}
|
||
|
|
||
|
/** @var InputfieldSubmit $field */
|
||
|
$field = $modules->get("InputfieldSubmit");
|
||
|
$field->attr('name', 'submit_clone');
|
||
|
$form->add($field);
|
||
|
|
||
|
$redirectPageID = (int) $this->wire()->input->get('redirect_page');
|
||
|
if($redirectPageID) {
|
||
|
/** @var InputfieldHidden $field */
|
||
|
$field = $modules->get('InputfieldHidden');
|
||
|
$field->attr('name', 'redirect_page');
|
||
|
$field->attr('value', $redirectPageID);
|
||
|
$form->add($field);
|
||
|
}
|
||
|
|
||
|
return $form;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Render a form asking for information to be used for the new cloned page.
|
||
|
*
|
||
|
* @return string
|
||
|
*
|
||
|
*/
|
||
|
protected function ___render() {
|
||
|
$form = $this->buildForm();
|
||
|
return $form->render();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* User has clicked submit, so we create the clone, then redirect to it in PageList
|
||
|
*
|
||
|
*/
|
||
|
protected function ___process() {
|
||
|
|
||
|
$page = clone $this->page;
|
||
|
$input = $this->input;
|
||
|
$languages = $this->wire()->languages;
|
||
|
$originalName = $page->name;
|
||
|
|
||
|
$this->session->CSRF->validate();
|
||
|
$form = $this->buildForm();
|
||
|
$form->processInput($input->post);
|
||
|
|
||
|
$titleField = $form->get('clone_page_title');
|
||
|
$nameField = $form->get('clone_page_name');
|
||
|
$cloneTree = $input->post('clone_page_tree') && $this->user->hasPermission('page-clone-tree', $this->page);
|
||
|
|
||
|
if($input->post('clone_page_unpublished')) {
|
||
|
$page->addStatus(Page::statusUnpublished);
|
||
|
}
|
||
|
|
||
|
if($nameField->useLanguages && $languages) {
|
||
|
foreach($languages as $language) {
|
||
|
/** @var Language $language */
|
||
|
$valueAttr = $language->isDefault() ? "value" : "value$language->id";
|
||
|
$nameAttr = $language->isDefault() ? "name" : "name$language->id";
|
||
|
$value = $nameField->get($valueAttr);
|
||
|
$page->set($nameAttr, $value);
|
||
|
}
|
||
|
} else {
|
||
|
$page->name = $nameField->attr('value');
|
||
|
}
|
||
|
|
||
|
set_time_limit(3600);
|
||
|
|
||
|
$clone = $this->pages->clone($page, $page->parent, $cloneTree);
|
||
|
|
||
|
if(!$clone->id) {
|
||
|
throw new WireException(sprintf($this->_("Unable to clone page %s"), $page->path));
|
||
|
}
|
||
|
|
||
|
if($titleField->getSetting('useLanguages') && is_object($clone->title) && $languages) {
|
||
|
foreach($languages as $language) {
|
||
|
/** @var Language $language */
|
||
|
$valueAttr = $language->isDefault() ? "value" : "value$language->id";
|
||
|
$value = $titleField->get($valueAttr);
|
||
|
/** @var LanguagesPageFieldValue $cloneTitle */
|
||
|
$cloneTitle = $clone->title;
|
||
|
$cloneTitle->setLanguageValue($language, $value);
|
||
|
}
|
||
|
} else {
|
||
|
$clone->title = $titleField->value;
|
||
|
}
|
||
|
|
||
|
$this->wire()->pages->save($clone, array('adjustName' => true));
|
||
|
|
||
|
$this->message(sprintf($this->_('Cloned page "%1$s" to "%2$s"'), $originalName, $clone->name));
|
||
|
|
||
|
$redirectURL = null;
|
||
|
$redirectID = (int) $input->post('redirect_page');
|
||
|
|
||
|
if($redirectID) {
|
||
|
$redirectPage = $this->wire()->pages->get($redirectID);
|
||
|
if($redirectPage->viewable()) {
|
||
|
$redirectURL = $redirectPage->url();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(!$redirectURL) {
|
||
|
$redirectURL = $this->adminUrl . "page/list/";
|
||
|
}
|
||
|
|
||
|
$redirectURL .= "?open=$clone->id";
|
||
|
|
||
|
$this->session->redirect($redirectURL);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Process an ajax clone request
|
||
|
*
|
||
|
* Outputs JSON result and exits
|
||
|
*
|
||
|
* @param Page $original
|
||
|
* @param bool $returnArray
|
||
|
* @return array|null
|
||
|
*
|
||
|
*/
|
||
|
public function processAjax(Page $original = null, $returnArray = false) {
|
||
|
|
||
|
$error = null;
|
||
|
if($original === null) $original = $this->page;
|
||
|
|
||
|
if($this->hasPermission($original)) {
|
||
|
$clone = $this->cloneAjax($original, $error);
|
||
|
} else {
|
||
|
$clone = new NullPage();
|
||
|
}
|
||
|
|
||
|
$result = array(
|
||
|
'action' => 'clone',
|
||
|
'success' => $clone->id > 0 && $clone->id != $original->id && empty($error),
|
||
|
'message' => '',
|
||
|
'page' => $clone->id,
|
||
|
);
|
||
|
|
||
|
if($clone->id) {
|
||
|
$result['message'] = $this->wire()->sanitizer->unentities(
|
||
|
sprintf($this->_('Cloned to: %s'), $clone->path)
|
||
|
);
|
||
|
} else {
|
||
|
// error
|
||
|
$result['message'] = $error ? $error : $this->_('Unable to clone page');
|
||
|
}
|
||
|
|
||
|
if($returnArray) return $result;
|
||
|
|
||
|
header("Content-type: application/json");
|
||
|
echo json_encode($result);
|
||
|
exit;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Perform a clone during ajax request
|
||
|
*
|
||
|
* @param Page $original Page to clone
|
||
|
* @param string $error Variable to populate error message in
|
||
|
* @return Page|NullPage
|
||
|
*
|
||
|
*/
|
||
|
protected function cloneAjax(Page $original, &$error) {
|
||
|
|
||
|
$page = clone $original;
|
||
|
$suggested = $this->getSuggestedNameAndTitle($page);
|
||
|
|
||
|
$cloneOptions = array(
|
||
|
'set' => array(
|
||
|
// keep original $page modified date and user id, since ajax mode doesn't
|
||
|
// give the user the option to edit the page before cloning it
|
||
|
'modified' => $original->modified,
|
||
|
'modified_users_id' => $original->modified_users_id,
|
||
|
// pages cloned in ajax are always unpublished
|
||
|
'status' => $page->status | Page::statusUnpublished,
|
||
|
'title' => $suggested['title'],
|
||
|
'name' => $suggested['name'],
|
||
|
)
|
||
|
);
|
||
|
|
||
|
$languages = $this->wire()->languages;
|
||
|
if($languages) {
|
||
|
foreach($languages as $language) {
|
||
|
/** @var Language $language */
|
||
|
if($language->isDefault()) continue;
|
||
|
if(!empty($suggested["name$language"])) {
|
||
|
$cloneOptions['set']["name$language"] = $suggested["name$language"];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$cloneTree = false; // clone tree mode not allowed in ajax mode
|
||
|
|
||
|
try {
|
||
|
$clone = $this->wire()->pages->clone($page, $page->parent, $cloneTree, $cloneOptions);
|
||
|
} catch(\Exception $e) {
|
||
|
$error = $e->getMessage();
|
||
|
$clone = new NullPage();
|
||
|
}
|
||
|
|
||
|
return $clone;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get Page being cloned
|
||
|
*
|
||
|
* @return null|Page
|
||
|
*
|
||
|
*/
|
||
|
public function getPage() {
|
||
|
return $this->page;
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|