Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
51.72% covered (warning)
51.72%
60 / 116
35.29% covered (danger)
35.29%
6 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
API
51.72% covered (warning)
51.72%
60 / 116
35.29% covered (danger)
35.29%
6 / 17
147.21
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 set_license_key
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 get_licence_details
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 get_saved_licence_information
61.54% covered (warning)
61.54%
8 / 13
0.00% covered (danger)
0.00%
0 / 1
3.51
 save_licence_information
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 deactivate_licence
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 activate_licence
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 get_plugin_information
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 get_remote_product_information
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 get_cached_product_information
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 get_check_update
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 schedule_immediate_background_update
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 get_remote_check_update
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
2.50
 get_cached_check_update
58.62% covered (warning)
58.62%
17 / 29
0.00% covered (danger)
0.00%
0 / 1
5.13
 is_update_available
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 get_available_version
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_current_version
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Should only run in background
4 * Should trigger when the wp transient is set and manually edit it afterwards/ set its own transient for same time
5 *
6 * TODO: basically does error checking then calls the integration's similar functions and then caches
7 * TODO: rate limit
8 *
9 * @package brianhenryie/bh-wp-plugin-updater
10 */
11
12namespace BrianHenryIE\WP_Plugin_Updater;
13
14use BrianHenryIE\WP_Plugin_Updater\Exception\Licence_Key_Not_Set_Exception;
15use BrianHenryIE\WP_Plugin_Updater\Exception\Plugin_Updater_Exception_Abstract;
16use BrianHenryIE\WP_Plugin_Updater\Integrations\Integration_Factory;
17use BrianHenryIE\WP_Plugin_Updater\Integrations\Integration_Factory_Interface;
18use BrianHenryIE\WP_Plugin_Updater\Integrations\Integration_Interface;
19use BrianHenryIE\WP_Plugin_Updater\Model\Plugin_Info_Interface;
20use BrianHenryIE\WP_Plugin_Updater\Model\Plugin_Update;
21use BrianHenryIE\WP_Plugin_Updater\Model\Plugin_Update_Interface;
22use BrianHenryIE\WP_Plugin_Updater\WP_Includes\Cron;
23use Composer\Semver\Comparator;
24use DateTimeImmutable;
25use JsonMapper\Handler\FactoryRegistry;
26use JsonMapper\Handler\PropertyMapper;
27use JsonMapper\JsonMapperBuilder;
28use Psr\Log\LoggerAwareTrait;
29use Psr\Log\LoggerInterface;
30
31class API implements API_Interface {
32    use LoggerAwareTrait;
33
34    protected Integration_Interface $service;
35
36    protected Licence $licence;
37
38    public function __construct(
39        protected Settings_Interface $settings,
40        LoggerInterface $logger,
41        ?Integration_Factory_Interface $integration_factory = null
42    ) {
43        $this->setLogger( $logger );
44
45        try {
46            $this->licence = $this->get_licence_details( false );
47        } catch ( Licence_Key_Not_Set_Exception $e ) {
48            $this->licence = new Licence();
49        }
50
51        $this->service = ( $integration_factory ?? new Integration_Factory( $logger ) )
52                            ->get_integration( $settings );
53    }
54
55    /**
56     * Set the licence key without activating it.
57     *
58     * Deactivates existing licence key if present.
59     *
60     * @param string $license_key
61     *
62     * @throws Plugin_Updater_Exception_Abstract If failing to deactivate the existing licence.
63     */
64    public function set_license_key( string $license_key ): Licence {
65
66        $existing_key = $this->licence->get_licence_key();
67        if ( $existing_key === $license_key ) {
68            return $this->licence;
69        }
70        if ( ! empty( $existing_key ) ) {
71            if ( $this->licence->get_status() === 'active' ) {
72                $this->service->deactivate_licence( $this->licence );
73            }
74        }
75
76        // TODO: Set the status to unknown?
77
78        $this->licence->set_licence_key( $license_key );
79        $this->save_licence_information( $this->licence );
80
81        return $this->licence;
82    }
83
84    /**
85     * Get the licence information, maybe cached, maybe remote, maybe an empty Licence object.
86     *
87     * @param bool|null $refresh True: force refresh from API; false: do not refresh; null: use cached value or refresh if missing.
88     *
89     * @throws Licence_Key_Not_Set_Exception
90     */
91    public function get_licence_details( ?bool $refresh = null ): Licence {
92
93        // TODO: refresh should never be true on a pageload.
94
95        // TODO: Do not continuously retry.
96
97        return match ( $refresh ) {
98            true => $this->service->refresh_licence_details( $this->licence ),
99            false => $this->get_saved_licence_information() ?? new Licence(), // throw new Licence_Key_Not_Set_Exception(),
100            default => $this->get_saved_licence_information() ?? $this->service->refresh_licence_details( $this->licence ),
101        };
102    }
103
104    /**
105     * Get the licence information from the WordPress options database table. Verifies it is a Licence object.
106     */
107    protected function get_saved_licence_information(): ?Licence {
108        // TODO: try / catch a malformed serialized object.
109        // TODO: serialize to an array, not to a serialized object, so the class can be changed in future.
110        $value = get_option(
111            $this->settings->get_licence_data_option_name(),
112            null
113        );
114        if ( is_null( $value ) ) {
115            $this->logger->debug( 'No licence information found in wp-options.' );
116            return null;
117        }
118        try {
119            $licence = new Licence();
120            $licence->__unserialize( $value );
121            return $licence;
122        } catch ( \Throwable $e ) {
123            $this->logger->error( 'Failed to unserialize licence information: ' . $e->getMessage(), array( 'value' => $value ) );
124            return null;
125        }
126    }
127
128    /**
129     *
130     *
131     * @param Licence $licence
132     *
133     * @return void
134     */
135    protected function save_licence_information( Licence $licence ): void {
136        $licence->set_last_updated( new DateTimeImmutable() );
137
138        update_option(
139            $this->settings->get_licence_data_option_name(),
140            $licence->__serialize()
141        );
142    }
143
144    /**
145     * Send a HTTP request to deactivate the licence from this site.
146     *
147     * Is this a good idea? Should it only be possible from the licence server?
148     *
149     * https://updatestest.bhwp.ie/wp-json/slswc/v1/deactivate?slug=a-plugin
150     *
151     * @throws Plugin_Updater_Exception_Abstract
152     */
153    public function deactivate_licence(): Licence {
154
155        if ( is_null( $this->licence->get_licence_key() ) ) {
156            throw new Licence_Key_Not_Set_Exception();
157        }
158
159        $licence = $this->service->deactivate_licence( $this->licence );
160
161        // TODO: save
162        $licence->set_last_updated( new DateTimeImmutable() );
163
164        return $licence;
165    }
166
167    /**
168     * Activate the licence on this site.
169     *
170     * @throws Plugin_Updater_Exception_Abstract
171     */
172    public function activate_licence(): Licence {
173
174        if ( is_null( $this->licence->get_licence_key() ) ) {
175            throw new Licence_Key_Not_Set_Exception();
176        }
177
178        $licence = $this->service->activate_licence( $this->licence );
179
180        // TODO: Let's record "last successfully updated" as well as "last updated". (or use a rate limiter)
181
182        $this->save_licence_information( $licence );
183
184        return $this->licence;
185    }
186
187    /**
188     * Product information should be available regardless of licence status.
189     *
190     * Get the remote product information for the {@see get_plugins()} information array.
191     *
192     * null when first run and no cached product information.
193     */
194    public function get_plugin_information( ?bool $refresh = null ): ?Plugin_Info_Interface {
195
196        if ( true !== $refresh ) {
197            // TODO: Add a background task to refresh the product information.
198            // TODO: Check the last time it was refreshed and rate limit the refreshing.
199        }
200
201        return match ( $refresh ) {
202            true => $this->get_remote_product_information(),
203            false => $this->get_cached_product_information(),
204            default => $this->get_cached_product_information() ?? $this->get_remote_product_information(),
205        };
206    }
207
208    protected function get_remote_product_information(): ?Plugin_Info_Interface {
209
210        $product = $this->service->get_remote_product_information( $this->licence );
211
212        update_option( $this->settings->get_plugin_information_option_name(), $product );
213
214        return $product;
215    }
216
217    protected function get_cached_product_information(): ?Plugin_Info_Interface {
218        $cached_product_information = get_option(
219            // plugin_slug_plugin_information
220            $this->settings->get_plugin_information_option_name(),
221            null
222        );
223        if ( $cached_product_information instanceof Plugin_Info_Interface ) {
224            $this->logger->debug( 'returning cached product information for ' . $cached_product_information->get_software_slug() );
225            return $cached_product_information;
226        }
227        $this->logger->debug( 'product not found in cache: ' . $this->settings->get_plugin_slug() );
228        return null;
229    }
230
231    /**
232     * Update information should be available regardless of licence status... alas, it is not.
233     *
234     * Get the remote product information for the {@see get_plugins()} information array.
235     *
236     * null when first run and no cached information.
237     */
238    public function get_check_update( ?bool $refresh = null ): ?Plugin_Update_Interface {
239
240        if ( true !== $refresh ) {
241            // TODO: Add a background task to refresh the product information.
242            // TODO: Check the last time it was refreshed and rate limit the refreshing.
243        }
244
245        return match ( $refresh ) {
246            true => $this->get_remote_check_update(),
247            false => $this->get_cached_check_update(),
248            default => $this->get_cached_check_update() ?? $this->get_remote_check_update(),
249        };
250    }
251
252    /**
253     * Schedule an immediate background update check.
254     *
255     * TODO: rate limit this.
256     */
257    public function schedule_immediate_background_update(): void {
258        $cron          = new Cron( $this, $this->settings );
259        $cron_job_name = $cron->get_immediate_update_check_cron_job_name();
260        wp_schedule_single_event( time(), $cron_job_name );
261    }
262
263    protected function get_remote_check_update(): ?Plugin_Update_Interface {
264
265        try {
266            $check_update = $this->service->get_remote_check_update( $this->licence );
267            update_option( $this->settings->get_check_update_option_name(), $check_update->__serialize() );
268        } catch ( \Exception $e ) {
269            $this->logger->error( $e->getMessage(), array( 'exception' => $e ) );
270            return null;
271        }
272
273        return $check_update;
274    }
275
276    protected function get_cached_check_update(): ?Plugin_Update_Interface {
277        $cached_check_update = get_option(
278            $this->settings->get_check_update_option_name(),
279            null
280        );
281
282        if ( is_null( $cached_check_update ) ) {
283            $this->logger->debug( 'check_update Plugin_Update_Interface not found in cache: ' . $this->settings->get_plugin_slug() );
284            return null;
285        }
286
287        $factory_registry = new FactoryRegistry();
288        $mapper           = JsonMapperBuilder::new()
289                                            ->withObjectConstructorMiddleware( $factory_registry )
290                                            ->withPropertyMapper( new PropertyMapper( $factory_registry ) )
291                                            ->build();
292
293        try {
294            $mapped_product_updated = $mapper->mapToClassFromString(
295                json_encode( $cached_check_update ),
296                Plugin_Update::class
297            );
298        } catch ( \Exception $e ) {
299            $this->logger->error(
300                'Failed to map cached check_update: ' . $e->getMessage(),
301                array(
302                    'exception'   => $e,
303                    'cache_value' => $cached_check_update,
304                )
305            );
306            return null;
307        }
308
309        if ( ! ( $mapped_product_updated instanceof Plugin_Update_Interface ) ) {
310            return null;
311        }
312
313        $this->logger->debug( 'returning cached check_update for ' . $this->settings->get_plugin_slug() );
314        return $mapped_product_updated;
315    }
316
317    /**
318     * Compare the currently installed version (or 0.0.0) with the available version.
319     */
320    public function is_update_available( ?bool $refresh = null ): bool {
321        return Comparator::greaterThan(
322            $this->get_available_version( $refresh ) ?? '0.0.0',
323            $this->get_current_version() ?? '0.0.0',
324        );
325    }
326
327    protected function get_available_version( ?bool $refresh = null ): ?string {
328        return $this->get_check_update( $refresh )?->get_version() ?? null;
329    }
330
331    protected function get_current_version(): ?string {
332        return get_plugins()[ $this->settings->get_plugin_basename() ]['Version'] ?? null;
333    }
334}