<?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 2024 by Ryan Cramer
 * https://processwire.com
 *
 * Optional GET variables: 
 * - redirect_page (int): Contains ID of page to redirect to after clone.
 *
 * @property bool|int $alwaysUseForm Disable the ajax shortcut for cloning pages and always use full form? 3.0.238+
 * @method InputfieldForm buildForm()
 * @method string render()
 * @method void process()
 * @method array getSuggestedNameAndTitle(Page $page) Return array with suggested 'name' and 'title' elements for given $page (3.0.238)
 *
 */

class ProcessPageClone extends Process implements ConfigurableModule {

	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' => 106, 
			'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 = '';

	/**
	 * Construct
	 * 
	 */
	public function __construct() {
		$this->set('alwaysUseForm', false);
		parent::__construct();
	}

	/**
	 * 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 && !$this->alwaysUseForm) {
				$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) {
		return $page->cloneable(false);
	}

	/**
	 * Return array with suggested 'name' and 'title' elements for given $page
	 * 
	 * Hookable as of 3.0.238. 
	 * 
	 * @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
		$titleField->icon = 'file-text-o';

		/** @var InputfieldPageName $nameField */
		$nameField = $modules->get("InputfieldPageName"); 
		$nameField->attr('name', 'clone_page_name'); 
		$nameField->attr('value', $suggested['name']);
		$nameField->icon = 'bookmark-o';
		$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 = (string) $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->icon = 'eye-slash';
		$field->label = $this->_("Make the new page unpublished?");
		if($this->wire()->input->requestMethod('GET')) $field->attr('checked', 'checked');
		$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)) { 
			$numDescendents = $this->wire()->pages->count("has_parent=$page"); 
			/** @var InputfieldCheckbox $field */
			$field = $modules->get("InputfieldCheckbox"); 
			$field->attr('name', 'clone_page_tree'); 
			$field->attr('value', 1); 
			$field->icon = 'child';
			$label = $this->_("Copy children too?");
			$field->label = $label . ' ' . sprintf($this->_n('(~%d page)', '(~%d pages)', $numDescendents), $numDescendents);
			$field->label2 = $label;
			$field->description = $this->_("If checked, all children, grandchildren, etc., will also be cloned with this page."); 
			if($numDescendents > 20) {
				$field->notes = $this->_("Warning: if there is a very large structure of pages below this, it may be time consuming or impossible to complete.");
			}
			$form->add($field);
		
			/** @var InputfieldCheckbox $field */
			$field = $modules->get("InputfieldCheckbox");
			$field->attr('name', 'clone_tree_unpublished');
			$field->attr('value', 1);
			$field->label = $this->_("Make cloned children unpublished?");
			$field->icon = 'eye-slash';
			$field->description = $this->_("If checked, all cloned children, grandchildren, etc., will also be unpublished.");
			$field->showIf = 'clone_page_tree=1';
			$form->add($field);
		}

		/** @var InputfieldSubmit $field */
		$submitLabel = $this->wire('processHeadline');
		$field = $modules->get("InputfieldSubmit"); 
		$field->attr('name', 'submit_clone'); 
		$field->icon = 'copy';
		$field->value = $submitLabel;
		$field->addActionValue('list', sprintf($this->_('%s + List'), $submitLabel), 'tree');
		$field->addActionValue('edit', sprintf($this->_('%s + Edit'), $submitLabel), 'edit');
		$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->wire()->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->page->cloneable(true);
		
		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);

		if($cloneTree && $input->post('clone_tree_unpublished')) {
			$this->wire()->pages->addHook('cloneReady', function(HookEvent $event) use($page) {
				$child = $event->arguments(1); /** @var Page $child */
				if($page->id !== $child->id) $child->addStatus(Page::statusUnpublished);
			}); 
		}
		
		$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($input->post('submit_clone') === 'edit' && $clone->id && $clone->editable()) {
			$redirectURL = $clone->editUrl();
		} else if($redirectID) {
			$redirectPage = $this->wire()->pages->get($redirectID);
			if($redirectPage->viewable()) {
				$redirectURL = $redirectPage->url();
			}
			$redirectURL .= "?open=$clone->id";
		}
		
		if(!$redirectURL) {
			$redirectURL = $this->adminUrl . "page/list/?open=$clone->id";
		}

		$this->wire()->session->location($redirectURL);
	}

	/**
	 * Process an ajax clone request
	 * 
	 * Outputs JSON result and exits
	 * 
	 * @param Page|null $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; 
	}

	/**
	 * Module configuration
	 * 
	 * @param InputfieldWrapper $inputfields
	 * @since 3.0.238
	 * 
	 */
	public function getModuleConfigInputfields(InputfieldWrapper $inputfields) {
		$f = $inputfields->InputfieldCheckbox;
		$f->attr('name', 'alwaysUseForm');
		$f->label = $this->_('Always use full clone form?'); 
		$f->description = 
			$this->_('When a page has no children, this module will use a time-saving shortcut to clone the page without leaving the page tree.') . ' ' . 
			$this->_('If you always want it to use the full clone options form (so that you can enter name, title, etc. before clone) then check this box.'); 
		$f->themeOffset = 1;
		if($this->alwaysUseForm) $f->attr('checked', 'checked');
		$inputfields->add($f);
	}

}
