PluginProbe ʕ •ᴥ•ʔ
Presto Player / 4.3.2
Presto Player v4.3.2
4.3.2 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 3 days ago
License.php
345 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 // Periodic remote license-validity check — DISABLED BY DEFAULT (#1133).
48 //
49 // This filter fires refreshOnUpdateCheck() on every plugin-update check.
50 // It has no freshness guard or negative-cache of its own and free-rode on
51 // core's `last_checked` throttle; when that throttle collapsed during the
52 // WP 7.0.1 rollout (update caches force-cleared fleet-wide + object-cache
53 // write pressure), it re-fired at HTTP-timeout speed and took down the
54 // licensing origin (prestomade 502 outage, 2026-07-17).
55 //
56 // The renewal push this existed for (lock expired users out until they
57 // renew) is complete, so the check is off by default. It is gated behind a
58 // filter rather than commented out so it can be killed or restored
59 // fleet-wide with a hotfix/mu-plugin filter — no release required — if the
60 // origin flaps again. Before flipping the default back to true, add a
61 // freshness guard (skip fetch() unless we checked > N hours ago) and a
62 // negative cache written even on failure.
63 if ( apply_filters( 'presto_player_enable_license_check', false ) ) {
64 add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'refreshOnUpdateCheck' ) );
65 }
66
67 // Any write to the license option clears the cached verdict.
68 $option = Setting::getGroupName( 'license' );
69 add_action( "update_option_{$option}", array( $this, 'onLicenseChange' ) );
70 add_action( "add_option_{$option}", array( $this, 'onLicenseChange' ) );
71
72 add_action( 'admin_notices', array( $this, 'maybeNotice' ) );
73 }
74
75 /**
76 * Licensing API endpoint — filterable for staging mirrors.
77 *
78 * @return string
79 */
80 public static function apiUrl() {
81 return (string) apply_filters( 'presto_player_license_api_url', self::API_URL );
82 }
83
84 /**
85 * Domain string sent to the licensing server.
86 *
87 * Server keys activations by exact (key, domain). MUST stay in the
88 * historical browser-context form `host[:port][/path]` without a scheme —
89 * normalizing (lowercase, slashes, default ports) orphans existing seats.
90 *
91 * preg_replace, not is_ssl(), keeps this stable across browser and
92 * WP-CLI cron contexts. Filterable for reverse proxies and domain mapping.
93 *
94 * TODO: Pro's `LicensedProduct::domain()` is a duplicate; fold into this.
95 *
96 * @return string
97 */
98 public static function canonicalDomain() {
99 $url = (string) get_bloginfo( 'wpurl' );
100 $domain = preg_replace( '#^https?://#i', '', $url );
101 return (string) apply_filters( 'presto_player_license_domain', (string) $domain );
102 }
103
104 /**
105 * Renewal CTA target — filterable.
106 *
107 * @return string
108 */
109 public static function renewUrl() {
110 return (string) apply_filters( 'presto_player_license_renew_url', self::RENEW_URL );
111 }
112
113 /**
114 * Pricing CTA target — filterable.
115 *
116 * @return string
117 */
118 public static function pricingUrl() {
119 return (string) apply_filters( 'presto_player_license_pricing_url', self::PRICING_URL );
120 }
121
122 /**
123 * Local-first read. No HTTP. Per-request memoized.
124 *
125 * False only when (a) no key is stored, or (b) the cached verdict is the
126 * literal `'invalid'`. Anything else (missing transient, malformed value)
127 * is treated as active — bias toward keeping paying customers active.
128 *
129 * Do NOT call `Plugin::isPro()` from here — it reads this method.
130 *
131 * @return bool
132 */
133 public static function isActive() {
134 if ( null !== self::$cache ) {
135 return self::$cache;
136 }
137 if ( self::isVipBuild() ) {
138 self::$cache = true;
139 return self::$cache;
140 }
141 if ( ! self::hasKey() ) {
142 self::$cache = false;
143 return self::$cache;
144 }
145 self::$cache = ( 'invalid' !== get_transient( self::TRANSIENT ) );
146 return self::$cache;
147 }
148
149 /**
150 * Whether a license key is stored (regardless of validity).
151 *
152 * @return bool
153 */
154 public static function hasKey() {
155 return '' !== (string) Setting::get( 'license', 'key' );
156 }
157
158 /**
159 * VIP builds of Pro ship without `inc/Services/License/`, so they can't go
160 * through this code path. VIP customers are entitled by WordPress.com VIP,
161 * not by a key — treat them as always active to keep `Plugin::isPro()` true
162 * and the admin notice suppressed.
163 *
164 * Detected by the same signal Pro itself uses in `Plugin::requiresLicensing()`:
165 * the absence of `\PrestoPlayer\Pro\Services\License\License`. Filterable
166 * for custom deployments and tests.
167 *
168 * @return bool
169 */
170 private static function isVipBuild() {
171 $is_vip = class_exists( '\PrestoPlayer\Pro\Plugin' )
172 && ! class_exists( '\PrestoPlayer\Pro\Services\License\License' );
173 return (bool) apply_filters( 'presto_player_license_is_vip', $is_vip );
174 }
175
176 /**
177 * Cron-only refresh hooked on WP's plugin-update-check filter.
178 *
179 * On transport failure `fetch()` returns null and we leave the transient
180 * untouched, so the prior verdict survives the outage.
181 *
182 * @param mixed $value Filter value (returned untouched).
183 * @return mixed
184 */
185 public function refreshOnUpdateCheck( $value ) {
186 if ( ! wp_doing_cron() || ! self::hasKey() ) {
187 return $value;
188 }
189
190 $verdict = $this->fetch();
191 if ( null !== $verdict && get_transient( self::TRANSIENT ) !== $verdict ) {
192 set_transient( self::TRANSIENT, $verdict, self::TTL );
193 self::$cache = null;
194 }
195
196 return $value;
197 }
198
199 /**
200 * Clears the cached verdict on any license-option write. Next cron
201 * tick reconfirms with the server.
202 *
203 * @return void
204 */
205 public function onLicenseChange() {
206 delete_transient( self::TRANSIENT );
207 self::$cache = null;
208 }
209
210 /**
211 * Render an admin notice on non-PP screens when the license is invalid.
212 *
213 * @return void
214 */
215 public function maybeNotice() {
216 if ( ! current_user_can( 'manage_options' ) ) {
217 return;
218 }
219 if ( ! class_exists( '\PrestoPlayer\Pro\Plugin' ) ) {
220 return;
221 }
222 if ( Utility::isPrestoPlayerPage() ) {
223 return;
224 }
225 if ( self::isActive() ) {
226 return;
227 }
228
229 $no_key = ! self::hasKey();
230 if ( $no_key ) {
231 // Pro installed but no key yet — yellow, points to in-app activation.
232 $notice_class = 'notice-warning';
233 $message = __( 'Activate your Presto Player Pro license to unlock Pro features and receive automatic updates.', 'presto-player' );
234 $primary_label = __( 'Activate License', 'presto-player' );
235 $primary_url = admin_url( 'admin.php?page=presto-dashboard&tab=Settings&section=license' );
236 $primary_external = false;
237 $secondary_label = __( 'Buy Subscription', 'presto-player' );
238 $secondary_url = self::pricingUrl();
239 } else {
240 // Expired — red, primary CTA renews.
241 $notice_class = 'notice-error';
242 $message = __( 'Your Presto Player Pro license has expired. Please renew to restore Pro Features, Premium Support & Automatic Plugin Updates.', 'presto-player' );
243 $primary_label = __( 'Renew Now', 'presto-player' );
244 $primary_url = self::renewUrl();
245 $primary_external = true;
246 $secondary_label = '';
247 $secondary_url = '';
248 }
249 ?>
250 <div class="notice <?php echo esc_attr( $notice_class ); ?> is-dismissible">
251 <p><strong><?php esc_html_e( 'Presto Player', 'presto-player' ); ?></strong></p>
252 <p><?php echo esc_html( $message ); ?></p>
253 <p>
254 <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>
255 <?php if ( ! empty( $secondary_label ) ) : ?>
256 <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>
257 <?php endif; ?>
258 </p>
259 </div>
260 <?php
261 }
262
263 /**
264 * Fetch fresh license verdict from the server.
265 *
266 * Returns `'active'` or `'invalid'` on an authoritative response, or null
267 * if the response says nothing about the license itself (transport failure,
268 * request-level error, malformed body) — caller preserves cache.
269 *
270 * Uses `activate` because `status-check` currently fatals on the
271 * prestomade.com server for non-active keys. With `canonicalDomain()`
272 * matching Pro's domain, `activate` is idempotent (returns `s101`).
273 *
274 * Instance method so tests can substitute it via Mockery partials.
275 *
276 * @return string|null
277 */
278 protected function fetch() {
279 $resp = wp_remote_get(
280 add_query_arg(
281 array(
282 'woo_sl_action' => 'activate',
283 'licence_key' => (string) Setting::get( 'license', 'key' ),
284 'product_unique_id' => self::PRODUCT_ID,
285 'domain' => self::canonicalDomain(),
286 ),
287 self::apiUrl()
288 ),
289 array( 'timeout' => 15 )
290 );
291
292 if ( is_wp_error( $resp ) || 200 !== (int) wp_remote_retrieve_response_code( $resp ) ) {
293 $this->logTransportFailure(
294 is_wp_error( $resp )
295 ? $resp->get_error_code()
296 : 'http_' . wp_remote_retrieve_response_code( $resp )
297 );
298 return null;
299 }
300
301 $body = json_decode( wp_remote_retrieve_body( $resp ) );
302 $first = is_array( $body ) ? ( isset( $body[0] ) ? $body[0] : null ) : $body;
303
304 if ( ! is_object( $first ) ) {
305 $this->logTransportFailure( 'malformed_response' );
306 return null;
307 }
308
309 // Only `licence_status: 'expired'` invalidates. To lock on a new state
310 // (e.g. blocked, suspended), extend this check.
311 if ( isset( $first->licence_status ) ) {
312 return ( 'expired' === $first->licence_status ) ? 'invalid' : 'active';
313 }
314
315 $code = isset( $first->status_code ) ? (string) $first->status_code : 'no_code';
316 $this->logTransportFailure( 'non_authoritative_' . $code );
317 return null;
318 }
319
320 /**
321 * Conditional error_log — only when WP debug logging is on, to avoid
322 * twice-daily noise on installs where the licensing endpoint flaps.
323 *
324 * Instance method so tests can mock it.
325 *
326 * @param string $code Error code.
327 * @return void
328 */
329 protected function logTransportFailure( $code ) {
330 if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
331 error_log( sprintf( '[Presto License] transport failure: %s', $code ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
332 }
333 }
334
335 /**
336 * Uninstall cleanup — called from the plugin's uninstall hook.
337 * Named static method (closures don't survive WP's uninstall boundary).
338 *
339 * @return void
340 */
341 public static function uninstall() {
342 delete_transient( self::TRANSIENT );
343 }
344 }
345