Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
56.31% covered (warning)
56.31%
58 / 103
56.25% covered (warning)
56.25%
9 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
Licensing
56.31% covered (warning)
56.31%
58 / 103
56.25% covered (warning)
56.25%
9 / 16
213.87
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
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 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     * Initialize endpoints required for Licensing package.
71     *
72     * @since 1.7.0
73     *
74     * @return void
75     */
76    public function initialize_endpoints() {
77        $endpoints = new Endpoints();
78        $endpoints->register_endpoints();
79    }
80
81    /**
82     * Get Jetpack connection manager instance.
83     *
84     * @return Connection_Manager
85     */
86    protected function connection() {
87        static $connection;
88
89        if ( null === $connection ) {
90            $connection = new Connection_Manager();
91        }
92
93        return $connection;
94    }
95
96    /**
97     * Get the last license attach request error that has occurred, if any.
98     *
99     * @return string Human-readable error message or an empty string.
100     */
101    public function last_error() {
102        return Jetpack_Options::get_option( 'licensing_error', '' );
103    }
104
105    /**
106     * Log an error to be surfaced to the user at a later time.
107     *
108     * @param string $error Human-readable error message.
109     * @return void
110     */
111    public function log_error( $error ) {
112        $substr = function_exists( 'mb_substr' ) ? 'mb_substr' : 'substr';
113        Jetpack_Options::update_option( 'licensing_error', $substr( $error, 0, 1024 ) );
114    }
115
116    /**
117     * Get all stored licenses.
118     *
119     * @return string[] License keys.
120     */
121    public function stored_licenses() {
122        $licenses = (array) get_option( self::LICENSES_OPTION_NAME, array() );
123        $licenses = array_filter( $licenses, 'is_scalar' );
124        $licenses = array_map( 'strval', $licenses );
125        $licenses = array_filter( $licenses );
126
127        return $licenses;
128    }
129
130    /**
131     * Append a license
132     *
133     * @param string $license A jetpack license key.
134     * @return bool True if the option was updated with the new license, false otherwise.
135     */
136    public function append_license( $license ) {
137        $licenses = $this->stored_licenses();
138
139        array_push( $licenses, $license );
140
141        return update_option( self::LICENSES_OPTION_NAME, $licenses );
142    }
143
144    /**
145     * Make an authenticated WP.com XMLRPC multicall request to attach the provided license keys.
146     *
147     * @param string[] $licenses License keys to attach.
148     * @return Jetpack_IXR_ClientMulticall
149     */
150    protected function attach_licenses_request( array $licenses ) {
151        $xml = new Jetpack_IXR_ClientMulticall( array( 'timeout' => 30 ) );
152
153        foreach ( $licenses as $license ) {
154            $xml->addCall( 'jetpack.attachLicense', $license );
155        }
156
157        $xml->query();
158
159        return $xml;
160    }
161
162    /**
163     * Attach the given licenses.
164     *
165     * @param string[] $licenses Licenses to attach.
166     * @return array|WP_Error Results for each license (which may include WP_Error instances) or a WP_Error instance.
167     */
168    public function attach_licenses( array $licenses ) {
169        if ( ! $this->connection()->has_connected_owner() ) {
170            return new WP_Error( 'not_connected', __( 'Jetpack doesn\'t have a connected owner.', 'jetpack-licensing' ) );
171        }
172
173        if ( empty( $licenses ) ) {
174            return array();
175        }
176
177        $xml = $this->attach_licenses_request( $licenses );
178
179        if ( $xml->isError() ) {
180            $error = new WP_Error( 'request_failed', __( 'License attach request failed.', 'jetpack-licensing' ) );
181            $error->add( $xml->getErrorCode(), $xml->getErrorMessage() );
182            return $error;
183        }
184
185        $results = array_map(
186            function ( $response ) {
187                if ( isset( $response['faultCode'] ) || isset( $response['faultString'] ) ) {
188                    return new WP_Error( $response['faultCode'], $response['faultString'] );
189                }
190
191                return $response;
192            },
193            (array) $xml->getResponse()
194        );
195
196        return $results;
197    }
198
199    /**
200     * Attach all stored licenses.
201     *
202     * @return array|WP_Error Results for each license (which may include WP_Error instances) or a WP_Error instance.
203     */
204    public function attach_stored_licenses() {
205        $licenses = $this->stored_licenses();
206        $results  = $this->attach_licenses( $licenses );
207
208        if ( is_wp_error( $results ) ) {
209            if ( 'request_failed' === $results->get_error_code() ) {
210                $this->log_error(
211                    __( 'Failed to attach your Jetpack license(s). Please try reconnecting Jetpack.', 'jetpack-licensing' )
212                );
213            }
214
215            return $results;
216        }
217
218        $failed = array();
219
220        foreach ( $results as $index => $result ) {
221            if ( isset( $licenses[ $index ] ) && is_wp_error( $result ) ) {
222                $failed[] = $licenses[ $index ];
223            }
224        }
225
226        if ( ! empty( $failed ) ) {
227            $this->log_error(
228                sprintf(
229                    /* translators: %s is a comma-separated list of license keys. */
230                    __( 'The following Jetpack licenses are invalid, already in use, or revoked: %s', 'jetpack-licensing' ),
231                    implode( ', ', $failed )
232                )
233            );
234        }
235
236        return $results;
237    }
238
239    /**
240     * Attach all stored licenses during connection flow for the connection owner.
241     *
242     * @return void
243     */
244    public function attach_stored_licenses_on_connection() {
245        if ( $this->connection()->is_connection_owner() ) {
246            $this->attach_stored_licenses();
247        }
248    }
249
250    /**
251     * Is the current user allowed to use the Licensing Input UI?
252     *
253     * @since 1.4.0
254     * @return bool
255     */
256    public static function is_licensing_input_enabled() {
257        /**
258         * Filter that checks if the user is allowed to see the Licensing UI. `true` enables it.
259         *
260         * @since 1.4.0
261         *
262         * @param bool False by default.
263         */
264        return apply_filters( 'jetpack_licensing_ui_enabled', false ) && current_user_can( 'jetpack_connect_user' );
265    }
266
267    /**
268     * Gets the user-licensing activation notice dismissal info.
269     *
270     * @since 10.4.0
271     * @return array
272     */
273    public function get_license_activation_notice_dismiss() {
274
275        $default = array(
276            'last_detached_count' => null,
277            'last_dismissed_time' => null,
278        );
279
280        if ( $this->connection()->is_user_connected() && $this->connection()->is_connection_owner() ) {
281            return Jetpack_Options::get_option( 'licensing_activation_notice_dismiss', $default );
282        }
283
284        return $default;
285    }
286
287    /**
288     * Load current user's licenses.
289     *
290     * @param bool $unattached_only Only return unattached and not revoked licenses.
291     *
292     * @return array
293     */
294    public function get_user_licenses( $unattached_only = false ) {
295        $licenses = Endpoints::get_user_licenses();
296
297        if ( empty( $licenses->items ) ) {
298            return array();
299        }
300
301        $items = $licenses->items;
302
303        if ( $unattached_only ) {
304            $items = array_filter(
305                $items,
306                static function ( $item ) {
307                    return $item->attached_at === null && $item->revoked_at === null;
308                }
309            );
310        }
311
312        return $items;
313    }
314
315    /**
316     * If the destination URL is checkout page,
317     * see if there are unattached licenses they could use instead of getting a new one.
318     * If found, redirect the user to license activation.
319     *
320     * @param string $dest_url User's destination URL.
321     *
322     * @return void
323     */
324    public function handle_user_connected_redirect( $dest_url ) {
325        if ( ! preg_match( '#^https://[^/]+/checkout/#i', $dest_url ) ) {
326            return;
327        }
328
329        $licenses    = $this->get_user_licenses( true );
330        $plugin_slug = null;
331
332        $query_string = wp_parse_url( $dest_url, PHP_URL_QUERY );
333        if ( $query_string ) {
334            parse_str( $query_string, $query_args );
335
336            if ( $query_args['redirect_to']
337                && preg_match( '/^admin\.php\?page=(jetpack-\w+)/i', $query_args['redirect_to'], $matches )
338            ) {
339                $plugin_slug = $matches[1];
340            }
341        }
342
343        /**
344         * Check for the user's unattached licenses.
345         *
346         * @since 3.8.2
347         *
348         * @param bool   $has_license Whether a license was already found.
349         * @param array  $licenses Unattached licenses belonging to the user.
350         * @param string $plugin_slug Slug of the plugin that initiated the flow.
351         */
352        if ( $plugin_slug && count( $licenses )
353            && apply_filters( 'jetpack_connection_user_has_license', false, $licenses, $plugin_slug )
354        ) {
355            wp_safe_redirect( '/wp-admin/admin.php?page=my-jetpack#/add-license' );
356            exit( 0 );
357        }
358    }
359}