Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 154
0.00% covered (danger)
0.00%
0 / 25
CRAP
n/a
0 / 0
zeroBS_api_rewrite_endpoint
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
jpcrm_api_process_pagination
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
jpcrm_api_process_search
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
jpcrm_api_process_replace_hyphens_in_json_keys
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
jpcrm_api_process_external_api_name
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
jpcrm_api_invalid_request
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
jpcrm_api_unauthorised_request
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
jpcrm_api_forbidden_request
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
jpcrm_api_invalid_method
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
jpcrm_api_teapot
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
jpcrm_api_check_authentication
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
jpcrm_api_check_http_method
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
zeroBSCRM_API_error
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
zeroBSCRM_API_locate_api_endpoint
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
zeroBSCRM_API_get_api_endpoint
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
jpcrm_is_api_request_authorised
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
56
zeroBSCRM_getAPIEndpoint
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
jpcrm_generate_api_publishable_key
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
jpcrm_generate_api_secret_key
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
zeroBSCRM_API_api_endpoint
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
30
hash_equals
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
jpcrm_generate_api_creds
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
zeroBSCRM_getAPIKey
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
zeroBSCRM_getAPISecret
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
jpcrm_api_replace_hyphens_in_json_keys_with_underscores
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/*
3 * Jetpack CRM
4 * https://jetpackcrm.com
5 * V2.0
6 *
7 * Copyright 2020 Automattic
8 *
9 * Date: 05/04/2017
10 */
11
12/*
13======================================================
14    Breaking Checks ( stops direct access )
15    ====================================================== */
16if ( ! defined( 'ZEROBSCRM_PATH' ) ) {
17    exit( 0 );
18}
19/*
20======================================================
21    / Breaking Checks
22    ====================================================== */
23
24// } We can do this below in the templater or templates? add_action( 'wp_enqueue_scripts', 'zeroBS_portal_enqueue_stuff' );
25// } ... in the end we can just dump the above line into the templates before get_header() - hacky but works
26
27// Adds the Rewrite Endpoint for the 'clients' area of the CRM.
28// } WH - this is dumped here now, because this whole thing is fired just AFTER init (to allow switch/on/off in main ZeroBSCRM.php)
29
30function zeroBS_api_rewrite_endpoint() {
31    add_rewrite_endpoint( 'zbs_api', EP_ROOT );
32}
33add_action( 'init', 'zeroBS_api_rewrite_endpoint' );
34
35/**
36 * Process the query and get page and items per page
37 */
38function jpcrm_api_process_pagination() {
39    // phpcs:disable WordPress.Security.NonceVerification.Recommended
40    $page     = isset( $_GET['page'] ) ? max( (int) $_GET['page'], 1 ) : 1;
41    $per_page = isset( $_GET['perpage'] ) ? max( (int) $_GET['perpage'], 1 ) : 10;
42    $order    = strtoupper( $_GET['order'] ?? '' ) === 'ASC' ? 'ASC' : 'DESC'; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
43    // phpcs:enable WordPress.Security.NonceVerification.Recommended    
44
45    return array(
46        'page'     => $page,
47        'per_page' => $per_page,
48        'order'    => $order,
49    );
50}
51
52/**
53 * Check and process if there is a search in the query
54 */
55function jpcrm_api_process_search() {
56    return ( isset( $_GET['zbs_query'] ) ? sanitize_text_field( $_GET['zbs_query'] ) : '' );
57}
58
59/**
60 * If there is a `replace_hyphens_with_underscores_in_json_keys` parameter in
61 * the request, it is returned as an int. Otherwise returns 0.
62 *
63 * @return int Parameter `replace_hyphens_with_underscores_in_json_keys` from request. 0 if it isn't set.
64 */
65function jpcrm_api_process_replace_hyphens_in_json_keys() {
66    return ( isset( $_GET['replace_hyphens_with_underscores_in_json_keys'] ) ? (int) $_GET['replace_hyphens_with_underscores_in_json_keys'] : 0 ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
67}
68
69/**
70 * If there is a `external_api_name` parameter in
71 * the request, it is returned as a string. Otherwise returns the bool false.
72 *
73 * @return string|bool Parameter `external_api_name` from request. Returns false if it isn't set.
74 */
75function jpcrm_api_process_external_api_name() {
76    return ( isset( $_GET['external_api_name'] ) ? sanitize_text_field( wp_unslash( $_GET['external_api_name'] ) ) : false ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
77}
78
79/**
80 * Generate API invalid request error
81 */
82function jpcrm_api_invalid_request() {
83    $reply = array(
84        'status'  => __( 'Bad request', 'zero-bs-crm' ),
85        'message' => __( 'The API request was invalid.', 'zero-bs-crm' ),
86    );
87    wp_send_json_error( $reply, 400 );
88}
89
90/**
91 * Generate API unauthorised request error
92 */
93function jpcrm_api_unauthorised_request() {
94    $reply = array(
95        'status'  => __( 'Unauthorized', 'zero-bs-crm' ),
96        'message' => __( 'Please ensure your Jetpack CRM API key and secret are correctly configured.', 'zero-bs-crm' ),
97    );
98    wp_send_json_error( $reply, 401 );
99}
100
101/**
102 * Generate API forbidden request error
103 */
104function jpcrm_api_forbidden_request() {
105    $reply = array(
106        'status'  => __( 'Forbidden', 'zero-bs-crm' ),
107        'message' => __( 'You do not have permission to access this resource.', 'zero-bs-crm' ),
108    );
109    wp_send_json_error( $reply, 403 );
110}
111
112/**
113 * Generate API invalid method error
114 */
115function jpcrm_api_invalid_method() {
116    $reply = array(
117        'status'  => __( 'Method not allowed', 'zero-bs-crm' ),
118        'message' => __( 'Please ensure you are using the proper method (e.g. POST or GET).', 'zero-bs-crm' ),
119    );
120    wp_send_json_error( $reply, 405 );
121}
122
123/**
124 * Generate API teapot error
125 */
126function jpcrm_api_teapot() {
127    $reply = array(
128        'status'  => 'I\'m a teapot',
129        'message' => 'As per RFC 2324 (section 2.3.2), this response is short and stout.',
130    );
131    wp_send_json_error( $reply, 418 );
132}
133
134/**
135 * Check if the request is authorised via the API key/secret
136 *
137 * @return bool or error
138 */
139function jpcrm_api_check_authentication() {
140
141    if ( ! jpcrm_is_api_request_authorised() ) {
142        jpcrm_api_unauthorised_request();
143    }
144
145    return true;
146}
147
148/**
149 * Check if the request matches the expected HTTP methods
150 *
151 * @param array $methods_allowed List of the request HTTP methods (GET, POST, PUSH, DELETE)
152 * @return bool or error
153 */
154function jpcrm_api_check_http_method( $methods_allowed = array( 'GET' ) ) {
155
156    if ( ! in_array( $_SERVER['REQUEST_METHOD'], $methods_allowed ) ) {
157        if ( $_SERVER['REQUEST_METHOD'] == 'BREW' ) {
158            jpcrm_api_teapot();
159        } else {
160            jpcrm_api_invalid_method();
161        }
162    }
163    return true;
164}
165
166/**
167 * Manage an API Error response with the correct headers and encode the data to JSON
168 *
169 * @param string $errorMsg
170 * @param int    $headerCode
171 */
172function zeroBSCRM_API_error( $errorMsg = 'Error', $header_code = 400 ) {
173
174    // } 400 = general error
175    // } 403 = perms
176    wp_send_json( array( 'error' => $errorMsg ), $header_code );
177}
178
179// now to locate the templates...
180// http://jeroensormani.com/how-to-add-template-files-in-your-plugin/
181
182/**
183 * Locate template.
184 *
185 * Locate the called template.
186 * Search Order:
187 * 1. /templates not over-ridable
188 *
189 * @since 1.2.7
190 *
191 * @param   string $template_name          Template to load.
192 * @param   string $string $template_path  Path to templates.
193 * @param   string $default_path           Default path to template files.
194 * @return  string                          Path to the template file.
195 */
196function zeroBSCRM_API_locate_api_endpoint( $template_name, $template_path = '', $default_path = '' ) {
197    // Set variable to search in zerobscrm-plugin-templates folder of theme.
198    if ( ! $template_path ) {
199        $template_path = 'zerobscrm-plugin-templates/';
200    }
201    // Set default plugin templates path.
202    if ( ! $default_path ) {
203        $default_path = ZEROBSCRM_PATH . 'api/'; // Path to the template folder
204    }
205    // Search template file in theme folder.
206    $template = locate_template(
207        array(
208            $template_path . $template_name,
209            $template_name,
210        )
211    );
212    // Get plugins template file.
213    if ( ! $template ) {
214        $template = $default_path . $template_name;
215    }
216    return apply_filters( 'zeroBSCRM_API_locate_api_endpoint', $template, $template_name, $template_path, $default_path );
217}
218
219/**
220 * Get template.
221 *
222 * Search for the template and include the file.
223 *
224 * @since 1.2.7
225 *
226 * @see zeroBSCRM_API_get_template()
227 *
228 * @param string $template_name          Template to load.
229 * @param array  $args                   Args passed for the template file.
230 * @param string $string $template_path  Path to templates.
231 * @param string $default_path           Default path to template files.
232 */
233function zeroBSCRM_API_get_api_endpoint( $template_name, $args = array(), $tempate_path = '', $default_path = '' ) {
234    if ( is_array( $args ) && isset( $args ) ) {
235        extract( $args );
236    }
237    $template_file = zeroBSCRM_API_locate_api_endpoint( $template_name, $tempate_path, $default_path );
238    if ( ! file_exists( $template_file ) ) {
239        _doing_it_wrong( __FUNCTION__, sprintf( '<code>%s</code> does not exist.', esc_html( $template_file ) ), '1.0.0' );
240        return;
241    }
242    include $template_file;
243}
244
245// function similar to is_user_logged_in()
246function jpcrm_is_api_request_authorised() {
247    // We should switch authentication method to "headers" not parameters - will be cleaner :)
248
249    // the the API key/secret are currently in the URL
250    $possible_api_key    = isset( $_GET['api_key'] ) ? sanitize_text_field( $_GET['api_key'] ) : '';
251    $possible_api_secret = isset( $_GET['api_secret'] ) ? sanitize_text_field( $_GET['api_secret'] ) : '';
252
253    // a required value is empty, so not authorised
254    if ( empty( $possible_api_key ) || empty( $possible_api_secret ) ) {
255        return false;
256    }
257
258    $api_key = zeroBSCRM_getAPIKey();
259
260    // provided key doesn't match, so not authorised
261    if ( ! hash_equals( $possible_api_key, $api_key ) ) {
262        return false;
263    }
264
265    global $zbs;
266    $zbs->load_encryption();
267    $hashed_possible_api_secret = $zbs->encryption->hash( $possible_api_secret );
268    $hashed_api_secret          = zeroBSCRM_getAPISecret();
269
270    // provided secret doesn't match, so not authorised
271    if ( ! hash_equals( $hashed_possible_api_secret, $hashed_api_secret ) ) {
272        return false;
273    }
274
275    return true;
276}
277
278function zeroBSCRM_getAPIEndpoint() {
279    return site_url( '/zbs_api/' ); // , 'https' );
280}
281
282function jpcrm_generate_api_publishable_key() {
283    global $zbs;
284    $zbs->load_encryption();
285    $api_publishable_key = 'jpcrm_pk_' . $zbs->encryption->get_rand_hex();
286    return $api_publishable_key;
287}
288
289function jpcrm_generate_api_secret_key() {
290    global $zbs;
291    $zbs->load_encryption();
292    $api_secret_key = 'jpcrm_sk_' . $zbs->encryption->get_rand_hex();
293    return $api_secret_key;
294}
295
296/*
297SAME CODE AS IN PORTAL, BUT REPLACED WITH api_endpoint stuff. Templates (to return the JSON) are in /api/endpoints/ folder
298*/
299
300add_filter( 'template_include', 'zeroBSCRM_API_api_endpoint', 99 );
301
302function zeroBSCRM_API_api_endpoint( $template ) {
303
304    $zbsAPIQuery = get_query_var( 'zbs_api', false );
305
306    // We only want to interfere where zbs_api is set :)
307    // ... as this is called for ALL page loads
308    if ( $zbsAPIQuery === false ) {
309        return $template;
310    }
311
312    // check if API key/secret are correct, or die with 403
313    jpcrm_api_check_authentication();
314
315    // Break it up if / present
316    if ( strpos( $zbsAPIQuery, '/' ) ) {
317        $zbsAPIRequest = explode( '/', $zbsAPIQuery );
318    } else {
319        // no / in it, so must just be a 1 worder like "invoices", here just jam in array so it matches prev exploded req.
320        $zbsAPIRequest = array( $zbsAPIQuery );
321    }
322
323    // no endpoint was specified, so die with 400
324    if ( empty( $zbsAPIRequest[0] ) ) {
325        jpcrm_api_invalid_request();
326    }
327
328    // hard-coded valid endpoints; we could at some point do this dynamically
329    $valid_api_endpoints = array(
330        'status',
331        'webhook',
332        'create_customer',
333        'create_transaction',
334        'create_event',
335        'create_company',
336        'customer_search',
337        'customers',
338        'invoices',
339        'quotes',
340        'events',
341        'companies',
342        'transactions',
343    );
344
345    // invalid endpoint was specified, so die with 400
346    if ( ! in_array( $zbsAPIRequest[0], $valid_api_endpoints ) ) {
347        jpcrm_api_invalid_request();
348    }
349
350    return zeroBSCRM_API_get_api_endpoint( $zbsAPIRequest[0] . '.php' );
351}
352
353if ( ! function_exists( 'hash_equals' ) ) {
354    function hash_equals( $str1, $str2 ) {
355        if ( strlen( $str1 ) != strlen( $str2 ) ) {
356            return false;
357        } else {
358            $res = (string) ( $str1 ^ $str2 );
359            $ret = 0;
360            for ( $i = strlen( $res ) - 1; $i >= 0; $i-- ) {
361                $ret |= ord( $res[ $i ] );
362            }
363            return ! $ret;
364        }
365    }
366}
367
368// generate new API credentials
369function jpcrm_generate_api_creds() {
370
371    global $zbs;
372    $new_publishable_key = jpcrm_generate_api_publishable_key();
373    $zbs->DAL->updateSetting( 'api_key', $new_publishable_key );
374
375    $new_secret_key    = jpcrm_generate_api_secret_key();
376    $hashed_api_secret = $zbs->encryption->hash( $new_secret_key );
377    $zbs->DAL->updateSetting( 'api_secret', $hashed_api_secret );
378
379    return array(
380        'key'    => $new_publishable_key,
381        'secret' => $new_secret_key,
382    );
383}
384
385// each CRM is  only given one API (for now)
386function zeroBSCRM_getAPIKey() {
387
388    global $zbs;
389    return $zbs->DAL->setting( 'api_key' );
390}
391
392// each CRM is  only given one API (for now)
393function zeroBSCRM_getAPISecret() {
394
395    global $zbs;
396    return $zbs->DAL->setting( 'api_secret' );
397}
398
399/**
400 * Replaces hyphens in key identifiers from a json array with underscores.
401 * Some services do not accept hyphens in their key identifiers, e.g. Zapier:
402 * https://github.com/zapier/zapier-platform/blob/master/packages/schema/docs/build/schema.md#keyschema
403 *
404 * If we expand use of this, we should consider making it recursive.
405 *
406 * @param array $input_array The array with keys needing to be changed, e.g.: [ { "id":"1", "custom-price":"10" }, { "id":"2", "custom-price":"20" }, ].
407 * @return array Array with changed keys, e.g.: [ { "id":"1", "custom_price":"10" }, { "id":"2", "custom_price":"20" }, ].
408 */
409function jpcrm_api_replace_hyphens_in_json_keys_with_underscores( $input_array ) {
410    $new_array = array();
411    foreach ( $input_array as $original_item ) {
412        $new_array[] = array_combine(
413            array_map(
414                function ( $key ) {
415                    return str_replace( '-', '_', $key );
416                },
417                array_keys( $original_item )
418            ),
419            $original_item
420        );
421    }
422    return $new_array;
423}