Rev 1 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | Download
<?php namespace ProcessWire;/*** ProcessWire Module Process** Provides list, install, and uninstall capability for ProcessWire modules** For more details about how Process modules work, please see:* /wire/core/Process.php** This version also lifts several pieces of code from Soma's Modules Manager* specific to the parts involved with downloading modules from the directory.** ProcessWire 3.x, Copyright 2022 by Ryan Cramer* https://processwire.com** @todo add support for module configuration inputfields with useLanguages option** @method string executeUpload($inputName = '')* @method string executeDownloadURL($url = '')* @method string executeDownload()* @method string executeEdit()* @method string executeInstallConfirm()* @method InputfieldForm buildDownloadConfirmForm(array $data, $update = false)* @method InputfieldForm buildDownloadSuccessForm($className)**/class ProcessModule extends Process {public static function getModuleInfo() {return array('title' => __('Modules', __FILE__), // getModuleInfo title'summary' => __('List, edit or install/uninstall modules', __FILE__), // getModuleInfo summary'version' => 120,'permanent' => true,'permission' => 'module-admin','useNavJSON' => true,'nav' => array(array('url' => '?site#tab_site_modules','label' => 'Site','icon' => 'plug','navJSON' => 'navJSON/?site=1'),array('url' => '?core#tab_core_modules','label' => 'Core','icon' => 'plug','navJSON' => 'navJSON/?core=1',),array('url' => '?configurable#tab_configurable_modules','label' => 'Configure','icon' => 'gear','navJSON' => 'navJSON/?configurable=1',),array('url' => '?install#tab_install_modules','label' => 'Install','icon' => 'sign-in','navJSON' => 'navJSON/?install=1',),array('url' => '?new#tab_new_modules','label' => 'New','icon' => 'plus',),array('url' => '?reset=1','label' => 'Refresh','icon' => 'refresh',)));}/*** New core modules allowed to appear in the "new" list** By default, core modules don't appear in the "new" list,* this array contains a list of core modules that are allowed to appear there.** @var array**/protected $newCoreModules = array('SystemNotifications','InputfieldCKEditor','FieldtypeOptions','InputfieldIcon','ProcessLogger',);protected $labels = array();/*** All modules indexed by class name and sorted by class name**/protected $modulesArray = array();/*** All modules that may be deleted**/protected $deleteableModules = array();/*** Categories of modules that we can't uninstall via this module**/protected $uninstallableCategories = array('language-pack','site-profile',);/*** Number of new modules found after a reset**/protected $numFound = 0;/*** @var ProcessModuleInstall|null**/protected $installer = null;/*** Construct**/public function __construct() {$this->labels['download'] = $this->_('Download');$this->labels['download_install'] = $this->_('Download and Install');$this->labels['get_module_info'] = $this->_('Get Module Info');$this->labels['modules'] = $this->_('Modules');$this->labels['module_information'] = $this->_x("Module Information", 'edit');$this->labels['download_now'] = $this->_('Download Now');$this->labels['download_dir'] = $this->_('Add Module From Directory');$this->labels['add_manually'] = $this->_('Add Module Manually');$this->labels['upload'] = $this->_('Upload');$this->labels['upload_zip'] = $this->_('Add Module From Upload');$this->labels['download_zip'] = $this->_('Add Module From URL');$this->labels['check_new'] = $this->_('Check for New Modules');$this->labels['installed_date'] = $this->_('Installed');$this->labels['requires'] = $this->_x('Requires', 'list'); // Label that precedes list of required prerequisite modules$this->labels['installs'] = $this->_x('Also Installs', 'list'); // Label that precedes list of other modules a given one installs$this->labels['reset'] = $this->_('Refresh');$this->labels['core'] = $this->_('Core');$this->labels['site'] = $this->_('Site');$this->labels['configure'] = $this->_('Configure');$this->labels['install_btn'] = $this->_x('Install', 'button'); // Label for Install button$this->labels['install'] = $this->_('Install'); // Label for Install tab$this->labels['cancel'] = $this->_('Cancel'); // Label for Cancel buttonrequire(dirname(__FILE__) . '/ProcessModuleInstall.php');}/*** Wired to API**/public function wired() {parent::wired();if($this->wire()->languages && !$this->wire()->user->language->isDefault()) {// Use previous translations when new labels aren't available (can be removed in PW 2.6+ when language packs assumed updated)if($this->labels['install'] == 'Install') $this->labels['install'] = $this->labels['install_btn'];if($this->labels['reset'] == 'Refresh') $this->labels['reset'] = $this->labels['check_new'];}if($this->wire()->input->get('update')) {$this->labels['download_install'] = $this->_('Download and Update');}}/*** @return ProcessModuleInstall**/public function installer() {if($this->installer === null) $this->installer = $this->wire(new ProcessModuleInstall());return $this->installer;}/*** Format a module version number from 999 to 9.9.9** @param string $version* @return string**/protected function formatVersion($version) {return $this->wire()->modules->formatVersion($version);}/*** Output JSON list of navigation items for this (intended to for ajax use)** For 2.5+ admin themes** @param array $options* @return string**/public function ___executeNavJSON(array $options = array()) {$page = $this->wire()->page;$input = $this->wire()->input;$modules = $this->wire()->modules;$site = (int) $input->get('site');$core = (int) $input->get('core');$configurable = (int) $input->get('configurable');$install = (int) $input->get('install');$moduleNames = array();$data = array('url' => $page->url,'label' => (string) $page->get('title|name'),'icon' => 'plug','list' => array(),);if($site || $install) $data['add'] = array('url' => "?new#tab_new_modules",'label' => __('Add New', '/wire/templates-admin/default.php'),'icon' => 'plus-circle',);if($install) {$moduleNames = array_keys($modules->getInstallable());} else {foreach($modules as $module) {$moduleNames[] = $module->className();}}sort($moduleNames);foreach($moduleNames as $moduleName) {$info = $modules->getModuleInfoVerbose($moduleName);if($site && $info['core']) continue;if($core && !$info['core']) continue;if($configurable) {if(!$info['configurable'] || !$info['installed']) continue;$flags = $modules->getFlags($moduleName);if($flags & Modules::flagsNoUserConfig) continue;}if($install) {// exclude already installed modulesif($info['installed']) continue;// check that it can be installed NOW (i.e. all dependencies met)if(!$modules->isInstallable($moduleName, true)) continue;}$label = $info['name'];$_label = $label;while(isset($data['list'][$_label])) $_label .= "_";if(empty($info['icon'])) $info['icon'] = $info['configurable'] ? 'gear' : 'plug';$url = $install ? "installConfirm" : "edit";$url .= "?name=$info[name]";if($configurable) $url .= "&collapse_info=1";$data['list'][$_label] = array('url' => $url,'label' => $label,'icon' => $info['icon'],);}ksort($data['list']);$data['list'] = array_values($data['list']);if($this->wire()->config->ajax) header("Content-Type: application/json");return json_encode($data);}/*** Load all modules, install any requested, and render a list of all modules**/public function ___execute() {$modules = $this->wire()->modules;$session = $this->wire()->session;$sanitizer = $this->wire()->sanitizer;$input = $this->wire()->input;foreach($modules as $module) {$this->modulesArray[$module->className()] = 1;}foreach($modules->getInstallable() as $module) {$this->modulesArray[basename(basename($module, '.php'), '.module')] = 0;}ksort($this->modulesArray);if($input->post('install')) {$session->CSRF->validate();$name = $sanitizer->name($input->post('install'));if($name && isset($this->modulesArray[$name]) && !$this->modulesArray[$name]) {$module = $modules->install($name, array('force' => true));if($module) {$this->modulesArray[$name] = 1;$session->message($this->_("Module Install") . " - $name"); // Message that precedes the name of the module installed$session->redirect("edit?name=$name");} else {$session->error($this->_('Error installing module') . " - $name");$session->redirect("./");}}}if($input->post('delete')) {$session->CSRF->validate();$name = $input->post('delete');if($name && isset($this->modulesArray[$name])) {$info = $modules->getModuleInfoVerbose($name);try {$modules->delete($name);$this->message($this->_('Deleted module files') . ' - ' . $info['title']);} catch(WireException $e) {$this->error($e->getMessage());}$session->redirect("./");}}if($input->post('download') && $input->post('download_name')) {$session->CSRF->validate();return $this->downloadConfirm($input->post('download_name'));} else if($input->get('download_name')) {return $this->downloadConfirm($input->get('download_name'));}if($input->post('upload')) {$session->CSRF->validate();$this->executeUpload('upload_module');}if($input->post('download_zip') && $input->post('download_zip_url')) {$session->CSRF->validate();$this->executeDownloadURL($input->post('download_zip_url'));}if($input->post('clear_file_compiler')) {$session->CSRF->validate();/** @var FileCompiler $compiler */$compiler = $this->wire(new FileCompiler($this->wire('config')->paths->siteModules));$compiler->clearCache(true);$session->message($this->_('Cleared file compiler cache'));$session->redirect('./');}if($input->get('update')) {$name = $sanitizer->name($input->get('update'));if(isset($this->modulesArray[$name])) return $this->downloadConfirm($name, true);}if($input->get('reset') == 1) {$modules->refresh(true);$this->message(sprintf($this->_('Modules cache refreshed (%d modules)'), count($modules)));$edit = $input->get->fieldName('edit');$duplicates = $modules->duplicates()->getDuplicates();foreach($duplicates as $className => $files) {$dup = $modules->duplicates()->getDuplicates($className);if(!count($dup['files'])) continue;$msg = sprintf($this->_('Module "%s" has multiple files (bold file is the one in use).'), $className) . ' ' ."<a href='./edit?name=$className'>" . $this->_('Click here to change which file is used') . "</a><pre>";foreach($dup['files'] as $file) {if($dup['using'] == $file) $file = "<b>$file</b>";$msg .= "\n$file";}$this->message("$msg</pre>", Notice::allowMarkup);}if($edit) {$session->redirect("./edit?name=$edit&reset=2");} else {$session->redirect("./?reset=2");}}return $this->renderList();}/*** Render a list of all modules**/protected function renderList() {$modules = $this->wire()->modules;$input = $this->wire()->input;$session = $this->wire()->session;// module arrays: array(moduleName => 0 (uninstalled) or 1 (installed))$modulesArray = $this->modulesArray;$installedArray = array();$uninstalledArray = array();$configurableArray = array();$uninstalledNames = array();$siteModulesArray = array();$coreModulesArray = array();$newModulesArray = array();if($input->post('new_seconds')) {$session->set('ProcessModuleNewSeconds', (int) $input->post('new_seconds'));}$newSeconds = (int) $session->get('ProcessModuleNewSeconds');if(!$newSeconds) $newSeconds = 86400;foreach($modulesArray as $name => $installed) {if($installed) {$installedArray[$name] = $installed;$errors = $modules->getDependencyErrors($name);if($errors) foreach($errors as $error) $this->error($error);} else {$uninstalledNames[] = $name;$uninstalledArray[$name] = $installed;}$info = $modules->getModuleInfoVerbose($name);$isNew = !$info['core'] || ($info['core'] && in_array($name, $this->newCoreModules));if($isNew) $isNew = $info['created'] > 0 && $info['created'] > (time()-$newSeconds);if($isNew) $newModulesArray[$name] = $installed;if($info['core']) {$coreModulesArray[$name] = $installed;} else {$siteModulesArray[$name] = $installed;}if($info['configurable'] && $info['installed']) {$flags = $modules->getFlags($name);if(!($flags & Modules::flagsNoUserConfig)) {$configurableArray[$name] = $installed;}}}/** @var InputfieldForm $form */$form = $modules->get('InputfieldForm');$form->attr('action', './');$form->attr('method', 'post');$form->attr('enctype', 'multipart/form-data');$form->attr('id', 'modules_form');$form->addClass('ModulesList');$modules->get('JqueryWireTabs');// site/** @var InputfieldWrapper $tab */$tab = $this->wire(new InputfieldWrapper());$tab->attr('id', 'tab_site_modules');$tab->attr('title', $this->labels['site']);$tab->attr('class', 'WireTab');/** @var InputfieldSubmit $button */$button = $modules->get('InputfieldSubmit');$button->attr('name', 'clear_file_compiler');$button->attr('value', $this->_('Clear compiled files'));$button->addClass('ui-priority-secondary');$button->icon = 'trash-o';/** @var InputfieldMarkup $markup */$markup = $modules->get('InputfieldMarkup');$markup->label = $this->_('/site/modules/ - Modules specific to your site');$markup->icon = 'folder-open-o';$markup->value .=$this->renderListTable($siteModulesArray, array('allowDelete' => true)) ."<p class='detail'>" . wireIconMarkup('star', 'fw') . " " .sprintf($this->_('Browse the modules directory at %s'), "<a target='_blank' href='https://processwire.com/modules/'>processwire.com/modules</a>") ."</p>" ."<p class='detail'>" . wireIconMarkup('eraser', 'fw') . " " .$this->_("To remove a module, click the module to edit, check the Uninstall box, then save. Once uninstalled, the module's file(s) may be removed from /site/modules/. If it still appears in the list above, you may need to click the Refresh button for ProcessWire to see the change.") . // Instructions on how to remove a module"</p>" ."<p class='detail'>" . wireIconMarkup('info-circle') . " " .$this->_('The button below clears compiled site modules and template files, forcing them to be re-compiled the next time they are accessed. Note that this may cause a temporary delay for one or more requests while files are re-compiled.') ."</p>" ."<p class='detail'>" . $button->render() . "</p>";$tab->add($markup);$form->add($tab);// core/** @var InputfieldWrapper $tab */$tab = $this->wire(new InputfieldWrapper());$tab->attr('id', 'tab_core_modules');$tab->attr('title', $this->labels['core']);$tab->attr('class', 'WireTab');/** @var InputfieldMarkup $markup */$markup = $modules->get('InputfieldMarkup');$markup->value = $this->renderListTable($coreModulesArray);$markup->label = $this->_('/wire/modules/ - Modules included with the ProcessWire core');$markup->icon = 'folder-open-o';$tab->add($markup);$form->add($tab);// configurable$tab = $this->wire(new InputfieldWrapper());$tab->attr('id', 'tab_configurable_modules');$tab->attr('title', $this->labels['configure']);$tab->attr('class', 'WireTab');/** @var InputfieldMarkup $markup */$markup = $modules->get('InputfieldMarkup');$markup->value = $this->renderListTable($configurableArray, array('allowDelete' => true,'allowType' => true));$markup->label = $this->_('Modules that have configuration options');$markup->icon = 'folder-open-o';$tab->add($markup);$form->add($tab);// installable$tab = $this->wire(new InputfieldWrapper());$tab->attr('id', 'tab_install_modules');$tabLabel = $this->labels['install'];$tab->attr('title', $tabLabel);$tab->attr('class', 'WireTab');$markup = $modules->get('InputfieldMarkup');$markup->value = $this->renderListTable($uninstalledArray, array('allowDelete' => true,'allowType' => true));$markup->label = $this->_('Modules on the file system that are not currently installed');$markup->icon = 'folder-open-o';$tab->add($markup);$form->add($tab);// missing$missing = $modules->findMissingModules();if(count($missing)) {$missingArray = array();$missingFiles = array();$rootPath = $this->wire()->config->paths->root;foreach($missing as $name => $item) {$missingArray[$name] = $modules->isInstalled($name);$missingFiles[$name] = sprintf($this->_('Missing module file(s) in: %s'),dirname(str_replace($rootPath, '/', $item['file'])) . '/');}$tab = $this->wire(new InputfieldWrapper());$tab->attr('id', 'tab_missing_modules');$tabLabel = $this->_('Missing');$tab->attr('title', $tabLabel);$tab->attr('class', 'WireTab');$markup = $modules->get('InputfieldMarkup');$markup->value = $this->renderListTable($missingArray, array('allowInstall' => false,'allowType' => true,'summaries' => $missingFiles,));$markup->label = $this->_('Modules in database that are not found on the file system. Click any module name below for options to fix.');$markup->icon = 'warning';$tab->add($markup);$form->add($tab);}// new/** @var InputfieldWrapper $tab */$tab = $this->wire(new InputfieldWrapper());$tab->attr('id', 'tab_new_modules');$tab->attr('title', $this->_('New'));$tab->attr('class', 'WireTab');$newModules = $session->get($this, 'newModules');if($newModules) foreach($newModules as $name => $created) {if(!is_numeric($name) && !isset($newModulesArray[$name])) {// add to newModulesArray and identify as uninstalled$newModulesArray[$name] = 0;}}/** @var InputfieldSelect $select */$select = $modules->get('InputfieldSelect');$select->attr('name', 'new_seconds');$select->addClass('modules_filter');$select->addOption(3600, $this->_('Within the last hour'));$select->addOption(86400, $this->_('Within the last day'));$select->addOption(604800, $this->_('Within the last week'));$select->addOption(2419200, $this->_('Within the last month'));$select->required = true;$select->attr('value', $newSeconds);/** @var InputfieldSubmit $btn */$btn = $modules->get('InputfieldSubmit');$btn->attr('hidden', 'hidden');$btn->attr('name', 'submit_check');$btn->textFormat = Inputfield::textFormatNone;$btn->icon = 'check';$btn->value = ' ';$btn->setSmall(true);$btn->setSecondary(true);$btn = "<button type='submit' id='submit_check' name='submit_check' value='1' hidden> </button>";/** @var InputfieldMarkup $markup */$markup = $modules->get('InputfieldMarkup');$markup->icon = 'lightbulb-o';$markup->value = $select->render() . ' ' . $btn .$this->renderListTable($newModulesArray, array('allowSections' => false,'allowDates' => true,'allowClasses' => true));$markup->label = $this->_('Recently Found and Installed Modules');$tab->add($markup);/** @var InputfieldFieldset $fieldset */$fieldset = $modules->get('InputfieldFieldset');$fieldset->label = $this->labels['download_dir'];$fieldset->icon = 'cloud-download';$tab->add($fieldset);//if($this->wire('input')->post('new_seconds')) $fieldset->collapsed = Inputfield::collapsedYes;if($this->installer()->canInstallFromDirectory(false)) {/** @var InputfieldName $f */$f = $modules->get('InputfieldName');$f->attr('id+name', 'download_name');$f->label = $this->_('Module Class Name');$f->description =$this->_('You may browse the modules directory and locate the module you want to download and install.') . ' ' .sprintf($this->_('Type or paste in the class name for the module you want to install, then click the “%s” button to proceed.'),$this->labels['get_module_info']);$f->notes = sprintf($this->_('The modules directory is located at %s'), '[processwire.com/modules](https://processwire.com/modules/)');$f->attr('placeholder', $this->_('ModuleClassName')); // placeholder$f->required = false;$fieldset->add($f);/** @var InputfieldSubmit $f */$f = $modules->get('InputfieldSubmit');$f->attr('id+name', 'download');$f->value = $this->labels['get_module_info'];$f->icon = $fieldset->icon;$fieldset->add($f);} else {$fieldset->description = $this->installer()->installDisabledLabel('directory');$fieldset->collapsed = Inputfield::collapsedYes;}/** @var InputfieldFieldset $fieldset */$fieldset = $modules->get('InputfieldFieldset');$fieldset->label = $this->labels['download_zip'];$fieldset->icon = 'download';$fieldset->collapsed = Inputfield::collapsedYes;$tab->add($fieldset);$trustNote = $this->_('Be absolutely certain that you trust the source of the ZIP file.');if($this->installer()->canInstallFromDownloadUrl(false)) {/** @var InputfieldURL $f */$f = $modules->get('InputfieldURL');$f->attr('id+name', 'download_zip_url');$f->label = $this->_('Module ZIP file URL');$f->description = $this->_('Download a ZIP file containing a module. If you download a module that is already installed, the installed version will be overwritten with the newly downloaded version.');$f->notes = $trustNote;$f->attr('placeholder', $this->_('http://domain.com/ModuleName.zip')); // placeholder$f->required = false;$fieldset->add($f);/** @var InputfieldSubmit $f */$f = $modules->get('InputfieldSubmit');$f->attr('id+name', 'download_zip');$f->value = $this->labels['download'];$f->icon = $fieldset->icon;$fieldset->add($f);} else {$fieldset->description = $this->installer()->installDisabledLabel('download');}/** @var InputfieldFieldset $fieldset */$fieldset = $modules->get('InputfieldFieldset');$fieldset->label = $this->labels['upload_zip'];$fieldset->icon = 'upload';$fieldset->collapsed = Inputfield::collapsedYes;$tab->add($fieldset);if($this->installer()->canInstallFromFileUpload(false)) {/** @var InputfieldFile $f */$f = $modules->get('InputfieldFile');$f->extensions = 'zip';$f->maxFiles = 1;$f->descriptionRows = 0;$f->overwrite = true;$f->attr('id+name', 'upload_module');$f->label = $this->_('Module ZIP File');$f->description = $this->_('Upload a ZIP file containing module file(s). If you upload a module that is already installed, it will be overwritten with the one you upload.');$f->notes = $trustNote;$f->required = false;$f->noCustomButton = true;$fieldset->add($f);/** @var InputfieldSubmit $f */$f = $modules->get('InputfieldSubmit');$f->attr('id+name', 'upload');$f->value = $this->labels['upload'];$f->icon = $fieldset->icon;$fieldset->add($f);} else {$fieldset->description = $this->installer()->installDisabledLabel('upload');}/** @var InputfieldFieldset $fieldset */$fieldset = $modules->get('InputfieldFieldset');$fieldset->attr('id', 'fieldset_check_new');$fieldset->label = $this->labels['add_manually'];$fieldset->icon = 'plug';$fieldset->collapsed = Inputfield::collapsedYes;$tab->add($fieldset);/** @var InputfieldMarkup $markup */$markup = $modules->get('InputfieldMarkup');$fieldset->add($markup);$moduleNameLabel = $this->_('ModuleName'); // Example module class/directory name$moduleNameDir = $this->wire()->config->urls->siteModules . $moduleNameLabel . '/';$instructions = array(sprintf($this->_('1. Copy a module’s files into a new directory %s on the server.'), "<u>$moduleNameDir</u>") . ' * ',sprintf($this->_('2. Click the “%s” button below, which will find the new module.'), $this->labels['reset']),sprintf($this->_('3. Locate and click the “%s” button next to the new module.'), $this->labels['install_btn']));$markup->value = '<p>' . implode('</p><p>', $instructions) . '</p>';$markup->notes = '* ' . sprintf($this->_('Replace “%s” with the actual module name, which is typically its PHP class name.'),$moduleNameLabel);/** @var InputfieldFieldset $fieldset *//*$fieldset = $this->modules->get('InputfieldFieldset');$fieldset->attr('id', 'fieldset_check_new');$fieldset->label = $this->labels['reset'];$fieldset->description = $this->_('If you have placed new modules in /site/modules/ yourself, click this button to find them.');$fieldset->collapsed = Inputfield::collapsedYes;$fieldset->icon = 'refresh';*//** @var InputfieldButton $submit */$submit = $modules->get('InputfieldButton');$submit->attr('href', './?reset=1');$submit->attr('id', 'reset_modules');$submit->showInHeader();$submit->attr('name', 'reset');$submit->attr('value', $this->labels['reset']);$submit->icon = 'refresh';$fieldset->add($submit);$tab->add($fieldset);$form->add($tab);// if($this->input->get->reset == 2 && !$this->numFound) $this->message($this->_("No new modules found"));$session->set('ModulesUninstalled', $uninstalledNames);return $form->render();}/*** Render a modules listing table, as it appears in the 'site' and 'core' tabs** @param array $modulesArray* @param array $options* `allowDelete` (bool): Whether or not delete is allowed (default=false)* `allowSections` (bool): Whether to show module sections/categories (default=true)* `allowDates` (bool): Whether to show created dates (default=false)* `allowClasses` (bool) Whether to show module class names (default=false)* `allowType` (bool): Whether to show if module is site or core (default=false)* `allowInstall` (bool): Whether or not install is allowed (default=true)* `summaries` (array): Replacement summary info indexed by module name (default=[])* @return string**/protected function renderListTable($modulesArray, array $options = array()) {$defaults = array('allowDelete' => false,'allowSections' => true,'allowDates' => false,'allowClasses' => false,'allowType' => false,'allowInstall' => true,'summaries' => array(),);$options = array_merge($defaults, $options);$session = $this->wire()->session;$modules = $this->wire()->modules;$input = $this->wire()->input;$sanitizer = $this->wire()->sanitizer;if(!count($modulesArray)) return "<div class='ProcessModuleNoneFound'>" . $this->_('No modules found.') . "</div>";static $numCalls = 0;$numCalls++;$uninstalledPrev = is_array($session->get('ModulesUninstalled')) ? $session->get('ModulesUninstalled') : array();$section = 'none';$tableHeader = array($this->_x('Module', 'list'), // Modules list table header for 'Module' column$this->_x('Version', 'list'), // Modules list table header for 'Version' column$this->_x('Summary', 'list') // Modules list table header for 'Summary' column);/** @var MarkupAdminDataTable|null $table */$table = null;$total = 0;$out = '';$this->numFound = 0;$newModules = $session->get($this, 'newModules');if(!is_array($newModules)) $newModules = array();$sections = array();$sectionsQty = array();foreach($modulesArray as $name => $installed) {if(strpos($name, $section) !== 0 || preg_match('/' . $section . '[^A-Z0-9]/', $name)) {if(!preg_match('/^([A-Za-z][a-z]+)/', $name, $matches)) $this->error(sprintf($this->_('Invalid module name: %s'), $name));if($options['allowSections'] || is_null($table)) {$section = $matches[1];$sections[] = $section;if($table) $out .= $table->render() . "</div>";$table = $modules->get("MarkupAdminDataTable");$table->setEncodeEntities(false);$table->headerRow($tableHeader);if($options['allowSections']) $out .= "\n<div class='modules_section modules_$section'><h2>$section</h2>";}}$info = $modules->getModuleInfoVerbose($name);$configurable = $info['configurable'];$title = !empty($info['title']) ? $sanitizer->entities1($info['title']) : substr($name, strlen($section));$title = "<span title='$name' uk-tooltip='delay:1000'>$title</span>";if($options['allowClasses']) $title .= "<br /><small class='ModuleClass ui-priority-secondary'>$name</small>";if($info['icon']) $title = wireIconMarkup($info['icon'], 'fw') . " $title";$class = $configurable ? 'ConfigurableModule' : '';if(!empty($info['permanent'])) $class .= ($class ? ' ' : '') . 'PermanentModule';if($class) $title = "<span class='$class'>$title</span>";$version = empty($info['version']) ? '?' : $this->formatVersion($info['version']);if($options['allowType']) $version .= "<br /><small class='ModuleClass ui-priority-secondary'>" . ($info['core'] ? $this->labels['core'] : $this->labels['site']) . "</small>";if(!empty($options['summaries'][$name])) $info['summary'] = $options['summaries'][$name];$summary = empty($info['summary']) ? '' : $sanitizer->entities1($info['summary']);if(strpos($summary, '<') !== false) $summary = preg_replace('/([^\s]{35})[^\s]{20,}/', '$1...', $summary); // prevent excessively long text without whitespace$summary .= empty($info['href']) ? '' : (" <a href='" . $sanitizer->entities($info['href']) . "'>" . $this->_('more') . "</a>");if($summary) $summary = "<p class='module-summary'>$summary</p>";$buttons = '';$confirmDeleteJS = "return confirm('" . sprintf($this->_('Delete %s?'), $name) . "')";$confirmInstallJS = "return confirm('" . sprintf($this->_('Module requirements are not fulfilled so installing may cause problems. Are you sure you want to install?'), $name) . "')";$editUrl = "edit?name={$name}";if(!$installed && $options['allowInstall']) {if(count($info['requires'])) {$requires = $modules->getRequiresForInstall($name);if(count($requires)) {foreach($requires as $key => $value) {$nameOnly = preg_replace('/^([_a-zA-Z0-9]+)[=<>]+.*$/', '$1', $value);$requiresInfo = $modules->getModuleInfo($nameOnly);if(!empty($requiresInfo['error'])) $requires[$key] = "<a href='./?download_name=$nameOnly'>$value</a>";}$summary .= "<span class='notes requires'>" . $this->labels['requires'] . " - " . implode(', ', $requires) . "</span>";}} else $requires = array();$nsClassName = $modules->getModuleClass($name, true);if(!wireInstanceOf($nsClassName, 'Module')) {$summary .= "<span class='notes requires'>" . $this->_('Module class must implement the “ProcessWire\Module” interface.') . "</span>";$requires[] = 'Module interface';}if(count($info['installs'])) {$summary .= "<span class='detail installs'>" . $this->labels['installs'] . " - " . implode(', ', $info['installs']) . "</span>";}$class = 'not_installed';if(count($uninstalledPrev) && !in_array($name, $uninstalledPrev)) {$class .= " new_module";if(!$input->get('uninstalled')) {$this->message($this->_("Found new module") . " - $name", Notice::noGroup); // Message that precedes module name when new module is found}$newModules[$name] = time();$this->numFound++;}$title = "<span data-name='$name' class='$class'>$title</span>";$isConfirm = count($modulesArray) == 1 && $input->get('name');$buttonState = 'ui-state-default';$buttonPriority = $isConfirm ? "ui-priority-primary" : "ui-priority-secondary";$buttonType = 'submit';$buttonWarning = '';if(count($requires)) {$buttonWarning = " onclick=\"$confirmInstallJS\"";$icon = 'warning';} else {$icon = 'sign-in';}$buttons .="<button type='$buttonType' name='install' $buttonWarning data-install='$name' " ."class='install_$name $buttonState ui-button $buttonPriority' value='$name'>" ."<span class='ui-button-text'>" .wireIconMarkup($icon) . " " .$this->labels['install_btn'] ."</span>" ."</button>";// install confirm, needs a cancel buttonif($isConfirm) $buttons .="<button type='$buttonType' name='cancel' class='cancel_$name ui-button ui-priority-secondary' value='$name'>" ."<span class='ui-button-text'>" .wireIconMarkup('times-circle') . " " .$this->labels['cancel'] ."</span>" ."</button>";if($options['allowDelete'] && $modules->isDeleteable($name)) $buttons .="<button type='submit' name='delete' data-delete='$name' " ."class='delete_$name ui-state-default ui-priority-secondary ui-button' " ."value='$name' onclick=\"$confirmDeleteJS\">" ."<span class='ui-button-text'>" .wireIconMarkup('eraser') . " " .$this->_x('Delete', 'button') ."</span>" ."</button>";$editUrl = '#';} else if($configurable) {$flags = $modules->getFlags($name);if(!($flags & Modules::flagsNoUserConfig)) {$buttons .="<button type='button' class='ProcessModuleSettings ui-state-default ui-button'>" ."<span class='ui-button-text'>" .wireIconMarkup('cog') . " " .$this->_x('Settings', 'button') ."</span>" ."</button>"; // Text for 'Settings' button}}if($buttons) $buttons = "<small class='buttons'>$buttons</small>";if($options['allowDates']) {$summary .= "<span class='detail date'>";$summary .= $installed ? $this->labels['installed_date'] : $this->_('Found');$created = isset($newModules[$name]) ? $newModules[$name] : $info['created'];$summary .= ': ' . wireRelativeTimeStr($created) . "</span>";}$row = array($title => $editUrl,$version,$summary . $buttons,);$table->row($row);$total++;if(!isset($sectionsQty[$section])) $sectionsQty[$section] = 0;$sectionsQty[$section]++;}$out .= $table->render();if($options['allowSections']) {$out .= "</div>";$select = "<p><select name='modules_section$numCalls' class='modules_filter modules_section_select'>";$select .= "<option value=''>" . $this->_('Show All') . "</option>";$current = $input->cookie("modules_section$numCalls");foreach($sections as $section) {$qty = $sectionsQty[$section];$selected = $current == $section ? " selected='selected'" : "";$select .= "<option$selected value='$section'>$section ($qty)</option>";}$select .= "</select></p>";$out = $select . $out;}// modules that have no file or info present get removed from newModules$resetNewModules = false;foreach($newModules as $key => $newModule) {$info = $modules->getModuleInfoVerbose($newModule);if(!$info['file'] || !file_exists($info['file'])) {unset($newModules[$key]);$resetNewModules = true;}}// if any new modules were found, this also forces rewrite of session dataif($this->numFound) $resetNewModules = true;// rewrite session dataif($resetNewModules) $session->set($this, 'newModules', $newModules);return $out;}/*** Checks for compatibility, polls the modules directory web service and returns rendered markup for the download info table and confirmation form** @param string $name Class name of module* @param bool $update Whether this is a 'check for updates' request* @return string**/protected function downloadConfirm($name, $update = false) {$config = $this->wire()->config;$sanitizer = $this->wire()->sanitizer;$session = $this->wire()->session;$name = $sanitizer->name($name);$info = self::getModuleInfo();$this->headline($this->labels['download_install']);$this->breadcrumb('./', $info['title']);if($update) $this->breadcrumb("./?edit=$name", $name);$redirectURL = $update ? "./edit?name=$name" : "./";$className = $name;$url = trim($config->moduleServiceURL, '/') . "/$className/?apikey=" . $sanitizer->name($config->moduleServiceKey);$http = $this->wire(new WireHttp()); /** @var WireHttp $http */$data = $http->get($url);if(empty($data)) {$this->error($this->_('Error retrieving data from web service URL') . ' - ' . $http->getError());$session->redirect($redirectURL);return '';}$data = json_decode($data, true);if(empty($data)) {$this->error($this->_('Error decoding JSON from web service'));$session->redirect($redirectURL);return '';}if($data['status'] !== 'success') {$this->error($this->_('Error reported by web service:') . ' ' . $sanitizer->entities($data['error']));$session->redirect($redirectURL);return '';}$installable = true;foreach($data['categories'] as $category) {if(!in_array($category['name'], $this->uninstallableCategories)) continue;$this->error(sprintf($this->_('Sorry modules of type "%s" are not installable from the admin.'), $category['title']));$installable = false;}if(!$installable) $session->redirect($redirectURL);$form = $this->buildDownloadConfirmForm($data, $update);return $form->render();}/*** Builds a confirmation form and table showing information about the requested module before download** @param array $data Array of information about the module from the directory service* @param bool $update Whether or not this is an 'update module' request* @return InputfieldForm**/protected function ___buildDownloadConfirmForm(array $data, $update = false) {$sanitizer = $this->wire()->sanitizer;$modules = $this->wire()->modules;$config = $this->wire()->config;$session = $this->wire()->session;$warnings = array();$authors = '';foreach($data['authors'] as $author) $authors .= $author['title'] . ", ";$authors = rtrim($authors, ", ");$compat = '';$isCompat = false;$myVersion = substr($config->version, 0, 3);foreach($data['pw_versions'] as $v) {$compat .= $v['name'] . ", ";if(version_compare($v['name'], $myVersion) >= 0) $isCompat = true;}$compat = trim($compat, ", ");if(!$isCompat) $warnings[] = $this->_('This module does not indicate compatibility with this version of ProcessWire. It may still work, but you may want to check with the module author.');/** @var InputfieldForm $form */$form = $modules->get('InputfieldForm');$form->attr('action', './download/');$form->attr('method', 'post');$form->attr('id', 'ModuleInfo');/** @var InputfieldMarkup $markup */$markup = $modules->get('InputfieldMarkup');$markup->label = $data['title'];$markup->icon = 'info-circle';$form->add($markup);$installed = $modules->isInstalled($data['class_name']) ? $modules->getModuleInfoVerbose($data['class_name']) : null;$moduleVersionNote = '';if($installed) {$installedVersion = $this->formatVersion($installed['version']);if($installedVersion == $data['module_version']) {$note = $this->_('Current installed version is already up-to-date');$installedVersion .= ' - ' . $note;$this->message($note);$session->redirect("./edit?name=$data[class_name]");} else {if(version_compare($installedVersion, $data['module_version']) < 0) {$this->message($this->_('An update to this module is available!'));} else {$moduleVersionNote = " <span class='ui-state-error-text'>(" . $this->_('older than the one you already have installed!') . ")</span>";}}} else {$installedVersion = $this->_x('Not yet', 'install-table');}/** @var MarkupAdminDataTable $table */$table = $modules->get('MarkupAdminDataTable');$table->setEncodeEntities(false);$table->row(array($this->_x('Class', 'install-table'), $sanitizer->entities($data['class_name'])));$table->row(array($this->_x('Version', 'install-table'), $sanitizer->entities($data['module_version']) . $moduleVersionNote));$table->row(array($this->_x('Installed?', 'install-table'), $installedVersion));$table->row(array($this->_x('Authors', 'install-table'), $sanitizer->entities($authors)));$table->row(array($this->_x('Summary', 'install-table'), $sanitizer->entities($data['summary'])));$table->row(array($this->_x('Release State', 'install-table'), $sanitizer->entities($data['release_state']['title'])));$table->row(array($this->_x('Compatibility', 'install-table'), $sanitizer->entities($compat)));// $this->message("<pre>" . print_r($data, true) . "</pre>", Notice::allowMarkup);$installable = true;if(!empty($data['requires_versions'])) {$requiresVersions = array();foreach($data['requires_versions'] as $name => $requires) {list($op, $ver) = $requires;$label = $ver ? $sanitizer->entities("$name $op $ver") : $sanitizer->entities($name);if($modules->isInstalled("$name$op$ver") || in_array($name, $data['installs'])) {// installed$requiresVersions[] = "$label " . wireIconMarkup('thumbs-up', 'fw');} else if($modules->isInstalled($name)) {// installed, but version isn't adequate$installable = false;$info = $modules->getModuleInfo($name);$requiresVersions[] = $sanitizer->entities($name) . " " . $modules->formatVersion($info['version']) . " " ."<span class='ui-state-error-text'>" .$sanitizer->entities("$op $ver") . " " .wireIconMarkup('thumbs-down', 'fw') ."</span>";} else {// not installed at all$requiresVersions[] ="<span class='ui-state-error-text'>" ."$label " .wireIconMarkup('thumbs-down', 'fw') ."</span>";$installable = false;}}$table->row(array($this->labels['requires'], implode('<br />', $requiresVersions)));if(!$installable) $this->error("Module is not installable because not all required dependencies are currently met.");}if(!empty($data['installs'])) {$installs = $sanitizer->entities(implode("\n", $data['installs']));$table->row(array($this->labels['installs'], nl2br($installs)));}$links = array();$moduleName = $sanitizer->entities1($data['name']);$links[] = "<a target='_blank' href='https://processwire.com/modules/$moduleName/'>" . $this->_('More Information') . "</a>";if($data['project_url']) {$projectURL = $sanitizer->entities($data['project_url']);$links[] = "<a target='_blank' href='$projectURL'>" . $this->_('Project Page') . "</a>";}if($data['forum_url']) {$forumURL = $sanitizer->entities($data['forum_url']);$links[] = "<a target='_blank' href='$forumURL'>" . $this->_('Support Page') . "</a>";}if(count($links)) $table->row(array($this->_x('Links', 'install-table'), implode(' / ', $links)));if($data['download_url']) {$downloadURL = $sanitizer->entities($data['download_url']);$table->row(array($this->_x('ZIP file', 'install-table'), $downloadURL));$warnings[] = $this->_('Ensure that you trust the source of the ZIP file above before continuing!');} else {$warnings[] = $this->_('This module has no download URL specified and must be installed manually.');}if(!$this->installer()->canInstallFromDirectory(false)) {$installable = false;$markup->notes = trim($markup->notes . ' ' . $this->installer()->installDisabledLabel('directory'));}foreach($warnings as $warning) {$table->row(array($this->_x('Please Note', 'install-table'), "<strong class='ui-state-error-text'> $warning</strong>"));}$markup->value = $table->render();if($installable && $data['download_url']) {/** @var InputfieldSubmit $btn */$btn = $modules->get('InputfieldSubmit');$btn->attr('id+name', 'godownload');$btn->value = $this->labels['download_now'];$btn->icon = 'cloud-download';$btn->showInHeader(true);if($update) $btn->value .= " ($data[module_version])";$form->add($btn);$session->set('ProcessModuleDownloadURL', $data['download_url']);$session->set('ProcessModuleClassName', $data['class_name']);} else {$session->remove('ProcessModuleDownloadURL');$session->remove('ProcessModuleClassName');}/** @var InputfieldButton $btn */$btn = $modules->get('InputfieldButton');$btn->attr('name', 'cancel');$btn->href = $update ? "./edit?name=$data[class_name]" : './';$btn->value = $this->labels['cancel'];$btn->icon = 'times-circle';$btn->class .= ' ui-priority-secondary';$form->add($btn);return $form;}/*** Triggered on the /download/ action - Downloads a module from the directory** Most code lifted from Soma's Modules Manager** @return string Rendered output or redirect* @throws WireException**/public function ___executeDownload() {$session = $this->wire()->session;if(!$this->wire()->input->post('godownload')) {$this->message($this->_('Download cancelled'));$session->redirect('../');return '';}$session->CSRF->validate();$this->wire()->modules->refresh();$url = $session->get('ProcessModuleDownloadURL');$className = $session->get('ProcessModuleClassName');$session->remove('ProcessModuleDownloadURL');$session->remove('ProcessModuleClassName');if(!$url) throw new WireException("No download URL specified");if(!$className) throw new WireException("No class name specified");$destinationDir = $this->wire()->config->paths->siteModules . $className . '/';$completedDir = $this->installer()->downloadModule($url, $destinationDir);if($completedDir) {return $this->buildDownloadSuccessForm($className)->render();} else {$session->redirect('../');return '';}}/*** Build the form that gets displayed after a module has been successfully downloaded** @param string $className* @return InputfieldForm**/protected function ___buildDownloadSuccessForm($className) {$modules = $this->wire()->modules;/** @var InputfieldForm $form */$form = $modules->get('InputfieldForm');// check if modules isn't already installed and this isn't an updateif(!$modules->isInstalled($className)) {$info = $modules->getModuleInfoVerbose($className);$requires = array();if(count($info['requires'])) $requires = $modules->getRequiresForInstall($className);if(count($requires)) {foreach($requires as $moduleName) {$this->warning("$className - " . sprintf($this->_('Requires module "%s" before it can be installed'), $moduleName), Notice::allowMarkup);}$this->wire()->session->redirect('../');}$this->headline($this->_('Downloaded:') . ' ' . $className);$form->description = sprintf($this->_('%s is ready to install'), $className);$form->attr('action', '../');$form->attr('method', 'post');$form->attr('id', 'install_confirm_form');/** @var InputfieldHidden $f */$f = $modules->get('InputfieldHidden');$f->attr('name', 'install');$f->attr('value', $className);$form->add($f);/** @var InputfieldSubmit $submit */$submit = $modules->get('InputfieldSubmit');$submit->attr('name', 'submit');$submit->attr('id', 'install_now');$submit->attr('value', $this->_('Install Now'));$submit->icon = 'sign-in';$form->add($submit);/** @var InputfieldButton $button */$button = $modules->get('InputfieldButton');$button->attr('href', '../');$button->attr('value', $this->_('Leave Uninstalled'));$button->class .= " ui-priority-secondary";$button->icon = 'times-circle';$button->attr('id', 'no_install');$form->add($button);} else {$this->headline($this->_('Updated:') . ' ' . $className);$form->description = sprintf($this->_('%s was updated successfully.'), $className);/** @var InputfieldButton $button */$button = $modules->get('InputfieldButton');$button->attr('href', "../?reset=1&edit=$className");$button->attr('value', $this->_('Continue to module settings'));$button->attr('id', 'gosettings');$form->add($button);}return $form;}/**********************************************************************************************************************************************************/public function ___executeUpload($inputName = '') {if(!$inputName) throw new WireException("This URL may not be accessed directly");$this->installer()->uploadModule($inputName);$this->wire()->session->redirect('./?reset=1');}public function ___executeDownloadURL($url = '') {if(!$url) throw new WireException("This URL may not be accessed directly");$this->installer()->downloadModuleFromUrl($url);$this->wire()->session->redirect('./?reset=1');}/**********************************************************************************************************************************************************//*** Load the form for editing a module's settings**/public function ___executeEdit() {$input = $this->wire()->input;$session = $this->wire()->session;$modules = $this->wire()->modules;$sanitizer = $this->wire()->sanitizer;$moduleName = $input->post('name');if($moduleName === null) $moduleName = $input->get('name');$moduleName = $sanitizer->name($moduleName);$info = $moduleName ? $modules->getModuleInfoVerbose($moduleName) : array();if(!$moduleName || empty($info)) {$session->message($this->_("No module specified"));$session->redirect('./');}if($input->get('edit_raw')) return $this->renderEditRaw($moduleName);if($input->get('info_raw')) return $this->renderInfoRaw($moduleName, $info);return $this->renderEdit($moduleName, $info);}/*** View module info in raw/JSON mode** @param string $moduleName* @param array $moduleInfoVerbose* @return string**/protected function renderInfoRaw($moduleName, $moduleInfoVerbose) {$sanitizer = $this->wire()->sanitizer;$modules = $this->wire()->modules;if(!$this->wire()->user->isSuperuser()) throw new WirePermissionException('Superuser required');if(!$this->wire()->config->advanced) throw new WireException('This feature requires config.advanced=true;');$moduleInfo = $modules->getModuleInfo($moduleName);$sinfo = self::getModuleInfo();// reduce module info to remove empty runtime added propertiesforeach($moduleInfo as $key => $value) {if(isset($moduleInfoVerbose[$key]) && $moduleInfoVerbose[$key] !== $value) {unset($moduleInfo[$key]);continue;} else if(empty($value)) {if($value === "0" || $value === 0 || $value === false) continue;unset($moduleInfo[$key]);}}$this->headline(sprintf($this->_('%s module info'), $moduleName));$this->breadcrumb("./", $sinfo['title']);$this->breadcrumb("./edit?name=$moduleName", $moduleName);/** @var InputfieldForm $form */$form = $modules->get('InputfieldForm');$form->attr('id', 'ModuleInfoRawForm');$form->attr('action', "edit?name=$moduleName&info_raw=1");$form->attr('method', 'post');$jsonFlags = JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;$moduleInfoJSON = json_encode($moduleInfo, $jsonFlags);$moduleInfoVerboseJSON = json_encode($moduleInfoVerbose, $jsonFlags);$moduleInfoLabel = $this->_('Module info');/** @var InputfieldMarkup $f */$f = $modules->get('InputfieldMarkup');$f->attr('name', 'module_info');$f->label = $moduleInfoLabel . ' ' . $this->_('(regular)');$f->value = "<pre>" . $sanitizer->entities($moduleInfoJSON) . "</pre>";$f->icon = 'code';$f->themeOffset = 1;$form->add($f);/** @var InputfieldMarkup $f */$f = $modules->get('InputfieldMarkup');$f->attr('name', 'module_info_verbose');$f->label = $moduleInfoLabel . ' ' . $this->_('(verbose)');$f->icon = 'code';$f->value = "<pre>" . $sanitizer->entities($moduleInfoVerboseJSON) . "</pre>";$f->themeOffset = 1;$form->add($f);$form->prependMarkup ="<p class='description'>" .$this->_('This data comes from the module or is determined at runtime, so it is not editable here.') ."</p>";return $form->render();}/*** Edit module in raw/JSON mode** @param string $moduleName* @throws WireException* @throws WirePermissionException* @return string**/protected function renderEditRaw($moduleName) {$modules = $this->wire()->modules;$session = $this->wire()->session;$config = $this->wire()->config;$input = $this->wire()->input;$user = $this->wire()->user;if(!$user->isSuperuser()) throw new WirePermissionException('Superuser required');if(!$config->advanced) throw new WireException('This feature requires config.advanced=true;');$moduleData = $modules->getModuleConfigData($moduleName);$sinfo = self::getModuleInfo();$this->headline(sprintf($this->_('%s raw config data'), $moduleName));$this->breadcrumb("./", $sinfo['title']);$this->breadcrumb("./edit?name=$moduleName", $moduleName);/** @var InputfieldForm $form */$form = $modules->get('InputfieldForm');$form->attr('id', 'ModuleEditRawForm');$form->attr('action', "edit?name=$moduleName&edit_raw=1");$form->attr('method', 'post');if(empty($moduleData) && !$input->is('post')) $this->warning($this->_('This module has no configuration data'));$moduleData['_name'] = $moduleName . ' (' . $this->_('do not remove this') . ')';unset($moduleData['submit_save_module'], $moduleData['uninstall']);$jsonFlags = JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;$moduleDataJSON = is_array($moduleData) ? json_encode($moduleData, $jsonFlags) : array();$rows = substr_count($moduleDataJSON, "\n") + 2;/** @var InputfieldMarkup $f */$f = $modules->get('InputfieldTextarea');$f->attr('name', 'module_config_json');$f->label = $this->_('Module config (raw/JSON)');$f->icon = 'code';$f->value = $moduleDataJSON;$f->attr('style', 'font-family:monospace;white-space:nowrap');$f->attr('rows', $rows > 5 ? $rows : 5);$form->add($f);/** @var InputfieldSubmit $submit */$submit = $modules->get('InputfieldSubmit');$submit->attr('name', 'submit_save_module_config_json');$submit->showInHeader(true);$submit->val($this->_('Save'));$form->add($submit);if(!$input->post('submit_save_module_config_json')) return $form->render();$form->processInput($input->post);$json = $f->val();$data = json_decode($json, true);if($data === null) {$this->error($this->_('Cannot save because JSON could not be parsed (invalid JSON)'));return $form->render();}if(empty($data['_name']) || strpos($data['_name'], "$moduleName ") !== 0) {$this->error($this->_('Cannot save because JSON not recognized as valid for module'));return $form->render();}$changes = array();unset($data['_name'], $moduleData['_name']);foreach($moduleData as $key => $value) {if(!array_key_exists($key, $data) || $data[$key] !== $value) $changes[$key] = $key;}foreach($data as $key => $value) {if(!array_key_exists($key, $moduleData) || $moduleData[$key] !== $value) $changes[$key] = $key;}if(count($changes)) {$modules->saveModuleConfigData($moduleName, $data);$this->message($this->_('Updated module config data') . ' (' . implode(', ', $changes) . ')');} else {$this->message($this->_('No changes detected'));}$session->location($form->action);return '';}/*** Build and render for the form for editing a module's settings** This method saves the settings if it's form has been posted** @param string $moduleName* @param array $moduleInfo* @return string**/protected function renderEdit($moduleName, $moduleInfo) {$wire = $this->wire();$adminTheme = $wire->adminTheme;$languages = $wire->languages;$sanitizer = $wire->sanitizer;$modules = $wire->modules;$session = $wire->session;$config = $wire->config;$input = $wire->input;$out = '';$moduleId = $modules->getModuleID($moduleName);$submitSave = $input->post('submit_save_module');$collapseInfo = '';if($submitSave || $input->get('collapse_info') || $input->get('modal')) {$collapseInfo = '&collapse_info=1';}if(!$moduleId) {$this->error($this->_('Unknown module'));$session->redirect('./');return '';}if($input->get('refresh') == $moduleName) {$modules->refresh();$session->redirect("./edit?name=$moduleName$collapseInfo");return '';}$sinfo = self::getModuleInfo();$flags = $modules->getFlags($moduleName);$allowDisabledFlag =($config->debug && $config->advanced && ($flags & Modules::flagsAutoload)) ||($flags & Modules::flagsDisabled);$this->breadcrumb('./', $sinfo['title']);$this->headline($moduleInfo['title']);$this->browserTitle(sprintf($this->_('Module: %s'), $moduleInfo['title']));/** @var InputfieldForm $form */$form = $modules->get("InputfieldForm");$form->attr('id', 'ModuleEditForm');$form->attr('action', "edit?name=$moduleName$collapseInfo");$form->attr('method', 'post');$dependents = $modules->getRequiredBy($moduleName, true);$requirements = $modules->getRequires($moduleName, false, true);$dependentsStr = '';$requirementsStr = '';foreach($dependents as $name) {$dependentsStr .= ($dependentsStr ? ', ' : '') . "<a href='./edit?name=$name'>$name</a>";}foreach($requirements as $name) {if(preg_match('/^([^<>!=]+)([<>!=]+.*)$/', $name, $matches)) {$name = $matches[1];$extra = "<span class='detail'>$matches[2]</span>";} else $extra = '';if($name == 'PHP' || $name == 'ProcessWire') {$requirementsStr .= ($requirementsStr ? ', ' : '') . "$name$extra";} else {$requirementsStr .= ($requirementsStr ? ', ' : '') . "<a href='./edit?name=$name'>$name</a>$extra";}}// identify duplicates$duplicates = $modules->duplicates()->getDuplicates($moduleName);if(count($duplicates['files'])) {/** @var InputfieldRadios $field */$field = $modules->get('InputfieldRadios');$field->attr('name', '_use_duplicate');$field->label = $this->_('Module file to use');$field->icon = 'files-o';$field->description = $this->_('There are multiple copies of this module. Select the module file you want to use.');foreach($duplicates['files'] as $file) {$field->addOption($file);}$field->attr('value', $duplicates['using']);$form->add($field);}$fields = $modules->getModuleConfigInputfields($moduleName, $form);if($fields) {foreach($fields as $field) {$form->add($field);}}$filename = $modules->getModuleFile($moduleName, array('guess' => true, 'fast' => false));$filenameUrl = str_replace($config->paths->root, $config->urls->root, $filename);$filenameExists = file_exists($filename);$filenameNote = '';if($filenameExists) {// Uninstall checkbox/** @var InputfieldCheckbox $field Uninstall checkbox */$field = $modules->get("InputfieldCheckbox");$field->attr('id+name', 'uninstall');$field->attr('value', $moduleName);$field->collapsed = Inputfield::collapsedYes;$field->icon = 'times-circle';$field->label = $this->_x("Uninstall", 'checkbox');$reason = $modules->isUninstallable($moduleName, true);$uninstallable = $reason === true;if($uninstallable) {$field->description = $this->_("Uninstall this module? After uninstalling, you may remove the modules files from the server if it is not in use by any other modules."); // Uninstall field descriptionif(count($moduleInfo['installs'])) {$uninstalls = $modules->getUninstalls($moduleName);if(count($uninstalls)) {$field->notes = $this->_("This will also uninstall other modules") . " - " . implode(', ', $uninstalls); // Text that precedes a list of modules that are also uninstalled}}} else {$field->attr('disabled', 'disabled');$field->label .= " " . $this->_("(Disabled)");$field->description = $this->_("Can't uninstall module") . " - " . $reason; // Text that precedes a reason why the module can't be uninstalled$dependents2 = $modules->getRequiresForUninstall($moduleName);if(count($dependents2)) {$field->notes = $this->_("You must first uninstall other modules") . " - " . implode(', ', $dependents2); // Text that precedes a list of modules that must be uninstalled first}}$form->add($field);} else {// Delete from datasbase checkbox$uninstallable = false;$filenameUrl = dirname($filenameUrl) . '/';$filenameNote ="<div class='ui-state-error-text'>" .wireIconMarkup('warning') . ' ' .$this->_('module file not found') ."</div>";$warning =sprintf($this->_('Module “%s” exists in database but not on the file system.'), $moduleName) . ' ' .sprintf($this->_('Consider placing the module files in %s or removing the module from the database.'), $filenameUrl);$moduleInfo['summary'] = $warning;if(!$input->requestMethod('POST')) $this->warning($warning, Notice::allowMarkup | Notice::noGroup);/** @var InputfieldCheckbox $field */$field = $modules->get('InputfieldCheckbox');$field->attr('name', 'remove_db');$field->label = $this->_('Remove this module from the database?');$field->value = $moduleName;$form->add($field);}// submit buttonif(count($form->children)) {/** @var InputfieldSubmit $field */$field = $modules->get("InputfieldSubmit");$field->attr('name', 'submit_save_module');$field->showInHeader();$field->addActionValue('exit', sprintf($this->_('%s + Exit'), $field->attr('value')), 'times');$form->append($field);} else {$this->message($this->_("This module doesn't have any fields to configure"));}if($languages && $fields) {// multi-language support for Inputfield with useLanguages==true// we populate the language values from module config data so module doesn't have to do this$data = $modules->getModuleConfigData($moduleName);foreach($fields->getAll() as $field) {if(!$field->getSetting('useLanguages')) continue;foreach($languages as $language) {if($language->isDefault()) continue;$name = $field->name . '__' . $language->id;if(!isset($data[$name])) continue;$field->set("value$language->id", $data[$name]);}}} else {$data = null;}// check for submitted formif($submitSave) {if(is_null($data)) $data = $modules->getModuleConfigData($moduleName);$form->processInput($input->post);$updatedNames = array();if(wireCount($fields)) foreach($fields->getAll() as $field) {// note field names beginning with '_' will not be storedif(($name = $field->attr('name')) && strpos($name, '_') !== 0) {if($name === 'submit_save_module') continue;$value = $field->attr('value');if(!isset($data[$name]) || $value != $data[$name]) $updatedNames[] = $name;$data[$name] = $value;// multi-language, if Inputfield specifies useLanguages==true// convert value1234 inputfield values to module config data in name__1234 formatif($languages && $field->getSetting('useLanguages')) {$_name = $name;foreach($languages as $language) {if($language->isDefault()) continue;$name = $_name . "__" . $language->id;$value = $field->get("value$language->id");if(!isset($data[$name]) || $value != $data[$name]) $updatedNames[] = $name;$data[$name] = $value;}}}}if($uninstallable && $input->post('uninstall') === $moduleName) {$modules->uninstall($moduleName);$session->message($this->_("Uninstalled Module") . " - $moduleName"); // Message shown before the name of a module that was just uninstalled$redirectURL = './?uninstalled=1';} else if(!$filenameExists && $input->post('remove_db') === $moduleName) {$modules->removeModuleEntry($moduleName);$session->message($this->_('Removed module from database') . " - $moduleName");$redirectURL = './?deleted=1';} else {unset($data['submit_save_module'], $data['uninstall']);$modules->saveModuleConfigData($moduleName, $data);$updatedNames = count($updatedNames) ? ' (' . implode(', ', $updatedNames) . ')' : '';$this->message($this->_("Saved Module") . " - $moduleName $updatedNames"); // Message shown before the name of a module that was just saved$redirectURL = $submitSave === 'exit' ? './' : "./edit?name=$moduleName$collapseInfo";if($allowDisabledFlag) {// module is autoload and has an option to diableif($input->post('_flags_disabled')) {// add disabled flagif(!($flags & Modules::flagsDisabled)) $modules->setFlag($moduleName, Modules::flagsDisabled, true);} else {// remove disabled flagif($flags & Modules::flagsDisabled) $modules->setFlag($moduleName, Modules::flagsDisabled, false);}}if(count($duplicates['files'])) {$file = $form->getChildByName('_use_duplicate')->attr('value');if($file != $duplicates['using'] && in_array($file, $duplicates['files'])) {$modules->duplicates()->setUseDuplicate($moduleName, $file);$this->message(sprintf($this->_('Updated module %1$s to use file: %2$s'), $moduleName, $file), Notice::debug);$redirectURL .= "&refresh=$moduleName";}}}$session->redirect($redirectURL);}// entity encode module info since it's turned off in our tableforeach($moduleInfo as $key => $value) {if(!is_string($value)) continue;$moduleInfo[$key] = $sanitizer->entities1($value);}$version = $this->formatVersion($moduleInfo['version']);if(!$moduleInfo['core']) {$version .= " - <a href='./?update=$moduleName'>" . $this->_('check for updates') . "</a>";}$hooksStr = $this->renderModuleHooks($moduleName);/** @var MarkupAdminDataTable $table Build a table that displays module info */$table = $modules->get("MarkupAdminDataTable");$table->setResponsive(false);$table->setEncodeEntities(false);$table->setSortable(false);$table->row(array($this->_x('Title', 'edit'), $sanitizer->entities($moduleInfo['title'])));$table->row(array($this->_x('Class', 'edit'), $sanitizer->entities($moduleName)));$table->row(array($this->_x('File', 'edit'),str_replace('/',// this sillyness allows for multi-line wrapping without the appearance of space.// someone please tell me if there is a better way to do this. I suspect there is, so// will leave it out here in the open rather hide it in ProcessModule.css'<span style="display:inline-block;width:1px;overflow:hidden;"> </span>/',(substr($filenameUrl, -1) === '/' ? "$filenameUrl…" : $filenameUrl)) . $filenameNote));if($config->debug) {if($moduleInfo['namespace'] === '') {$namespace = "\\" . __NAMESPACE__ . ' ' . $this->_('(default namespace)');} else if($moduleInfo['namespace'] === "\\") {$namespace = $this->_('None (root namespace)');} else {$namespace = $moduleInfo['namespace'];}if(!empty($namespace)) {$table->row(array($this->_x('Namespace', 'edit'), $namespace));}$table->row(array($this->_x('ID', 'edit'), $moduleId));}if(!empty($moduleInfo['version'])) {$table->row(array($this->_x('Version', 'edit'), $version));}if(!empty($moduleInfo['created'])) {$table->row(array($this->labels['installed_date'], $sanitizer->entities(wireRelativeTimeStr($moduleInfo['created']))));}if(!empty($moduleInfo['author'])) {$table->row(array($this->_x('Author', 'edit'), $moduleInfo['author']));}if(!empty($moduleInfo['summary'])) {$table->row(array($this->_x('Summary', 'edit'), $moduleInfo['summary']));}if($requirementsStr) {$table->row(array($this->_x('Requires', 'edit'), $requirementsStr));}if($dependentsStr) {$table->row(array($this->_x('Required By', 'edit'), $dependentsStr));}if(!empty($moduleInfo['permission'])) {$table->row(array($this->_x('Required Permission', 'edit'), $moduleInfo['permission']));}if($hooksStr) {$table->row(array($this->_x('Hooks To', 'edit'), $hooksStr));}$languageFiles = $languages ? $modules->getModuleLanguageFiles($moduleName) : array();if(count($languageFiles)) {$languages = wireIconMarkup('language') . ' ' . $sanitizer->entities(implode(', ', array_keys($languageFiles)));$languages .= " - <a href='{$config->urls->admin}module/translation/?name=$moduleName'>" . $this->_('install translations') . "</a>";$table->row(array($this->_x('Languages', 'edit'), $languages));}if(!empty($moduleInfo['href'])) {$table->row(array($this->_x('More Information', 'edit'), "<a target='_blank' class='label' href='$moduleInfo[href]'>$moduleInfo[href]</a>"));}if($allowDisabledFlag) {$checkboxClass = $adminTheme ? $adminTheme->getClass('input-checkbox') : '';$checked = ($flags & Modules::flagsDisabled ? " checked='checked'" : "");$table->row(array('* ' . $this->_x('Debug', 'edit'),"<label class='checkbox'>" ."<input class='$checkboxClass' type='checkbox' name='_flags_disabled' value='1' $checked /> " .$this->_('Autoload disabled?') . ' ' ."<span class='detail'>" .$this->_('Be careful, checking this box can break the module or your site. Use for temporary testing only.') ."</span>" ."</label>"));}if($config->advanced && $this->wire()->user->isSuperuser()) {$table->row(array('* ' . $this->_x('Advanced', 'edit'),"<a href='./edit?name=$moduleName&edit_raw=1'>" . wireIconMarkup('pencil') . ' ' . $this->_('Raw config') . "</a> " ."<a href='./edit?name=$moduleName&info_raw=1'>" . wireIconMarkup('info-circle') . ' ' . $this->_('Raw info') . "</a>"));}/** @var InputfieldMarkup $field */$field = $modules->get("InputfieldMarkup");$field->attr('id', 'ModuleInfo');$field->attr('value', $table->render());$field->label = $this->labels['module_information'];$field->icon = 'info-circle';if($config->advanced) {$field->appendMarkup .= "<p class='detail' style='text-align:right'>* " . $this->_('Options available in advanced mode only.') . "</p>";}if($collapseInfo) $field->collapsed = Inputfield::collapsedYes;$form->prepend($field);$out .= $form->render();return $out;}protected function renderModuleHooks($moduleName) {$out = '';$hooks = array_merge($this->wire()->getHooks('*'), $this->wire()->hooks->getAllLocalHooks());foreach($hooks as $hook) {$toObject = !empty($hook['toObject']) ? $hook['toObject'] : '';if(empty($toObject) || wireClassName($toObject, false) != $moduleName) continue;$suffix = $hook['options']['type'] == 'method' ? '()' : '';$when = '';if($hook['options']['before']) $when .= $this->_('before');if($hook['options']['after']) $when .= ($when ? '+' : '') . $this->_('after');if($when) $when .= ".";if($out) $when = ", $when";$when = "<span class='detail'>$when</span>";$out .= "$when" . ($hook['options']['fromClass'] ? $hook['options']['fromClass'] . '::' : '') . "$hook[method]$suffix";}return $out;}public function ___executeInstallConfirm() {$name = $this->wire()->input->get->name('name');if(!$name) throw new WireException("No module name specified");if(!$this->wire()->modules->isInstallable($name, true)) throw new WireException("Module is not currently installable");$this->headline($this->labels['install']);/** @var InputfieldForm $form */$form = $this->modules->get('InputfieldForm');$form->attr('action', './');$form->attr('method', 'post');$form->attr('id', 'modules_install_confirm_form');$form->addClass('ModulesList');$form->description = sprintf($this->_('Install %s?'), $name);$modulesArray[$name] = (int) $this->modules->isInstalled($name);/** @var InputfieldMarkup $markup */$markup = $this->modules->get('InputfieldMarkup');$markup->value = $this->renderListTable($modulesArray, array('allowSections' => false,'allowClasses' => true,'allowType' => true,));$form->add($markup);return $form->render();}/*** Languages translations import** @return string* @since 3.0.181**/public function ___executeTranslation() {$languages = $this->wire()->languages;$modules = $this->wire()->modules;$session = $this->wire()->session;$input = $this->wire()->input;$config = $this->wire()->config;$moduleName = $input->get->name('name');if(empty($moduleName)) throw new WireException('No module name specified');if(!$modules->isInstalled($moduleName)) throw new WireException("Unknown module: $moduleName");$moduleEditUrl = $modules->getModuleEditUrl($moduleName);$languageFiles = $modules->getModuleLanguageFiles($moduleName);if(!$languages || !count($languageFiles)){$session->message($this->_('No module language files available'));$session->location($moduleEditUrl);}$this->headline($this->_('Module language translations'));$this->breadcrumb($config->urls->admin . 'modules/', $this->labels['modules']);$this->breadcrumb($moduleEditUrl, $moduleName);/** @var InputfieldForm $form */$form = $modules->get('InputfieldForm');$form->attr('id', 'ModuleImportTranslationForm');$form->attr('action', $config->urls->admin . "module/translation/?name=$moduleName");$form->attr('method', 'post');$form->description = sprintf($this->_('Import translations for module %s'), $moduleName);foreach($languages as $language) {/** @var InputfieldSelect $lang */$langLabel = $language->get('title');$langLabel .= $langLabel ? " ($language->name)" : $language->name;/** @var InputfieldSelect $f */$f = $modules->get('InputfieldSelect');$f->attr('name', "language_$language->name");$f->label = sprintf($this->_('Import into %s'), $langLabel);$f->addOption('');foreach($languageFiles as $basename => $filename) {$f->addOption($basename);}$form->append($f);}if($input->post('submit_import_translations')) {foreach($languages as $language) {$basename = $input->post->pageName("language_$language->name");if(empty($basename)) continue;if(empty($languageFiles[$basename])) continue;$file = $languageFiles[$basename];if(!is_file($file)) {$session->error($this->_('Cannot find CSV file') . " - " . basename($file));continue;}$languages->importTranslationsFile($language, $file);}$session->location($moduleEditUrl);}/** @var InputfieldSubmit $f */$f = $modules->get('InputfieldSubmit');$f->attr('name', 'submit_import_translations');$f->showInHeader(true);$form->add($f);return $form->render();}/*** URL to redirect to after non-authenticated user is logged-in, or false if module does not support** When supported, module should gather any input GET vars and URL segments that it recognizes,* sanitize them, and return a URL for that request. ProcessLogin will redirect to the returned URL* after user has successfully authenticated.** If module does not support this, or only needs to support an integer 'id' GET var, then this* method can return false.** @param Page $page* @return bool|string* @sine 3.0.167**/public static function getAfterLoginUrl(Page $page) {$url = $page->url();$action = $page->wire()->input->urlSegmentStr;$name = $page->wire()->input->get->fieldName('name');if($action === 'edit' && $name && $page->wire()->modules->isInstalled($name)) {$url .= "edit?name=$name";$collapse = (int) $page->wire()->input->get('collapse_info');if($collapse) $url .= "&collapse_info=$collapse";}return $url;}}