Completed
Push — master ( 74e43f...ea0d74 )
by Francis
01:42
created

REST   F

Complexity

Total Complexity 61

Size/Duplication

Total Lines 353
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 157
c 3
b 0
f 0
dl 0
loc 353
rs 3.52
wmc 61

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 41 4
B authenticate() 0 25 8
A custom_auth() 0 12 4
B api_rest_limit_by_ip_address() 0 28 7
A process_auth() 0 8 6
C api_key_auth() 0 46 12
A ip_auth() 0 3 2
A basic_auth() 0 5 4
A bearer_auth() 0 18 6
A handle_response() 0 8 3
A get_authorization_header() 0 16 5

How to fix   Complexity   

Complex Class

Complex classes like REST often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use REST, and based on these observations, apply Extract Interface, too.

1
<?php
2
declare(strict_types=1);
3
defined('BASEPATH') OR exit('No direct script access allowed');
4
5
require_once('RESTAuth.php');
6
require_once('RESTResponse.php');
7
require_once('RESTExceptions.php');
8
9
class REST {
10
11
  /**
12
   * [private description]
13
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
14
   */
15
  private $ci;
16
  /**
17
   * [private description]
18
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
19
   */
20
  private $api_key_limit_column;
21
  /**
22
   * [private description]
23
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
24
   */
25
  private $api_key_column;
26
  /**
27
   * [private description]
28
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
29
   */
30
  private $per_hour;
31
  /**
32
   * [private description]
33
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
34
   */
35
  private $ip_per_hour;
36
  /**
37
   * [private description]
38
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
39
   */
40
  private $show_header;
41
  /**
42
   * [private description]
43
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
44
   */
45
  private $whitelist;
46
  /**
47
   * [private description]
48
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
49
   */
50
  private $checked_rate_limit = false;
51
  /**
52
   * [private description]
53
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
54
   */
55
  private $header_prefix;
56
  /**
57
   * [private description]
58
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
59
   */
60
  private $limit_api;
61
62
  /**
63
   * [public description]
64
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
65
   */
66
  public  $userId;
67
  /**
68
   * [public description]
69
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
70
   */
71
  public  $apiKeyHeader;
72
  /**
73
   * [public description]
74
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
75
   */
76
  public  $token;
77
  /**
78
   * [public description]
79
   * @var [type]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
80
   */
81
  public $allowedIPs;
82
  /**
83
   * [PACKAGE description]
84
   * @var string
85
   */
86
  const   PACKAGE    = "francis94c/ci-rest";
87
  /**
88
   * [RATE_LIMIT description]
89
   * @var string
90
   */
91
  const   RATE_LIMIT = "RateLimit";
92
93
  /**
94
   * [__construct This is the part of the code that takes care of all
95
   * authentiations. allowijg you to focus on build wonderfult things at REST.
96
   * pun intended ;-)]
97
   * @param array|null $params Initialization parameters from the Slint system.
98
   *                           There's no use for this arg yet.
99
   */
100
  function __construct($params=null) {
0 ignored issues
show
Unused Code introduced by
The parameter $params is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

100
  function __construct(/** @scrutinizer ignore-unused */ $params=null) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
101
    $this->ci =& get_instance();
0 ignored issues
show
Bug introduced by
The function get_instance was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

101
    $this->ci =& /** @scrutinizer ignore-call */ get_instance();
Loading history...
102
    // Load Config If Exists.
103
    $this->ci->config->load('rest', true, true);
104
    // Load Database.
105
    $this->ci->load->database();
106
    // Load Model.
107
    $this->ci->load->splint(self::PACKAGE, '*RESTModel', 'rest_model');
108
    $this->rest_model =& $this->ci->rest_model;
0 ignored issues
show
Bug Best Practice introduced by
The property rest_model does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
109
    $config = [
110
      'users_table'           => $this->ci->config->item('rest')['basic_auth']['users_table'] ?? null,
111
      'users_id_column'       => $this->ci->config->item('rest')['basic_auth']['id_column'] ?? null,
112
      'users_username_column' => $this->ci->config->item('rest')['basic_auth']['username_column'] ?? null,
113
      'users_email_column'    => $this->ci->config->item('rest')['basic_auth']['email_column'] ?? null,
114
      'users_password_column' => $this->ci->config->item('rest')['basic_auth']['password_column'] ?? null,
115
      'api_key_table'         => $this->ci->config->item('rest')['api_key_auth']['api_key_table'] ?? null,
116
      'api_key_column'        => $this->ci->config->item('rest')['api_key_auth']['api_key_column'] ?? null,
117
      'api_key_limit_column'  => $this->ci->config->item('rest')['api_key_auth']['api_key_limit_column'] ?? null
118
    ];
119
    $this->rest_model->init($config);
120
    // Load Variable(s) from Config.
121
    $this->allowedIPs = $this->ci->config->item('rest')['allowed_ips'] ?? ['127.0.0.1', '[::1]'];
122
    $this->apiKeyHeader = $this->ci->config->item('rest')['api_key_header'] ?? 'X-API-KEY';
123
    $this->api_key_limit_column = $this->ci->config->item('rest')['api_key_auth']['api_key_limit_column'] ?? null;
124
    $this->api_key_column = $this->ci->config->item('rest')['api_key_auth']['api_key_column'] ?? null;
125
    $this->limit_api = $this->ci->config->item('rest')['api_limiter']['api_limiter'] ?? false;
126
    $this->per_hour = $this->ci->config->item('rest')['api_limiter']['per_hour'] ?? 100;
127
    $this->ip_per_hour = $this->ci->config->item('rest')['api_limiter']['ip_per_hour'] ?? 50;
128
    $this->show_header = $this->ci->config->item('rest')['api_limiter']['show_header'] ?? null;
129
    $this->whitelist = $this->ci->config->item('rest')['api_limiter']['whitelist'] ?? null;
130
    $this->header_prefix = $this->ci->config->item('rest')['api_limiter']['header_prefix'] ?? 'X-RateLimit-';
131
    // Authenticate
132
    $this->authenticate();
133
134
    // Generic Rate Limiter.
135
    if ($this->limit_api && !$this->checked_rate_limit &&
136
    ($this->ci->config->item('rest')['api_limiter']['limit_by_ip'] ?? false)) {
137
      $this->api_rest_limit_by_ip_address();
138
    }
139
140
    log_message('debug', 'REST Request Authenticated and REST Library Initialized.');
0 ignored issues
show
Bug introduced by
The function log_message was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

140
    /** @scrutinizer ignore-call */ 
141
    log_message('debug', 'REST Request Authenticated and REST Library Initialized.');
Loading history...
141
  }
142
  /**
143
   * [authenticate description]
144
   */
145
  private function authenticate():void {
146
    $uri_auths = $this->ci->config->item('rest')['uri_auth'] ?? null;
147
    // Match Auth Routes.
148
    // The below algorithm is similar to the one Code Igniter uses in its
149
    // Routing Class.
150
    if ($uri_auths == null || !is_array($uri_auths)) return;
151
    $auths = null;
152
    foreach ($uri_auths as $uri => $auth_array) {
153
      // Convert wildcards to RegEx.
154
			$uri = str_replace(array(':any', ':num'), array('[^/]+', '[0-9]+'), $uri);
155
      if (preg_match('#^'.$uri.'$#', uri_string())) $auths = $auth_array; // Assign Authentication Steps.
0 ignored issues
show
Bug introduced by
The function uri_string was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

155
      if (preg_match('#^'.$uri.'$#', /** @scrutinizer ignore-call */ uri_string())) $auths = $auth_array; // Assign Authentication Steps.
Loading history...
156
      break;
157
    }
158
    //$auths = $this->ci->config->item('rest')['uri_auth'][uri_string()] ?? null;
159
    if ($auths == null) return; // No authentication(s) to acrry out.
160
    // $this->process_auth() terminates the script if authentication fails
161
    // It will call the callable in the rest.php config file under
162
    // response_callbacks which matches the necesarry RESTResponse constant
163
    // before exiting. Which callable is called in any situation is documented
164
    // in README.md
165
    if (is_scalar($auths)) {
166
      $this->process_auth($auths);
167
      return;
168
    }
169
    foreach ($auths as $auth) $this->process_auth($auth);
170
  }
171
  /**
172
   * [process_auth description]
173
   * @param  string $auth [description]
174
   * @return bool         [description]
175
   */
176
  private function process_auth(string &$auth):void {
177
    switch ($auth) {
178
      case RESTAuth::IP: $this->ip_auth(); break;
179
      case RESTAuth::BASIC: $this->basic_auth(); break;
180
      case RESTAuth::API_KEY: $this->api_key_auth(); break;
181
      case RESTAuth::OAUTH2: $this->bearer_auth(RESTAuth::OAUTH2); break;
182
      case RESTAuth::BEARER: $this->bearer_auth(); break;
183
      default: $this->custom_auth($auth);
184
    }
185
  }
186
  /**
187
   * [ip_auth description]
188
   */
189
  private function ip_auth():void {
190
    if (!in_array($this->ci->input->ip_address(), $this->allowedIPs)) {
191
      $this->handle_response(RESTResponse::UN_AUTHORIZED, RESTAuth::IP); // Exits.
192
    }
193
  }
194
  /**
195
   * [bearer_auth description]
196
   */
197
  private function bearer_auth($auth=RESTAuth::BEARER):void {
198
    $authorization = $this->get_authorization_header();
199
    if ($authorization == null || substr_count($authorization, " ") != 1) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $authorization of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
200
      $this->handle_response(RESTResponse::BAD_REQUEST, $auth); // Exits.
201
    }
202
    $token = explode(" ", $authorization);
203
    if ($token[0] != "Bearer") {
204
      $this->handle_response(RESTResponse::BAD_REQUEST, $auth); // Exits.
205
    }
206
    $this->token = $token[1];
207
    // Call Up Custom Implemented Bearer/Token Authorization.
208
    // Callback Check.
209
    if (!isset($this->ci->config->item('rest')['auth_callbacks'][$auth])) {
210
      $this->handle_response(RESTResponse::NOT_IMPLEMENTED, $auth); // Exits.
211
    }
212
    // Authorization.
213
    if (!$this->ci->config->item('rest')['auth_callbacks'][$auth]($this, $this->token)) {
214
      $this->handle_response(RESTResponse::UN_AUTHORIZED, $auth); // Exits.
215
    }
216
  }
217
  /**
218
   * [basic_auth description]
219
   */
220
  private function basic_auth():void {
221
    $username = $_SERVER['PHP_AUTH_USER'] ?? null;
222
    $password = $_SERVER['PHP_AUTH_PW'] ?? null;
223
    if (!$username || !$password) $this->handle_response(RESTResponse::BAD_REQUEST, RESTAuth::BASIC); // Exits.
224
    if (!$this->rest_model->basicAuth($this, $username, $password)) $this->handle_response(RESTResponse::UN_AUTHORIZED, RESTAuth::BASIC); // Exits.
225
  }
226
  /**
227
   * [api_key_auth description]
228
   */
229
  private function api_key_auth():void {
230
    if (!isset($_SERVER['HTTP_' . str_replace("-", "_", $this->apiKeyHeader)])) {
231
      $this->handle_response(RESTResponse::BAD_REQUEST, RESTAuth::API_KEY); // Exits.
232
    }
233
    $apiKey = $this->rest_model->getAPIKeyData(
234
      $_SERVER['HTTP_' . str_replace("-", "_", $this->apiKeyHeader)]
235
    );
236
    if ($apiKey == null) {
237
      $this->handle_response(RESTResponse::UN_AUTHORIZED, RESTAuth::API_KEY); // Exits.
238
    }
239
    // API KEY Auth Passed Above.
240
    if ($this->limit_api && $this->api_key_limit_column != null && $apiKey[$this->api_key_limit_column] == 1) {
241
      // Trunctate Rate Limit Data.
242
      $this->rest_model->truncateRatelimitData();
243
      // Check Whitelist.
244
      if (in_array($this->ci->input->ip_address(), $this->whitelist)) {
0 ignored issues
show
Bug introduced by
It seems like $this->whitelist can also be of type null; however, parameter $haystack of in_array() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

244
      if (in_array($this->ci->input->ip_address(), /** @scrutinizer ignore-type */ $this->whitelist)) {
Loading history...
245
        $this->checked_rate_limit = true; // Ignore Limit By IP.
246
        return;
247
      }
248
      // Should we acyually Limit?
249
      if ($this->per_hour > 0) {
250
        $client = hash('md5', $this->ci->input->ip_address() . "%" . $apiKey[$this->api_key_column]);
251
        $limitData = $this->rest_model->getLimitData($client, '_api_keyed_user');
252
        if ($limitData == null) {
253
          $limitData = [];
254
          $limitData['count'] = 0;
255
          $limitData['reset_epoch'] = gmdate('d M Y H:i:s', time() + (60 * 60));
256
          $limitData['start'] = date('d M Y H:i:s');
257
        }
258
        if ($this->per_hour - $limitData['count'] > 0) {
259
          if (!$this->rest_model->insertLimitData($client, '_api_keyed_user')) {
260
            $this->handle_response(RESTResponse::INTERNAL_SERVER_ERROR, self::RATE_LIMIT); // Exits.
261
          }
262
          ++$limitData['count'];
263
          if ($this->show_header) {
264
            header($this->header_prefix.'Limit: '.$this->per_hour);
265
            header($this->header_prefix.'Remaining: '.($this->per_hour - $limitData['count']));
266
            header($this->header_prefix.'Reset: '.strtotime($limitData['reset_epoch']));
267
          }
268
        } else {
269
          header('Retry-After: '.(strtotime($limitData['reset_epoch']) - strtotime(gmdate('d M Y H:i:s'))));
270
          $this->handle_response(RESTResponse::TOO_MANY_REQUESTS, self::RATE_LIMIT); // Exits.
271
        }
272
      }
273
    }
274
    $this->checked_rate_limit = true; // Ignore Limit By IP.
275
  }
276
  /**
277
   * [api_rest_limit_by_ip_address description]
278
   * TODO: Implement.
279
   */
280
  private function api_rest_limit_by_ip_address():void {
281
    // Trunctate Rate Limit Data.
282
    $this->rest_model->truncateRatelimitData();
283
    // Check Whitelist.
284
    if (in_array($this->ci->input->ip_address(), $this->whitelist)) return;
0 ignored issues
show
Bug introduced by
It seems like $this->whitelist can also be of type null; however, parameter $haystack of in_array() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

284
    if (in_array($this->ci->input->ip_address(), /** @scrutinizer ignore-type */ $this->whitelist)) return;
Loading history...
285
    // Should we acyually Limit?
286
    if ($this->ip_per_hour > 0) {
287
      $client = hash('md5', $this->ci->input->ip_address());
288
      $limitData = $this->rest_model->getLimitData($client, '_ip_address');
289
      if ($limitData == null) {
290
        $limitData = [];
291
        $limitData['count'] = 0;
292
        $limitData['reset_epoch'] = gmdate('d M Y H:i:s', time() + (60 * 60));
293
        $limitData['start'] = date('d M Y H:i:s');
294
      }
295
      if ($this->ip_per_hour - $limitData['count'] > 0) {
296
        if (!$this->rest_model->insertLimitData($client, '_ip_address')) {
297
          $this->handle_response(RESTResponse::INTERNAL_SERVER_ERROR, self::RATE_LIMIT); // Exits.
298
        }
299
        ++$limitData['count'];
300
        if ($this->show_header) {
301
          header($this->header_prefix.'Limit: '.$this->ip_per_hour);
302
          header($this->header_prefix.'Remaining: '.($this->ip_per_hour - $limitData['count']));
303
          header($this->header_prefix.'Reset: '.strtotime($limitData['reset_epoch']));
304
        }
305
      } else {
306
        header('Retry-After: '.(strtotime($limitData['reset_epoch']) - strtotime(gmdate('d M Y H:i:s'))));
307
        $this->handle_response(RESTResponse::TOO_MANY_REQUESTS, self::RATE_LIMIT); // Exits.
308
      }
309
    }
310
  }
311
  /**
312
   * [custom_auth description]
313
   * @param string $auth [description]
314
   */
315
  private function custom_auth(string &$auth):void {
316
    // Header Check.
317
    if (!isset($_SERVER[$auth])) {
318
      $this->handle_response(RESTResponse::BAD_REQUEST, $auth);
319
    }
320
    // Callback Check.
321
    if (!isset($this->ci->config->item('rest')['auth_callbacks'][$auth])) {
322
      $this->handle_response(RESTResponse::NOT_IMPLEMENTED, $auth); // Exits.
323
    }
324
    // Authentication.
325
    if (!$this->ci->config->item('rest')['auth_callbacks'][$auth]($this, $this->ci->security->xss_clean($_SERVER[$auth]))) {
326
      $this->handle_response(RESTResponse::UN_AUTHORIZED, $auth); // Exits.
327
    }
328
  }
329
  /**
330
   * [get_authorization_header description]
331
   * @return [type] [description]
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
332
   */
333
  private function get_authorization_header():?string {
334
    if (isset($_SERVER['Authorization'])) {
335
      return trim($_SERVER["Authorization"]);
336
    } else if (isset($_SERVER['HTTP_AUTHORIZATION'])) { //Nginx or fast CGI
337
      return trim($_SERVER["HTTP_AUTHORIZATION"]);
338
    } elseif (function_exists('apache_request_headers')) {
339
      $requestHeaders = apache_request_headers();
340
341
      // Avoid Surprises.
342
      $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders));
0 ignored issues
show
Bug introduced by
It seems like $requestHeaders can also be of type false; however, parameter $input of array_values() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

342
      $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values(/** @scrutinizer ignore-type */ $requestHeaders));
Loading history...
Bug introduced by
It seems like $requestHeaders can also be of type false; however, parameter $input of array_keys() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

342
      $requestHeaders = array_combine(array_map('ucwords', array_keys(/** @scrutinizer ignore-type */ $requestHeaders)), array_values($requestHeaders));
Loading history...
343
344
      if (isset($requestHeaders['Authorization'])) {
345
        return trim($requestHeaders['Authorization']);
346
      }
347
    }
348
    return null;
349
  }
350
  /**
351
   * [handle_response description]
352
   * @param int $code [description]
353
   */
354
  private function handle_response(int $code, $auth=null):void {
355
    http_response_code($code);
356
    header("Content-Type: application/json");
357
    if (isset($this->ci->config->item('rest')['response_callbacks'][$code])) {
358
      $this->ci->config->item('rest')['response_callbacks'][$code]($auth);
359
    }
360
    if (ENVIRONMENT != 'testing') exit($code);
0 ignored issues
show
Bug introduced by
The constant ENVIRONMENT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
361
    throw new Exception("Error $code in $auth", $code);
362
  }
363
}
364
?>
0 ignored issues
show
Best Practice introduced by
It is not recommended to use PHP's closing tag ?> in files other than templates.

Using a closing tag in PHP files that only contain PHP code is not recommended as you might accidentally add whitespace after the closing tag which would then be output by PHP. This can cause severe problems, for example headers cannot be sent anymore.

A simple precaution is to leave off the closing tag as it is not required, and it also has no negative effects whatsoever.

Loading history...
365