Blame | Last modification | View Log | Download
<?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 2018 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() {$this->headline($this->_('Copy Page')); // Headline$error = $this->_("Unable to load page");$id = (int) $this->wire('input')->get('id');if(!$id) 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($this->wire('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($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) {/** @var Pages $pages */$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);if($this->wire('modules')->isInstalled('LanguageSupportPageNames')) {foreach($this->wire('languages') as $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() {/** @var Page $page */$page = $this->page;/** @var InputfieldForm $form */$form = $this->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 = $this->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 = $this->modules->get("InputfieldPageName");$nameField->attr('name', 'clone_page_name');$nameField->attr('value', $suggested['name']);$nameField->parentPage = $page->parent;/** @var Languages $languages */$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 = $this->wire('modules')->isInstalled('LanguageSupportPageNames');if($titleUseLanguages) $titleField->useLanguages = true;if($nameUseLanguages) $nameField->useLanguages = true;foreach($languages as $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 = $this->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 = $this->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 = $this->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 = $this->wire('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;$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) {foreach($this->wire('languages') as $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)) {foreach($this->wire('languages') as $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'],));if($this->wire('languages')) {foreach($this->wire('languages') as $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 modetry {$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;}}