✏️ 编辑:class-addon-plugin.php
路径:
/home/forge/kingkrunch.com/wp/wp-content/plugins/simple-history/inc/class-addon-plugin.php
大小:18.2 KB · 修改:2026-09-25 00:42:29 · 权限:0644 · 可写
← 返回目录
👁 查看
⬇ 下载
<?php namespace Simple_History; /** * Class with information and data for a plus plugin. */ class AddOn_Plugin { /** * Id of plugin, eg basenamed path + index file: "simple-history-plus-woocommerce/index.php". * * @var string */ public $id; /** * Slug of plugin, eg "simple-history-plus-woocommerce". * * @var string */ public $slug; /** * Current version of plugin, eg "1.0.0". * * @var string */ public $version; /** * Name of plugin, eg "Simple History Plus for WooCommerce". * * @var string */ public $name; /** * ID of product that this plugin is for. * Will be used to check that the entered licence key is for this product. * For example History PLUS has id 105282. * * @var int */ public $product_id; private const OPTION_PREFIX = 'simple_history_plusplugin_'; /** * Default values for the licence message option. * * @var array<string,mixed> */ private array $message_defaults = [ 'key' => null, 'key_activated' => false, 'key_instance_id' => null, 'key_created_at' => null, 'key_expires_at' => null, 'product_id' => null, 'product_name' => null, 'customer_name' => null, 'customer_email' => null, // Refreshed by every update check, see update_license_status_from_response(). // Absent from options written by older versions, so read them with ??. 'license_status' => null, 'license_valid' => null, 'license_error' => '', 'license_activation_limit' => null, 'license_activation_usage' => null, 'license_checked_at' => null, ]; /** * @param string $id Id of plugin, eg basenamed path + index file: "simple-history-plus-woocommerce/index.php". * @param string $slug Slug of plugin, eg "simple-history-plus-woocommerce". * @param string $version Current version of plugin, eg "1.0.0". * @param string $name Name of plugin, eg "Simple History Plus for WooCommerce". * @param int|null $product_id ID of product that this plugin is for. */ public function __construct( $id, $slug, $version, $name = '', $product_id = null ) { $this->id = $id; $this->slug = $slug; $this->version = $version; $this->name = $name; $this->product_id = $product_id; } /** * Get the licence key for this plugin. * * @return mixed|null Licence key, or null if no key. */ public function get_license_key() { $message = $this->get_license_message(); return $message['key'] ?? null; } /** * Get the licence message for this plugin. * * @return array<string,mixed> Licence message. */ public function get_license_message() { $stored = get_option( $this->get_license_message_option_name(), $this->message_defaults ); if ( is_array( $stored ) ) { /** @var array<string,mixed> */ return wp_parse_args( $stored, $this->message_defaults ); } return $this->message_defaults; } /** * Set the licence message for this plugin. * * @param array<string,mixed> $new_licence_message Licence message. * @return bool True if option was updated, false if not. */ public function set_licence_message( $new_licence_message ) { return update_option( $this->get_license_message_option_name(), $new_licence_message ); } /** * Merge the license object from an update-check response into the stored license message. * * The activation response is stored once and never refreshed by Lemon Squeezy, * so a renewed subscription looks expired locally. The update endpoint on * simple-history.com now returns the key's current status and expiry on every * check; this stores it next to the activation data. * * Ignores anything that is not a well-formed license object, and never * creates a license for a site that has none: a missing or null object * means "could not check", and the previous state must survive. * * @param mixed $license Decoded `license` array from the update response. * @return bool True when the option was written. */ public function update_license_status_from_response( $license ) { if ( ! is_array( $license ) ) { return false; } // checked_at is the marker that a real check happened; without it the // object is not from our endpoint. if ( ! isset( $license['checked_at'] ) || ! is_string( $license['checked_at'] ) ) { return false; } $message = $this->get_license_message(); if ( empty( $message['key'] ) || empty( $message['key_activated'] ) ) { return false; } $status = $license['status'] ?? null; if ( $status !== null && ! is_string( $status ) ) { return false; } $expires_at = $license['expires_at'] ?? null; if ( $expires_at !== null && ! is_string( $expires_at ) ) { $expires_at = null; } // An empty string means no expiry, same as a missing or null value. if ( $expires_at === '' ) { $expires_at = null; } // A string that strtotime() cannot parse is worse than no date at // all: storing it would make get_license_state() silently treat the // license as having no expiry. Reject the whole response instead. if ( is_string( $expires_at ) && strtotime( $expires_at ) === false ) { return false; } $license_error = isset( $license['error'] ) && is_string( $license['error'] ) ? $license['error'] : ''; // Cap server-supplied strings before storing: they end up in an // autoloaded option and this is the only validation they get. $message['key_expires_at'] = $this->cap_string_length( $expires_at ); $message['license_status'] = $this->cap_string_length( $status ); $message['license_valid'] = ! empty( $license['valid'] ); $message['license_error'] = $this->cap_string_length( $license_error ); $message['license_activation_limit'] = isset( $license['activation_limit'] ) && is_int( $license['activation_limit'] ) ? $license['activation_limit'] : null; $message['license_activation_usage'] = isset( $license['activation_usage'] ) && is_int( $license['activation_usage'] ) ? $license['activation_usage'] : null; $message['license_checked_at'] = $this->cap_string_length( $license['checked_at'] ); $this->set_licence_message( $message ); return true; } /** * Record that the update endpoint answered successfully without a license object. * * Lemon Squeezy was not reachable for details on this check, but the * update endpoint itself accepted the key. That is not enough to learn a * renewal's new expiry, but it is enough to clear a stored problem * verdict from an earlier check (expired, disabled, or unrecognised) so * a renewed subscriber is not told to renew until the next check fills * in the details. A stored active/inactive verdict, or no verdict at * all, is left untouched. * * @return bool True when the option was written. */ public function note_valid_key_without_details() { $message = $this->get_license_message(); if ( empty( $message['key'] ) || empty( $message['key_activated'] ) ) { return false; } $status = isset( $message['license_status'] ) && is_string( $message['license_status'] ) ? $message['license_status'] : null; $checked_at = isset( $message['license_checked_at'] ) && is_string( $message['license_checked_at'] ) ? $message['license_checked_at'] : null; $is_stored_problem = $status === 'expired' || $status === 'disabled' || ( $status === null && $checked_at !== null ); if ( ! $is_stored_problem ) { return false; } $message['license_status'] = 'active'; $message['license_error'] = ''; $message['license_checked_at'] = gmdate( 'Y-m-d\TH:i:s\Z' ); return $this->set_licence_message( $message ); } /** * Derive one license state from the stored license message. * * Prefers the status from the last update check. When no check has run * since activation, there is no way to tell an expired key from one that * has since been renewed, so it is reported as active rather than * guessed as expired from the stale activation-time date. * * @return array{ * state: string, * source: string, * expires_at: ?string, * expires_timestamp: ?int, * is_lifetime: bool, * checked_at: ?string, * error: string, * activation_limit: ?int, * activation_usage: ?int * } state is one of none, active, inactive, expired, disabled, invalid. source is one of none, activation, update_check. */ public function get_license_state() { $message = $this->get_license_message(); $state = [ 'state' => 'none', 'source' => 'none', 'expires_at' => null, 'expires_timestamp' => null, 'is_lifetime' => false, 'checked_at' => null, 'error' => '', 'activation_limit' => null, 'activation_usage' => null, ]; if ( empty( $message['key'] ) || empty( $message['key_activated'] ) ) { return $state; } $expires_at = isset( $message['key_expires_at'] ) && is_string( $message['key_expires_at'] ) ? $message['key_expires_at'] : null; $expires_timestamp = $expires_at !== null ? strtotime( $expires_at ) : false; if ( $expires_timestamp === false ) { $expires_timestamp = null; $expires_at = null; } $state['expires_at'] = $expires_at; $state['expires_timestamp'] = $expires_timestamp; $state['error'] = isset( $message['license_error'] ) && is_string( $message['license_error'] ) ? $message['license_error'] : ''; $state['activation_limit'] = isset( $message['license_activation_limit'] ) && is_int( $message['license_activation_limit'] ) ? $message['license_activation_limit'] : null; $state['activation_usage'] = isset( $message['license_activation_usage'] ) && is_int( $message['license_activation_usage'] ) ? $message['license_activation_usage'] : null; $checked_at = isset( $message['license_checked_at'] ) && is_string( $message['license_checked_at'] ) ? $message['license_checked_at'] : null; if ( $checked_at === null ) { // Never checked: we cannot tell an expired key from one that has // since been renewed, so treat it as active until a real check // says otherwise. $state['source'] = 'activation'; $state['state'] = 'active'; } else { $state['source'] = 'update_check'; $state['checked_at'] = $checked_at; $status = isset( $message['license_status'] ) && is_string( $message['license_status'] ) ? $message['license_status'] : null; if ( $status === 'active' ) { $state['state'] = 'active'; } elseif ( $status === 'inactive' ) { $state['state'] = 'inactive'; } elseif ( $status === 'expired' ) { $state['state'] = 'expired'; } elseif ( $status === 'disabled' ) { $state['state'] = 'disabled'; } else { $state['state'] = 'invalid'; } } $state['is_lifetime'] = $state['state'] === 'active' && $expires_at === null; return $state; } /** * One-sentence, plain-text description of the license state for the Licenses tab. * * An activation-source state is always `active` (see get_license_state()), * but its expiry date is the stale one recorded at activation time. When * that date has already passed, we still don't know whether the license * was renewed since, so this makes no claim about expiry: it returns an * empty string rather than an unverified "valid until" date. * * @param array<string,mixed>|null $license_state Pass an already-computed state to avoid recomputing it. Defaults to computing it. * @return string Empty when there is no license, or when the only evidence about expiry is a stale, already-past activation-time date. */ public function get_license_state_description( ?array $license_state = null ) { $state = $license_state ?? $this->get_license_state(); $date = $state['expires_timestamp'] !== null ? wp_date( get_option( 'date_format' ), $state['expires_timestamp'] ) : ''; if ( $state['source'] === 'activation' && $state['expires_timestamp'] !== null ) { if ( $state['expires_timestamp'] < time() ) { return ''; } /* translators: %s: date */ return sprintf( __( 'Valid until %s.', 'simple-history' ), $date ); } $usage = ''; if ( $state['activation_limit'] !== null && $state['activation_usage'] !== null ) { $usage = sprintf( /* translators: 1: number of sites the key is activated on, 2: number of sites the key allows. */ __( 'Activated on %1$d of %2$d sites.', 'simple-history' ), $state['activation_usage'], $state['activation_limit'] ); } switch ( $state['state'] ) { case 'active': if ( $state['is_lifetime'] ) { $text = __( 'Lifetime license.', 'simple-history' ); } else { /* translators: %s: date */ $text = sprintf( __( 'Valid until %s.', 'simple-history' ), $date ); } break; case 'inactive': $text = __( 'The key is valid but not activated on any site. Activate it again to tie it to this site.', 'simple-history' ); break; case 'expired': /* translators: %s: date */ $text = $date !== '' ? sprintf( __( 'Expired on %s.', 'simple-history' ), $date ) : __( 'License has expired.', 'simple-history' ); break; case 'disabled': $text = __( 'The key has been disabled.', 'simple-history' ); break; case 'invalid': $text = __( 'The key was not found, check it for typos.', 'simple-history' ); break; default: return ''; } return trim( $text . ' ' . $usage ); } /** * Cap a server-supplied value's length before it is stored. * * @param mixed $value Value to cap. * @return mixed The capped string, or the value unchanged when it is not a string. */ private function cap_string_length( $value ) { if ( ! is_string( $value ) ) { return $value; } // wp_check_invalid_utf8() turns an invalid byte sequence into an // empty string, which the readers of these fields already treat as // absent. return wp_check_invalid_utf8( substr( $value, 0, 255 ) ); } /** * Forget the cached update-check response for this add-on. * * Mirrors Plugin_Updater::$cache_key. Called on (de)activation so the next * check asks the server instead of replaying a stale answer. */ private function purge_updater_cache() { delete_transient( Plugin_Updater::get_cache_key_for_slug( $this->slug ) ); } /** * Get the option name for the licence message for this plugin. * * @return string Option name. */ private function get_license_message_option_name() { return self::OPTION_PREFIX . 'message_' . $this->slug; } /** * Activate a license key. * Stores API result in option. * * @param string $license_key License key to activate. * @return array<mixed>|null Array with info about key activation, or null if invalid. */ public function activate_license( $license_key ) { $activation_url = add_query_arg( array( 'license_key' => $license_key, 'instance_name' => home_url(), ), SIMPLE_HISTORY_LICENCES_API_URL . '/activate' ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get $response = wp_remote_get( $activation_url, array( 'sslverify' => false, 'timeout' => 3, ) ); if ( is_wp_error( $response ) || ( wp_remote_retrieve_response_code( $response ) !== 200 && wp_remote_retrieve_response_code( $response ) !== 400 ) || empty( wp_remote_retrieve_body( $response ) ) ) { return [ 'success' => false, 'message' => __( 'Unknown error', 'simple-history' ), ]; } $remote_body_json = json_decode( wp_remote_retrieve_body( $response ), true ); if ( is_null( $remote_body_json ) || ! is_array( $remote_body_json ) ) { return [ 'success' => false, 'message' => __( 'Unknown error', 'simple-history' ), ]; } // Bail when bad request. if ( wp_remote_retrieve_response_code( $response ) === 400 ) { $this->set_licence_message( $this->message_defaults ); $message = null; // Get error. Can be a single field (for example when licence key is not found), // or array of messages (for example when no licence_key exists in query string). if ( isset( $remote_body_json['data']['errors'] ) && is_array( $remote_body_json['data']['errors'] ) ) { $message = $remote_body_json['data']['errors'][0]['detail']; } elseif ( isset( $remote_body_json['data']['error'] ) ) { $message = $remote_body_json['data']['error']; } return [ 'success' => false, 'message' => $message, ]; } // Key was activated successfully. $message = [ 'key_activated' => true, 'key' => $remote_body_json['data']['license_key']['key'] ?? null, 'key_instance_id' => $remote_body_json['data']['instance']['id'] ?? null, 'key_created_at' => $remote_body_json['data']['instance']['created_at'] ?? null, 'key_expires_at' => $remote_body_json['data']['license_key']['expires_at'] ?? null, 'product_id' => $remote_body_json['data']['meta']['product_id'] ?? null, 'product_name' => $remote_body_json['data']['meta']['product_name'] ?? null, 'customer_name' => $remote_body_json['data']['meta']['customer_name'] ?? null, 'customer_email' => $remote_body_json['data']['meta']['customer_email'] ?? null, ]; $this->set_licence_message( $message ); $this->purge_updater_cache(); // Deactivate and bail if activation was for another product. if ( $this->product_id && $this->product_id !== $remote_body_json['data']['meta']['product_id'] ) { $this->deactivate_license(); return [ 'success' => false, 'message' => 'The license key is not valid for this plugin.', ]; } return [ 'success' => true, 'message' => 'Licence key successfully activated.', ]; } /** * Deactivate a license key. * * @return bool|null True if deactivated, null if error. */ public function deactivate_license() { $license_key = $this->get_license_key(); $licence_message = $this->get_license_message(); $instance_id = $licence_message['key_instance_id']; $activation_url = add_query_arg( array( 'license_key' => $license_key, 'instance_id' => $instance_id, ), SIMPLE_HISTORY_LICENCES_API_URL . '/deactivate' ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get $response = wp_remote_get( $activation_url, array( 'sslverify' => false, 'timeout' => 3, ) ); if ( wp_remote_retrieve_response_code( $response ) === 200 ) { $this->set_licence_message( $this->message_defaults ); $this->purge_updater_cache(); return true; } return null; } }
💾 保存
取消
保存为 UTF-8,换行统一为 LF