Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
61.32% covered (warning)
61.32%
65 / 106
66.67% covered (warning)
66.67%
12 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
Licensing
61.32% covered (warning)
61.32%
65 / 106
66.67% covered (warning)
66.67%
12 / 18
174.83
0.00% covered (danger)
0.00%
0 / 1
 instance
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 initialize
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 configure
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 add_script_data
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 initialize_endpoints
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 connection
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 last_error
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 log_error
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 stored_licenses
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 append_license
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 attach_licenses_request
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 attach_licenses
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
6
 attach_stored_licenses
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
7
 attach_stored_licenses_on_connection
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 is_licensing_input_enabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 get_license_activation_notice_dismiss
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 get_user_licenses
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 handle_user_connected_redirect
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
72
1<?php
2/**
3 * A Terms of Service class for Jetpack.
4 *
5 * @package automattic/jetpack-licensing
6 */
7
8namespace Automattic\Jetpack;
9
10use Automattic\Jetpack\Connection\Manager as Connection_Manager;
11use Automattic\Jetpack\Licensing\Endpoints;
12use Jetpack_IXR_ClientMulticall;
13use Jetpack_Options;
14use WP_Error;
15
16/**
17 * Class Licensing.
18 * Helper class that is responsible for attaching licenses to the current site.
19 *
20 * @since 1.1.1
21 */
22class Licensing {
23    /**
24     * Name of the WordPress option that holds all known Jetpack licenses.
25     *
26     * @const string
27     */
28    const LICENSES_OPTION_NAME = 'jetpack_licenses';
29
30    /**
31     * Name of the WordPress transient that holds the last license attaching error, if any.
32     *
33     * @const string
34     */
35    const ERROR_TRANSIENT_NAME = 'jetpack_licenses_error';
36
37    /**
38     * Holds the singleton instance of this class.
39     *
40     * @var self
41     */
42    protected static $instance = false;
43
44    /**
45     * Singleton.
46     *
47     * @static
48     */
49    public static function instance() {
50        if ( ! self::$instance ) {
51            self::$instance = new self();
52        }
53
54        return self::$instance;
55    }
56
57    /**
58     * Initialize.
59     *
60     * @return void
61     */
62    public function initialize() {
63        add_action( 'add_option_' . self::LICENSES_OPTION_NAME, array( $this, 'attach_stored_licenses' ) );
64        add_action( 'update_option_' . self::LICENSES_OPTION_NAME, array( $this, 'attach_stored_licenses' ) );
65        add_action( 'jetpack_authorize_ending_authorized', array( $this, 'attach_stored_licenses_on_connection' ) );
66        add_action( 'rest_api_init', array( $this, 'initialize_endpoints' ) );
67    }
68
69    /**
70     * Register the hooks the package needs on every request.
71     *
72     * Separate from `initialize()`, which consumers call only when they offer the
73     * licensing UI: the activation screen also renders from the Jetpack plugin's
74     * own route, so the illustrations need a URL there too.
75     *
76     * @since 4.1.0
77     *
78     * @return void
79     */
80    public static function configure() {
81        add_filter( 'jetpack_admin_js_script_data', array( self::class, 'add_script_data' ) );
82    }
83
84    /**
85     * Add the package's image base URL to the admin script data.
86     *
87     * Other plugins' bundles request the activation screen's images from this copy, so never rename one.
88     *
89     * @since 4.1.0
90     *
91     * @param array $data Script data.
92     * @return array
93     */
94    public static function add_script_data( $data ) {
95        $data['licensing']['assetsUrl'] = trailingslashit( plugins_url( 'assets/images/', __DIR__ ) );
96
97        return $data;
98    }
99
100    /**
101     * Initialize endpoints required for Licensing package.
102     *
103     * @since 1.7.0
104     *
105     * @return void
106     */
107    public function initialize_endpoints() {
108        $endpoints = new Endpoints();
109        $endpoints->register_endpoints();
110    }
111
112    /**
113     * Get Jetpack connection manager instance.
114     *
115     * @return Connection_Manager
116     */
117    protected function connection() {
118        static $connection;
119
120        if ( null === $connection ) {
121            $connection = new Connection_Manager();
122        }
123
124        return $connection;
125    }
126
127    /**
128     * Get the last license attach request error that has occurred, if any.
129     *
130     * @return string Human-readable error message or an empty string.
131     */
132    public function last_error() {
133        return Jetpack_Options::get_option( 'licensing_error', '' );
134    }
135
136    /**
137     * Log an error to be surfaced to the user at a later time.
138     *
139     * @param string $error Human-readable error message.
140     * @return void
141     */
142    public function log_error( $error ) {
143        $substr = function_exists( 'mb_substr' ) ? 'mb_substr' : 'substr';
144        Jetpack_Options::update_option( 'licensing_error', $substr( $error, 0, 1024 ) );
145    }
146
147    /**
148     * Get all stored licenses.
149     *
150     * @return string[] License keys.
151     */
152    public function stored_licenses() {
153        $licenses = (array) get_option( self::LICENSES_OPTION_NAME, array() );
154        $licenses = array_filter( $licenses, 'is_scalar' );
155        $licenses = array_map( 'strval', $licenses );
156        $licenses = array_filter( $licenses );
157
158        return $licenses;
159    }
160
161    /**
162     * Append a license
163     *
164     * @param string $license A jetpack license key.
165     * @return bool True if the option was updated with the new license, false otherwise.
166     */
167    public function append_license( $license ) {
168        $licenses = $this->stored_licenses();
169
170        array_push( $licenses, $license );
171
172        return update_option( self::LICENSES_OPTION_NAME, $licenses );
173    }
174
175    /**
176     * Make an authenticated WP.com XMLRPC multicall request to attach the provided license keys.
177     *
178     * @param string[] $licenses License keys to attach.
179     * @return Jetpack_IXR_ClientMulticall
180     */
181    protected function attach_licenses_request( array $licenses ) {
182        $xml = new Jetpack_IXR_ClientMulticall( array( 'timeout' => 30 ) );
183
184        foreach ( $licenses as $license ) {
185            $xml->addCall( 'jetpack.attachLicense', $license );
186        }
187
188        $xml->query();
189
190        return $xml;
191    }
192
193    /**
194     * Attach the given licenses.
195     *
196     * @param string[] $licenses Licenses to attach.
197     * @return array|WP_Error Results for each license (which may include WP_Error instances) or a WP_Error instance.
198     */
199    public function attach_licenses( array $licenses ) {
200        if ( ! $this->connection()->has_connected_owner() ) {
201            return new WP_Error( 'not_connected', __( 'Jetpack doesn\'t have a connected owner.', 'jetpack-licensing' ) );
202        }
203
204        if ( empty( $licenses ) ) {
205            return array();
206        }
207
208        $xml = $this->attach_licenses_request( $licenses );
209
210        if ( $xml->isError() ) {
211            $error = new WP_Error( 'request_failed', __( 'License attach request failed.', 'jetpack-licensing' ) );
212            $error->add( $xml->getErrorCode(), $xml->getErrorMessage() );
213            return $error;
214        }
215
216        $results = array_map(
217            function ( $response ) {
218                if ( isset( $response['faultCode'] ) || isset( $response['faultString'] ) ) {
219                    return new WP_Error( $response['faultCode'], $response['faultString'] );
220                }
221
222                return $response;
223            },
224            (array) $xml->getResponse()
225        );
226
227        return $results;
228    }
229
230    /**
231     * Attach all stored licenses.
232     *
233     * @return array|WP_Error Results for each license (which may include WP_Error instances) or a WP_Error instance.
234     */
235    public function attach_stored_licenses() {
236        $licenses = $this->stored_licenses();
237        $results  = $this->attach_licenses( $licenses );
238
239        if ( is_wp_error( $results ) ) {
240            if ( 'request_failed' === $results->get_error_code() ) {
241                $this->log_error(
242                    __( 'Failed to attach your Jetpack license(s). Please try reconnecting Jetpack.', 'jetpack-licensing' )
243                );
244            }
245
246            return $results;
247        }
248
249        $failed = array();
250
251        foreach ( $results as $index => $result ) {
252            if ( isset( $licenses[ $index ] ) && is_wp_error( $result ) ) {
253                $failed[] = $licenses[ $index ];
254            }
255        }
256
257        if ( ! empty( $failed ) ) {
258            $this->log_error(
259                sprintf(
260                    /* translators: %s is a comma-separated list of license keys. */
261                    __( 'The following Jetpack licenses are invalid, already in use, or revoked: %s', 'jetpack-licensing' ),
262                    implode( ', ', $failed )
263                )
264            );
265        }
266
267        return $results;
268    }
269
270    /**
271     * Attach all stored licenses during connection flow for the connection owner.
272     *
273     * @return void
274     */
275    public function attach_stored_licenses_on_connection() {
276        if ( $this->connection()->is_connection_owner() ) {
277            $this->attach_stored_licenses();
278        }
279    }
280
281    /**
282     * Is the current user allowed to use the Licensing Input UI?
283     *
284     * @since 1.4.0
285     * @return bool
286     */
287    public static function is_licensing_input_enabled() {
288        /**
289         * Filter that checks if the user is allowed to see the Licensing UI. `true` enables it.
290         *
291         * @since 1.4.0
292         *
293         * @param bool False by default.
294         */
295        return apply_filters( 'jetpack_licensing_ui_enabled', false ) && current_user_can( 'jetpack_connect_user' );
296    }
297
298    /**
299     * Gets the user-licensing activation notice dismissal info.
300     *
301     * @since 10.4.0
302     * @return array
303     */
304    public function get_license_activation_notice_dismiss() {
305
306        $default = array(
307            'last_detached_count' => null,
308            'last_dismissed_time' => null,
309        );
310
311        if ( $this->connection()->is_user_connected() && $this->connection()->is_connection_owner() ) {
312            return Jetpack_Options::get_option( 'licensing_activation_notice_dismiss', $default );
313        }
314
315        return $default;
316    }
317
318    /**
319     * Load current user's licenses.
320     *
321     * @param bool $unattached_only Only return unattached and not revoked licenses.
322     *
323     * @return array
324     */
325    public function get_user_licenses( $unattached_only = false ) {
326        $licenses = Endpoints::get_user_licenses();
327
328        if ( empty( $licenses->items ) ) {
329            return array();
330        }
331
332        $items = $licenses->items;
333
334        if ( $unattached_only ) {
335            $items = array_filter(
336                $items,
337                static function ( $item ) {
338                    return $item->attached_at === null && $item->revoked_at === null;
339                }
340            );
341        }
342
343        return $items;
344    }
345
346    /**
347     * If the destination URL is checkout page,
348     * see if there are unattached licenses they could use instead of getting a new one.
349     * If found, redirect the user to license activation.
350     *
351     * @param string $dest_url User's destination URL.
352     *
353     * @return void
354     */
355    public function handle_user_connected_redirect( $dest_url ) {
356        if ( ! preg_match( '#^https://[^/]+/checkout/#i', $dest_url ) ) {
357            return;
358        }
359
360        $licenses    = $this->get_user_licenses( true );
361        $plugin_slug = null;
362
363        $query_string = wp_parse_url( $dest_url, PHP_URL_QUERY );
364        if ( $query_string ) {
365            parse_str( $query_string, $query_args );
366
367            if ( $query_args['redirect_to']
368                && preg_match( '/^admin\.php\?page=(jetpack-\w+)/i', $query_args['redirect_to'], $matches )
369            ) {
370                $plugin_slug = $matches[1];
371            }
372        }
373
374        /**
375         * Check for the user's unattached licenses.
376         *
377         * @since 3.8.2
378         *
379         * @param bool   $has_license Whether a license was already found.
380         * @param array  $licenses Unattached licenses belonging to the user.
381         * @param string $plugin_slug Slug of the plugin that initiated the flow.
382         */
383        if ( $plugin_slug && count( $licenses )
384            && apply_filters( 'jetpack_connection_user_has_license', false, $licenses, $plugin_slug )
385        ) {
386            wp_safe_redirect( '/wp-admin/admin.php?page=my-jetpack#/add-license' );
387            exit( 0 );
388        }
389    }
390}