__('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; } }