Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Server
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 6
182
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 set_codec
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 attempt_request_lock
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 get_concurrent_request_transient_name
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 remove_request_lock
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 receive
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
56
1<?php
2/**
3 * Sync server.
4 *
5 * @package automattic/jetpack-sync
6 */
7
8namespace Automattic\Jetpack\Sync;
9
10use WP_Error;
11
12/**
13 * Simple version of a Jetpack Sync Server - just receives arrays of events and
14 * issues them locally with the 'jetpack_sync_remote_action' action.
15 */
16class Server {
17    /**
18     * Codec used to decode sync events.
19     *
20     * @access private
21     *
22     * @var \Automattic\Jetpack\Sync\Codec_Interface
23     */
24    private $codec;
25
26    /**
27     * Maximum time for processing sync actions.
28     *
29     * @access public
30     *
31     * @var int
32     */
33    const MAX_TIME_PER_REQUEST_IN_SECONDS = 15;
34
35    /**
36     * Prefix of the blog lock transient.
37     *
38     * @access public
39     *
40     * @var string
41     */
42    const BLOG_LOCK_TRANSIENT_PREFIX = 'jp_sync_req_lock_';
43
44    /**
45     * Lifetime of the blog lock transient.
46     *
47     * @access public
48     *
49     * @var int
50     */
51    const BLOG_LOCK_TRANSIENT_EXPIRY = 60; // Seconds.
52
53    /**
54     * Constructor.
55     *
56     * This is necessary because you can't use "new" when you declare instance properties >:(
57     *
58     * @access public
59     */
60    public function __construct() {
61        $this->codec = new JSON_Deflate_Array_Codec();
62    }
63
64    /**
65     * Set the codec instance.
66     *
67     * @access public
68     *
69     * @param Automattic\Jetpack\Sync\Codec_Interface $codec Codec instance.
70     */
71    public function set_codec( Codec_Interface $codec ) {
72        $this->codec = $codec;
73    }
74
75    /**
76     * Attempt to lock the request when the server receives concurrent requests from the same blog.
77     *
78     * @access public
79     *
80     * @param int $blog_id ID of the blog.
81     * @param int $expiry  Blog lock transient lifetime.
82     * @return boolean True if succeeded, false otherwise.
83     */
84    public function attempt_request_lock( $blog_id, $expiry = self::BLOG_LOCK_TRANSIENT_EXPIRY ) {
85        $transient_name = $this->get_concurrent_request_transient_name( $blog_id );
86        $locked_time    = get_site_transient( $transient_name );
87        if ( $locked_time ) {
88            return false;
89        }
90        set_site_transient( $transient_name, microtime( true ), $expiry );
91
92        return true;
93    }
94
95    /**
96     * Retrieve the blog lock transient name for a particular blog.
97     *
98     * @access public
99     *
100     * @param int $blog_id ID of the blog.
101     * @return string Name of the blog lock transient.
102     */
103    private function get_concurrent_request_transient_name( $blog_id ) {
104        return self::BLOG_LOCK_TRANSIENT_PREFIX . $blog_id;
105    }
106
107    /**
108     * Remove the request lock from a particular blog ID.
109     *
110     * @access public
111     *
112     * @param int $blog_id ID of the blog.
113     */
114    public function remove_request_lock( $blog_id ) {
115        delete_site_transient( $this->get_concurrent_request_transient_name( $blog_id ) );
116    }
117
118    /**
119     * Receive and process sync events.
120     *
121     * @access public
122     *
123     * @param array  $data           Sync events.
124     * @param object $token          The auth token used to invoke the API.
125     * @param int    $sent_timestamp Timestamp (in seconds) when the actions were transmitted.
126     * @param string $queue_id       ID of the queue from which the event was sent (`sync` or `full_sync`).
127     * @return array Processed sync events.
128     */
129    public function receive( $data, $token = null, $sent_timestamp = null, $queue_id = null ) {
130        $start_time = microtime( true );
131        if ( ! is_array( $data ) ) {
132            return new WP_Error( 'action_decoder_error', 'Events must be an array' );
133        }
134
135        if ( $token && ! $this->attempt_request_lock( $token->blog_id ) ) {
136            /**
137             * Fires when the server receives two concurrent requests from the same blog
138             *
139             * @since 1.6.3
140             * @since-jetpack 4.2.0
141             *
142             * @param token The token object of the misbehaving site
143             */
144            do_action( 'jetpack_sync_multi_request_fail', $token );
145
146            return new WP_Error( 'concurrent_request_error', 'There is another request running for the same blog ID' );
147        }
148
149        $events           = wp_unslash( array_map( array( $this->codec, 'decode' ), $data ) );
150        $events_processed = array();
151
152        /**
153         * Fires when an array of actions are received from a remote Jetpack site
154         *
155         * @since 1.6.3
156         * @since-jetpack 4.2.0
157         *
158         * @param array Array of actions received from the remote site
159         */
160        do_action( 'jetpack_sync_remote_actions', $events, $token );
161
162        foreach ( $events as $key => $event ) {
163            list( $action_name, $args, $user_id, $timestamp, $silent ) = $event;
164
165            /**
166             * Fires when an action is received from a remote Jetpack site
167             *
168             * @since 1.6.3
169             * @since-jetpack 4.2.0
170             *
171             * @param string $action_name The name of the action executed on the remote site
172             * @param array $args The arguments passed to the action
173             * @param int $user_id The external_user_id who did the action
174             * @param bool $silent Whether the item was created via import
175             * @param double $timestamp Timestamp (in seconds) when the action occurred
176             * @param double $sent_timestamp Timestamp (in seconds) when the action was transmitted
177             * @param string $queue_id ID of the queue from which the event was sent (sync or full_sync)
178             * @param array $token The auth token used to invoke the API
179             */
180            do_action( 'jetpack_sync_remote_action', $action_name, $args, $user_id, $silent, $timestamp, $sent_timestamp, $queue_id, $token );
181
182            $events_processed[] = $key;
183
184            if ( microtime( true ) - $start_time > self::MAX_TIME_PER_REQUEST_IN_SECONDS ) {
185                break;
186            }
187        }
188
189        if ( $token ) {
190            $this->remove_request_lock( $token->blog_id );
191        }
192
193        return $events_processed;
194    }
195}