Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

License checking and Bitbucket #199

Closed
froger-me opened this issue Apr 29, 2018 · 3 comments

Comments

@froger-me
Copy link

@froger-me froger-me commented Apr 29, 2018

Hi Jānis,

I am attempting to integrate plugin-update-checker, wp-update-server, and Software License Manager.
I have the license part figured out and working. The issue is with the update process itself.

I am using Bitbucket to store the releases of my plugin, and have a consumer properly set up. I have followed your post on how to secure plugin updates but it doesn't really say much about its compatibility with APIs.

Here are my issues (refer to the full code below):

  • CustomServer::initRequest is never called
  • CustomServer::filterMetadata is never called
  • CustomServer::checkAuthorization is never called
  • CustomServer::verifyLicenseExists is never called
  • CustomServer::isLicenseValid is never called
  • my_plugin_filter_update_checks is never called

... and the plugin gets updated even without license (valid or not).

Is it because of $update_checker is using Bitbucket?
I am not sure to understand - should the repo and consumer info be on the server side instead, and transmitted to the client after the license validity has been confirmed? If yes, how?

The code below is for reference - it is a work in progress, but because the functions of the custom server are never called, I can't even test the server side .

Server side

Plugin file:

/*
Plugin Name: WP Plugin Update Server
Plugin URI: https://froger.me/
Description: Update server for custom plugins.
Version: 1.0
Author: Alexandre Froger
Author URI: https://froger.me/
*/

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

if ( ! defined( 'WP_PUS_PLUGIN_PATH' ) ) {
	define( 'WP_PUS_PLUGIN_PATH', plugin_dir_path( __FILE__ ) );
}

if ( ! defined( 'WP_PUS_PLUGIN_URL' ) ) {
	define( 'WP_PUS_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
}

function wp_pus_run() {
        require_once ABSPATH . 'wp-admin/includes/plugin.php';
	require_once WP_PUS_PLUGIN_PATH . 'lib/wp-update-server/loader.php';
	require_once WP_PUS_PLUGIN_PATH . 'inc/class-wp-plugin-update-server.php';
	require_once WP_PUS_PLUGIN_PATH . 'lib/custom-server.php';

	$wp_plugin_update_server = new WP_Plugin_Update_Server();
}
add_action( 'plugins_loaded', 'wp_pus_run', 5, 0 );

Plugin main class class-wp-plugin-update-server.php:

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WP_Plugin_Update_Server {
	protected $update_server;

	public function __construct() {
		error_log('here1');
		$this->update_server = new CustomServer( home_url( '/' ) );

		add_filter( 'query_vars', array( $this, 'addquery_variables' ) );
		add_action( 'template_redirect', array( $this, 'handle_update_api_request' ) );
	}

	public function addquery_variables( $query_variables ) {
		$query_variables = array_merge( $query_variables, array(
			'update_action',
			'update_slug',
			'update_secret_key',
			'update_license_key',
		) );

		return $query_variables;
	}

	public function handle_update_api_request() {

		if ( get_query_var( 'update_action' ) ) {
			$this->update_server->handleRequest( array_merge( $_GET, array(
				'action'      => get_query_var( 'update_action' ),
				'slug'        => get_query_var( 'update_slug' ),
				'secret_key'  => get_query_var( 'update_secret_key' ),
				'license_key' => get_query_var( 'update_license_key' ),
			) ) );
		}
	}
}

Plugin custom server class custom-server.php:

<?php

class CustomServer extends Wpup_UpdateServer {

	public function __construct( $serverUrl ) {
		parent::__construct( $serverUrl );
	}

	protected function initRequest( $query = null, $headers = null ) {
		$request = parent::initRequest( $query, $headers );

		//Load the license, if any.
		$license = null;

		if ( $request->param( 'license_key' ) && $request->param( 'secret_key' ) ) {
			$result = $this->verifyLicenseExists(
				$request->slug,
				$request->param( 'license_key' ),
				$request->param( 'secret_key' )
			);

			$request->license = $result;
		}

		return $request;
	}

	protected function filterMetadata( $meta, $request ) {
		$meta = parent::filterMetadata( $meta, $request );

		//Include license information in the update metadata. This saves an HTTP request
		//or two since the plugin doesn't need to explicitly fetch license details.
		$license = $request->license;

		if ( null !== $license ) {
			$meta['license'] = $this->prepareLicenseForOutput( $license );
		}

		//Only include the download URL if the license is valid.
		if ( $license && $this->isLicenseValid( $license ) ) {
			//Append the license key or to the download URL.
			$args                 = array( 'license_key' => $request->param( 'license_key' ) );
			$meta['download_url'] = self::addQueryArg( $args, $meta['download_url'] );
		} else {
			//No license = no download link.
			unset( $meta['download_url'] );
		}

		return $meta;
	}

	protected function checkAuthorization($request) {
		parent::checkAuthorization( $request );

		//Prevent download if the user doesn't have a valid license.
		$license = $request->license;

		if ( 'download' === $request->action && ! ( $license && $this->isLicenseValid( $license ) ) ) {

			if ( ! isset( $license ) ) {
				$message = 'You must provide a license key to download this plugin.';
			} else {
				$error   = $license->get( 'error' );
				$message = isset( $error ) ? $error : 'Sorry, your license is not valid.';
			}
			$this->exitWithError( $message, 403 );
		}
	}

	private function verifyLicenseExists($slug, $license_key, $secret_key) {
		$result = null;

		if ( is_plugin_active( 'software-license-manager/slm_bootstrap.php' ) ) {
			$api_params = array(
				'slm_action'  => 'slm_check',
				'secret_key'  => $secret_key,
				'license_key' => $license_key,
			);
			$response   = wp_remote_get( add_query_arg( $api_params, $this->serverUrl ), array(
				'timeout'   => 20,
				'sslverify' => false,
			) );

			$license_data = json_decode( wp_remote_retrieve_body( $response ) );

			if ( 'error' === $license_data->result && isset( $license_data->error_code ) && 60 === absint( $license_data->error_code ) ) {
				$result = new WP_Error( 'invalid_license', $license_data->message, $license_data->data );
			} else {
				$result = $license_data->data;
			}
		} else {
			$result = new WP_Error( 'server_error', 'Could not validate the license. Please contact the developer.' );
		}

		return $result;
	}

	private function prepareLicenseForOutput( $license ) {

		if ( is_plugin_active( 'software-license-manager/slm_bootstrap.php' ) ) {
			// TODO
		}

		return null;
	}

	private function isLicenseValid( $license ) {
		$valid = false;

		if ( $license && ! is_wp_error( $license ) && is_plugin_active( 'software-license-manager/slm_bootstrap.php' ) ) {

			if ( 'active' === $license->status ) {
				$valid = true;
			}
		}

		return $valid;
	}
}

Client side

Code added to my plugin in an included updater.php:

$slug  = 'my-plugin';
$repo_url = 'https://bitbucket.org/user/my-plugin';

require MY_PLUGIN_PATH . 'lib/plugin-update-checker/plugin-update-checker.php';

$update_checker = Puc_v4_Factory::buildUpdateChecker(
	$repo_url,
	MY_PLUGIN_PATH . $slug . '.php',
	$slug
);

$update_checker->addQueryArgFilter( 'my_plugin_filter_update_checks' );
function my_plugin_filter_update_checks( $query_args ) {
	$path              = untrailingslashit( MY_PLUGIN_PATH );
	$parts             = explode( '/', $path );
	$plugin_name = end( $parts ) . '/my-plugin';

	$license = get_option( 'license_key_' . $plugin_name );

	if ( $license ) {
		$query_args['update_license_key'] = $license;
		$query_args['update_secret_key']  = MY_PLUGIN_SECRET_KEY;
	}
	return $query_args;
}

//Optional: If you're using a private repository, create an OAuth consumer
//and set the authentication credentials like this:
//Note: For now you need to check "This is a private consumer" when
//creating the consumer to work around #134:
// https://github.com/YahnisElsts/plugin-update-checker/issues/134
$update_checker->setAuthentication( array(
	'consumer_key' => 'blahblahblah',
	'consumer_secret' => 'blahblahblah',
) );

//Optional: Set the branch that contains the stable release.
$update_checker->setBranch( 'master' );
@YahnisElsts

This comment has been minimized.

Copy link
Owner

@YahnisElsts YahnisElsts commented Apr 29, 2018

The post about securing updates only applies to the case where you're running your own update server. That approach won't work with BitBucket since you can't run your custom server code on bitbucket.org.

I don't have a ready solution for combining license restrictions with BitBucket access. I've never done something like that. Providing API credentials to clients who have a valid license could work. Another option would be to proxy update requests through your own server: configure the update checker to use your server, make the custom update server load updates from BitBucket (e.g. by downloading the latest ZIP to packages), then have the server return that update info to the update checker.

@froger-me

This comment has been minimized.

Copy link
Author

@froger-me froger-me commented Apr 30, 2018

Hi Jānis!
Thank you for the prompt answer! I'll dig more in the code itself then, and explore solutions!

@YahnisElsts

This comment has been minimized.

Copy link
Owner

@YahnisElsts YahnisElsts commented Jul 6, 2019

I'm closing this issue because it has been inactive for more than a year.

If you're still interested in license management features, please comment on #222 or open a new issue.

@YahnisElsts YahnisElsts closed this Jul 6, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
2 participants
You can’t perform that action at this time.