fbg13 Posted March 13, 2017 Posted March 13, 2017 Is it possible to update a module without it being in the module directory? The same way that one in the module directory is.
kongondo Posted March 13, 2017 Posted March 13, 2017 Not sure I follow...yes, you can update by: Pointing to a URL (e.g. GitHub download link to the module) Uploading from a local folder... ProcessWire will then update it. Watch out for folder names though, sometimes the module can end up at 'ProcessMyModule' rather than 'MyModule'. In such cases you should get an error about 'cannot redeclare class...'
fbg13 Posted March 13, 2017 Author Posted March 13, 2017 I mean going to the module page and clicking the check for updates link and having a download and update button if there's a new version. https://gfycat.com/DiligentRichAlaskanmalamute
kixe Posted March 13, 2017 Posted March 13, 2017 Read this http://modules.processwire.com/export-json/ You have the option to change the following in your config.php $config->moduleServiceURL = 'http://modules.processwire.com/export-json/'; $config->moduleServiceKey = (__NAMESPACE__ ? 'pw300' : 'pw280'); Example for a requesthttp://modules.processwire.com/export-json/FieldtypeSelectExtOption/?apikey=pw300 created from moduleServiceURL, ModuleClassName, moduleServiceKey (GET Parameter) 1
fbg13 Posted March 13, 2017 Author Posted March 13, 2017 @kixe Won't changing those options affect other modules from the directory?
kixe Posted March 13, 2017 Posted March 13, 2017 Yes for sure. Sorry for answering unclear. I just wanted to say that the information about the module version is pulled from the processwire modules directory by default. You could add a hook before ProcessModule::executeEdit() and change this only if the classname of the event object matches your specific module.
fbg13 Posted March 13, 2017 Author Posted March 13, 2017 Thank you @kixe I managed to achieve what I need. Any idea what the key (pw300/pw200) is doing? For whoever is interested. In my module I added public function init() { wire()->addHookBefore("ProcessModule::execute", $this, "changeModuleService"); } public function changeModuleService(HookEvent $event) { if($_GET["update"] === "ModuleName") { wire("config")->moduleServiceURL = "http://domain.tld/"; wire("config")->moduleServiceKey = (__NAMESPACE__ ? "pw300" : "pw280"); } } And the module autoload property is set to "autoload" => "path=/admin/module/" Now whenever the "check for updates" link is clicked it searches for a new version on domain.tld/ModuleName, this has to return a json similar to the one posted above. 3
BitPoet Posted March 13, 2017 Posted March 13, 2017 Just because it's Monday: <?php /** * ProcessWire module. * * Allow depending modules to be updated from non-standard repositories. * * In absence of this module, your module will continue to work but updates * will point at the regular repository. If you want to avoid that, add * a "requires" entry in your module * * To trigger the custom update routines, add an array "extendedUpdate" to * your getModuleInfo array. It can either contain a "url" entry with the * URL to the JSON data that describes the available update, or a "customMethod" * entry with the name of a public method in your module that returns that * URL. * * If you go the "url" way, the value there is run through wirePopulateStringTags. * You can add placeholders for all of the modules moduleInfo values, the module's * configuration values, the "moduleServiceKey" (PW base version, i.e. pw300, pw280 etc.) * and the module's name as, yeah, you guessed it, "name". * * If you use customMethod, it gets passed an associative array with all these values. * * A simple example for a module using custom updates: * * class TestModule extends WireData implements Module { * public static function getModuleInfo() { * return array( * "title" => _("Test Module"), * "summary" => _("Test for a module with a custom update location"), * "version" => "0.0.1", * "requires" => array("ModuleUpdateCustomUrl"), * "extendedUpdate" => array( * "url" => "https://bitpoet.ddns.net/update/{name}.json?apiVersion={moduleServiceKey}", * // Alternative way to generate the URL through a method in your module: * //"customMethod" => "generateUrl" * ) * ); * } * * public function generateUrl($data) { * return "https://bitpoet.ddns.net/update/" . $data['name'] . ".json?apiVersion=" . $data['moduleServiceKey']; * } * * } * * * A somewhat minimal JSON example response for the test module above: * { * "status":"success", * "class_name":"TestModule", * "module_version":"0.0.2", * "authors":[ * { * "title":"BitPoet" * }, * { * "title":"Anonymous" * } * ], * "pw_versions":[ * { * "name":"2.7" * }, * { * "name":"2.8" * }, * { * "name":"3.0" * } * ], * "download_url":"https://bitpoet.ddns.net/update/TestModule_0.0.2.zip", * "release_state":{ * "title":"Beta" * }, * "summary":"Test for a module with a custom update location", * "module_home":"https://bitpoet.ddns.org/modules/TestModule" * } * * For a complete example, see the live data from the official repo, e.g. * http://modules.processwire.com/export-json/Helloworld/?apikey=pw300 * * The response must either return a property "success" with a true value * or populate an "error" property with a description of what went wrong. * */ class ModuleUpdateCustomUrl extends WireData implements Module { public static function getModuleInfo() { return array( "title" => _("Module Update from Custom Url"), "summary" => _("Extension module that allows modules to be updated from non-standard repos."), "version" => "0.0.4", "autoload" => true, ); } public function init() { $this->addHookBefore("ProcessModule::execute", $this, "hookProcessModuleExecute_customUpdate"); } public function hookProcessModuleExecute_customUpdate(HookEvent $event) { if($this->input->get->update) { $name = $this->sanitizer->name($this->input->get->update); $info = $this->modules->getModuleInfo($name); if(! $info["extendedUpdate"]) return; if(! isset($info["extendedUpdate"]["url"]) && !isset($info["extendedUpdate"]["customMethod"])) { $this->error($this->_('Neither URL nor custom method set in extendedUpdate configuration in module info')); return; } $event->return = $this->downloadDialog($name, $info); $event->replace = true; } } public function downloadDialog($name, $info) { $redirectURL = "./edit?name=$name"; $className = $name; $cfgdata = $this->modules->getModuleConfigData($name); $params = array_merge($info, $cfgdata); $params["moduleServiceKey"] = $this->wire('sanitizer')->name($this->wire('config')->moduleServiceKey); $params["name"] = $name; if(isset($info["extendedUpdate"]["customMethod"])) { $method = $info["extendedUpdate"]["customMethod"]; $module = $this->modules->get($name); $url = $module->$method($params); } else { $url = trim(wirePopulateStringTags($info["extendedUpdate"]["url"], $params)); } $http = $this->wire(new WireHttp()); $data = $http->get($url); if(empty($data)) { $this->error($this->_('Error retrieving data from web service URL') . ' - ' . $http->getError()); return $this->session->redirect($redirectURL); } $data = json_decode($data, true); if(empty($data)) { $this->error($this->_('Error decoding JSON from web service')); return $this->session->redirect($redirectURL); } if($data['status'] !== 'success') { $this->error($this->_('Error reported by web service:') . ' ' . $this->wire('sanitizer')->entities($data['error'])); return $this->session->redirect($redirectURL); } $form = $this->buildDownloadConfirmForm($data); return $form->render(); } public function buildDownloadConfirmForm($data) { $warnings = array(); $authors = ''; foreach($data['authors'] as $author) $authors .= $author['title'] . ", "; $authors = rtrim($authors, ", "); $compat = ''; $isCompat = false; $myVersion = substr($this->wire('config')->version, 0, 3); foreach($data['pw_versions'] as $v) { $compat .= $v['name'] . ", "; if(version_compare($v['name'], $myVersion) >= 0) $isCompat = true; } $compat = trim($compat, ", "); if(!$isCompat) $warnings[] = $this->_('This module does not indicate compatibility with this version of ProcessWire. It may still work, but you may want to check with the module author.'); $form = $this->wire('modules')->get('InputfieldForm'); $form->attr('action', './download/'); $form->attr('method', 'post'); $form->attr('id', 'ModuleInfo'); $markup = $this->wire('modules')->get('InputfieldMarkup'); $form->add($markup); $installed = $this->modules->isInstalled($data['class_name']) ? $this->modules->getModuleInfoVerbose($data['class_name']) : null; $moduleVersionNote = ''; if($installed) { $installedVersion = $this->formatVersion($installed['version']); if($installedVersion == $data['module_version']) { $note = $this->_('Current installed version is already up-to-date'); $installedVersion .= ' - ' . $note; $this->message($note); $this->session->redirect("./edit?name=$data[class_name]"); } else { if(version_compare($installedVersion, $data['module_version']) < 0) { $this->message($this->_('An update to this module is available!')); } else { $moduleVersionNote = " <span class='ui-state-error-text'>(" . $this->_('older than the one you already have installed!') . ")</span>"; } } } else { $installedVersion = $this->_x('Not yet', 'install-table'); } $table = $this->wire('modules')->get('MarkupAdminDataTable'); $table->setEncodeEntities(false); $table->row(array($this->_x('Class', 'install-table'), $this->wire('sanitizer')->entities($data['class_name']))); $table->row(array($this->_x('Version', 'install-table'), $this->wire('sanitizer')->entities($data['module_version']) . $moduleVersionNote)); $table->row(array($this->_x('Installed?', 'install-table'), $installedVersion)); $table->row(array($this->_x('Authors', 'install-table'), $this->wire('sanitizer')->entities($authors))); $table->row(array($this->_x('Summary', 'install-table'), $this->wire('sanitizer')->entities($data['summary']))); $table->row(array($this->_x('Release State', 'install-table'), $this->wire('sanitizer')->entities($data['release_state']['title']))); $table->row(array($this->_x('Compatibility', 'install-table'), $this->wire('sanitizer')->entities($compat))); // $this->message("<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 ? $this->sanitizer->entities("$name $op $ver") : $this->sanitizer->entities($name); if($this->modules->isInstalled("$name$op$ver") || in_array($name, $data['installs'])) { // installed $requiresVersions[] = "$label <i class='fa fa-fw fa-thumbs-up'></i>"; } else if($this->modules->isInstalled($name)) { // installed, but version isn't adequate $installable = false; $info = $this->modules->getModuleInfo($name); $requiresVersions[] = $this->sanitizer->entities($name) . " " . $this->modules->formatVersion($info['version']) . " " . "<span class='ui-state-error-text'>" . $this->sanitizer->entities("$op $ver") . " " . "<i class='fa fa-fw fa-thumbs-down'></i></span>"; } else { // not installed at all $requiresVersions[] = "<span class='ui-state-error-text'>$label <i class='fa fa-fw fa-thumbs-down'></i></span>"; $installable = false; } } $table->row(array($this->_("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 = $this->sanitizer->entities(implode("\n", $data['installs'])); $table->row(array($this->_("Installs"), nl2br($installs))); } $links = array(); $moduleName = $this->wire('sanitizer')->entities1($data['name']); if($data['module_home']) { $moduleURL = $this->wire('sanitizer')->entities($data['forum_url']); $links[] = "<a target='_blank' href='$moduleURL'>" . $this->_('More Information') . "</a>"; } if($data['project_url']) { $projectURL = $this->wire('sanitizer')->entities($data['project_url']); $links[] = "<a target='_blank' href='$projectURL'>" . $this->_('Project Page') . "</a>"; } if($data['forum_url']) { $forumURL = $this->wire('sanitizer')->entities($data['forum_url']); $links[] = "<a target='_blank' href='$forumURL'>" . $this->_('Support Page') . "</a>"; } if(count($links)) $table->row(array($this->_x('Links', 'install-table'), implode(' / ', $links))); if($data['download_url']) { $downloadURL = $this->wire('sanitizer')->entities($data['download_url']); $table->row(array($this->_x('ZIP file', 'install-table'), $downloadURL)); $warnings[] = $this->_('Ensure that you trust the source of the ZIP file above before continuing!'); } else { $warnings[] = $this->_('This module has no download URL specified and must be installed manually.'); } foreach($warnings as $warning) { $table->row(array($this->_x('Please Note', 'install-table'), "<strong class='ui-state-error-text'> $warning</strong>")); } $markup->value = $table->render(); if($installable && $data['download_url']) { $btn = $this->wire('modules')->get('InputfieldSubmit'); $btn->attr('id+name', 'godownload'); $btn->value = $this->_("Download and install"); $btn->icon = 'cloud-download'; $btn->value .= " ($data[module_version])"; $form->add($btn); $this->session->ProcessModuleDownloadURL = $data['download_url']; $this->session->ProcessModuleClassName = $data['class_name']; } else { $this->session->remove('ProcessModuleDownloadURL'); $this->session->remove('ProcessModuleClassName'); } $btn = $this->wire('modules')->get('InputfieldButton'); $btn->attr('name', 'cancel'); $btn->href ="./edit?name=$data[class_name]"; $btn->value = $this->_("Cancel"); $btn->icon = 'times-circle'; $btn->class .= ' ui-priority-secondary'; $form->add($btn); $form->description = $this->wire('sanitizer')->entities($data['title']); return $form; } /** * 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); } } Created with massive theft of code from ProcessModule.module I'm going to toy around with this in our local environment and see if I can stitch together a nice workflow with the local git repo and a post-receive hook (once I find the time). 5
kixe Posted March 13, 2017 Posted March 13, 2017 Ask @ryan about the apikey. Maybe its just an option to limit requests in the future if needed. Currently only pw300 and pw280 is valid. I didn't check if there is a different return.
jploch Posted March 2, 2022 Posted March 2, 2022 Iam sorry to bump an old thread, but Iam currently working on a comercial module and looking for ways to make the update process as easy as possible. When I try fgb13 solution above I get this error after clicking on the "check for updates" link on my module settings page:ProcessModule: Error decoding JSON from web service My url is pointing to a zip file with my module files. Is there anything else, that needs to be done? EDIT: Ok I got it working. I had to link to a json file instead. Here is an example of how that file might look.
Juergen Posted May 4, 2023 Posted May 4, 2023 Hello I have also tried to implement this solution to one of my modules, but I cannot get it to work and there are some questions open: On 3/2/2022 at 7:00 PM, jploch said: Ok I got it working. I had to link to a json file instead. Should I create this file manually and store it inside the module folder? I have tried this, but I cannot point the update url to this file by changing the config values as pointed out in this post: On 3/13/2017 at 8:39 PM, fbg13 said: public function changeModuleService(HookEvent $event) { if($_GET["update"] === "ModuleName") { wire("config")->moduleServiceURL = "http://domain.tld/"; wire("config")->moduleServiceKey = (__NAMESPACE__ ? "pw300" : "pw280"); } } I have replaced "http://domain.tld/" with my GitHub account url "https://github.com/juergenweb/" but this points not to my manually created json file. It leads to "https://github.com/juergenweb/nameOfMyModule/?apikey=pw300" and not to "https://github.com/juergenweb/nameOfMyModule/myupdatefile.json". So the "?apikey=pw300" will not be resolved to point to the json file. Is there a special naming convention for this json file, if it should be created manually? Is this important to change too? On 3/13/2017 at 8:39 PM, fbg13 said: And the module autoload property is set to "autoload" => "path=/admin/module/" Can someone give me an example of how I should do it by using a module located on a GitHub account?
Recommended Posts
Create an account or sign in to comment
You need to be a member in order to leave a comment
Create an account
Sign up for a new account in our community. It's easy!
Register a new accountSign in
Already have an account? Sign in here.
Sign In Now