__('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() { 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'); } } /** * @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 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->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 "$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->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 .= "" . 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('
" . $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->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 ? ', ' : '') . "$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); } $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 = "* " . $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; } }