praiadeseselle/wire/modules/Process/ProcessModule/ProcessModule.module

2133 lines
76 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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 2022 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',
'FieldtypeOptions',
'InputfieldIcon',
'ProcessLogger',
);
protected $labels = array();
/**
* All modules indexed by class name and sorted by class name
*
*/
protected $modulesArray = array();
/**
* All modules that may be deleted
*
*/
protected $deleteableModules = array();
/**
* Categories of modules that we can't uninstall via this module
*
*/
protected $uninstallableCategories = array(
'language-pack',
'site-profile',
);
/**
* Number of new modules found after a reset
*
*/
protected $numFound = 0;
/**
* @var ProcessModuleInstall|null
*
*/
protected $installer = null;
/**
* Construct
*
*/
public function __construct() {
parent::__construct();
$this->labels['download'] = $this->_('Download');
$this->labels['download_install'] = $this->_('Download and Install');
$this->labels['get_module_info'] = $this->_('Get Module Info');
$this->labels['modules'] = $this->_('Modules');
$this->labels['module_information'] = $this->_x("Module Information", 'edit');
$this->labels['download_now'] = $this->_('Download Now');
$this->labels['download_dir'] = $this->_('Add Module From Directory');
$this->labels['add_manually'] = $this->_('Add Module Manually');
$this->labels['upload'] = $this->_('Upload');
$this->labels['upload_zip'] = $this->_('Add Module From Upload');
$this->labels['download_zip'] = $this->_('Add Module From URL');
$this->labels['check_new'] = $this->_('Check for New Modules');
$this->labels['installed_date'] = $this->_('Installed');
$this->labels['requires'] = $this->_x('Requires', 'list'); // Label that precedes list of required prerequisite modules
$this->labels['installs'] = $this->_x('Also Installs', 'list'); // Label that precedes list of other modules a given one installs
$this->labels['reset'] = $this->_('Refresh');
$this->labels['core'] = $this->_('Core');
$this->labels['site'] = $this->_('Site');
$this->labels['configure'] = $this->_('Configure');
$this->labels['install_btn'] = $this->_x('Install', 'button'); // Label for Install button
$this->labels['install'] = $this->_('Install'); // Label for Install tab
$this->labels['cancel'] = $this->_('Cancel'); // Label for Cancel button
require_once(dirname(__FILE__) . '/ProcessModuleInstall.php');
}
/**
* Wired to API
*
*/
public function wired() {
parent::wired();
if($this->wire()->languages && !$this->wire()->user->language->isDefault()) {
// Use previous translations when new labels aren't available (can be removed in PW 2.6+ when language packs assumed updated)
if($this->labels['install'] == 'Install') $this->labels['install'] = $this->labels['install_btn'];
if($this->labels['reset'] == 'Refresh') $this->labels['reset'] = $this->labels['check_new'];
}
if($this->wire()->input->get('update')) {
$this->labels['download_install'] = $this->_('Download and Update');
}
}
/**
* @return ProcessModuleInstall
*
*/
public function installer() {
if($this->installer === null) $this->installer = $this->wire(new ProcessModuleInstall());
return $this->installer;
}
/**
* Format a module version number from 999 to 9.9.9
*
* @param string $version
* @return string
*
*/
protected function formatVersion($version) {
return $this->wire()->modules->formatVersion($version);
}
/**
* Output JSON list of navigation items for this (intended to for ajax use)
*
* For 2.5+ admin themes
*
* @param array $options
* @return string
*
*/
public function ___executeNavJSON(array $options = array()) {
$page = $this->wire()->page;
$input = $this->wire()->input;
$modules = $this->wire()->modules;
$site = (int) $input->get('site');
$core = (int) $input->get('core');
$configurable = (int) $input->get('configurable');
$install = (int) $input->get('install');
$moduleNames = array();
$data = array(
'url' => $page->url,
'label' => (string) $page->get('title|name'),
'icon' => 'plug',
'list' => array(),
);
if($site || $install) $data['add'] = array(
'url' => "?new#tab_new_modules",
'label' => __('Add New', '/wire/templates-admin/default.php'),
'icon' => 'plus-circle',
);
if($install) {
$moduleNames = array_keys($modules->getInstallable());
} else {
foreach($modules as $module) {
$moduleNames[] = $module->className();
}
}
sort($moduleNames);
foreach($moduleNames as $moduleName) {
$info = $modules->getModuleInfoVerbose($moduleName);
if($site && $info['core']) continue;
if($core && !$info['core']) continue;
if($configurable) {
if(!$info['configurable'] || !$info['installed']) continue;
$flags = $modules->getFlags($moduleName);
if($flags & Modules::flagsNoUserConfig) continue;
}
if($install) {
// exclude already installed modules
if($info['installed']) continue;
// check that it can be installed NOW (i.e. all dependencies met)
if(!$modules->isInstallable($moduleName, true)) continue;
}
$label = $info['name'];
$_label = $label;
while(isset($data['list'][$_label])) $_label .= "_";
if(empty($info['icon'])) $info['icon'] = $info['configurable'] ? 'gear' : 'plug';
$url = $install ? "installConfirm" : "edit";
$url .= "?name=$info[name]";
if($configurable) $url .= "&collapse_info=1";
$data['list'][$_label] = array(
'url' => $url,
'label' => $label,
'icon' => $info['icon'],
);
}
ksort($data['list']);
$data['list'] = array_values($data['list']);
if($this->wire()->config->ajax) header("Content-Type: application/json");
return json_encode($data);
}
/**
* Load all modules, install any requested, and render a list of all modules
*
*/
public function ___execute() {
$modules = $this->wire()->modules;
$session = $this->wire()->session;
$sanitizer = $this->wire()->sanitizer;
$input = $this->wire()->input;
$config = $this->wire()->config;
foreach($modules as $module) {
$this->modulesArray[$module->className()] = 1;
}
foreach($modules->getInstallable() as $module) {
$this->modulesArray[basename(basename($module, '.php'), '.module')] = 0;
}
ksort($this->modulesArray);
if($input->post('install')) {
$session->CSRF->validate();
$name = $sanitizer->name($input->post('install'));
if($name && isset($this->modulesArray[$name]) && !$this->modulesArray[$name]) {
$module = $modules->install($name, array('force' => true));
if($module) {
$this->modulesArray[$name] = 1;
$session->message($this->_("Module Install") . " - $name"); // Message that precedes the name of the module installed
$session->redirect("edit?name=$name");
} else {
$session->error($this->_('Error installing module') . " - $name");
$session->redirect("./");
}
}
}
if($input->post('delete')) {
$session->CSRF->validate();
$name = $input->post('delete');
if($name && isset($this->modulesArray[$name])) {
$info = $modules->getModuleInfoVerbose($name);
try {
$modules->delete($name);
$this->message($this->_('Deleted module files') . ' - ' . $info['title']);
} catch(WireException $e) {
$this->error($e->getMessage());
}
$session->redirect("./");
}
}
if($input->post('download') && $input->post('download_name')) {
$session->CSRF->validate();
return $this->downloadConfirm($input->post('download_name'));
} else if($input->get('download_name')) {
return $this->downloadConfirm($input->get('download_name'));
}
if($input->post('upload')) {
$session->CSRF->validate();
$this->executeUpload('upload_module');
}
if($input->post('download_zip') && $input->post('download_zip_url')) {
$session->CSRF->validate();
$this->executeDownloadURL($input->post('download_zip_url'));
}
if($input->post('clear_file_compiler')) {
$session->CSRF->validate();
/** @var FileCompiler $compiler */
$compiler = $this->wire(new FileCompiler($config->paths->siteModules));
$compiler->clearCache(true);
$session->message($this->_('Cleared file compiler cache'));
$session->redirect('./');
}
if($input->get('update')) {
$name = $sanitizer->name($input->get('update'));
if(isset($this->modulesArray[$name])) return $this->downloadConfirm($name, true);
}
if($input->get('reset') == 1) {
$modules->refresh(true);
$this->message(sprintf($this->_('Modules cache refreshed (%d modules)'), count($modules)));
$edit = $input->get->fieldName('edit');
$duplicates = $modules->duplicates()->getDuplicates();
foreach($duplicates as $className => $files) {
$dup = $modules->duplicates()->getDuplicates($className);
if(!count($dup['files'])) continue;
$msg = sprintf($this->_('Module "%s" has multiple files (bold file is the one in use).'), $className) . ' ' .
"<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->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>&nbsp;</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 modules 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, '&lt;') !== 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->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(' &nbsp;/&nbsp; ', $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()->downloadModule($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->getModuleConfigData($moduleName);
$sinfo = self::getModuleInfo();
$this->headline(sprintf($this->_('%s raw config data'), $moduleName));
$this->breadcrumb("./", $sinfo['title']);
$this->breadcrumb("./edit?name=$moduleName", $moduleName);
/** @var InputfieldForm $form */
$form = $modules->get('InputfieldForm');
$form->attr('id', 'ModuleEditRawForm');
$form->attr('action', "edit?name=$moduleName&edit_raw=1");
$form->attr('method', 'post');
if(empty($moduleData) && !$input->is('post')) $this->warning($this->_('This module has no configuration data'));
$moduleData['_name'] = $moduleName . ' (' . $this->_('do not remove this') . ')';
unset($moduleData['submit_save_module'], $moduleData['uninstall']);
$jsonFlags = JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
$moduleDataJSON = is_array($moduleData) ? json_encode($moduleData, $jsonFlags) : array();
$rows = substr_count($moduleDataJSON, "\n") + 2;
/** @var InputfieldMarkup $f */
$f = $modules->get('InputfieldTextarea');
$f->attr('name', 'module_config_json');
$f->label = $this->_('Module config (raw/JSON)');
$f->icon = 'code';
$f->value = $moduleDataJSON;
$f->attr('style', 'font-family:monospace;white-space:nowrap');
$f->attr('rows', $rows > 5 ? $rows : 5);
$form->add($f);
/** @var InputfieldSubmit $submit */
$submit = $modules->get('InputfieldSubmit');
$submit->attr('name', 'submit_save_module_config_json');
$submit->showInHeader(true);
$submit->val($this->_('Save'));
$form->add($submit);
if(!$input->post('submit_save_module_config_json')) return $form->render();
$form->processInput($input->post);
$json = $f->val();
$data = json_decode($json, true);
if($data === null) {
$this->error($this->_('Cannot save because JSON could not be parsed (invalid JSON)'));
return $form->render();
}
if(empty($data['_name']) || strpos($data['_name'], "$moduleName ") !== 0) {
$this->error($this->_('Cannot save because JSON not recognized as valid for module'));
return $form->render();
}
$changes = array();
unset($data['_name'], $moduleData['_name']);
foreach($moduleData as $key => $value) {
if(!array_key_exists($key, $data) || $data[$key] !== $value) $changes[$key] = $key;
}
foreach($data as $key => $value) {
if(!array_key_exists($key, $moduleData) || $moduleData[$key] !== $value) $changes[$key] = $key;
}
if(count($changes)) {
$modules->saveModuleConfigData($moduleName, $data);
$this->message($this->_('Updated module config data') . ' (' . implode(', ', $changes) . ')');
} else {
$this->message($this->_('No changes detected'));
}
$session->location($form->action);
return '';
}
/**
* Build and render for the form for editing a module's settings
*
* This method saves the settings if it's form has been posted
*
* @param string $moduleName
* @param array $moduleInfo
* @return string
*
*/
protected function renderEdit($moduleName, $moduleInfo) {
$wire = $this->wire();
$adminTheme = $wire->adminTheme;
$languages = $wire->languages;
$sanitizer = $wire->sanitizer;
$modules = $wire->modules;
$session = $wire->session;
$config = $wire->config;
$input = $wire->input;
$out = '';
$moduleId = $modules->getModuleID($moduleName);
$submitSave = $input->post('submit_save_module');
$collapseInfo = '';
if($submitSave || $input->get('collapse_info') || $input->get('modal')) {
$collapseInfo = '&collapse_info=1';
}
if(!$moduleId) {
$this->error($this->_('Unknown module'));
$session->redirect('./');
return '';
}
if($input->get('refresh') == $moduleName) {
$modules->refresh();
$session->redirect("./edit?name=$moduleName$collapseInfo");
return '';
}
$sinfo = self::getModuleInfo();
$flags = $modules->getFlags($moduleName);
$allowDisabledFlag =
($config->debug && $config->advanced && ($flags & Modules::flagsAutoload)) ||
($flags & Modules::flagsDisabled);
$this->breadcrumb('./', $sinfo['title']);
$this->headline($moduleInfo['title']);
$this->browserTitle(sprintf($this->_('Module: %s'), $moduleInfo['title']));
/** @var InputfieldForm $form */
$form = $modules->get("InputfieldForm");
$form->attr('id', 'ModuleEditForm');
$form->attr('action', "edit?name=$moduleName$collapseInfo");
$form->attr('method', 'post');
$dependents = $modules->getRequiredBy($moduleName, true);
$requirements = $modules->getRequires($moduleName, false, true);
$dependentsStr = '';
$requirementsStr = '';
foreach($dependents as $name) {
$dependentsStr .= ($dependentsStr ? ', ' : '') . "<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);
}
$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->getModuleConfigData($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->getModuleConfigData($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') 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->setFlag($moduleName, Modules::flagsDisabled, true);
} else {
// remove disabled flag
if($flags & Modules::flagsDisabled) $modules->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&hellip;" : $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&amp;edit_raw=1'>" . wireIconMarkup('pencil') . ' ' . $this->_('Raw config') . "</a> &nbsp; &nbsp; " .
"<a href='./edit?name=$moduleName&amp;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;
}
}