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 object|null $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       $events Array of actions received from the remote site
159         * @param object|null $token  The auth token used to invoke the API
160         */
161        do_action( 'jetpack_sync_remote_actions', $events, $token );
162
163        foreach ( $events as $key => $event ) {
164            list( $action_name, $args, $user_id, $timestamp, $silent ) = $event;
165
166            /**
167             * Fires when an action is received from a remote Jetpack site
168             *
169             * @since 1.6.3
170             * @since-jetpack 4.2.0
171             *
172             * @param string      $action_name    The name of the action executed on the remote site
173             * @param array       $args           The arguments passed to the action
174             * @param int         $user_id        The external_user_id who did the action
175             * @param bool        $silent         Whether the item was created via import
176             * @param double      $timestamp      Timestamp (in seconds) when the action occurred
177             * @param double      $sent_timestamp Timestamp (in seconds) when the action was transmitted
178             * @param string      $queue_id       ID of the queue from which the event was sent (sync or full_sync)
179             * @param object|null $token          The auth token used to invoke the API
180             */
181            do_action( 'jetpack_sync_remote_action', $action_name, $args, $user_id, $silent, $timestamp, $sent_timestamp, $queue_id, $token );
182
183            $events_processed[] = $key;
184
185            if ( microtime( true ) - $start_time > self::MAX_TIME_PER_REQUEST_IN_SECONDS ) {
186                break;
187            }
188        }
189
190        if ( $token ) {
191            $this->remove_request_lock( $token->blog_id );
192        }
193
194        return $events_processed;
195    }
196}