__('Modules', __FILE__), // getModuleInfo title 'summary' => __('List, edit or install/uninstall modules', __FILE__), // getModuleInfo summary 'version' => 117, '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' => '?reset=1', 'label' => 'Refresh', 'icon' => 'refresh', ) ) ); } 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; public function __construct() { $this->labels['download'] = $this->_('Download'); if($this->input->get->update) { $this->labels['download_install'] = $this->_('Download and Update'); } else { $this->labels['download_install'] = $this->_('Download and Install'); } $this->labels['download_dir'] = $this->_('Add Module From Directory'); $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 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']; } require(dirname(__FILE__) . '/ProcessModuleInstall.php'); } /** * 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 * */ public function ___executeNavJSON(array $options = array()) { $page = $this->wire('page'); $data = array( 'url' => $page->url, 'label' => (string) $page->get('title|name'), 'icon' => 'plug', 'list' => array(), ); $site = $this->wire('input')->get('site'); $core = $this->wire('input')->get('core'); $configurable = $this->wire('input')->get('configurable'); $install = $this->wire('input')->get('install'); if($site || $install) $data['add'] = array( 'url' => "?new#tab_new_modules", 'label' => __('Add New', '/wire/templates-admin/default.php'), ); $modules = $this->wire('modules'); $moduleNames = array(); if($install) { $moduleNames = array_keys($modules->getInstallable()); } else { foreach($modules as $module) $moduleNames[] = $module->className(); } sort($moduleNames); foreach($moduleNames as $moduleName) { $info = $this->wire('modules')->getModuleInfoVerbose($moduleName); if($site && $info['core']) continue; if($core && !$info['core']) continue; if($configurable && (!$info['configurable'] || !$info['installed'])) continue; if($install) { // exclude already installed modules if($info['installed']) continue; // check that it can be installed NOW (i.e. all dependencies met) if(!$this->wire('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]"; $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() { foreach($this->modules as $module) { $this->modulesArray[$module->className()] = 1; } foreach($this->modules->getInstallable() as $module) { $this->modulesArray[basename($module, '.module')] = 0; } ksort($this->modulesArray); if($this->input->post->install) { $this->session->CSRF->validate(); $name = $this->input->post->install; if($name && isset($this->modulesArray[$name]) && !$this->modulesArray[$name]) { $module = $this->modules->get($name); $this->modulesArray[$name] = 1; $this->session->message($this->_("Module Install") . " - " . $module->className); // Message that precedes the name of the module installed $this->session->redirect("edit?name={$module->className}"); } } if($this->input->post->delete) { $this->session->CSRF->validate(); $name = $this->input->post->delete; if($name && isset($this->modulesArray[$name])) { $info = $this->modules->getModuleInfoVerbose($name); try { $this->modules->delete($name); $this->message($this->_('Deleted module files') . ' - ' . $info['title']); } catch(WireException $e) { $this->error($e->getMessage()); } $this->session->redirect("./"); } } if($this->input->post->download && $this->input->post->download_name) { $this->session->CSRF->validate(); return $this->downloadConfirm($this->input->post->download_name); } else if($this->input->get->download_name) { return $this->downloadConfirm($this->input->get->download_name); } if($this->input->post->upload) { $this->session->CSRF->validate(); $this->executeUpload('upload_module'); } if($this->input->post->download_zip && $this->input->post->download_zip_url) { $this->session->CSRF->validate(); $this->executeDownloadURL($this->input->post->download_zip_url); } if($this->input->get->update) { $name = $this->sanitizer->name($this->input->get->update); if(isset($this->modulesArray[$name])) return $this->downloadConfirm($name, true); } if($this->input->get->reset == 1) { $this->modules->resetCache(); $edit = $this->input->get->edit; if($edit) $this->session->redirect("./edit?name=" . $this->sanitizer->fieldName($edit) . "&reset=2"); else $this->session->redirect("./?reset=2"); } return $this->renderList(); } /** * Render a list of all modules * */ protected function renderList() { $modulesArray = $this->modulesArray; $installedArray = array(); $uninstalledArray = array(); $configurableArray = array(); $uninstalledNames = array(); $siteModulesArray = array(); $coreModulesArray = array(); $newModulesArray = array(); // 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 $newCoreModules = array( 'InputfieldCKEditor', 'TextformatterImgQA', ); if($this->wire('input')->post('new_seconds')) { $this->wire('session')->set('ProcessModuleNewSeconds', (int) $this->wire('input')->post('new_seconds')); } $newSeconds = (int) $this->wire('session')->get('ProcessModuleNewSeconds'); if(!$newSeconds) $newSeconds = 86400; foreach($modulesArray as $name => $installed) { if($installed) { $installedArray[$name] = $installed; $errors = $this->modules->getDependencyErrors($name); if($errors) foreach($errors as $error) $this->error($error); } else { $uninstalledNames[] = $name; $uninstalledArray[$name] = $installed; } $info = $this->modules->getModuleInfoVerbose($name); $isNew = !$info['core'] || ($info['core'] && in_array($name, $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']) $configurableArray[$name] = $installed; } $form = $this->modules->get('InputfieldForm'); $form->attr('action', './'); $form->attr('method', 'post'); $form->attr('enctype', 'multipart/form-data'); $form->attr('id', 'modules_form'); $this->modules->get('JqueryWireTabs'); // site $tab = new InputfieldWrapper(); $tab->attr('id', 'tab_site_modules'); $tab->attr('title', $this->labels['site']); $tab->attr('class', 'WireTab'); $markup = $this->modules->get('InputfieldMarkup'); $markup->label = $this->_('/site/modules/ - Modules specific to your site'); $markup->icon = 'folder-open-o'; $markup->value .= $this->renderListTable($siteModulesArray, true); $markup->value .= "

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

"; $markup->value .= "

" . $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 'check for new modules' button for ProcessWire to see the change."); // Instructions on how to remove a module $tab->add($markup); $form->add($tab); // core $tab = new InputfieldWrapper(); $tab->attr('id', 'tab_core_modules'); $tab->attr('title', $this->labels['core']); $tab->attr('class', 'WireTab'); $markup = $this->modules->get('InputfieldMarkup'); $markup->value = $this->renderListTable($coreModulesArray); $markup->label = $this->_('/wire/modules/ - Modules included with the ProcessWire core'); $markup->icon = 'folder-open-o'; $tab->add($markup); $form->add($tab); // configurable $tab = new InputfieldWrapper(); $tab->attr('id', 'tab_configurable_modules'); $tab->attr('title', $this->labels['configure']); $tab->attr('class', 'WireTab'); $markup = $this->modules->get('InputfieldMarkup'); $markup->value = $this->renderListTable($configurableArray, true, true, false, false, true); $markup->label = $this->_('Modules that have configuration options'); $markup->icon = 'folder-open-o'; $tab->add($markup); $form->add($tab); // installable $tab = new InputfieldWrapper(); $tab->attr('id', 'tab_uninstalled_modules'); $tabLabel = $this->labels['install']; $tab->attr('title', $tabLabel); $tab->attr('class', 'WireTab'); $markup = $this->modules->get('InputfieldMarkup'); $markup->value = $this->renderListTable($uninstalledArray, true, true, false, false, 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); // new $tab = new InputfieldWrapper(); $tab->attr('id', 'tab_new_modules'); $tab->attr('title', $this->_('New')); $tab->attr('class', 'WireTab'); if($this->wire('session')->ProcessModuleNewModules) { foreach($this->wire('session')->ProcessModuleNewModules as $name => $created) { if(!is_numeric($name) && !isset($newModulesArray[$name])) $newModulesArray[$name] = 0; } } $select = $this->wire('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); $markup = $this->modules->get('InputfieldMarkup'); $markup->icon = 'lightbulb-o'; $markup->value = $select->render() . $this->renderListTable($newModulesArray, false, false, true, true); $markup->label = $this->_('Recently Found and Installed Modules'); $tab->add($markup); $fieldset = $this->modules->get('InputfieldFieldset'); $fieldset->label = $this->labels['download_dir']; $fieldset->icon = 'cloud-download'; //if($this->wire('input')->post('new_seconds')) $fieldset->collapsed = Inputfield::collapsedYes; $f = $this->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. Type or paste in the "class name" for the module you want to install.'); $f->notes = $this->_('The modules directory is located at [modules.processwire.com](http://modules.processwire.com)'); $f->attr('placeholder', $this->_('ModuleClassName')); // placeholder $f->required = false; $fieldset->add($f); $f = $this->modules->get('InputfieldSubmit'); $f->attr('id+name', 'download'); $f->value = $this->labels['download_install']; $f->icon = $fieldset->icon; $fieldset->add($f); $tab->add($fieldset); $fieldset = $this->modules->get('InputfieldFieldset'); $fieldset->label = $this->labels['download_zip']; $fieldset->icon = 'download'; $fieldset->collapsed = Inputfield::collapsedYes; $trustNote = $this->_('Be absolutely certain that you trust the source of the ZIP file.'); $f = $this->modules->get('InputfieldURL'); $f->attr('id+name', 'download_zip_url'); $f->label = $this->_('Module ZIP file URL'); $f->description = $this->_('Download a ZIP file containing a module. If you download a module that is already installed, the installed version will be overwritten with the newly downloaded version.'); $f->notes = $trustNote; $f->attr('placeholder', $this->_('http://domain.com/ModuleName.zip')); // placeholder $f->required = false; $fieldset->add($f); $f = $this->modules->get('InputfieldSubmit'); $f->attr('id+name', 'download_zip'); $f->value = $this->labels['download']; $f->icon = $fieldset->icon; $fieldset->add($f); $tab->add($fieldset); $fieldset = $this->modules->get('InputfieldFieldset'); $fieldset->label = $this->labels['upload_zip']; $fieldset->icon = 'upload'; $fieldset->collapsed = Inputfield::collapsedYes; $f = $this->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; $fieldset->add($f); $f = $this->modules->get('InputfieldSubmit'); $f->attr('id+name', 'upload'); $f->value = $this->labels['upload']; $f->icon = $fieldset->icon; $fieldset->add($f); $tab->add($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'; $submit = $this->modules->get('InputfieldButton'); $submit->attr('href', './?reset=1'); $submit->attr('id', 'reset_modules'); $submit->attr('class', $submit->attr('class') . ' head_button_clone'); $submit->attr('name', 'reset'); $submit->attr('value', $this->labels['reset']); $submit->icon = $fieldset->icon; $fieldset->add($submit); $tab->add($fieldset); $form->add($tab); if($this->input->get->reset == 2 && !$this->numFound) $this->message($this->_("No new modules found")); $this->session->ModulesUninstalled = $uninstalledNames; return $form->render(); } /** * Render a modules listing table, as it appears in the 'site' and 'core' tabs * * @param array $modulesArray * @param bool $allowDelete Whether or not delete is allowed (default=false) * @param bool $allowSections Whether to show module sections/categories (default=true) * @param bool $allowDates Whether to show created dates (default=false) * @param bool $allowClasses Whether to show module class names(default=false) * @param bool $allowType Whether to show if module is site or core * @return string * */ protected function renderListTable($modulesArray, $allowDelete = false, $allowSections = true, $allowDates = false, $allowClasses = false, $allowType = false) { if(!count($modulesArray)) return "

" . $this->_('No modules found.') . "
"; static $numCalls = 0; $numCalls++; $uninstalledPrev = is_array($this->session->ModulesUninstalled) ? $this->session->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 ); $table = null; $total = 0; $out = ''; $this->numFound = 0; $newModules = $this->wire('session')->get('ProcessModuleNewModules'); if(!is_array($newModules)) $newModules = array(); $sections = array(); $sectionsQty = array(); foreach($modulesArray as $name => $installed) { if(strpos($name, $section) !== 0 || preg_match('/' . $section . '[^A-Z]/', $name)) { if(!preg_match('/^([A-Za-z][a-z]+)/', $name, $matches)) $this->error(sprintf($this->_('Invalid module name: %s'), $name)); if($allowSections || is_null($table)) { $section = $matches[1]; $sections[] = $section; if($table) $out .= $table->render() . ""; $table = $this->modules->get("MarkupAdminDataTable"); $table->setEncodeEntities(false); $table->headerRow($tableHeader); if($allowSections) $out .= "\n

$section

"; } } $info = $this->modules->getModuleInfoVerbose($name); // $interfaces = @class_implements($name, false); // $configurable = is_array($interfaces) && in_array('ConfigurableModule', $interfaces); $configurable = $info['configurable']; $title = !empty($info['title']) ? $this->wire('sanitizer')->entities1($info['title']) : substr($name, strlen($section)); if($allowClasses) $title .= "
$name"; if($info['icon']) $title = " $title"; $class = $configurable ? 'ConfigurableModule' : ''; if(!empty($info['permanent'])) $class .= ($class ? ' ' : '') . 'PermanentModule'; if($class) $title = "$title"; $version = $this->formatVersion(isset($info['version']) ? $info['version'] : 0); if($allowType) $version .= "
" . ($info['core'] ? $this->labels['core'] : $this->labels['site']) . ""; $summary = empty($info['summary']) ? '' : $this->wire('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 = ''; $confirmJS = "return confirm('" . sprintf($this->_('Delete %s?'), $name) . "')"; $editUrl = "edit?name={$name}"; if(!$installed) { if(count($info['requires'])) { $requires = $this->modules->getRequiresForInstall($name); if(count($requires)) { foreach($requires as $key => $value) { $nameOnly = preg_replace('/^([_a-zA-Z0-9]+)[=<>]+.*$/', '$1', $value); $requiresInfo = $this->modules->getModuleInfo($nameOnly); if(!empty($requiresInfo['error'])) $requires[$key] = "$value"; } $summary .= "" . $this->labels['requires'] . " - " . implode(', ', $requires) . ""; } } else $requires = array(); 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(!$this->input->get->uninstalled) $this->message($this->_("Found new module") . " - $name", Notice::allowMarkup); // Message that precedes module name when new module is found $newModules[$name] = time(); $this->numFound++; } $title = "$title"; if(count($requires)) { //$buttonState = 'ui-state-default ui-state-disabled'; //$buttonType = 'button'; } else { $isConfirm = count($modulesArray) == 1 && $this->wire('input')->get('name'); $buttonState = 'ui-state-default'; $buttonType = 'submit'; $buttonPriority = $isConfirm ? "ui-priority-primary" : "ui-priority-secondary"; $buttons .= ""; // Text for 'Install' button // install confirm, needs a cancel button if($isConfirm) $buttons .= ""; // Text for 'Cancel' button } if($allowDelete && $this->wire('modules')->isDeleteable($name)) $buttons .= ""; // Text for 'Delete' button $editUrl = '#'; } else if($configurable) { $buttons .= ""; // Text for 'Settings' button } if($buttons) $buttons = "$buttons"; if($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->render(); if($allowSections) { $out .= "
"; $select = "

"; $out = $select . $out; } $resetNewModules = false; foreach($newModules as $key => $newModule) { $info = $this->wire('modules')->getModuleInfoVerbose($newModule); if(!$info['file'] || !file_exists($info['file'])) { unset($newModules[$key]); $resetNewModules = true; } } if($this->numFound) $resetNewModules = true; if($resetNewModules) $this->wire('session')->set('ProcessModuleNewModules', $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 $name Class name of module * @param bool $update Whether this is a 'check for updates' request * @return string * */ protected function downloadConfirm($name, $update = false) { $name = $this->wire('sanitizer')->name($name); $info = self::getModuleInfo(); $this->wire('processHeadline', $this->labels['download_install']); $this->wire('breadcrumbs')->add(new Breadcrumb('./', $info['title'])); if($update) $this->wire('breadcrumbs')->add(new Breadcrumb("./?edit=$name", $name)); $redirectURL = $update ? "./edit?name=$name" : "./"; $className = $name; $url = trim($this->wire('config')->moduleServiceURL, '/') . "/$className/?apikey=" . $this->wire('sanitizer')->name($this->wire('config')->moduleServiceKey); $http = new WireHttp(); $data = $http->get($url); if(empty($data)) { $this->error($this->_('Error retrieving data from web service URL') . ' - ' . $http->getError()); return $this->session->redirect($redirectURL); } $data = json_decode($data, true); if(empty($data)) { $this->error($this->_('Error decoding JSON from web service')); return $this->session->redirect($redirectURL); } if($data['status'] !== 'success') { $this->error($this->_('Error reported by web service:') . ' ' . wire('sanitizer')->entities($data['error'])); return $this->session->redirect($redirectURL); } $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) $this->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) { $warnings = array(); $authors = ''; foreach($data['authors'] as $author) $authors .= $author['title'] . ", "; $authors = rtrim($authors, ", "); $compat = ''; $isCompat = false; $myVersion = substr($this->wire('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.'); $form = $this->wire('modules')->get('InputfieldForm'); $form->attr('action', './download/'); $form->attr('method', 'post'); $form->attr('id', 'ModuleInfo'); $markup = $this->wire('modules')->get('InputfieldMarkup'); $form->add($markup); $installed = $this->modules->isInstalled($data['class_name']) ? $this->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); $this->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'); } $table = $this->wire('modules')->get('MarkupAdminDataTable'); $table->setEncodeEntities(false); $table->row(array($this->_x('Class', 'install-table'), $this->wire('sanitizer')->entities($data['class_name']))); $table->row(array($this->_x('Version', 'install-table'), $this->wire('sanitizer')->entities($data['module_version']) . $moduleVersionNote)); $table->row(array($this->_x('Installed?', 'install-table'), $installedVersion)); $table->row(array($this->_x('Authors', 'install-table'), $this->wire('sanitizer')->entities($authors))); $table->row(array($this->_x('Summary', 'install-table'), $this->wire('sanitizer')->entities($data['summary']))); $table->row(array($this->_x('Release State', 'install-table'), $this->wire('sanitizer')->entities($data['release_state']['title']))); $table->row(array($this->_x('Compatibility', 'install-table'), $this->wire('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 ? $this->sanitizer->entities("$name $op $ver") : $this->sanitizer->entities($name); if($this->modules->isInstalled("$name$op$ver") || in_array($name, $data['installs'])) { // installed $requiresVersions[] = "$label "; } else if($this->modules->isInstalled($name)) { // installed, but version isn't adequate $installable = false; $info = $this->modules->getModuleInfo($name); $requiresVersions[] = $this->sanitizer->entities($name) . " " . $this->modules->formatVersion($info['version']) . " " . "" . $this->sanitizer->entities("$op $ver") . " " . ""; } else { // not installed at all $requiresVersions[] = "$label "; $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 = $this->sanitizer->entities(implode("\n", $data['installs'])); $table->row(array($this->labels['installs'], nl2br($installs))); } $links = array(); $moduleName = wire('sanitizer')->entities1($data['name']); $links[] = "" . $this->_('More Information') . ""; if($data['project_url']) { $projectURL = wire('sanitizer')->entities($data['project_url']); $links[] = "" . $this->_('Project Page') . ""; } if($data['forum_url']) { $forumURL = wire('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 = wire('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.'); } foreach($warnings as $warning) { $table->row(array($this->_x('Please Note', 'install-table'), " $warning")); } $markup->value = $table->render(); if($installable && $data['download_url']) { $btn = $this->wire('modules')->get('InputfieldSubmit'); $btn->attr('id+name', 'godownload'); $btn->value = $this->labels['download_install']; $btn->icon = 'cloud-download'; if($update) $btn->value .= " ($data[module_version])"; $form->add($btn); $this->session->ProcessModuleDownloadURL = $data['download_url']; $this->session->ProcessModuleClassName = $data['class_name']; } else { $this->session->remove('ProcessModuleDownloadURL'); $this->session->remove('ProcessModuleClassName'); } $btn = $this->wire('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); $form->description = $this->wire('sanitizer')->entities($data['title']); 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() { if(!$this->input->post->godownload) { $this->message($this->_('Download cancelled')); return $this->session->redirect('../'); } $this->session->CSRF->validate(); $this->modules->resetCache(); $url = $this->session->ProcessModuleDownloadURL; $className = $this->session->ProcessModuleClassName; $this->session->remove('ProcessModuleDownloadURL'); $this->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 . '/'; $install = new ProcessModuleInstall(); $completedDir = $install->downloadModule($url, $destinationDir); if($completedDir) { return $this->buildDownloadSuccessForm($className)->render(); } else { return $this->session->redirect('../'); } } /** * Build the form that gets displayed after a module has been successfully downloaded * * @param string $className * @return InputfieldForm * */ protected function ___buildDownloadSuccessForm($className) { $form = $this->modules->get('InputfieldForm'); // check if modules isn't already installed and this isn't an update if(!$this->modules->isInstalled($className)) { $info = $this->wire('modules')->getModuleInfoVerbose($className); $requires = array(); if(count($info['requires'])) $requires = $this->modules->getRequiresForInstall($className); if(count($requires)) { foreach($requires as $moduleName) { $this->error("$className - " . sprintf($this->_('Requires module "%s" before it can be installed'), $moduleName), Notice::warning | Notice::allowMarkup); } $this->wire('session')->redirect('../'); } $this->wire('processHeadline', $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'); $f = $this->modules->get('InputfieldHidden'); $f->attr('name', 'install'); $f->attr('value', $className); $form->add($f); $submit = $this->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); $button = $this->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->wire('processHeadline', $this->_('Updated:') . ' ' . $className); $form->description = sprintf($this->_('%s was updated successfully.'), $className); $button = $this->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 be be accessed directly"); $install = new ProcessModuleInstall(); $install->uploadModule($inputName); $this->session->redirect('./'); } public function ___executeDownloadURL($url = '') { if(!$url) throw new WireException("This URL may be be accessed directly"); $install = new ProcessModuleInstall(); $install->downloadModule($url); $this->session->redirect('./'); } /**********************************************************************************************************************************************************/ /** * Load the form for editing a module's settings * */ public function ___executeEdit() { $info = null; $moduleName = ''; $out = ''; if(isset($_POST['name'])) $moduleName = $_POST['name']; else if(isset($_GET['name'])) $moduleName = $_GET['name']; $moduleName = $this->sanitizer->name($moduleName); if(!$moduleName || !$info = $this->modules->getModuleInfoVerbose($moduleName)) { $this->session->message($this->_("No module specified")); $this->session->redirect("./"); } return $this->renderEdit($moduleName, $info); } /** * Build and render for the form for editing a module's settings * * This method saves the settings if it's form has been posted * */ protected function renderEdit($moduleName, $moduleInfo) { $out = ''; $moduleId = $this->modules->getModuleID($moduleName); if(!$moduleId) { $this->error("Unknown module"); return $this->session->redirect('./'); } $sinfo = self::getModuleInfo(); $this->fuel('breadcrumbs')->add(new Breadcrumb('./', $sinfo['title'])); $this->setFuel('processHeadline', $moduleInfo['title']); $form = $this->modules->get("InputfieldForm"); $form->attr('id', 'ModuleEditForm'); $form->attr('action', "edit?name=$moduleName"); $form->attr('method', 'post'); $fields = array(); $data = array(); $dependents = $this->modules->getRequiredBy($moduleName, true); $requirements = $this->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"; } } // include module configuration fields if provided if(in_array('ConfigurableModule', class_implements($moduleName))) { $data = $this->modules->getModuleConfigData($moduleName); $fields = call_user_func(array($moduleName, 'getModuleConfigInputfields'), $data); if($fields instanceof InputfieldWrapper) { foreach($fields as $field) $form->append($field); } else { $this->error("$moduleName::getModuleConfigInputfields() did not return the expected value (InputfieldWrapper)"); } } // uninstall checkbox $field = $this->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 = $this->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 = $this->wire('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 = $this->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->append($field); // submit button if(count($form->children)) { $field = $this->modules->get("InputfieldSubmit"); $field->attr('name', 'submit_save_module'); $field->addClass('head_button_clone'); $form->append($field); } else { $this->message($this->_("This module doesn't have any fields to configure")); } // check for submitted form if($this->input->post->submit_save_module) { $form->processInput($this->input->post); if(count($fields)) foreach($fields->getAll() as $field) { // note field names beginning with '_' will not be stored if(($name = $field->attr('name')) && strpos($name, '_') !== 0) { $data[$name] = $field->attr('value'); if(wire('config')->debug) $this->message('Saved Module Config Data: ' . $name); } } if($uninstallable && $this->input->post->uninstall === $moduleName) { $this->modules->uninstall($moduleName); $this->session->message($this->_("Uninstalled Module") . " - $moduleName"); // Message shown before the name of a module that was just uninstalled $this->session->redirect('./?uninstalled=1'); } else { $this->modules->saveModuleConfigData($moduleName, $data); $this->message($this->_("Saved Module") . " - $moduleName"); // Message shown before the name of a module that was just saved $this->session->redirect("./edit?name=$moduleName"); return; } } // entity encode module info since it's turned off in our table foreach($moduleInfo as $key => $value) { if(!is_string($value)) continue; $moduleInfo[$key] = $this->wire('sanitizer')->entities1($value); } $version = $this->formatVersion($moduleInfo['version']); $filename = str_replace($this->wire('config')->paths->root, '/', $moduleInfo['file']); if(!$moduleInfo['core']) { $version .= " - " . $this->_('check for updates') . ""; } $hooksStr = $this->renderModuleHooks($moduleName); // build a table that displays module info $table = $this->modules->get("MarkupAdminDataTable"); $table->setEncodeEntities(false); $table->row(array($this->_x('Title', 'edit'), $moduleInfo['title'])); $table->row(array($this->_x('Class', 'edit'), $moduleName)); $table->row(array($this->_x('File', 'edit'), $filename)); $table->row(array($this->_x('ID', 'edit'), $moduleId)); $table->row(array($this->_x('Version', 'edit'), $version)); if(!empty($moduleInfo['created'])) $table->row(array($this->labels['installed_date'], wireRelativeTimeStr($moduleInfo['created']))); if(!empty($moduleInfo['author'])) $table->row(array($this->_x('Author', 'edit'), $moduleInfo['author'])); $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)); if(!empty($moduleInfo['href'])) $table->row(array($this->_x('More Information', 'edit'), "$moduleInfo[href]")); $field = $this->modules->get("InputfieldMarkup"); $field->attr('id', 'ModuleInfo'); $field->attr('value', $table->render()); $field->label = $this->_x("Module Information", 'edit'); $field->icon = 'info-circle'; $form->prepend($field); $out .= $form->render(); return $out; } protected function renderModuleHooks($moduleName) { $out = ''; $hooks = array_merge($this->wire()->getHooks('*'), Wire::$allLocalHooks); foreach($hooks as $hook) { $toObject = !empty($hook['toObject']) ? $hook['toObject'] : ''; if(empty($toObject) || get_class($toObject) != $moduleName) continue; $suffix = $hook['options']['type'] == 'method' ? '()' : ''; $out .= "" . ($hook['options']['fromClass'] ? $hook['options']['fromClass'] . '::' : '') . "$hook[method]$suffix, "; } return trim($out, ", "); } public function ___executeInstallConfirm() { $name = $this->wire('input')->get('name'); if(!$name) throw new WireException("No module name specified"); $name = $this->wire('sanitizer')->fieldName($name); if(!$this->wire('modules')->isInstallable($name, true)) throw new WireException("Module is not currently installable"); $this->headline($this->labels['install']); $form = $this->modules->get('InputfieldForm'); $form->attr('action', './'); $form->attr('method', 'post'); $form->description = sprintf($this->_('Install %s?'), $name); $modulesArray[$name] = (int) $this->modules->isInstalled($name); $markup = $this->modules->get('InputfieldMarkup'); $markup->value = $this->renderListTable($modulesArray, false, false, false, true, true); $form->add($markup); return $form->render(); } }