diff --git a/wire/config.php b/wire/config.php index e48bf63e..09d384e2 100644 --- a/wire/config.php +++ b/wire/config.php @@ -1189,7 +1189,30 @@ $config->moduleServiceURL = 'https://modules.processwire.com/export-json/'; * @var string * */ -$config->moduleServiceKey = (__NAMESPACE__ ? 'pw300' : 'pw280'); +$config->moduleServiceKey = 'pw301'; + +/** + * Allowed module installation options (in admin) + * + * Module installation options you want to be available from the admin Modules > Install tab. + * For any of the options below, specify boolean `true` to allow, `false` to disallow, or + * specify string `'debug'` to allow only when ProcessWire is in debug mode. + * + * - `directory`: Allow installation or upgrades from ProcessWire modules directory? + * - `upload`: Allow installation by file upload? + * - `download`: Allow installation by file download from URL? + * + * @todo consider whether the 'directory' option should also be limited to 'debug' only. + * + * @var array + * @since 3.0.163 + * + */ +$config->moduleInstall = array( + 'directory' => true, // allow install from ProcessWire modules directory? + 'upload' => 'debug', // allow install by module file upload? + 'download' => 'debug', // allow install by download from URL? +); /** * Substitute modules diff --git a/wire/core/Config.php b/wire/core/Config.php index 00f765b6..6636e367 100644 --- a/wire/core/Config.php +++ b/wire/core/Config.php @@ -130,6 +130,7 @@ * @property string $moduleServiceKey API key for modules web service #pw-group-modules * @property bool $moduleCompile Allow use of compiled modules? #pw-group-modules * @property array $wireMail Default WireMail module settings. #pw-group-modules + * @property array $moduleInstall Admin module install options you allow. #pw-group-modules * * @property array $substituteModules Associative array with names of substitute modules for when requested module doesn't exist #pw-group-modules * @property array $logs Additional core logs to keep #pw-group-admin diff --git a/wire/modules/Process/ProcessModule/ProcessModule.js b/wire/modules/Process/ProcessModule/ProcessModule.js index 2b44bda6..946e49f3 100644 --- a/wire/modules/Process/ProcessModule/ProcessModule.js +++ b/wire/modules/Process/ProcessModule/ProcessModule.js @@ -6,7 +6,7 @@ $(document).ready(function() { var $btn = $(".install_" + name + ":visible"); var disabled = $btn.attr('disabled'); - if($btn.size()) { + if($btn.length) { $btn.effect('highlight', 1000); } else { var color = $(this).css('color'); @@ -52,7 +52,8 @@ $(document).ready(function() { }); $("#Inputfield_new_seconds").change(function() { - $(this).parents('form').submit(); + $('#submit_check').removeAttr('hidden').click(); + $(this).closest('form').submit(); }); $("#wrap_upload_module").removeClass('InputfieldItemList'); diff --git a/wire/modules/Process/ProcessModule/ProcessModule.min.js b/wire/modules/Process/ProcessModule/ProcessModule.min.js index 4f52b145..7b22168c 100644 --- a/wire/modules/Process/ProcessModule/ProcessModule.min.js +++ b/wire/modules/Process/ProcessModule/ProcessModule.min.js @@ -1 +1 @@ -$(document).ready(function(){$(".not_installed").parent("a").css("opacity",0.6).click(function(){var b=$(this).children(".not_installed").attr("data-name");var d=$(".install_"+b+":visible");var c=d.attr("disabled");if(d.size()){d.effect("highlight",1000)}else{var a=$(this).css("color");$(this).closest("tr").find(".requires").attr("data-color",$(this).css("color")).css("color",a).effect("highlight",1000)}return false});$("button.ProcessModuleSettings").click(function(){var a=$(this).parents("tr").find(".ConfigurableModule").parent("a");window.location.href=a.attr("href")+"&collapse_info=1"});if($("#modules_form").length>0){$("#modules_form").WireTabs({items:$(".Inputfields li.WireTab"),rememberTabs:true})}$("select.modules_section_select").change(function(){var b=$(this).val();var a=$(this).parent("p").siblings(".modules_section");if(b==""){a.show()}else{a.hide();a.filter(".modules_"+b).show()}document.cookie=$(this).attr("name")+"="+b;return true}).change();$(document).on("click","#head_button a",function(){document.cookie="WireTabs=tab_new_modules";return true});$("#Inputfield_new_seconds").change(function(){$(this).parents("form").submit()});$("#wrap_upload_module").removeClass("InputfieldItemList")}); \ No newline at end of file +$(document).ready(function(){$(".not_installed").parent("a").css("opacity",.6).click(function(){var name=$(this).children(".not_installed").attr("data-name");var $btn=$(".install_"+name+":visible");var disabled=$btn.attr("disabled");if($btn.length){$btn.effect("highlight",1e3)}else{var color=$(this).css("color");$(this).closest("tr").find(".requires").attr("data-color",$(this).css("color")).css("color",color).effect("highlight",1e3)}return false});$("button.ProcessModuleSettings").click(function(){var $a=$(this).parents("tr").find(".ConfigurableModule").parent("a");window.location.href=$a.attr("href")+"&collapse_info=1"});if($("#modules_form").length>0){$("#modules_form").WireTabs({items:$(".Inputfields li.WireTab"),rememberTabs:true})}$("select.modules_section_select").change(function(){var section=$(this).val();var $sections=$(this).parent("p").siblings(".modules_section");if(section==""){$sections.show()}else{$sections.hide();$sections.filter(".modules_"+section).show()}document.cookie=$(this).attr("name")+"="+section;return true}).change();$(document).on("click","#head_button a",function(){document.cookie="WireTabs=tab_new_modules";return true});$("#Inputfield_new_seconds").change(function(){$("#submit_check").removeAttr("hidden").click();$(this).closest("form").submit()});$("#wrap_upload_module").removeClass("InputfieldItemList")}); \ No newline at end of file diff --git a/wire/modules/Process/ProcessModule/ProcessModule.module b/wire/modules/Process/ProcessModule/ProcessModule.module index 117d029c..c53823ac 100644 --- a/wire/modules/Process/ProcessModule/ProcessModule.module +++ b/wire/modules/Process/ProcessModule/ProcessModule.module @@ -11,7 +11,7 @@ * 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 2019 by Ryan Cramer + * ProcessWire 3.x, Copyright 2020 by Ryan Cramer * https://processwire.com * * @todo add support for module configuration inputfields with useLanguages option @@ -32,7 +32,7 @@ class ProcessModule extends Process { return array( 'title' => __('Modules', __FILE__), // getModuleInfo title 'summary' => __('List, edit or install/uninstall modules', __FILE__), // getModuleInfo summary - 'version' => 118, + 'version' => 119, 'permanent' => true, 'permission' => 'module-admin', 'useNavJSON' => true, @@ -121,6 +121,16 @@ class ProcessModule extends Process { */ protected $numFound = 0; + /** + * @var ProcessModuleInstall|null + * + */ + protected $installer = null; + + /** + * Construct + * + */ public function __construct() { $this->labels['download'] = $this->_('Download'); if($this->input->get('update')) { @@ -132,6 +142,7 @@ class ProcessModule extends Process { $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'); @@ -156,6 +167,14 @@ class ProcessModule extends Process { require(dirname(__FILE__) . '/ProcessModuleInstall.php'); } + /** + * @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 @@ -515,100 +534,151 @@ class ProcessModule extends Process { $select->addOption(2419200, $this->_('Within the last month')); $select->required = true; $select->attr('value', $newSeconds); + + /** @var InputfieldSubmit $btn */ + $btn = $this->modules->get('InputfieldSubmit'); + $btn->attr('hidden', 'hidden'); + $btn->attr('name', 'submit_check'); + $btn->textFormat = Inputfield::textFormatNone; + $btn->icon = 'check'; + $btn->value = ' '; + $btn->setSmall(true); + $btn->setSecondary(true); + $btn = ""; /** @var InputfieldMarkup $markup */ $markup = $this->modules->get('InputfieldMarkup'); $markup->icon = 'lightbulb-o'; - $markup->value = $select->render() . $this->renderListTable($newModulesArray, false, false, true, true); + $markup->value = $select->render() . ' ' . $btn . $this->renderListTable($newModulesArray, false, false, true, true); $markup->label = $this->_('Recently Found and Installed Modules'); - $tab->add($markup); + $tab->add($markup); /** @var InputfieldFieldset $fieldset */ $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; - /** @var InputfieldName $f */ - $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.') . ' ' . - 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 = $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); - - /** @var InputfieldSubmit $f */ - $f = $this->modules->get('InputfieldSubmit'); - $f->attr('id+name', 'download'); - $f->value = $this->labels['get_module_info']; - $f->icon = $fieldset->icon; - $fieldset->add($f); $tab->add($fieldset); + //if($this->wire('input')->post('new_seconds')) $fieldset->collapsed = Inputfield::collapsedYes; + + if($this->installer()->canInstallFromDirectory(false)) { + /** @var InputfieldName $f */ + $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.') . ' ' . + 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'), '[modules.processwire.com](https://modules.processwire.com)'); + $f->attr('placeholder', $this->_('ModuleClassName')); // placeholder + $f->required = false; + $fieldset->add($f); + + /** @var InputfieldSubmit $f */ + $f = $this->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 = $this->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.'); - /** @var InputfieldURL $f */ - $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); - - /** @var InputfieldSubmit $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); + if($this->installer()->canInstallFromDownloadUrl(false)) { + /** @var InputfieldURL $f */ + $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); + + /** @var InputfieldSubmit $f */ + $f = $this->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 = $this->modules->get('InputfieldFieldset'); $fieldset->label = $this->labels['upload_zip']; $fieldset->icon = 'upload'; $fieldset->collapsed = Inputfield::collapsedYes; - - /** @var InputfieldFile $f */ - $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; - $f->noCustomButton = true; - $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); + if($this->installer()->canInstallFromFileUpload(false)) { + /** @var InputfieldFile $f */ + $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; + $f->noCustomButton = true; + $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); + } else { + $fieldset->description = $this->installer()->installDisabledLabel('upload'); + } + /** @var InputfieldFieldset $fieldset */ $fieldset = $this->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 = $this->modules->get('InputfieldMarkup'); + $fieldset->add($markup); + $moduleNameLabel = $this->_('ModuleName'); // Example module class/directory name + $moduleNameDir = $this->wire()->config->urls->siteModules . $moduleNameLabel . '/'; + $instructions = array( + sprintf($this->_('1. Copy a module’s files into a new directory %s on the server.'), "$moduleNameDir") . ' * ', + sprintf($this->_('2. Click the “%s” button below, which will find the new module.'), $this->labels['reset']), + sprintf($this->_('3. Locate and click the “%s” button next to the new module.'), $this->labels['install_btn']) + ); + $markup->value = '
' . implode('
', $instructions) . '
'; + $markup->notes = '* ' . sprintf( + $this->_('Replace “%s” with the actual module name, which is typically its PHP class name.'), + $moduleNameLabel + ); + + /** @var InputfieldFieldset $fieldset */ + /* + $fieldset = $this->modules->get('InputfieldFieldset'); + $fieldset->attr('id', 'fieldset_check_new'); $fieldset->label = $this->labels['reset']; $fieldset->description = $this->_('If you have placed new modules in /site/modules/ yourself, click this button to find them.'); $fieldset->collapsed = Inputfield::collapsedYes; $fieldset->icon = 'refresh'; + */ /** @var InputfieldButton $submit */ $submit = $this->modules->get('InputfieldButton'); @@ -617,7 +687,7 @@ class ProcessModule extends Process { $submit->showInHeader(); $submit->attr('name', 'reset'); $submit->attr('value', $this->labels['reset']); - $submit->icon = $fieldset->icon; + $submit->icon = 'refresh'; $fieldset->add($submit); $tab->add($fieldset); @@ -1009,6 +1079,11 @@ class ProcessModule extends Process { } else { $warnings[] = $this->_('This module has no download URL specified and must be installed manually.'); } + + if(!$this->installer()->canInstallFromDirectory(false)) { + $installable = false; + $markup->notes = trim($markup->notes . ' ' . $this->installer()->installDisabledLabel('directory')); + } foreach($warnings as $warning) { $table->row(array($this->_x('Please Note', 'install-table'), " $warning")); @@ -1073,9 +1148,8 @@ class ProcessModule extends Process { if(!$className) throw new WireException("No class name specified"); $destinationDir = $this->wire('config')->paths->siteModules . $className . '/'; - $install = $this->wire(new ProcessModuleInstall()); - $completedDir = $install->downloadModule($url, $destinationDir); + $completedDir = $this->installer()->downloadModule($url, $destinationDir); if($completedDir) { return $this->buildDownloadSuccessForm($className)->render(); } else { @@ -1158,15 +1232,13 @@ class ProcessModule extends Process { public function ___executeUpload($inputName = '') { if(!$inputName) throw new WireException("This URL may not be accessed directly"); - $install = $this->wire(new ProcessModuleInstall()); - $install->uploadModule($inputName); + $this->installer()->uploadModule($inputName); $this->session->redirect('./?reset=1'); } public function ___executeDownloadURL($url = '') { if(!$url) throw new WireException("This URL may not be accessed directly"); - $install = $this->wire(new ProcessModuleInstall()); - $install->downloadModule($url); + $this->installer()->downloadModuleFromUrl($url); $this->session->redirect('./?reset=1'); } diff --git a/wire/modules/Process/ProcessModule/ProcessModuleInstall.php b/wire/modules/Process/ProcessModule/ProcessModuleInstall.php index bab27c3d..fb08d33b 100644 --- a/wire/modules/Process/ProcessModule/ProcessModuleInstall.php +++ b/wire/modules/Process/ProcessModule/ProcessModuleInstall.php @@ -1,10 +1,13 @@ wire()->config; + if($type) { + $a = $config->moduleInstall; + $allow = is_array($a) && isset($a[$type]) ? $a[$type] : false; + if($allow === 'debug' && !$config->debug) $allow = false; + if(!$allow) { + if($notify) $this->error( + sprintf($this->_('Module install option “%s”'), $type) . ' - ' . + $this->installDisabledLabel($type) + ); + return false; + } + } $can = true; - if(!is_writable($this->config->paths->cache)) { + if(!is_writable($config->paths->cache)) { if($notify) $this->error($this->_('Make sure /site/assets/cache/ directory is writeable for PHP.')); $can = false; } - if(!is_writable($this->config->paths->siteModules)) { + if(!is_writable($config->paths->siteModules)) { if($notify) $this->error($this->_('Make sure your site modules directory (/site/modules/) is writeable for PHP.')); $can = false; } @@ -50,6 +67,40 @@ class ProcessModuleInstall extends Wire { return $can; } + /** + * Module upload allowed? + * + * @param bool $notify + * @return bool + * + * + */ + public function canInstallFromFileUpload($notify = true) { + return $this->canUploadDownload($notify, 'upload'); + } + + /** + * Module download from URL allowed? + * + * @param bool $notify + * @return bool + * + */ + public function canInstallFromDownloadUrl($notify = true) { + return $this->canUploadDownload($notify, 'download'); + } + + /** + * Module install/upgrade from directory allowed? + * + * @param bool $notify + * @return bool + * + */ + public function canInstallFromDirectory($notify = true) { + return $this->canUploadDownload($notify, 'directory'); + } + /** * Find all module files, recursively in Path * @@ -347,7 +398,7 @@ class ProcessModuleInstall extends Wire { */ public function uploadModule($inputName = 'upload_module', $destinationDir = '') { - if(!$this->canUploadDownload()) { + if(!$this->canInstallFromFileUpload()) { $this->error($this->_('Unable to complete upload')); return false; } @@ -381,16 +432,18 @@ class ProcessModuleInstall extends Wire { /** * Given a URL to a ZIP file, download it, unzip it, and move to /site/modules/[ModuleName] * - * @param $url + * @param string $url Download URL * @param string $destinationDir Optional destination path for files (omit to auto-determine) + * @param string $type Specify type of 'download' or 'directory' * @return bool|string Returns destinationDir on success, false on failure. * */ - public function downloadModule($url, $destinationDir = '') { - - if(!$this->canUploadDownload()) { - $this->error($this->_('Unable to complete download')); - return false; + public function downloadModule($url, $destinationDir = '', $type = 'download') { + + if($type === 'directory') { + if(!$this->canInstallFromDirectory()) return false; + } else { + if(!$this->canInstallFromDownloadUrl()) return false; } if(!preg_match('{^https?://}i', $url)) { @@ -424,7 +477,61 @@ class ProcessModuleInstall extends Wire { return $success ? $destinationDir : false; } - + + /** + * Download module from URL + * + * @param string $url + * @param string $destinationDir + * @return bool|string + * @since 3.0.162 + * + */ + public function downloadModuleFromUrl($url, $destinationDir = '') { + return $this->downloadModule($url, $destinationDir, 'download'); + } + + /** + * Download module from directory + * + * @param string $url + * @param string $destinationDir + * @return bool|string + * @since 3.0.162 + * + */ + public function downloadModuleFromDirectory($url, $destinationDir = '') { + return $this->downloadModule($url, $destinationDir, 'directory'); + } + + /** + * Return label to indicate option is disabled and how to enable it + * + * @param string $type + * @return string + * @since 3.0.162 + * + */ + public function installDisabledLabel($type) { + $config = $this->wire()->config; + $a = $config->moduleInstall; + $debug = !empty($a[$type]) && $a[$type] === 'debug'; + $opt1 = "`\$config->moduleInstall('$type', true);` " . $this->_('to enable always'); + $opt2 = "`\$config->debug = true;` " . $this->_('temporarily'); + $opt3 = "`\$config->moduleInstall('$type', 'debug');` " . $this->_('to enable in debug mode only'); + $file = $config->urls->site . 'config.php'; + $inst = $this->_('To enable, edit file %1$s and specify: %2$s …or… %3$s'); + if($debug) { + return + $this->_('This install option is configured to be available only in debug mode.') . ' ' . + sprintf($inst, "$file", "\n$opt2", "\n$opt1"); + + } else { + return + $this->_('This install option is currently disabled.') . ' ' . + sprintf($inst, "$file", "\n$opt1", "\n$opt3"); + } + } }