ライセンスキー認証でプラグインの更新を制御

はじめに

先日、Twitter 上でテーマの再販によるトラブルが話題になっていました。私の販売しているアドオンは GPL 100% なので再販されても罪に問えません。しかし、GPL の範囲内で必要最低限の再販などの防御策をとろうと思い尋ねたところ、「プラグインアップデートの更新通知をライセンスキーで制御」が、一般的な方法であると教わりコードを書いて運用し始めました。コードを掲載します。ある程度汎用的に作ってあるので、コンストラクタとショートコード内の情報を変更して記せば動作すると思います。

ライセンスキー認証

ライセンスキー認証については、以前書いた記事の方法で行います。ライセンスキーに関して間に合っている方は、飛ばして次の「プラグイン更新通知の制御」を読んでください。

Software License Manager Client の「license-checker/class-slmclient.php」と「license-checker/jquery.slmclient.js」を売りたいプラグインのベースディレクトリ直下に配置します。プラグインのベースディレクトリにあるプラグインのベースファイルには以下を記述します。

/* license checker */
if ( ! class_exists( 'SlmClient' ) ) {
	require_once dirname( __FILE__ ) . '/license-checker/class-slmclient.php';
}
if ( ! class_exists( 'LicenseChecker' ) ) {
	require_once dirname( __FILE__ ) . '/license-checker/class-licensechecker.php';
}

次に、同じフォルダ「license-checker」内に「class-licensechecker.php」をコーディングしていきます。

まずは、コンストラクタ

$licensechecker = new LicenseChecker();

/** ==================================================
 * Class Main function
 *
 * @since 1.00
 */
class LicenseChecker {

	/** ==================================================
	 * plugin slug
	 *
	 * @var $plugin_slug  plugin_slug.
	 */
	private $plugin_slug;

	/** ==================================================
	 * plugin base name
	 *
	 * @var $plugin_base_name  plugin_base_name.
	 */
	private $plugin_base_name;

	/** ==================================================
	 * License attribute
	 *
	 * @var $license_atts  license_atts.
	 */
	private $license_atts;

	/** ==================================================
	 * URL of metadata json
	 *
	 * @var $metadata_json_url  metadata_json_url.
	 */
	private $metadata_json_url;

	/** ==================================================
	 * Construct
	 *
	 * @since 1.00
	 */
	public function __construct() {

		/* main plugin path name */
		/* 配布するプラグインのスラグとベースネームを決めます */
		$this->plugin_slug = 'myplugin-add-on';
		$this->plugin_base_name = $this->plugin_slug . '/mypluginaddon.php';

		/* for license */
		/* ライセンスキー用の配列の値を決めます */
		$this->license_atts = array(
			'item_reference' => $this->plugin_slug,
			'license_server_url' => 'https://license.test.com/',
			'special_secretkey' => '5d1291a361ef14.63243498',
		);

		/* ライセンスキー入力のページのアクションフック */
		add_action( 'plugin_license_key_page', array( $this, 'license_credit_page' ) );

		/* Software License Manager Client のクラスを読み込んでおきます */
		if ( ! class_exists( 'SlmClient' ) ) {
			require_once( dirname( __FILE__ ) . '/class-slmclient.php' );
		}

		/* text for license credit page by shortcode */
		/* ライセンスキー入力ページのショートコードのテキスト部分のフィルター */
		add_filter(
			'slmclient_activate_title',
			function() {
				return 'ライセンス管理';
			},
			10,
			1
		);
		add_filter(
			'slmclient_activate_enter_license_key',
			function() {
				return '有効化するには、この商品のライセンスキーを入力してください。アイテムを購入したときにメールでライセンスキーが与えられます。';
			},
			10,
			1
		);
		add_filter(
			'slmclient_activate_purchase_button',
			function() {
				return '購入';
			},
			10,
			1
		);
		add_filter(
			'slmclient_activate_activated',
			function() {
				return 'この商品は有効化されています。';
			},
			10,
			1
		);
		add_filter(
			'slmclient_activate_license_key',
			function() {
				return 'ライセンスキー';
			},
			10,
			1
		);
		add_filter(
			'slmclient_activate_activate_button',
			function() {
				return '有効化';
			},
			10,
			1
		);
		add_filter(
			'slmclient_activate_confirm_button',
			function() {
				return '確認';
			},
			10,
			1
		);

	}

ライセンスキー入力ページ、Software License Manager Client のショートコードを挿入しアクションフックでプラグインの管理画面に読み込めます。

	/** ==================================================
	 * License Credit
	 *
	 * @since 1.00
	 */
	public function license_credit_page() {

		if ( class_exists( 'SlmClient' ) ) {
			echo do_shortcode( '[slmcl item_reference="' . $this->license_atts['item_reference'] . '" sales_site_url="https://shop.test.com/myplugin-add-on/" license_server_url="' . $this->license_atts['license_server_url'] . '" special_secretkey="' . $this->license_atts['special_secretkey'] . '"]' );
		}

	}

プラグイン更新通知の制御

コンストラクタに以下を追加

	/** ==================================================
	 * Construct
	 *
	 * @since 1.00
	 */
	public function __construct() {

		/* for plugin update data */
		/* 以前使用していた、Plugin Update Checker 用のメタデータの JSON ファイルの URL を決めます */
		/* 便利なのでそのまま流用します  */
		/* https://github.com/YahnisElsts/plugin-update-checker */
		$this->metadata_json_url = 'https://test.com/myplugin-add-on/metadata.json';

		/* プラグインの「詳細表示」は、wordpress.org を読んでプラグインの情報を出力します */
		/* 自作プラグインは、呼べないのでこのフィルターに情報を渡してあげます */
		add_filter( 'plugins_api', array( $this, 'plugins_api_handler' ), 10, 3 );

		/* 以下の2つのフィルターはプラグインの更新情報をフックします */
		/* バージョンが変更された時点で更新通知を出します */
		/* ライセンス認証の有無で分岐させ更新通知の有無を決めます */
		add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_for_update' ) );
		add_filter( 'pre_set_transient_update_plugins', array( $this, 'check_for_update' ) );

	}

Plugin Update Checker 用のメタデータの JSON ファイルの例

{
	"name" : "My Plugin Add On",
	"version" : "1.01",
	"download_url" : "https://test.com/myplugin-add-on/myplugin-add-on.zip",
	"homepage": "https://shop.test.com/myplugin-add-on/",
	"requires": "4.6",
	"tested": "5.6",
	"last_updated": "2020-10-13 23:20:00",
	"author" : "Katsushi Kawamori",
	"author_homepage": "https://test.com/",
	"slug" : "myplugin-add-on",
	"sections" : {
		"description" : "This add-on can register and execute Cron Event by \"My Plugin\"",
		"changelog" : "A license checker for updates has been introduced."
	},
	"icons" : {
		"2x" : "https://ps.w.org/myplugin/assets/icon-256x256.png"
    },
	"banners": {
		"low": "https://ps.w.org/myplugin/assets/banner-772x250.png",
		"high": "https://ps.w.org/myplugin/assets/banner-1544x500.png"
	}
}

プラグインのメタデータの JSON ファイルをパースしてプラグインのデータを返す関数

	/** ==================================================
	 * Get plugin info for json file
	 *
	 * @param string $url   url.
	 * @return object $new_plugin_data
	 * @since 1.00
	 */
	public function get_new_plugin_info( $url ) {

		/* for the wp_remote_get(). */
		$options = array(
			'timeout' => 10, /* seconds */
			'headers' => array(
				'Accept' => 'application/json',
			),
		);
		$response = wp_remote_get( $url, $options );

		/* Check for error in the response */
		if ( is_wp_error( $response ) ) {
			return 'no_response';
		}

		/* License data. */
		$new_plugin_data = json_decode( wp_remote_retrieve_body( $response ) );

		return $new_plugin_data;

	}

アップデートをチェックし、更新通知の有無を振り分ける関数

	/** ==================================================
	 * Check license for update
	 *
	 * @param object $transient  transient.
	 * @since 1.00
	 */
	public function check_for_update( $transient ) {

		if ( empty( $transient->checked ) ) {
			return $transient;
		}

		if ( ! empty( $transient->checked[ $this->plugin_base_name ] ) ) {

			/* Software License Manager Client の関数でライセンス認証をチェックします */
			$slmclient = new SlmClient();
			$licensed = $slmclient->is_licensed( $this->license_atts );

			/* ライセンス認証されていたらプラグインの情報を渡します */
			/* 認証されていないと情報が渡らないので更新の有無のチェックができません */
			if ( $licensed ) {

				if ( ! function_exists( 'get_plugin_data' ) ) {
					require_once( ABSPATH . 'wp-admin/includes/plugin.php' );
				}
				/* current plugin */
				$plugin_file_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $this->plugin_base_name );

				/* new plugin */
				$new_plugin_data = $this->get_new_plugin_info( $this->metadata_json_url );

				if ( is_object( $new_plugin_data ) ) {
					if ( property_exists( $new_plugin_data, 'version' ) &&
							property_exists( $new_plugin_data, 'slug' ) &&
							property_exists( $new_plugin_data, 'homepage' ) &&
							property_exists( $new_plugin_data, 'download_url' ) ) {
						$slug = $new_plugin_data->slug;
						$version = $new_plugin_data->version;
						$homepage = $new_plugin_data->homepage;
						$download_url = $new_plugin_data->download_url;
						$icons = array();
						if ( property_exists( $new_plugin_data, 'icons' ) ) {
							$icons = (array) $new_plugin_data->icons;
						}
					}
				} else if ( 'no_response' === $new_plugin_data ) {
					return $transient;
				}

				if ( version_compare( $plugin_file_data['Version'], $version, '<' ) ) {
					$plugin_data = array(
						'slug'        => $slug,
						'plugin'      => $plugin_file_data,
						'new_version' => $version,
						'url'         => $homepage,
						'package'     => $download_url,
						'icons'       => $icons,
					);
					/* set transient */
					$transient->response[ $this->plugin_base_name ] = (object) $plugin_data;
				}
			}
		}

		return $transient;

	}

プラグインの情報を整形して、プラグインの「詳細を表示」に対応させる関数

	/** ==================================================
	 * WordPress "plugins_api" filter
	 *
	 * @param bool|object $res  The result object, or false (= default value).
	 * @param string      $action  The Plugins API action. We're interested in 'plugin_information'.
	 * @param array       $args  The Plugins API parameters.
	 *
	 * @return object|false  API response.
	 */
	public function plugins_api_handler( $res, $action, $args ) {

		if ( 'plugin_information' == $action ) {
			/* If the request is for this plugin */
			if ( isset( $args->slug ) && plugin_basename( $this->plugin_slug ) == $args->slug ) {

				/* new plugin */
				$new_plugin_data = $this->get_new_plugin_info( $this->metadata_json_url );

				/* api input */
				if ( property_exists( $new_plugin_data, 'name' ) ) {
					$name = $new_plugin_data->name;
				} else {
					$name = null;
				}
				if ( property_exists( $new_plugin_data, 'author' ) ) {
					$author_link = $new_plugin_data->author;
					if ( property_exists( $new_plugin_data, 'author_homepage' ) ) {
						$author_link = '<a href="' . $new_plugin_data->author_homepage . '">' . $new_plugin_data->author . '</a>';
					}
				} else {
					$author_link = null;
				}
				if ( property_exists( $new_plugin_data, 'tested' ) ) {
					$tested = $new_plugin_data->tested;
				} else {
					$tested = null;
				}
				if ( property_exists( $new_plugin_data, 'requires' ) ) {
					$requires = $new_plugin_data->requires;
				} else {
					$requires = null;
				}
				if ( property_exists( $new_plugin_data, 'last_updated' ) ) {
					$last_updated = $new_plugin_data->last_updated;
				} else {
					$last_updated = null;
				}
				if ( property_exists( $new_plugin_data, 'homepage' ) ) {
					$homepage = $new_plugin_data->homepage;
				} else {
					$homepage = null;
				}
				if ( property_exists( $new_plugin_data, 'sections' ) ) {
					$sections = (array) $new_plugin_data->sections;
				} else {
					$sections = array(
						'description' => null,
						'changelog' => null,
					);
				}
				if ( property_exists( $new_plugin_data, 'banners' ) ) {
					$banners = (array) $new_plugin_data->banners;
				} else {
					$banners = array(
						'low' => null,
						'high' => null,
					);
				}

				$res = (object) array(
					'name' => $name,
					'author' => $author_link,
					'version' => $new_plugin_data->version,
					'slug' => $new_plugin_data->slug,
					'download_link' => $new_plugin_data->download_url,
					'tested' => $tested,
					'requires' => $requires,
					'last_updated' => $last_updated,
					'homepage' => $homepage,
					'sections' => $sections,
					'banners' => $banners,
					'external' => true,
				);

				return $res;
			}
		}

		return false;

	}

全コード(/license-checker/class-licensechecker.php)

<?php
/**
 * License Key Checker
 *
 * @package    License Key Checker
 * @subpackage License Key Checker
/**
	Copyright (c) 2020- Katsushi Kawamori (email : dodesyoswift312@gmail.com)
	This program is free software; you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation; version 2 of the License.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

$licensechecker = new LicenseChecker();

/** ==================================================
 * Class Main function
 *
 * @since 1.00
 */
class LicenseChecker {

	/** ==================================================
	 * plugin slug
	 *
	 * @var $plugin_slug  plugin_slug.
	 */
	private $plugin_slug;

	/** ==================================================
	 * plugin base name
	 *
	 * @var $plugin_base_name  plugin_base_name.
	 */
	private $plugin_base_name;

	/** ==================================================
	 * License attribute
	 *
	 * @var $license_atts  license_atts.
	 */
	private $license_atts;

	/** ==================================================
	 * URL of metadata json
	 *
	 * @var $metadata_json_url  metadata_json_url.
	 */
	private $metadata_json_url;

	/** ==================================================
	 * Construct
	 *
	 * @since 1.00
	 */
	public function __construct() {

		/* main plugin path name */
		$this->plugin_slug = 'myplugin-add-on';
		$this->plugin_base_name = $this->plugin_slug . '/mypluginaddon.php';

		/* for license */
		$this->license_atts = array(
			'item_reference' => $this->plugin_slug,
			'license_server_url' => 'https://license.test.com/',
			'special_secretkey' => '5d1291a361ef14.63243498',
		);

		/* for plugin update data */
		$this->metadata_json_url = 'https://test.com/myplugin-add-on/metadata.json';

		add_action( 'plugin_license_key_page', array( $this, 'license_credit_page' ) );

		if ( ! class_exists( 'SlmClient' ) ) {
			require_once( dirname( __FILE__ ) . '/class-slmclient.php' );
		}

		add_filter( 'plugins_api', array( $this, 'plugins_api_handler' ), 10, 3 );

		add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_for_update' ) );
		add_filter( 'pre_set_transient_update_plugins', array( $this, 'check_for_update' ) );

		/* text for license credit page by shortcode */
		add_filter(
			'slmclient_activate_title',
			function() {
				return 'ライセンス管理';
			},
			10,
			1
		);
		add_filter(
			'slmclient_activate_enter_license_key',
			function() {
				return '有効化するには、この商品のライセンスキーを入力してください。アイテムを購入したときにメールでライセンスキーが与えられます。';
			},
			10,
			1
		);
		add_filter(
			'slmclient_activate_purchase_button',
			function() {
				return '購入';
			},
			10,
			1
		);
		add_filter(
			'slmclient_activate_activated',
			function() {
				return 'この商品は有効化されています。';
			},
			10,
			1
		);
		add_filter(
			'slmclient_activate_license_key',
			function() {
				return 'ライセンスキー';
			},
			10,
			1
		);
		add_filter(
			'slmclient_activate_activate_button',
			function() {
				return '有効化';
			},
			10,
			1
		);
		add_filter(
			'slmclient_activate_confirm_button',
			function() {
				return '確認';
			},
			10,
			1
		);

	}

	/** ==================================================
	 * License Credit
	 *
	 * @since 1.00
	 */
	public function license_credit_page() {

		if ( class_exists( 'SlmClient' ) ) {
			echo do_shortcode( '[slmcl item_reference="' . $this->license_atts['item_reference'] . '" sales_site_url="https://shop.test.com/myplugin-add-on/" license_server_url="' . $this->license_atts['license_server_url'] . '" special_secretkey="' . $this->license_atts['special_secretkey'] . '"]' );
		}

	}

	/** ==================================================
	 * Check license for update
	 *
	 * @param object $transient  transient.
	 * @since 1.00
	 */
	public function check_for_update( $transient ) {

		if ( empty( $transient->checked ) ) {
			return $transient;
		}

		if ( ! empty( $transient->checked[ $this->plugin_base_name ] ) ) {

			$slmclient = new SlmClient();
			$licensed = $slmclient->is_licensed( $this->license_atts );

			if ( $licensed ) {

				if ( ! function_exists( 'get_plugin_data' ) ) {
					require_once( ABSPATH . 'wp-admin/includes/plugin.php' );
				}
				/* current plugin */
				$plugin_file_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $this->plugin_base_name );

				/* new plugin */
				$new_plugin_data = $this->get_new_plugin_info( $this->metadata_json_url );

				if ( is_object( $new_plugin_data ) ) {
					if ( property_exists( $new_plugin_data, 'version' ) &&
							property_exists( $new_plugin_data, 'slug' ) &&
							property_exists( $new_plugin_data, 'homepage' ) &&
							property_exists( $new_plugin_data, 'download_url' ) ) {
						$slug = $new_plugin_data->slug;
						$version = $new_plugin_data->version;
						$homepage = $new_plugin_data->homepage;
						$download_url = $new_plugin_data->download_url;
						$icons = array();
						if ( property_exists( $new_plugin_data, 'icons' ) ) {
							$icons = (array) $new_plugin_data->icons;
						}
					}
				} else if ( 'no_response' === $new_plugin_data ) {
					return $transient;
				}

				if ( version_compare( $plugin_file_data['Version'], $version, '<' ) ) {
					$plugin_data = array(
						'slug'        => $slug,
						'plugin'      => $plugin_file_data,
						'new_version' => $version,
						'url'         => $homepage,
						'package'     => $download_url,
						'icons'       => $icons,
					);
					/* set transient */
					$transient->response[ $this->plugin_base_name ] = (object) $plugin_data;
				}
			}
		}

		return $transient;

	}

	/** ==================================================
	 * Get plugin info for json file
	 *
	 * @param string $url   url.
	 * @return object $new_plugin_data
	 * @since 1.00
	 */
	public function get_new_plugin_info( $url ) {

		/* for the wp_remote_get(). */
		$options = array(
			'timeout' => 10, /* seconds */
			'headers' => array(
				'Accept' => 'application/json',
			),
		);
		$response = wp_remote_get( $url, $options );

		/* Check for error in the response */
		if ( is_wp_error( $response ) ) {
			return 'no_response';
		}

		/* License data. */
		$new_plugin_data = json_decode( wp_remote_retrieve_body( $response ) );

		return $new_plugin_data;

	}

	/** ==================================================
	 * WordPress "plugins_api" filter
	 *
	 * @param bool|object $res  The result object, or false (= default value).
	 * @param string      $action  The Plugins API action. We're interested in 'plugin_information'.
	 * @param array       $args  The Plugins API parameters.
	 *
	 * @return object|false  API response.
	 */
	public function plugins_api_handler( $res, $action, $args ) {

		if ( 'plugin_information' == $action ) {
			/* If the request is for this plugin */
			if ( isset( $args->slug ) && plugin_basename( $this->plugin_slug ) == $args->slug ) {

				/* new plugin */
				$new_plugin_data = $this->get_new_plugin_info( $this->metadata_json_url );

				/* api input */
				if ( property_exists( $new_plugin_data, 'name' ) ) {
					$name = $new_plugin_data->name;
				} else {
					$name = null;
				}
				if ( property_exists( $new_plugin_data, 'author' ) ) {
					$author_link = $new_plugin_data->author;
					if ( property_exists( $new_plugin_data, 'author_homepage' ) ) {
						$author_link = '<a href="' . $new_plugin_data->author_homepage . '">' . $new_plugin_data->author . '</a>';
					}
				} else {
					$author_link = null;
				}
				if ( property_exists( $new_plugin_data, 'tested' ) ) {
					$tested = $new_plugin_data->tested;
				} else {
					$tested = null;
				}
				if ( property_exists( $new_plugin_data, 'requires' ) ) {
					$requires = $new_plugin_data->requires;
				} else {
					$requires = null;
				}
				if ( property_exists( $new_plugin_data, 'last_updated' ) ) {
					$last_updated = $new_plugin_data->last_updated;
				} else {
					$last_updated = null;
				}
				if ( property_exists( $new_plugin_data, 'homepage' ) ) {
					$homepage = $new_plugin_data->homepage;
				} else {
					$homepage = null;
				}
				if ( property_exists( $new_plugin_data, 'sections' ) ) {
					$sections = (array) $new_plugin_data->sections;
				} else {
					$sections = array(
						'description' => null,
						'changelog' => null,
					);
				}
				if ( property_exists( $new_plugin_data, 'banners' ) ) {
					$banners = (array) $new_plugin_data->banners;
				} else {
					$banners = array(
						'low' => null,
						'high' => null,
					);
				}

				$res = (object) array(
					'name' => $name,
					'author' => $author_link,
					'version' => $new_plugin_data->version,
					'slug' => $new_plugin_data->slug,
					'download_link' => $new_plugin_data->download_url,
					'tested' => $tested,
					'requires' => $requires,
					'last_updated' => $last_updated,
					'homepage' => $homepage,
					'sections' => $sections,
					'banners' => $banners,
					'external' => true,
				);

				return $res;
			}
		}

		return false;

	}

}


この記事を書いた人