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