Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.62% covered (success)
90.62%
58 / 64
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Data
90.62% covered (success)
90.62%
58 / 64
66.67% covered (warning)
66.67%
4 / 6
19.30
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 start
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 init
69.23% covered (warning)
69.23%
9 / 13
0.00% covered (danger)
0.00%
0 / 1
4.47
 scripts
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
1
 delete_token_on_401_response
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 authenticate
92.00% covered (success)
92.00%
23 / 25
0.00% covered (danger)
0.00%
0 / 1
9.04
1<?php
2
3namespace NewfoldLabs\WP\Module\Data;
4
5use NewfoldLabs\WP\Module\Data\API\Capabilities;
6use NewfoldLabs\WP\ModuleLoader\Plugin;
7use wpscholar\Url;
8use function WP_Forge\Helpers\dataGet;
9
10/**
11 * Main class for the data plugin module
12 */
13class Data {
14
15    /**
16     * Hiive Connection instance
17     *
18     * @var HiiveConnection
19     */
20    public $hiive;
21
22    /**
23     * Last instantiated instance of this class.
24     *
25     * @used-by EventManager::rest_api_init()
26     *
27     * @var Data
28     */
29    public static $instance;
30
31    /**
32     * Dependency injection container.
33     *
34     * @var Plugin
35     */
36    protected $plugin;
37
38    /**
39     * Event manager instance.
40     *
41     * @var EventManager $event_manager
42     */
43    protected $event_manager;
44
45    /**
46     * Data constructor.
47     *
48     * @param Plugin        $plugin        Dependency injection container.
49     * @param ?EventManager $event_manager Event manager instance.
50     */
51    public function __construct(
52        Plugin $plugin,
53        ?EventManager $event_manager = null
54    ) {
55        self::$instance = $this;
56
57        $this->plugin = $plugin;
58
59        $this->event_manager = $event_manager ?? new EventManager();
60    }
61
62    /**
63     * Start up the plugin module
64     *
65     * Do this separately so it isn't tied to class creation
66     *
67     * @see bootstrap.php
68     * @see \NewfoldLabs\WP\ModuleLoader\register()
69     */
70    public function start(): void {
71
72        // The minutely schedule must exist on every request, even when we bail out of init()
73        // below because the site isn't connected to Hiive. A previously scheduled
74        // nfd_data_sync_cron event survives disconnection and WP-Cron cannot reschedule it
75        // without the schedule being registered.
76        $this->event_manager->register_cron_schedule();
77
78        // Delays our primary module setup until init
79        add_action( 'init', array( $this, 'init' ) );
80        add_filter( 'rest_authentication_errors', array( $this, 'authenticate' ) );
81
82        // If we ever get a 401 response from the Hiive API, delete the token.
83        add_filter( 'http_response', array( $this, 'delete_token_on_401_response' ), 10, 3 );
84        // Register the admin scripts.
85        add_action( 'admin_enqueue_scripts', array( $this, 'scripts' ) );
86    }
87
88    /**
89     * Initialize all other module functionality
90     *
91     * @hooked init
92     */
93    public function init(): void {
94
95        $this->hiive = new HiiveConnection();
96
97        $this->event_manager->initialize_rest_endpoint();
98
99        // Initialize the required verification endpoints
100        $this->hiive->register_verification_hooks();
101
102        // If not connected, attempt to connect and
103        // bail before registering the subscribers/listeners
104        if ( ! $this->hiive::is_connected() ) {
105
106            // Attempt to connect
107            $this->hiive->connect();
108
109            return;
110        }
111
112        $this->event_manager->init();
113
114        $this->event_manager->add_subscriber( $this->hiive );
115
116        if ( defined( 'NFD_DATA_DEBUG' ) && NFD_DATA_DEBUG ) {
117            $this->logger = new Logger();
118            $this->event_manager->add_subscriber( $this->logger );
119        }
120
121        // Register endpoint for clearing capabilities cache
122        $capabilities_api = new Capabilities( new SiteCapabilities() );
123        add_action( 'rest_api_init', array( $capabilities_api, 'register_routes' ) );
124    }
125
126    /**
127     * Enqueue admin scripts for our click events and other tracking.
128     */
129    public function scripts(): void {
130        wp_enqueue_script(
131            'newfold-hiive-events',
132            $this->plugin->url . 'vendor/newfold-labs/wp-module-data/assets/click-events.js',
133            array( 'wp-api-fetch', 'nfd-runtime' ),
134            $this->plugin->version,
135            true
136        );
137
138        // Inline script for global vars for ctb
139        wp_localize_script(
140            'newfold-hiive-events',
141            'nfdHiiveEvents',
142            array(
143                'eventEndpoint' => esc_url_raw( get_home_url() . '/index.php?rest_route=/newfold-data/v1/events/' ),
144                'brand'         => $this->plugin->brand,
145            )
146        );
147    }
148
149    /**
150     * Check HTTP responses for 401 authentication errors from Hiive, delete the invalid token.
151     *
152     * @hooked http_response
153     * @see WP_Http::request()
154     *
155     * @param array  $response The successful HTTP response.
156     * @param array  $args HTTP request arguments.
157     * @param string $url The request URL.
158     *
159     * @return array
160     */
161    public function delete_token_on_401_response( array $response, array $args, string $url ): array {
162
163        if ( strpos( $url, constant( 'NFD_HIIVE_URL' ) ) === 0 && absint( wp_remote_retrieve_response_code( $response ) ) === 401 ) {
164            delete_option( 'nfd_data_token' );
165        }
166
167        return $response;
168    }
169
170    /**
171     * Authenticate incoming REST API requests.
172     *
173     * Sets current user to user id provided in `$_GET['user_id']` or the first admin user if no user ID is provided.
174     *
175     * @hooked rest_authentication_errors
176     *
177     * @param  bool|null|\WP_Error $errors Current authentication result.
178     *
179     * @return bool|null|\WP_Error
180     * @see WP_REST_Server::check_authentication()
181     *
182     * @used-by ConnectSite::verifyToken() in Hiive.
183     */
184    public function authenticate( $errors ) {
185
186        // Make sure there wasn't a different authentication method used before this
187        if ( ! is_null( $errors ) ) {
188            return $errors;
189        }
190
191        // Make sure this is a REST API request
192        if ( ! defined( 'REST_REQUEST' ) || ! constant( 'REST_REQUEST' ) ) {
193            return $errors;
194        }
195
196        // If no auth header included, bail to allow a different auth method
197        if ( empty( $_SERVER['HTTP_AUTHORIZATION'] ) ) {
198            return null;
199        }
200
201        $token = str_replace( 'Bearer ', '', $_SERVER['HTTP_AUTHORIZATION'] );
202
203        $data = array(
204            'method'    => $_SERVER['REQUEST_METHOD'],
205            'url'       => Url::getCurrentUrl(),
206            'body'      => file_get_contents( 'php://input' ),
207            'timestamp' => dataGet( getallheaders(), 'X-Timestamp' ),
208        );
209
210        $hash = hash( 'sha256', wp_json_encode( $data ) );
211        $salt = hash( 'sha256', strrev( HiiveConnection::get_auth_token() ) );
212
213        $is_valid = hash( 'sha256', $hash . $salt ) === $token;
214
215        // Allow access if token is valid
216        if ( $is_valid ) {
217
218            if ( isset( $_GET['user_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
219
220                // If a user ID is provided, use it to find the desired user.
221                $user = get_user_by( 'id', filter_input( INPUT_GET, 'user_id', FILTER_SANITIZE_NUMBER_INT ) );
222
223            } else {
224
225                // If no user ID is provided, find the first admin user.
226                $admins = get_users( array( 'role' => 'administrator' ) );
227                $user   = array_shift( $admins );
228
229            }
230
231            if ( ! empty( $user ) && is_a( $user, \WP_User::class ) ) {
232                wp_set_current_user( $user->ID );
233
234                return true;
235            }
236        }
237
238        // Don't return false, since we could be interfering with a basic auth implementation.
239        return $errors;
240    }
241}