__('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 .=
"
" .
" " . $this->labels['install_btn'] . " "; // Text for 'Install' button
// install confirm, needs a cancel button
if($isConfirm) $buttons .=
"
" .
" " . $this->labels['cancel'] . " "; // Text for 'Cancel' button
}
if($allowDelete && $this->wire('modules')->isDeleteable($name)) $buttons .=
"
" .
" " . $this->_x('Delete', 'button') . " "; // Text for 'Delete' button
$editUrl = '#';
} else if($configurable) {
$buttons .=
"
" .
" " . $this->_x('Settings', 'button') . " "; // 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 = "";
$select .= "" . $this->_('Show All') . " ";
$current = $this->wire('input')->cookie("modules_section$numCalls");
foreach($sections as $section) {
$qty = $sectionsQty[$section];
$selected = $current == $section ? " selected='selected'" : "";
$select .= "$section ($qty) ";
}
$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();
}
}