PluginProbe ʕ •ᴥ•ʔ
Robin Image Optimizer – Unlimited Image Optimization, WebP & AVIF / trunk
Robin Image Optimizer – Unlimited Image Optimization, WebP & AVIF vtrunk
2.0.5 trunk 1.3.7 1.4.0 1.4.1 1.4.2 1.4.6 1.5.0 1.5.3 1.5.6 1.5.8 1.6.5 1.6.6 1.6.9 1.7.0 1.7.4 1.8.1 1.8.2 1.9.0 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4
robin-image-optimizer / includes / classes / class-wrio-license.php
robin-image-optimizer / includes / classes Last commit date
models 4 months ago processing 4 months ago processors 4 months ago class-rio-attachment.php 4 months ago class-rio-backup.php 5 months ago class-rio-bulk-optimization.php 5 months ago class-rio-cron.php 6 months ago class-rio-image-query.php 4 months ago class-rio-image-statistic.php 4 months ago class-rio-media-library.php 4 months ago class-rio-multisite.php 6 months ago class-rio-optimization-orchestrator.php 4 months ago class-rio-optimization-tools.php 6 months ago class-rio-views.php 4 months ago class-wrio-license.php 6 months ago class-wrio-premium-provider.php 6 months ago class-wrio-support.php 6 months ago index.php 6 months ago
class-wrio-license.php
458 lines
1 <?php
2 /**
3 * License data class
4 *
5 * Parses and exposes license data from both Freemius (wbcr_io_license) and
6 * ThemeIsle SDK ({namespace}_license_data) storage formats.
7 *
8 * @package Robin_Image_Optimizer
9 * @subpackage Classes
10 */
11
12 // Exit if accessed directly
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 /**
18 * Class WRIO_License
19 *
20 * Handles license data parsing and validation for both Freemius and ThemeIsle SDK licenses.
21 */
22 class WRIO_License {
23
24 /**
25 * License source: Freemius
26 */
27 const SOURCE_FREEMIUS = 'freemius';
28
29 /**
30 * License source: ThemeIsle SDK
31 */
32 const SOURCE_SDK = 'sdk';
33
34 /**
35 * Raw license data from database
36 *
37 * @var array<string, mixed>
38 */
39 private $data;
40
41 /**
42 * License-specific data (normalized)
43 *
44 * @var array<string, mixed>
45 */
46 private $license_data;
47
48 /**
49 * License source (freemius or sdk)
50 *
51 * @var string|null
52 */
53 private $source = null;
54
55 /**
56 * Constructor
57 *
58 * Loads license data from the database, detecting the source automatically.
59 */
60 public function __construct() {
61 $this->detect_and_load_license();
62 }
63
64 /**
65 * Detect license source and load data
66 *
67 * Checks SDK option first, then falls back to Freemius storage.
68 *
69 * @return void
70 */
71 private function detect_and_load_license() {
72 // Get SDK namespace using existing helper
73 $namespace = WRIO_Plugin::get_sdk_namespace();
74 $sdk_option = $namespace . '_license_data';
75
76 // Check SDK option first
77 // SDK stores data as stdClass object, convert to array for consistent access
78 $sdk_data = get_option( $sdk_option, null );
79 if ( ! empty( $sdk_data ) ) {
80 // Force convert to array (handles both object and array input)
81 $sdk_data = (array) $sdk_data;
82
83 if ( isset( $sdk_data['license'] ) && 'valid' === $sdk_data['license'] ) {
84 $this->source = self::SOURCE_SDK;
85 $this->data = $sdk_data;
86 $this->license_data = $this->normalize_sdk_data( $sdk_data );
87 return;
88 }
89 }
90
91 // Fallback to Freemius storage
92 $freemius_data = get_option( 'wbcr_io_license', [] );
93 if ( ! empty( $freemius_data['license']['secret_key'] ) ) {
94 $this->source = self::SOURCE_FREEMIUS;
95 $this->data = $freemius_data;
96 $this->license_data = $freemius_data['license'] ?? [];
97 return;
98 }
99
100 // No license found
101 $this->source = null;
102 $this->data = [];
103 $this->license_data = [];
104 }
105
106 /**
107 * Normalize SDK license data to match internal format
108 *
109 * Translates SDK field names to the Freemius-style format used internally.
110 *
111 * @param array<string, mixed> $data The SDK license data (already converted to array).
112 * @return array<string, mixed> Normalized license data.
113 */
114 private function normalize_sdk_data( array $data ) {
115 return [
116 'secret_key' => $data['key'] ?? null,
117 'expiration' => $data['expires'] ?? null,
118 'plan_title' => 'Premium',
119 'plan_id' => $data['price_id'] ?? 0,
120 'activated' => 1,
121 'is_cancelled' => in_array( $data['is_expired'] ?? '', [ 'yes', true, 1, '1' ], true ),
122 'is_block_features' => false,
123 'billing_cycle' => null, // SDK doesn't track subscription status
124 'download_id' => $data['download_id'] ?? null,
125 ];
126 }
127
128 /**
129 * Get the license source
130 *
131 * @return string|null 'freemius', 'sdk', or null if no license.
132 */
133 public function get_source() {
134 return $this->source;
135 }
136
137 /**
138 * Get masked license key showing first 8 and last 4 characters
139 *
140 * Example: sk_abc12****wxyz
141 *
142 * @return string
143 */
144 public function get_masked_key() {
145 $key = $this->get_key();
146 if ( empty( $key ) ) {
147 return '';
148 }
149
150 $length = strlen( $key );
151 if ( $length <= 12 ) {
152 return substr( $key, 0, 8 ) . '****';
153 }
154
155 return substr( $key, 0, 8 ) . '****' . substr( $key, -4 );
156 }
157
158 /**
159 * Get unified expiration display string
160 *
161 * Returns "Lifetime", "Renews on X" (for subscriptions), or "Expires on X".
162 *
163 * @return string
164 */
165 public function get_expiration_display() {
166 if ( $this->is_lifetime() ) {
167 return __( 'Lifetime', 'robin-image-optimizer' );
168 }
169
170 $expiration = $this->license_data['expiration'] ?? null;
171 if ( empty( $expiration ) ) {
172 return '';
173 }
174
175 $date = date_i18n( get_option( 'date_format' ), strtotime( $expiration ) );
176 $billing_cycle = $this->get_billing_cycle();
177
178 // Only Freemius has billing_cycle for subscriptions
179 if ( $billing_cycle ) {
180 /* translators: %s is the renewal date */
181 return sprintf( __( 'Renews on %s', 'robin-image-optimizer' ), $date );
182 }
183
184 /* translators: %s is the expiration date */
185 return sprintf( __( 'Expires on %s', 'robin-image-optimizer' ), $date );
186 }
187
188 /**
189 * Get CSS status class for license display
190 *
191 * @return string 'status-valid', 'status-warning', or 'status-expired'
192 */
193 public function get_status_class() {
194 if ( $this->is_expired() ) {
195 return 'status-expired';
196 }
197
198 // Warning if expiring within 30 days
199 $days = $this->get_expiration_time( 'days' );
200 if ( 999 !== $days && 30 >= $days ) {
201 return 'status-warning';
202 }
203
204 return 'status-valid';
205 }
206
207 /**
208 * Check if license data exists
209 *
210 * @return bool
211 */
212 public function has_license() {
213 return ! empty( $this->license_data ) && ! empty( $this->get_key() );
214 }
215
216 /**
217 * Get license key (secret_key)
218 *
219 * @return string|null
220 */
221 public function get_key() {
222 return isset( $this->license_data['secret_key'] ) ? $this->license_data['secret_key'] : null;
223 }
224
225 /**
226 * Get hidden license key with masked middle portion
227 *
228 * Example: sk_abc***xyz
229 *
230 * @return string
231 */
232 public function get_hidden_key() {
233 $key = $this->get_key();
234 if ( empty( $key ) ) {
235 return '';
236 }
237
238 $length = strlen( $key );
239 if ( $length <= 12 ) {
240 return substr( $key, 0, 4 ) . '******';
241 }
242
243 return substr_replace( $key, '******', 15, 6 );
244 }
245
246 /**
247 * Get expiration time in various formats.
248 *
249 * @param string $format Return format: 'time' (raw), 'days' (remaining days), 'date' (Y-m-d).
250 * @return mixed
251 */
252 public function get_expiration_time( $format = 'time' ) {
253 $expiration = isset( $this->license_data['expiration'] ) ? $this->license_data['expiration'] : null;
254
255 if ( 'days' === $format ) {
256 if ( $this->is_lifetime() ) {
257 return 999;
258 }
259
260 if ( empty( $expiration ) ) {
261 return 0;
262 }
263
264 $remaining = strtotime( $expiration ) - time();
265 return max( 0, floor( $remaining / 86400 ) );
266 }
267
268 if ( 'date' === $format ) {
269 if ( empty( $expiration ) ) {
270 return '';
271 }
272 return gmdate( 'Y-m-d', strtotime( $expiration ) );
273 }
274
275 return $expiration;
276 }
277
278 /**
279 * Get sites quota (number of allowed sites)
280 *
281 * @return int|null Null means unlimited
282 */
283 public function get_sites_quota() {
284 return isset( $this->license_data['quota'] ) ? $this->license_data['quota'] : null;
285 }
286
287 /**
288 * Get count of currently active sites
289 *
290 * @return int
291 */
292 public function get_count_active_sites() {
293 return isset( $this->license_data['activated'] ) ? (int) $this->license_data['activated'] : 0;
294 }
295
296 /**
297 * Check if this is a lifetime license (no expiration)
298 *
299 * @return bool
300 */
301 public function is_lifetime() {
302 return empty( $this->license_data['expiration'] );
303 }
304
305 /**
306 * Check if license is valid (not expired)
307 *
308 * @return bool
309 */
310 public function is_valid() {
311 return ! $this->is_expired();
312 }
313
314 /**
315 * Check if license has expired
316 *
317 * @return bool
318 */
319 public function is_expired() {
320 if ( $this->is_lifetime() ) {
321 return false;
322 }
323
324 $expiration = $this->license_data['expiration'];
325 if ( empty( $expiration ) ) {
326 return true;
327 }
328
329 return strtotime( $expiration ) < time();
330 }
331
332 /**
333 * Check if license is cancelled
334 *
335 * @return bool
336 */
337 public function is_cancelled() {
338 return ! empty( $this->license_data['is_cancelled'] );
339 }
340
341 /**
342 * Check if license is active (not cancelled)
343 *
344 * @return bool
345 */
346 public function is_active() {
347 return ! $this->is_cancelled();
348 }
349
350 /**
351 * Check if features are enabled
352 *
353 * Features may be blocked after expiration depending on license settings.
354 *
355 * @return bool
356 */
357 public function is_features_enabled() {
358 if ( ! $this->is_active() ) {
359 return false;
360 }
361
362 $is_block_features = isset( $this->license_data['is_block_features'] ) ? $this->license_data['is_block_features'] : true;
363
364 return ! $is_block_features || ! $this->is_expired();
365 }
366
367 /**
368 * Get plan title
369 *
370 * @return string|null
371 */
372 public function get_plan() {
373 return isset( $this->license_data['plan_title'] ) ? $this->license_data['plan_title'] : null;
374 }
375
376 /**
377 * Get plan ID
378 *
379 * @return int|null
380 */
381 public function get_plan_id() {
382 return isset( $this->license_data['plan_id'] ) ? (int) $this->license_data['plan_id'] : null;
383 }
384
385 /**
386 * Get billing cycle
387 *
388 * @return int|null 1 = monthly, 12 = yearly, null = lifetime/one-time
389 */
390 public function get_billing_cycle() {
391 return isset( $this->license_data['billing_cycle'] ) ? $this->license_data['billing_cycle'] : null;
392 }
393
394 /**
395 * Check if quota is unlimited
396 *
397 * @return bool
398 */
399 public function is_unlimited() {
400 return is_null( $this->get_sites_quota() );
401 }
402
403 /**
404 * Check if this is a single-site license
405 *
406 * @return bool
407 */
408 public function is_single_site() {
409 $quota = $this->get_sites_quota();
410 return is_numeric( $quota ) && 1 === $quota;
411 }
412
413 /**
414 * Get license ID
415 *
416 * @return int|null
417 */
418 public function get_id() {
419 return isset( $this->license_data['id'] ) ? (int) $this->license_data['id'] : null;
420 }
421
422 /**
423 * Get user ID associated with license
424 *
425 * @return int|null
426 */
427 public function get_user_id() {
428 return isset( $this->license_data['user_id'] ) ? (int) $this->license_data['user_id'] : null;
429 }
430
431 /**
432 * Get raw license data array
433 *
434 * @return array<string, mixed>
435 */
436 public function to_array() {
437 return $this->license_data;
438 }
439
440 /**
441 * Get site data from license
442 *
443 * @return array<string, mixed>
444 */
445 public function get_site_data() {
446 return isset( $this->data['site'] ) ? $this->data['site'] : [];
447 }
448
449 /**
450 * Get user data from license
451 *
452 * @return array<string, mixed>
453 */
454 public function get_user_data() {
455 return isset( $this->data['user'] ) ? $this->data['user'] : [];
456 }
457 }
458