Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Self-Hosted WordPress Plugin Updates
Drazen Bebic
Drazen BebicSubscriber

Posted on • Edited on • Originally published atbebic.dev

     

Self-Hosted WordPress Plugin Updates

So you developed your own plugin and now want to monetize on it. Since it's not free, you can't use the WordPress Plugin Repository for this purpose because it only supports free plugins. You will need to either host it on a marketplace or host it yourself. If you chose the latter and don't know how, then this guide is for you.

What will we be doing?

It takes a solid amount of effort, but it's not too complex. It basically boils down to two things:

  1. Client - Point your WordPress plugin to your own server for updates
  2. Server - Build & deploy an update server to handle said updates

The first point is rather simple. It takes a couple of hooks to modify your plugin in such a way that it points to a custom server for plugin updates.

The most effort lies in developing an update server which makes sense for you. This is also the part that is up to you on how to design it, since there is no single approach that will work for everyone. For the sake of simplicity, I have developed this as a WordPress plugin. In the past I also used an app built on the PERN stack. Anything goes.

The Client Plugin

I have created a simple plugin where everything is located in the main plugin file. Of course you can split this up into separate files, use classes, composer with autoloading, etc. But for simplicity's sake we will throw everything into one pot 🍲

Preparation

Before we start with the actual code, let's define some constants to make our life a little bit easier.

/*Plugin Name: Self-Hosted WordPress Plugin Updates - ClientDescription: Demo plugin showcasing a client plugin which updates from a custom update server.Version: 1.0.0Author: Drazen BebicAuthor URI: https://drazen.bebic.devText Domain: shwpucDomain Path: /languages*/// Current plugin version.define("SHWPUC_PLUGIN_VERSION","1.0.0");// Output of this will be// "self-hosted-plugin-updates/self-hosted-plugin-updates.php".define("SHWPUC_PLUGIN_SLUG",plugin_basename(__FILE__));// Set the server base URL. This should// be replaced with the actual URL of// your update server.define("SHWPUC_API_BASE_URL","https://example.com/wp-json/shwpus/v1");/** * Returns the plugin slug: self-hosted-plugin-updates * * @return string */functionshwpuc_get_plugin_slug(){// We split this string because we need the// slug without the fluff.list($t1,$t2)=explode('/',SHWPUC_PLUGIN_SLUG);// This will remove the ".php" from the// "self-hosted-plugin-updates.php" string// and leave us with the slug only.returnstr_replace('.php','',$t2);}
Enter fullscreen modeExit fullscreen mode

We defined a new plugin, constants for the plugin version, slug, and the base URL of our update server. Another thing we added is a function to retrieve the plugin slug, without the ".php" ending.

Package download

The very first thing we want to do is to add a filter to thepre_set_site_transient_update_plugins hook. We will modify the response for our plugin so that it checks the remote server for a newer version.

/** * Add our self-hosted auto-update plugin * to the filter transient. * * @param $transient * * @return object $transient */functionshwpuc_check_for_update($transient){// This will be "self-hosted-plugin-updates-client"$slug=shwpuc_get_plugin_slug();// Set the server base URL. This should be replaced// with the actual URL of your update server.$api_base=SHWPUC_API_BASE_URL;// This needs to be obtained from the// site settings. Somewhere set by a// setting your plugin provides.$license_i_surely_paid_for='XXX-YYY-ZZZ';// Get the remote version.$remote_version=shwpuc_get_remote_version($slug);// This is the URL the new plugin// version will be downloaded from.$download_url="$api_base/package/$slug.$remote_version.zip?license=$license_i_surely_paid_for";// If a newer version is available, add the update.if($remote_version&&version_compare(SHWPUC_PLUGIN_VERSION,$remote_version,'<')){$obj=newstdClass();$obj->slug=$slug;$obj->new_version=$remote_version;$obj->url=$download_url;$obj->package=$download_url;$transient->response[SHWPUC_PLUGIN_SLUG]=$obj;}return$transient;}// Define the alternative API for updating checkingadd_filter('pre_set_site_transient_update_plugins','shwpuc_check_for_update');
Enter fullscreen modeExit fullscreen mode

This function alone already does quite a lot of the heavy lifting, it...

  1. Retrieves the latest plugin version from the remote server.
  2. Checks if the remote version is greater than the currently installed version.
  3. Passes thelicense URL parameter to the download link.
  4. Stores update information into the transient if there is a newer version available.

Version Check

You probably noticed theshwpu_get_remote_version() function, so let's get into that now.

/** * Return the latest version of a plugin on * the remote update server. * * @return string|null $remote_version */functionshwpuc_get_remote_version($slug){$api_base=SHWPUC_API_BASE_URL;$license='XXX-YYY-ZZZ';$url="$api_base/version/$slug?license=$license";$request=wp_remote_get($url);if(!is_wp_error($request)||wp_remote_retrieve_response_code($request)===200){return$request['body'];}returnnull;}
Enter fullscreen modeExit fullscreen mode

Pretty straightforward: Send the request and pass on the response.

Plugin Information

Now our plugin knows that there is a new version, but what about the "What's new?" section and the changelog for this new fancy-pants version? Gues what? We needanother hook for this.

/** * Add our self-hosted description to the filter * * @param boolean  $false * @param array    $action * @param stdClass $arg * * @return bool|stdClass */functionshwpuc_check_info($false,$action,$arg){// This will be "self-hosted-plugin-updates"$slug=shwpuc_get_plugin_slug();// Abort early if this isn't our plugin.if($arg->slug!==$slug){returnfalse;}// Set the server base URL. This should be replaced// with the actual URL of your update server.$api_base=SHWPUC_API_BASE_URL;$license='XXX-YYY-ZZZ';$url="$api_base/info/$slug?license=$license";$request=wp_remote_get($url);if(!is_wp_error($request)||wp_remote_retrieve_response_code($request)===200){returnunserialize($request['body']);}returnnull;}// Define the alternative response for information checkingadd_filter('plugins_api','shwpuc_check_info',10,3);
Enter fullscreen modeExit fullscreen mode

This hook will trigger when you go to check the changelog of the newly available update for your plugin. Your update server needs to return information about the new version, like the description, changelog, and anything else you think is important to know.

The Update Server

Now that we covered the basics about what the client plugin should do, let's do the same for the update server. Like I said before, this part leaves a lot more room for interpretation, because it is a 3rd party application which you can design and run on anything you want. You only need to make sure that the response is compatible with WordPress.

For this demo, I decided to use a simple WordPress plugin which you would install on a regular WordPress instance. This WordPress instance will then act as your plugin update server.

Important: The client and server plugin will not work on the same WordPress instance! When the client tries to perform the update, it will automatically turn on maintenance mode on the WordPress instance, which disables the REST API, which makes the download of the new package version fail.

API routes

This server plugin will have to provide a handful of API routes which we have previously mentioned in the client plugin, and those are:

  1. /v1/version/:plugin - Used to check the latest version of the plugin.
  2. /v1/info/:plugin - Used to check the information about the latest version of the plugin.
  3. /v1/package/:plugin - Used to download the latest version of the plugin.

Registering the routes

The very first thing you need to do is to register the necessary REST API routes with WordPress. We will register one route for every endpoint mentioned previously. Pretty straightforward:

/** * Registers the routes needed by the plugins. * * @return void */functionshwpus_register_routes(){register_rest_route('shwpus/v1','/version/(?P<plugin>[\w-]+)',array(array('methods'=>WP_REST_Server::READABLE,'callback'=>'shwpus_handle_plugin_version_request','permission_callback'=>'shwpus_handle_permission_callback','args'=>array('plugin'=>array('description'=>'The plugin slug, i.e. "my-plugin"','type'=>'string',),),),));register_rest_route('shwpus/v1','/info/(?P<plugin>[\w-]+)',array(array('methods'=>WP_REST_Server::READABLE,'callback'=>'shwpus_handle_plugin_info_request','permission_callback'=>'shwpus_handle_permission_callback','args'=>array('plugin'=>array('description'=>'The plugin slug, i.e. "my-plugin"','type'=>'string',),),),));register_rest_route('shwpus/v1','/package/(?P<plugin>[\w.-]+)',array(array('methods'=>WP_REST_Server::READABLE,'callback'=>'shwpus_handle_plugin_package_request','permission_callback'=>'shwpus_handle_permission_callback','args'=>array('plugin'=>array('description'=>'The plugin slug with the version, ending in .zip, i.e. "my-plugin.2.0.0.zip"','type'=>'string',),),),));}
Enter fullscreen modeExit fullscreen mode

Permission Callback

You'll notice that I set thepermission_callback toshwpus_handle_permission_callback. This function checks whether the license your client passed along is valid, so you know that the client is actually authorized for future updates.

You could also remove this check for the version and info routes, so that everyone gets notified about new version and knows what's new, but only the customers with valid licenses can actually update. To do this simply set thepermission_callback to__return_true, which is a WordPress utility function which returnstrue right away.

Here's how our permission callback function looks like:

/** * @param WP_REST_Request $request * * @return true|WP_Error */functionshwpus_handle_permission_callback($request){$slug=$request->get_param('plugin');$license=$request->get_param('license');if($license!=='XXX-YYY-ZZZ'){returnnewWP_Error(401,'Invalid license',array('slug'=>$slug,'license'=>$license));}returntrue;}
Enter fullscreen modeExit fullscreen mode

Check the version

This route fetches the latest version of the given plugin from your database or whatever else you have. It needs to return it astext/html with nothing but the version number as a response.

/** * Finds the latest version for a given plugin. * * @param WP_REST_Request $request * * @return void */functionshwpus_handle_plugin_version_request($request){// Retrieve the plugin slug from the// request. Use this slug to find the// latest version of your plugin.$slug=$request->get_param('plugin');// This is hardcoded for demo purposes.// Normally you would fetch this from// your database or whatever other// source of truth you have.$version='1.0.1';header('Content-Type: text/html; charset=utf-8');echo$version;die();}
Enter fullscreen modeExit fullscreen mode

After you've done that, your plugin should be able to tell you that there's a new version.

New Version Update Available

Plugin Information

This is where it gets interesting. This route needs to return the plugin information in a specific structure as a serialized PHP object. If you're using Node.js don't worry - there is a nifty npm package calledphp-serialize which will let you do just that.

Since we're using PHP, there's no need for that and we can just call the PHP nativeserialize() function.

/** * Fetches information about the latest version * of the plugin with the given slug. * * @param WP_REST_Request $request * * @return void */functionshwpus_handle_plugin_info_request($request){$slug=$request->get_param('plugin');$version='1.0.1';// This data should be fetched dynamically// but for demo purposes it is hardcoded.$info=newstdClass();$info->name='Self-Hosted WordPress Plugin Updates - Client';$info->slug='self-hosted-plugin-updates-client';$info->plugin_name='self-hosted-plugin-updates-client';$info->new_version=$version;$info->requires='6.0';$info->tested='6.5.3';$info->downloaded=12540;$info->last_updated='2024-05-23';$info->sections=array('description'=>'            <h1>Self-Hosted WordPress Plugin Updates - Client</h1>            <p>                Demo plugin showcasing a client plugin                which updates from a custom update                server.            </p>        ','changelog'=>'            <h1>We did exactly 3 things!</h1>            <p>                You thought this is going to be a huge update.                But it\'s not. Sad face.            </p>            <ul>                <li>Added a cool new feature</li>                <li>Added another cool new feature</li>                <li>Fixed an old feature</li>            </ul>        ',// You can add more sections this way.'new_tab'=>'            <h1>Woah!</h1>            <p>We are so cool, we know how to add a new tab.</p>        ',);$info->url='https://drazen.bebic.dev';$info->download_link=get_rest_url(null,"/shwpus/v1/package/$slug.$version.zip");header('Content-Type: text/html; charset=utf-8');http_response_code(200);echoserialize($info);die();}
Enter fullscreen modeExit fullscreen mode

This should make your changes in the frontend visible.

Plugin Information Window

Package Download

This is where your plugin will be downloaded from. For demo purposes I simply put the plugin .zip files in apackages directory which I put intowp-content. You can of course integrate whatever other file storage you have and fetch your plugin zips from there.

/** * @param WP_REST_Request $request * * @return void */functionshwpus_handle_plugin_package_request($request){// Contains the plugin name, version, and .zip// extension. Example:// self-hosted-plugin-updates-server.1.0.1.zip$plugin=$request->get_param('plugin');// The packages are located in wp-content for// demo purposes.$file=WP_CONTENT_DIR."/packages/$plugin";if(!file_exists($file)){header('Content-Type: text/plain');http_response_code(404);echo"The file$file does not exist.";die();}$file_size=filesize($file);header('Content-Type: application/octet-stream');header("Content-Length:$file_size");header("Content-Disposition: attachment; filename=\"$plugin\"");header('Access-Control-Allow-Origin: *');http_response_code(200);readfile($file);die();}
Enter fullscreen modeExit fullscreen mode

And last but not least, your plugin can now be fully updated!

Plugin Update Complete

Conclusion

Hosting your own plugin update server is very much doable. The complexity increases with your requirements for the "backend" administration. If you need a UI then you will need to expand on the server part quite a lot.

The client part is pretty easy and straightforward, there's not much that you need to do except add a few hooks. You could go a step further and disable the plugin if there is no valid license present.

Resources

I added the source code for these two plugins into two comprehensive GitHub gists.

  1. Client Plugin
  2. Server Plugin

Top comments(3)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
usbpaul profile image
Paul Bakker
Java developer and WordPress enthousiast.
  • Joined
• Edited on• Edited

Thanks for your article.
Maybe you could add to it how the 'shwpus_register_routes' action could be added to the 'rest_api_init' hook.
I.e. with something like

add_action( 'rest_api_init', 'shwpus_register_routes');
Enter fullscreen modeExit fullscreen mode
CollapseExpand
 
drazenbebic profile image
Drazen Bebic
I’m a Fullstack developer balancing code, family life, and gaming. By day, I’m building apps; by night, I’m a dad, husband, and gamer. Who needs sleep when you’ve got caffeine, code, and chaos?
  • Location
    Vienna, Austria
  • Work
    Moonshiner
  • Joined

Very good point, thank you! I'll update the article ASAP.

btw, have you managed to get it working? Any difficulties?

CollapseExpand
 
usbpaul profile image
Paul Bakker
Java developer and WordPress enthousiast.
  • Joined

Not finished with my implementation yet. Will let you know.

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

I’m a Fullstack developer balancing code, family life, and gaming. By day, I’m building apps; by night, I’m a dad, husband, and gamer. Who needs sleep when you’ve got caffeine, code, and chaos?
  • Location
    Vienna, Austria
  • Work
    Moonshiner
  • Joined

More fromDrazen Bebic

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp