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