Completed
Push — master ( 9c2b09...5e7564 )
by Francis
01:33
created

REST   F

Complexity

Total Complexity 66

Size/Duplication

Total Lines 427
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 169
c 5
b 0
f 0
dl 0
loc 427
rs 3.12
wmc 66

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 62 5
B authenticate() 0 51 11
A custom_auth() 0 13 4
B api_rest_limit_by_ip_address() 0 28 7
A process_auth() 0 8 6
C api_key_auth() 0 53 13
A ip_auth() 0 3 2
A basic_auth() 0 5 4
A bearer_auth() 0 18 6
A handle_response() 0 9 3
A get_authorization_header() 0 17 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
  /**
18
   * [private description]
19
   * @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...
20
   */
21
  private $api_key_limit_column;
22
23
  /**
24
   * [private description]
25
   * @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...
26
   */
27
  private $api_key_column;
28
29
  /**
30
   * [private description]
31
   * @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...
32
   */
33
  private $per_hour;
34
35
  /**
36
   * [private description]
37
   * @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...
38
   */
39
  private $ip_per_hour;
40
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 $show_header;
46
47
  /**
48
   * [private description]
49
   * @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...
50
   */
51
  private $whitelist;
52
53
  /**
54
   * [private description]
55
   * @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...
56
   */
57
  private $checked_rate_limit = false;
58
59
  /**
60
   * [private description]
61
   * @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...
62
   */
63
  private $header_prefix;
64
65
  /**
66
   * [private description]
67
   * @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...
68
   */
69
  private $limit_api;
70
71
  /**
72
   * [public description]
73
   * @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...
74
   */
75
  public  $userId;
76
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  $apiKeyHeader;
82
83
  /**
84
   * [public description]
85
   * @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...
86
   */
87
  public  $token;
88
89
  /**
90
   * [public description]
91
   * @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...
92
   */
93
  public $allowedIps;
94
95
  /**
96
   * [PACKAGE description]
97
   * @var string
98
   */
99
  const PACKAGE = "francis94c/ci-rest";
100
101
  /**
102
   * [RATE_LIMIT description]
103
   * @var string
104
   */
105
  const RATE_LIMIT = "RateLimit";
106
107
  /**
108
   * [__construct This is the part of the code that takes care of all
109
   * authentiations. allowing you to focus on building wonderful things at REST.
110
   * pun intended ;-)]
111
   * @param array|null $params Initialization parameters from the Slint system.
112
   *                           There's no use for this arg yet.
113
   */
114
  function __construct(?array $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

114
  function __construct(/** @scrutinizer ignore-unused */ ?array $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...
115
  {
116
    $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

116
    $this->ci =& /** @scrutinizer ignore-call */ get_instance();
Loading history...
117
118
    if ($this->ci->input->is_cli_request()) return;
119
120
    // Load Config If Exists.
121
    $this->ci->config->load('rest', true, true);
122
123
    // Load Database.
124
    $this->ci->load->database();
125
126
    // load URL Helper
127
    $this->ci->load->helper('url');
128
129
    // Load REST Helper.
130
    $this->ci->load->splint(self::PACKAGE, '%rest');
131
132
    // Load Model.
133
    $this->ci->load->splint(self::PACKAGE, '*RESTModel', 'rest_model');
134
    $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...
135
136
    $config = [
137
      'users_table'           => $this->ci->config->item('rest')['basic_auth']['users_table'] ?? null,
138
      'users_id_column'       => $this->ci->config->item('rest')['basic_auth']['id_column'] ?? null,
139
      'users_username_column' => $this->ci->config->item('rest')['basic_auth']['username_column'] ?? null,
140
      'users_email_column'    => $this->ci->config->item('rest')['basic_auth']['email_column'] ?? null,
141
      'users_password_column' => $this->ci->config->item('rest')['basic_auth']['password_column'] ?? null,
142
      'api_key_table'         => $this->ci->config->item('rest')['api_key_auth']['api_key_table'] ?? null,
143
      'api_key_column'        => $this->ci->config->item('rest')['api_key_auth']['api_key_column'] ?? null,
144
      'api_key_limit_column'  => $this->ci->config->item('rest')['api_key_auth']['api_key_limit_column'] ?? null
145
    ];
146
147
    $this->rest_model->init($config);
148
149
    // Load Variable(s) from Config.
150
    $this->allowedIps = $this->ci->config->item('rest')['allowed_ips'] ?? ['127.0.0.1', '[::1]'];
151
    $this->apiKeyHeader = $this->ci->config->item('rest')['api_key_header'] ?? 'X-API-KEY';
152
    $this->api_key_limit_column = $this->ci->config->item('rest')['api_key_auth']['api_key_limit_column'] ?? null;
153
    $this->api_key_column = $this->ci->config->item('rest')['api_key_auth']['api_key_column'] ?? null;
154
    $this->limit_api = $this->ci->config->item('rest')['api_limiter']['api_limiter'] ?? false;
155
    $this->per_hour = $this->ci->config->item('rest')['api_limiter']['per_hour'] ?? 100;
156
    $this->ip_per_hour = $this->ci->config->item('rest')['api_limiter']['ip_per_hour'] ?? 50;
157
    $this->show_header = $this->ci->config->item('rest')['api_limiter']['show_header'] ?? null;
158
    $this->whitelist = $this->ci->config->item('rest')['api_limiter']['whitelist'] ?? null;
159
    $this->header_prefix = $this->ci->config->item('rest')['api_limiter']['header_prefix'] ?? 'X-RateLimit-';
160
161
    // Limit Only?
162
    //if ($this->ci->config->item('rest')['api_limiter']['api_limit_only'] ?? false) {
163
      //return;
164
    //}
165
166
    // Authenticate
167
    $this->authenticate();
168
169
    // Generic Rate Limiter.
170
    if ($this->limit_api && !$this->checked_rate_limit &&
171
    ($this->ci->config->item('rest')['api_limiter']['limit_by_ip'] ?? false)) {
172
      $this->api_rest_limit_by_ip_address();
173
    }
174
175
    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

175
    /** @scrutinizer ignore-call */ 
176
    log_message('debug', 'REST Request Authenticated and REST Library Initialized.');
Loading history...
176
  }
177
178
  /**
179
   * [authenticate description]
180
   * @date 2020-01-30
181
   */
182
  private function authenticate():void
183
  {
184
    $auths = null;
185
186
    $globalAuths = $this->ci->config->item('rest')['global_auth'] ?? null;
187
188
    if ($globalAuths != null) {
189
      if (is_array($globalAuths)) {
190
        $auths = $globalAuths;
191
      } else {
192
        $auths = [$globalAuths];
193
      }
194
    }
195
196
    $uri_auths = $this->ci->config->item('rest')['uri_auth'] ?? null;
197
198
    // Match Auth Routes.
199
    // The below algorithm is similar to the one Code Igniter uses in its
200
    // Routing Class.
201
    if ($uri_auths != null || is_array($uri_auths)) {
202
      foreach ($uri_auths as $uri => $auth_array) {
203
        // Convert wildcards to RegEx.
204
  			$uri = str_replace(array(':any', ':num'), array('[^/]+', '[0-9]+'), $uri);
205
        if (preg_match('#^'.$uri.'$#', uri_string())) {
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

205
        if (preg_match('#^'.$uri.'$#', /** @scrutinizer ignore-call */ uri_string())) {
Loading history...
206
          // Assign Authentication Steps.
207
          if (is_array($auth_array)) {
208
            foreach ($auth_array as $auth) {
209
              $auths[] = $auth;
210
            }
211
          } else {
212
            $auths[] = $auth_array;
213
          }
214
        }
215
        break;
216
      }
217
    }
218
219
    //$auths = $this->ci->config->item('rest')['uri_auth'][uri_string()] ?? null;
220
    if ($auths == null) return; // No authentication(s) to carry out.
221
222
    // $this->process_auth() terminates the script if authentication fails
223
    // It will call the callable in the rest.php config file under
224
    // response_callbacks which matches the necesarry RESTResponse constant
225
    // before exiting. Which callable is called in any situation is documented
226
    // in README.md
227
    //if (is_scalar($auths)) {
228
      //$this->process_auth($auths);
229
      //return;
230
    //}
231
232
    foreach ($auths as $auth) $this->process_auth($auth);
233
  }
234
  /**
235
   * [process_auth description]
236
   * @param  string $auth [description]
237
   * @return bool         [description]
238
   */
239
  private function process_auth(string &$auth):void {
240
    switch ($auth) {
241
      case RESTAuth::IP: $this->ip_auth(); break;
242
      case RESTAuth::BASIC: $this->basic_auth(); break;
243
      case RESTAuth::API_KEY: $this->api_key_auth(); break;
244
      case RESTAuth::OAUTH2: $this->bearer_auth(RESTAuth::OAUTH2); break;
245
      case RESTAuth::BEARER: $this->bearer_auth(); break;
246
      default: $this->custom_auth($auth);
247
    }
248
  }
249
  /**
250
   * [ip_auth description]
251
   */
252
  private function ip_auth():void {
253
    if (!in_array($this->ci->input->ip_address(), $this->allowedIps)) {
254
      $this->handle_response(RESTResponse::UN_AUTHORIZED, RESTAuth::IP); // Exits.
255
    }
256
  }
257
  /**
258
   * [bearer_auth description]
259
   */
260
  private function bearer_auth($auth=RESTAuth::BEARER):void {
261
    $authorization = $this->get_authorization_header();
262
    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...
263
      $this->handle_response(RESTResponse::BAD_REQUEST, $auth); // Exits.
264
    }
265
    $token = explode(" ", $authorization);
266
    if ($token[0] != "Bearer") {
267
      $this->handle_response(RESTResponse::BAD_REQUEST, $auth); // Exits.
268
    }
269
    $this->token = $token[1];
270
    // Call Up Custom Implemented Bearer/Token Authorization.
271
    // Callback Check.
272
    if (!isset($this->ci->config->item('rest')['auth_callbacks'][$auth])) {
273
      $this->handle_response(RESTResponse::NOT_IMPLEMENTED, $auth); // Exits.
274
    }
275
    // Authorization.
276
    if (!$this->ci->config->item('rest')['auth_callbacks'][$auth]($this, $this->token)) {
277
      $this->handle_response(RESTResponse::UN_AUTHORIZED, $auth); // Exits.
278
    }
279
  }
280
  /**
281
   * [basic_auth description]
282
   */
283
  private function basic_auth():void {
284
    $username = $_SERVER['PHP_AUTH_USER'] ?? null;
285
    $password = $_SERVER['PHP_AUTH_PW'] ?? null;
286
    if (!$username || !$password) $this->handle_response(RESTResponse::BAD_REQUEST, RESTAuth::BASIC); // Exits.
287
    if (!$this->rest_model->basicAuth($this, $username, $password)) $this->handle_response(RESTResponse::UN_AUTHORIZED, RESTAuth::BASIC); // Exits.
288
  }
289
  /**
290
   * [api_key_auth description]
291
   */
292
  private function api_key_auth():void
293
  {
294
    if (uri_string() == '')  return;
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

294
    if (/** @scrutinizer ignore-call */ uri_string() == '')  return;
Loading history...
295
296
    if (!$this->ci->input->get_request_header($this->apiKeyHeader, true)) {
297
    // if (!isset($_SERVER['HTTP_' . str_replace("-", "_", $this->apiKeyHeader)])) {
298
      $this->handle_response(RESTResponse::BAD_REQUEST, RESTAuth::API_KEY); // Exits.
299
    }
300
301
    $apiKey = $this->rest_model->getAPIKeyData(
302
      $this->ci->input->get_request_header($this->apiKeyHeader, true)
303
    );
304
305
    if ($apiKey == null) {
306
      $this->handle_response(RESTResponse::UN_AUTHORIZED, RESTAuth::API_KEY); // Exits.
307
    }
308
309
    // API KEY Auth Passed Above.
310
    if ($this->limit_api && $this->api_key_limit_column != null && $apiKey[$this->api_key_limit_column] == 1) {
311
      // Trunctate Rate Limit Data.
312
      $this->rest_model->truncateRatelimitData();
313
      // Check Whitelist.
314
      if (in_array($this->ci->input->ip_address(), $this->whitelist)) {
315
        $this->checked_rate_limit = true; // Ignore Limit By IP.
316
        return;
317
      }
318
      // Should we acyually Limit?
319
      if ($this->per_hour > 0) {
320
        $client = hash('md5', $this->ci->input->ip_address() . "%" . $apiKey[$this->api_key_column]);
321
        $limitData = $this->rest_model->getLimitData($client, '_api_keyed_user');
322
        if ($limitData == null) {
323
          $limitData = [];
324
          $limitData['count'] = 0;
325
          $limitData['reset_epoch'] = gmdate('d M Y H:i:s', time() + (60 * 60));
326
          $limitData['start'] = date('d M Y H:i:s');
327
        }
328
        if ($this->per_hour - $limitData['count'] > 0) {
329
          if (!$this->rest_model->insertLimitData($client, '_api_keyed_user')) {
330
            $this->handle_response(RESTResponse::INTERNAL_SERVER_ERROR, self::RATE_LIMIT); // Exits.
331
          }
332
          ++$limitData['count'];
333
          if ($this->show_header) {
334
            header($this->header_prefix.'Limit: '.$this->per_hour);
335
            header($this->header_prefix.'Remaining: '.($this->per_hour - $limitData['count']));
336
            header($this->header_prefix.'Reset: '.strtotime($limitData['reset_epoch']));
337
          }
338
        } else {
339
          header('Retry-After: '.(strtotime($limitData['reset_epoch']) - strtotime(gmdate('d M Y H:i:s'))));
340
          $this->handle_response(RESTResponse::TOO_MANY_REQUESTS, self::RATE_LIMIT); // Exits.
341
        }
342
      }
343
    }
344
    $this->checked_rate_limit = true; // Ignore Limit By IP.
345
  }
346
  /**
347
   * [api_rest_limit_by_ip_address description]
348
   * TODO: Implement.
349
   */
350
  private function api_rest_limit_by_ip_address():void {
351
    // Trunctate Rate Limit Data.
352
    $this->rest_model->truncateRatelimitData();
353
    // Check Whitelist.
354
    if (in_array($this->ci->input->ip_address(), $this->whitelist)) return;
355
    // Should we acyually Limit?
356
    if ($this->ip_per_hour > 0) {
357
      $client = hash('md5', $this->ci->input->ip_address());
358
      $limitData = $this->rest_model->getLimitData($client, '_ip_address');
359
      if ($limitData == null) {
360
        $limitData = [];
361
        $limitData['count'] = 0;
362
        $limitData['reset_epoch'] = gmdate('d M Y H:i:s', time() + (60 * 60));
363
        $limitData['start'] = date('d M Y H:i:s');
364
      }
365
      if ($this->ip_per_hour - $limitData['count'] > 0) {
366
        if (!$this->rest_model->insertLimitData($client, '_ip_address')) {
367
          $this->handle_response(RESTResponse::INTERNAL_SERVER_ERROR, self::RATE_LIMIT); // Exits.
368
        }
369
        ++$limitData['count'];
370
        if ($this->show_header) {
371
          header($this->header_prefix.'Limit: '.$this->ip_per_hour);
372
          header($this->header_prefix.'Remaining: '.($this->ip_per_hour - $limitData['count']));
373
          header($this->header_prefix.'Reset: '.strtotime($limitData['reset_epoch']));
374
        }
375
      } else {
376
        header('Retry-After: '.(strtotime($limitData['reset_epoch']) - strtotime(gmdate('d M Y H:i:s'))));
377
        $this->handle_response(RESTResponse::TOO_MANY_REQUESTS, self::RATE_LIMIT); // Exits.
378
      }
379
    }
380
  }
381
  /**
382
   * [custom_auth description]
383
   * @param string $auth [description]
384
   */
385
  private function custom_auth(string &$auth):void
386
  {
387
    // Header Check.
388
    if (!isset($_SERVER[$auth])) {
389
      $this->handle_response(RESTResponse::BAD_REQUEST, $auth);
390
    }
391
    // Callback Check.
392
    if (!isset($this->ci->config->item('rest')['auth_callbacks'][$auth])) {
393
      $this->handle_response(RESTResponse::NOT_IMPLEMENTED, $auth); // Exits.
394
    }
395
    // Authentication.
396
    if (!$this->ci->config->item('rest')['auth_callbacks'][$auth]($this, $this->ci->security->xss_clean($_SERVER[$auth]))) {
397
      $this->handle_response(RESTResponse::UN_AUTHORIZED, $auth); // Exits.
398
    }
399
  }
400
  /**
401
   * [get_authorization_header description]
402
   * @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...
403
   */
404
  private function get_authorization_header():?string
405
  {
406
    if (isset($_SERVER['Authorization'])) {
407
      return trim($_SERVER["Authorization"]);
408
    } else if (isset($_SERVER['HTTP_AUTHORIZATION'])) { //Nginx or fast CGI
409
      return trim($_SERVER["HTTP_AUTHORIZATION"]);
410
    } elseif (function_exists('apache_request_headers')) {
411
      $requestHeaders = apache_request_headers();
412
413
      // Avoid Surprises.
414
      $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_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

414
      $requestHeaders = array_combine(array_map('ucwords', array_keys(/** @scrutinizer ignore-type */ $requestHeaders)), array_values($requestHeaders));
Loading history...
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

414
      $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values(/** @scrutinizer ignore-type */ $requestHeaders));
Loading history...
415
416
      if (isset($requestHeaders['Authorization'])) {
417
        return trim($requestHeaders['Authorization']);
418
      }
419
    }
420
    return null;
421
  }
422
423
  /**
424
   * [handle_response description]
425
   * @param int $code [description]
426
   */
427
  private function handle_response(int $code, $auth=null):void
428
  {
429
    http_response_code($code);
430
    header("Content-Type: application/json");
431
    if (isset($this->ci->config->item('rest')['response_callbacks'][$code])) {
432
      $this->ci->config->item('rest')['response_callbacks'][$code]($auth);
433
    }
434
    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...
435
    throw new Exception("Error $code in $auth", $code);
436
  }
437
}
438
?>
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...
439