__('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', 'InputfieldTinyMCE', '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() { parent::__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 button require_once(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'); } /** @var JqueryUI $jQueryUI */ $jQueryUI = $this->wire()->modules->getModule('JqueryUI'); if($jQueryUI) $jQueryUI->use('modal'); } /** * @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->flags->getFlags($moduleName); if($flags & Modules::flagsNoUserConfig) continue; } if($install) { // exclude already installed modules if($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; $config = $this->wire()->config; 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($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) . ' ' . "" . $this->_('Click here to change which file is used') . "
";
				foreach($dup['files'] as $file) {
					if($dup['using'] == $file) $file = "$file";
					$msg .= "\n$file";
				}
				$this->message("$msg
", 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'] || 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->flags->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)) . "

" . wireIconMarkup('star', 'fw') . " " . sprintf($this->_('Browse the modules directory at %s'), "processwire.com/modules") . "

" . "

" . 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 "

" . "

" . 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.') . "

" . "

" . $button->render() . "

"; $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 /** @var InputfieldWrapper $tab */ $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 /** @var InputfieldWrapper $tab */ $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'])) . '/' ); } /** @var InputfieldWrapper $tab */ $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 = ""; /** @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->_('https://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.'), "$moduleNameDir") . ' * ', 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 = '

' . implode('

', $instructions) . '

'; $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 "
" . $this->_('No modules found.') . "
"; 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)) { $example = $sanitizer->alpha($sanitizer->fieldName($name)); $example = strtoupper(substr($example, 0, 1)) . strtolower(substr($example, 1, 1)) . substr($example, 2); $this->error( sprintf($this->_('Invalid module name: “%s”'), $name) . ' ' . $this->_('Please use uppercase [A-Z] for first character, lowercase [a-z] for 2nd character, and [a-z A-Z 0-9] for the rest.') . ' ' . sprintf($this->_('For example: “%s”'), $example) ); continue; } if($options['allowSections'] || is_null($table)) { $section = $matches[1]; $sections[] = $section; if($table) $out .= $table->render() . ""; /** @var MarkupAdminDataTable $table */ $table = $modules->get("MarkupAdminDataTable"); $table->setEncodeEntities(false); $table->headerRow($tableHeader); if($options['allowSections']) $out .= "\n

$section

"; } } $info = $modules->getModuleInfoVerbose($name); $configurable = $info['configurable']; $title = !empty($info['title']) ? $sanitizer->entities1($info['title']) : substr($name, strlen($section)); $title = "$title"; if($options['allowClasses']) $title .= "
$name"; if($info['icon']) $title = wireIconMarkup($info['icon'], 'fw') . " $title"; $class = $configurable ? 'ConfigurableModule' : ''; if(!empty($info['permanent'])) $class .= ($class ? ' ' : '') . 'PermanentModule'; if($class) $title = "$title"; $version = empty($info['version']) ? '?' : $this->formatVersion($info['version']); if($options['allowType']) $version .= "
" . ($info['core'] ? $this->labels['core'] : $this->labels['site']) . ""; 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']) ? '' : (" " . $this->_('more') . ""); if($summary) $summary = "

$summary

"; $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] = "$value"; } $summary .= "" . $this->labels['requires'] . " - " . implode(', ', $requires) . ""; } } else $requires = array(); $nsClassName = $modules->getModuleClass($name, true); if(!wireInstanceOf($nsClassName, 'Module')) { $summary .= "" . $this->_('Module class must implement the “ProcessWire\Module” interface.') . ""; $requires[] = 'Module interface'; } if(count($info['installs'])) { $summary .= "" . $this->labels['installs'] . " - " . implode(', ', $info['installs']) . ""; } $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 = "$title"; $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 .= ""; // install confirm, needs a cancel button if($isConfirm) $buttons .= ""; if($options['allowDelete'] && $modules->isDeleteable($name)) $buttons .= ""; $editUrl = '#'; } else if($configurable) { $flags = $modules->flags->getFlags($name); if(!($flags & Modules::flagsNoUserConfig)) { $buttons .= ""; // Text for 'Settings' button } } if($buttons) $buttons = "$buttons"; if($options['allowDates']) { $summary .= ""; $summary .= $installed ? $this->labels['installed_date'] : $this->_('Found'); $created = isset($newModules[$name]) ? $newModules[$name] : $info['created']; $summary .= ': ' . wireRelativeTimeStr($created) . ""; } $row = array( $title => $editUrl, $version, $summary . $buttons, ); $table->row($row); $total++; if(!isset($sectionsQty[$section])) $sectionsQty[$section] = 0; $sectionsQty[$section]++; } $out .= $table ? $table->render() : ''; if($options['allowSections']) { $out .= "
"; $select = "

"; $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 data if($this->numFound) $resetNewModules = true; // rewrite session data if($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 = " (" . $this->_('older than the one you already have installed!') . ")"; } } } 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("
" . print_r($data, true) . "
", 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']) . " " . "" . $sanitizer->entities("$op $ver") . " " . wireIconMarkup('thumbs-down', 'fw') . ""; } else { // not installed at all $requiresVersions[] = "" . "$label " . wireIconMarkup('thumbs-down', 'fw') . ""; $installable = false; } } $table->row(array($this->labels['requires'], implode('
', $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[] = "" . $this->_('More Information') . ""; if($data['project_url']) { $projectURL = $sanitizer->entities($data['project_url']); $links[] = "" . $this->_('Project Page') . ""; } if($data['forum_url']) { $forumURL = $sanitizer->entities($data['forum_url']); $links[] = "" . $this->_('Support Page') . ""; } 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'), " $warning")); } $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()->downloadModuleFromDirectory($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 update if(!$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 properties foreach($moduleInfo as $key => $value) { if(isset($moduleInfoVerbose[$key]) && $moduleInfoVerbose[$key] !== $value) { unset($moduleInfo[$key]); } 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 = "
" . $sanitizer->entities($moduleInfoJSON) . "
"; $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 = "
" . $sanitizer->entities($moduleInfoVerboseJSON) . "
"; $f->themeOffset = 1; $form->add($f); $form->prependMarkup = "

" . $this->_('This data comes from the module or is determined at runtime, so it is not editable here.') . "

"; 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->getConfig($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->flags->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 ? ', ' : '') . "$name"; } foreach($requirements as $name) { if(preg_match('/^([^<>!=]+)([<>!=]+.*)$/', $name, $matches)) { $name = $matches[1]; $extra = "$matches[2]"; } else $extra = ''; if($name == 'PHP' || $name == 'ProcessWire') { $requirementsStr .= ($requirementsStr ? ', ' : '') . "$name$extra"; } else { $requirementsStr .= ($requirementsStr ? ', ' : '') . "$name$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); } if($input->get('upgrade')) { // force it to do an upgrade check $this->wire()->modules->getModule($moduleName, array('configOnly' => true)); } $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 description if(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 = "
" . wireIconMarkup('warning') . ' ' . $this->_('module file not found') . "
"; $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 button if(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->getConfig($moduleName); foreach($fields->getAll() as $field) { if(!$field->getSetting('useLanguages')) continue; foreach($languages as $language) { /** @var Language $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 form if($submitSave) { if(is_null($data)) $data = $modules->getConfig($moduleName); $form->processInput($input->post); $updatedNames = array(); if(wireCount($fields)) foreach($fields->getAll() as $field) { // note field names beginning with '_' will not be stored if(($name = $field->attr('name')) && strpos($name, '_') !== 0) { if($name === 'submit_save_module' || $name === 'uninstall') 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 format if($languages && $field->getSetting('useLanguages')) { $_name = $name; foreach($languages as $language) { /** @var Language $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 diable if($input->post('_flags_disabled')) { // add disabled flag if(!($flags & Modules::flagsDisabled)) $modules->flags->setFlag($moduleName, Modules::flagsDisabled, true); } else { // remove disabled flag if($flags & Modules::flagsDisabled) $modules->flags->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 table foreach($moduleInfo as $key => $value) { if(!is_string($value)) continue; $moduleInfo[$key] = $sanitizer->entities1($value); } $version = $this->formatVersion($moduleInfo['version']); if(!$moduleInfo['core']) { $version .= " - " . $this->_('check for updates') . ""; } $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 ' /', (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 .= " - " . $this->_('install translations') . ""; $table->row(array($this->_x('Languages', 'edit'), $languages)); } if(!empty($moduleInfo['href'])) { $table->row(array($this->_x('More Information', 'edit'), "$moduleInfo[href]")); } if($allowDisabledFlag) { $checkboxClass = $adminTheme ? $adminTheme->getClass('input-checkbox') : ''; $checked = ($flags & Modules::flagsDisabled ? " checked='checked'" : ""); $table->row(array('* ' . $this->_x('Debug', 'edit'), "" )); } if($config->advanced && $this->wire()->user->isSuperuser()) { $table->row(array( '* ' . $this->_x('Advanced', 'edit'), "" . wireIconMarkup('pencil') . ' ' . $this->_('Raw config') . "     " . "" . wireIconMarkup('info-circle') . ' ' . $this->_('Raw info') . "" )); } /** @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 .= "

* " . $this->_('Options available in advanced mode only.') . "

"; } 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 = "$when"; $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 Language $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); } /** @var InputfieldSubmit $f */ $f = $modules->get('InputfieldSubmit'); $f->attr('name', 'submit_import_translations'); $f->showInHeader(true); $form->add($f); if($form->isSubmitted('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); } 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 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; } }