Jump to content

Updating modules


fbg13
 Share

Recommended Posts

Not sure I follow...yes, you can update by:

  1. Pointing to a URL (e.g. GitHub download link to the module)
  2. 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...'

Link to comment
Share on other sites

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 request
http://modules.processwire.com/export-json/FieldtypeSelectExtOption/?apikey=pw300

created from moduleServiceURL, ModuleClassName, moduleServiceKey (GET Parameter)

  • Like 1
Link to comment
Share on other sites

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.

Link to comment
Share on other sites

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.

  • Like 3
Link to comment
Share on other sites

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).

  • Like 5
Link to comment
Share on other sites

  • 4 years later...

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.

Link to comment
Share on other sites

  • 1 year later...

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?

Link to comment
Share on other sites

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 account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
×
  • Create New...