PluginProbe ʕ •ᴥ•ʔ
Presto Player / 4.3.1
Presto Player v4.3.1
4.3.1 4.3.0 4.2.4 4.2.3 4.2.2 4.2.0 4.2.1 trunk 1.10.0 1.10.1 1.10.2 1.11.0 1.12.0 1.13.0 1.14.0 1.14.1 1.5.10 1.5.11 1.5.12 1.5.13 1.5.14 1.5.15 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.10 1.6.11 1.6.12 1.6.13 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.8.0 1.8.1 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.11 2.0.12 2.0.13 2.0.14 2.0.15 2.0.16 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.2.0 2.2.1 2.2.2 2.2.3 2.2.3-beta1 2.3.0 2.3.1 2.3.2 2.3.3 3.0.0 3.0.0-beta1 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.1.0 3.1.1 3.1.2 3.1.3 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4
presto-player / inc / Services / License / License.php
presto-player / inc / Services / License Last commit date
License.php 1 month ago
License.php
330 lines
1 <?php
2 /**
3 * License validity check (free plugin, core-side).
4 *
5 * Caches a 24h transient `presto_player_license_state` (`'active'` | `'invalid'`)
6 * refreshed on WP's plugin-update-check cron. `Plugin::isPro()` reads it.
7 * Only path to `'invalid'`: licensing server emits `licence_status: 'expired'`.
8 *
9 * @package PrestoPlayer\Services\License
10 */
11
12 namespace PrestoPlayer\Services\License;
13
14 use PrestoPlayer\Models\Setting;
15 use PrestoPlayer\Support\Utility;
16
17 /**
18 * License validity service.
19 *
20 * Owns the read path (`isActive()`), the cron-driven refresh, the key-change
21 * reactive trigger, and the admin notice. State lives in a 24h transient —
22 * no custom cron, no model.
23 */
24 class License {
25
26 const TRANSIENT = 'presto_player_license_state';
27 const TTL = DAY_IN_SECONDS;
28 const API_URL = 'https://my.prestomade.com/index.php';
29 const PRODUCT_ID = 'presto-player-pro';
30
31 const RENEW_URL = 'https://my.prestomade.com/my-account/';
32 const PRICING_URL = 'https://prestoplayer.com/pricing/';
33
34 /**
35 * Per-request memoization. Reset on key change and refresh.
36 *
37 * @var bool|null
38 */
39 private static $cache = null;
40
41 /**
42 * Wire up hooks.
43 *
44 * @return void
45 */
46 public function register() {
47 // Gated on wp_doing_cron() inside the callback — the same filter fires
48 // on foreground admin requests too, and a 15s HTTP call would block
49 // the pageload.
50 add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'refreshOnUpdateCheck' ) );
51
52 // Any write to the license option clears the cached verdict.
53 $option = Setting::getGroupName( 'license' );
54 add_action( "update_option_{$option}", array( $this, 'onLicenseChange' ) );
55 add_action( "add_option_{$option}", array( $this, 'onLicenseChange' ) );
56
57 add_action( 'admin_notices', array( $this, 'maybeNotice' ) );
58 }
59
60 /**
61 * Licensing API endpoint — filterable for staging mirrors.
62 *
63 * @return string
64 */
65 public static function apiUrl() {
66 return (string) apply_filters( 'presto_player_license_api_url', self::API_URL );
67 }
68
69 /**
70 * Domain string sent to the licensing server.
71 *
72 * Server keys activations by exact (key, domain). MUST stay in the
73 * historical browser-context form `host[:port][/path]` without a scheme —
74 * normalizing (lowercase, slashes, default ports) orphans existing seats.
75 *
76 * preg_replace, not is_ssl(), keeps this stable across browser and
77 * WP-CLI cron contexts. Filterable for reverse proxies and domain mapping.
78 *
79 * TODO: Pro's `LicensedProduct::domain()` is a duplicate; fold into this.
80 *
81 * @return string
82 */
83 public static function canonicalDomain() {
84 $url = (string) get_bloginfo( 'wpurl' );
85 $domain = preg_replace( '#^https?://#i', '', $url );
86 return (string) apply_filters( 'presto_player_license_domain', (string) $domain );
87 }
88
89 /**
90 * Renewal CTA target — filterable.
91 *
92 * @return string
93 */
94 public static function renewUrl() {
95 return (string) apply_filters( 'presto_player_license_renew_url', self::RENEW_URL );
96 }
97
98 /**
99 * Pricing CTA target — filterable.
100 *
101 * @return string
102 */
103 public static function pricingUrl() {
104 return (string) apply_filters( 'presto_player_license_pricing_url', self::PRICING_URL );
105 }
106
107 /**
108 * Local-first read. No HTTP. Per-request memoized.
109 *
110 * False only when (a) no key is stored, or (b) the cached verdict is the
111 * literal `'invalid'`. Anything else (missing transient, malformed value)
112 * is treated as active — bias toward keeping paying customers active.
113 *
114 * Do NOT call `Plugin::isPro()` from here — it reads this method.
115 *
116 * @return bool
117 */
118 public static function isActive() {
119 if ( null !== self::$cache ) {
120 return self::$cache;
121 }
122 if ( self::isVipBuild() ) {
123 self::$cache = true;
124 return self::$cache;
125 }
126 if ( ! self::hasKey() ) {
127 self::$cache = false;
128 return self::$cache;
129 }
130 self::$cache = ( 'invalid' !== get_transient( self::TRANSIENT ) );
131 return self::$cache;
132 }
133
134 /**
135 * Whether a license key is stored (regardless of validity).
136 *
137 * @return bool
138 */
139 public static function hasKey() {
140 return '' !== (string) Setting::get( 'license', 'key' );
141 }
142
143 /**
144 * VIP builds of Pro ship without `inc/Services/License/`, so they can't go
145 * through this code path. VIP customers are entitled by WordPress.com VIP,
146 * not by a key — treat them as always active to keep `Plugin::isPro()` true
147 * and the admin notice suppressed.
148 *
149 * Detected by the same signal Pro itself uses in `Plugin::requiresLicensing()`:
150 * the absence of `\PrestoPlayer\Pro\Services\License\License`. Filterable
151 * for custom deployments and tests.
152 *
153 * @return bool
154 */
155 private static function isVipBuild() {
156 $is_vip = class_exists( '\PrestoPlayer\Pro\Plugin' )
157 && ! class_exists( '\PrestoPlayer\Pro\Services\License\License' );
158 return (bool) apply_filters( 'presto_player_license_is_vip', $is_vip );
159 }
160
161 /**
162 * Cron-only refresh hooked on WP's plugin-update-check filter.
163 *
164 * On transport failure `fetch()` returns null and we leave the transient
165 * untouched, so the prior verdict survives the outage.
166 *
167 * @param mixed $value Filter value (returned untouched).
168 * @return mixed
169 */
170 public function refreshOnUpdateCheck( $value ) {
171 if ( ! wp_doing_cron() || ! self::hasKey() ) {
172 return $value;
173 }
174
175 $verdict = $this->fetch();
176 if ( null !== $verdict && get_transient( self::TRANSIENT ) !== $verdict ) {
177 set_transient( self::TRANSIENT, $verdict, self::TTL );
178 self::$cache = null;
179 }
180
181 return $value;
182 }
183
184 /**
185 * Clears the cached verdict on any license-option write. Next cron
186 * tick reconfirms with the server.
187 *
188 * @return void
189 */
190 public function onLicenseChange() {
191 delete_transient( self::TRANSIENT );
192 self::$cache = null;
193 }
194
195 /**
196 * Render an admin notice on non-PP screens when the license is invalid.
197 *
198 * @return void
199 */
200 public function maybeNotice() {
201 if ( ! current_user_can( 'manage_options' ) ) {
202 return;
203 }
204 if ( ! class_exists( '\PrestoPlayer\Pro\Plugin' ) ) {
205 return;
206 }
207 if ( Utility::isPrestoPlayerPage() ) {
208 return;
209 }
210 if ( self::isActive() ) {
211 return;
212 }
213
214 $no_key = ! self::hasKey();
215 if ( $no_key ) {
216 // Pro installed but no key yet — yellow, points to in-app activation.
217 $notice_class = 'notice-warning';
218 $message = __( 'Activate your Presto Player Pro license to unlock Pro features and receive automatic updates.', 'presto-player' );
219 $primary_label = __( 'Activate License', 'presto-player' );
220 $primary_url = admin_url( 'admin.php?page=presto-dashboard&tab=Settings&section=license' );
221 $primary_external = false;
222 $secondary_label = __( 'Buy Subscription', 'presto-player' );
223 $secondary_url = self::pricingUrl();
224 } else {
225 // Expired — red, primary CTA renews.
226 $notice_class = 'notice-error';
227 $message = __( 'Your Presto Player Pro license has expired. Please renew to restore Pro Features, Premium Support & Automatic Plugin Updates.', 'presto-player' );
228 $primary_label = __( 'Renew Now', 'presto-player' );
229 $primary_url = self::renewUrl();
230 $primary_external = true;
231 $secondary_label = '';
232 $secondary_url = '';
233 }
234 ?>
235 <div class="notice <?php echo esc_attr( $notice_class ); ?> is-dismissible">
236 <p><strong><?php esc_html_e( 'Presto Player', 'presto-player' ); ?></strong></p>
237 <p><?php echo esc_html( $message ); ?></p>
238 <p>
239 <a href="<?php echo esc_url( $primary_url ); ?>" class="button button-primary"<?php echo $primary_external ? ' target="_blank" rel="noopener noreferrer"' : ''; ?>><?php echo esc_html( $primary_label ); ?></a>
240 <?php if ( ! empty( $secondary_label ) ) : ?>
241 <a href="<?php echo esc_url( $secondary_url ); ?>" class="button" target="_blank" rel="noopener noreferrer" style="margin-left: 6px;"><?php echo esc_html( $secondary_label ); ?></a>
242 <?php endif; ?>
243 </p>
244 </div>
245 <?php
246 }
247
248 /**
249 * Fetch fresh license verdict from the server.
250 *
251 * Returns `'active'` or `'invalid'` on an authoritative response, or null
252 * if the response says nothing about the license itself (transport failure,
253 * request-level error, malformed body) — caller preserves cache.
254 *
255 * Uses `activate` because `status-check` currently fatals on the
256 * prestomade.com server for non-active keys. With `canonicalDomain()`
257 * matching Pro's domain, `activate` is idempotent (returns `s101`).
258 *
259 * Instance method so tests can substitute it via Mockery partials.
260 *
261 * @return string|null
262 */
263 protected function fetch() {
264 $resp = wp_remote_get(
265 add_query_arg(
266 array(
267 'woo_sl_action' => 'activate',
268 'licence_key' => (string) Setting::get( 'license', 'key' ),
269 'product_unique_id' => self::PRODUCT_ID,
270 'domain' => self::canonicalDomain(),
271 ),
272 self::apiUrl()
273 ),
274 array( 'timeout' => 15 )
275 );
276
277 if ( is_wp_error( $resp ) || 200 !== (int) wp_remote_retrieve_response_code( $resp ) ) {
278 $this->logTransportFailure(
279 is_wp_error( $resp )
280 ? $resp->get_error_code()
281 : 'http_' . wp_remote_retrieve_response_code( $resp )
282 );
283 return null;
284 }
285
286 $body = json_decode( wp_remote_retrieve_body( $resp ) );
287 $first = is_array( $body ) ? ( isset( $body[0] ) ? $body[0] : null ) : $body;
288
289 if ( ! is_object( $first ) ) {
290 $this->logTransportFailure( 'malformed_response' );
291 return null;
292 }
293
294 // Only `licence_status: 'expired'` invalidates. To lock on a new state
295 // (e.g. blocked, suspended), extend this check.
296 if ( isset( $first->licence_status ) ) {
297 return ( 'expired' === $first->licence_status ) ? 'invalid' : 'active';
298 }
299
300 $code = isset( $first->status_code ) ? (string) $first->status_code : 'no_code';
301 $this->logTransportFailure( 'non_authoritative_' . $code );
302 return null;
303 }
304
305 /**
306 * Conditional error_log — only when WP debug logging is on, to avoid
307 * twice-daily noise on installs where the licensing endpoint flaps.
308 *
309 * Instance method so tests can mock it.
310 *
311 * @param string $code Error code.
312 * @return void
313 */
314 protected function logTransportFailure( $code ) {
315 if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
316 error_log( sprintf( '[Presto License] transport failure: %s', $code ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
317 }
318 }
319
320 /**
321 * Uninstall cleanup — called from the plugin's uninstall hook.
322 * Named static method (closures don't survive WP's uninstall boundary).
323 *
324 * @return void
325 */
326 public static function uninstall() {
327 delete_transient( self::TRANSIENT );
328 }
329 }
330