Passed
Pull Request — develop (#925)
by Shandak
06:54
created

RApi   F

Complexity

Total Complexity 71

Size/Duplication

Total Lines 619
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 71
eloc 189
c 1
b 0
f 0
dl 0
loc 619
rs 2.7199

15 Methods

Rating   Name   Duplication   Size   Complexity  
A setDebug() 0 3 1
A setApi() 0 3 1
A loadExtensionLanguage() 0 12 4
A __construct() 0 15 1
A getInstance() 0 37 5
B setOptionsFromHeader() 0 44 9
A sendHeaders() 0 9 2
F getPostedData() 0 87 16
C getHeaderVariablesFromGlobals() 0 83 14
A execute() 0 3 1
A clearHeaders() 0 9 2
A rearrangeFiles() 0 17 3
A render() 0 3 1
B parsePut() 0 111 9
A setHeader() 0 9 2

How to fix   Complexity   

Complex Class

Complex classes like RApi 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 RApi, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @package     Redcore
4
 * @subpackage  Api
5
 *
6
 * @copyright   Copyright (C) 2008 - 2021 redWEB.dk. All rights reserved.
7
 * @license     GNU General Public License version 2 or later, see LICENSE.
8
 */
9
10
defined('JPATH_BASE') or die;
11
12
use Joomla\Utilities\ArrayHelper;
13
14
/**
15
 * Interface to handle api calls
16
 *
17
 * @package     Redcore
18
 * @subpackage  Api
19
 * @since       1.2
20
 */
21
class RApi extends RApiBase
22
{
23
	/**
24
	 * @var    array  RApi instances container.
25
	 * @since  1.2
26
	 */
27
	public static $instances = array();
28
29
	/**
30
	 * @var    string  Name of the Api
31
	 * @since  1.2
32
	 */
33
	public $apiName = '';
34
35
	/**
36
	 * @var    string  Operation that will be preformed with this Api call. supported: CREATE, READ, UPDATE, DELETE
37
	 * @since  1.2
38
	 */
39
	public $operation = 'read';
40
41
	/**
42
	 * The start time for measuring the execution time.
43
	 *
44
	 * @var    float
45
	 * @since  1.2
46
	 */
47
	public $startTime;
48
49
	/**
50
	 * Method to return a RApi instance based on the given options.  There is one global option and then
51
	 * the rest are specific to the Api.  The 'api' option defines which RApi class is
52
	 * used for, default is 'hal'.
53
	 *
54
	 * Instances are unique to the given options and new objects are only created when a unique options array is
55
	 * passed into the method.  This ensures that we don't end up with unnecessary api resources.
56
	 *
57
	 * @param   array  $options  Parameters to be passed to the creating api.
58
	 *
59
	 * @return  RApi  Api object.
60
	 *
61
	 * @since   1.2
62
	 * @throws  RuntimeException
63
	 */
64
	public static function getInstance($options = array())
65
	{
66
		// Sanitize the api options.
67
		$options['api'] = (isset($options['api'])) ? preg_replace('/[^A-Z0-9_\.-]/i', '', $options['api']) : 'hal';
68
69
		// Get the options signature for the api connector.
70
		$signature = md5(serialize($options));
71
72
		// If we already have a api connector instance for these options then just use that.
73
		if (!empty(self::$instances[$signature]))
74
		{
75
			return self::$instances[$signature];
76
		}
77
78
		// Derive the class name from the driver.
79
		$class = 'RApi' . ucfirst(strtolower($options['api'])) . ucfirst(strtolower($options['api']));
80
81
		// If the class still doesn't exist we have nothing left to do but throw an exception.
82
		if (!class_exists($class))
83
		{
84
			throw new RuntimeException(JText::sprintf('LIB_REDCORE_API_UNABLE_TO_LOAD_API', $options['api']));
85
		}
86
87
		// Create our new RApi connector based on the options given.
88
		try
89
		{
90
			$instance = new $class($options);
91
		}
92
		catch (RuntimeException $e)
93
		{
94
			throw new RuntimeException(JText::sprintf('LIB_REDCORE_API_UNABLE_TO_CONNECT_TO_API', $e->getMessage()));
95
		}
96
97
		// Set the new connector to the global instances based on signature.
98
		self::$instances[$signature] = $instance;
99
100
		return self::$instances[$signature];
101
	}
102
103
	/**
104
	 * Method to instantiate the file-based api call.
105
	 *
106
	 * @param   mixed  $options  Optional custom options to load. JRegistry or array format
107
	 *
108
	 * @since   1.2
109
	 */
110
	public function __construct($options = null)
111
	{
112
		$this->startTime = microtime(true);
0 ignored issues
show
Documentation Bug introduced by
It seems like microtime(true) can also be of type string. However, the property $startTime is declared as type double. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
113
114
		// Initialise / Load options
115
		$this->setOptions($options);
116
117
		// Main properties
118
		$this->setOptionsFromHeader();
119
120
		// Main properties
121
		$this->setApi($this->options->get('api', 'hal'));
122
123
		// Load Library language
124
		$this->loadExtensionLanguage('lib_joomla', JPATH_ADMINISTRATOR);
125
	}
126
127
	/**
128
	 * Set options received from Headers of the request
129
	 *
130
	 * @return  RApi
131
	 *
132
	 * @since   1.7
133
	 */
134
	public function setOptionsFromHeader()
135
	{
136
		$app = JFactory::getApplication();
137
		$headers = self::getHeaderVariablesFromGlobals();
138
139
		// Setting the language from the header options information
140
		if (isset($headers['ACCEPT_LANGUAGE']))
141
		{
142
			// We are only using header options if the URI does not contain lang parameter as it have higher priority
143
			if ($app->input->get('lang', '') == '')
144
			{
145
				$acceptLanguages = explode(',', $headers['ACCEPT_LANGUAGE']);
146
147
				// We go through all proposed languages. First language that is found installed on the website is used
148
				foreach ($acceptLanguages as $acceptLanguage)
149
				{
150
					$acceptLanguage = explode(';', $acceptLanguage);
151
152
					if (RTranslationHelper::setLanguage($acceptLanguage[0]))
153
					{
154
						$this->options->set('lang', $acceptLanguage[0]);
155
						$app->input->set('lang', $acceptLanguage[0]);
156
157
						break;
158
					}
159
				}
160
			}
161
		}
162
163
		// Setting option for compressed output
164
		if (isset($headers['ACCEPT_ENCODING']))
165
		{
166
			$acceptCompression = strpos(strtolower($headers['ACCEPT_ENCODING']), 'gzip') !== false ? 1 : 0;
167
			$this->options->set('enable_gzip_compression', $acceptCompression);
168
		}
169
170
		// Setting option for compressed output
171
		if (isset($headers['CONTENT_ENCODING']))
172
		{
173
			$acceptCompression = strpos(strtolower($headers['CONTENT_ENCODING']), 'gzip') !== false ? 1 : 0;
174
			$this->options->set('enable_gzip_input_compression', $acceptCompression);
175
		}
176
177
		return $this;
178
	}
179
180
	/**
181
	 * Change the Api
182
	 *
183
	 * @param   string  $apiName  Api instance to render
184
	 *
185
	 * @return  void
186
	 *
187
	 * @since   1.2
188
	 */
189
	public function setApi($apiName)
190
	{
191
		$this->apiName = $apiName;
192
	}
193
194
	/**
195
	 * Change the debug mode
196
	 *
197
	 * @param   boolean  $debug  Enable / Disable debug
198
	 *
199
	 * @return  void
200
	 *
201
	 * @since   1.2
202
	 */
203
	public function setDebug($debug)
204
	{
205
		$this->options->set('debug', (boolean) $debug);
206
	}
207
208
	/**
209
	 * Load extension language file.
210
	 *
211
	 * @param   string  $option  Option name
212
	 * @param   string  $path    Path to language file
213
	 *
214
	 * @return  object
215
	 */
216
	public function loadExtensionLanguage($option, $path = JPATH_SITE)
217
	{
218
		// Load common and local language files.
219
		$lang = JFactory::getLanguage();
220
221
		// Load language file
222
		$lang->load($option, $path, null, false, false)
223
		|| $lang->load($option, $path . "/components/$option", null, false, false)
224
		|| $lang->load($option, $path, $lang->getDefault(), false, false)
225
		|| $lang->load($option, $path . "/components/$option", $lang->getDefault(), false, false);
226
227
		return $this;
228
	}
229
230
	/**
231
	 * @return array
232
	 * @since  __DEPLOY_VERSION__
233
	 */
234
	private static function parsePut(): array
235
	{
236
		$rawData = file_get_contents("php://input");
237
238
		// Fetch content and determine boundary
239
		$boundary = substr($rawData, 0, strpos($rawData, "\r\n"));
240
241
		if (empty($boundary))
242
		{
243
			parse_str($rawData, $data);
244
245
			return $data;
246
		}
247
248
		// Fetch each part
249
		$parts = array_slice(explode($boundary, $rawData), 1);
250
		$str   = [];
251
		$files = [];
252
253
		foreach ($parts as $part)
254
		{
255
			// If this is the last part, break
256
			if ($part == "--\r\n")
257
			{
258
				break;
259
			}
260
261
			// Separate content from headers
262
			$part = ltrim($part, "\r\n");
263
			list($rawHeaders, $body) = explode("\r\n\r\n", $part, 2);
264
265
			// Parse the headers list
266
			$rawHeaders = explode("\r\n", $rawHeaders);
267
			$headers     = [];
268
269
			foreach ($rawHeaders as $header)
270
			{
271
				list($name, $value) = explode(':', $header);
272
				$headers[strtolower($name)] = ltrim($value, ' ');
273
			}
274
275
			// Parse the Content-Disposition to get the field name, etc.
276
			if (isset($headers['content-disposition']))
277
			{
278
				$filename = null;
279
				preg_match(
280
					'/^(.+); *name="([^"]+)"(; *filename="([^"]+)")?/',
281
					$headers['content-disposition'],
282
					$matches
283
				);
284
				list(, , $name) = $matches;
285
286
				// Parse File
287
				if (isset($matches[4]))
288
				{
289
					// Get filename
290
					$filename = $matches[4];
291
292
					// Get tmp name
293
					$tmpName = tempnam(ini_get('upload_tmp_dir'), rand());
294
					$values  = [
295
						'error'    => 0,
296
						'name'     => $filename,
297
						'tmp_name' => $tmpName,
298
						'size'     => strlen($body),
299
						'type'     => trim($value),
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $value does not seem to be defined for all execution paths leading up to this point.
Loading history...
300
					];
301
302
					$exploded = explode('[', $name);
303
304
					$first = array_shift($exploded);
305
306
					if (!empty($exploded))
307
					{
308
						$last = '[' . implode('[', $exploded);
309
					}
310
					else
311
					{
312
						$last = '';
313
					}
314
315
					foreach ($values as $key => $val)
316
					{
317
						$files[] = $first . '[' . $key . ']' . $last . '=' . $val;
318
					}
319
320
					// Place in temporary directory
321
					file_put_contents($tmpName, $body);
322
323
					// Register a shutdown function to cleanup the temporary file
324
					register_shutdown_function(
325
						function () use ($tmpName)
326
						{
327
							unlink($tmpName);
328
						}
329
					);
330
				}
331
332
				// Parse Field
333
				else
334
				{
335
					$str[] = $name . '=' . substr($body, 0, strlen($body) - 2);
336
				}
337
			}
338
		}
339
340
		parse_str(implode('&', $str), $data);
341
		parse_str(implode('&', $files), $list);
342
		$_FILES = array_replace($_FILES, $list);
343
344
		return $data;
345
	}
346
347
	/**
348
	 * @param   string        $group   Group
349
	 * @param   array|string  $values  Values
350
	 *
351
	 * @return array
352
	 * @since  __DEPLOY_VERSION__
353
	 */
354
	private static function rearrangeFiles(string $group, $values): array
355
	{
356
		if (is_array($values))
357
		{
358
			$return = [];
359
360
			foreach ($values as $k => $v)
361
			{
362
				$return[$k] = static::rearrangeFiles($group, $v);
363
			}
364
365
			return $return;
366
		}
367
		else
368
		{
369
			return [
370
				$group => $values,
371
			];
372
		}
373
	}
374
375
	/**
376
	 * Returns posted data in array format
377
	 *
378
	 * @return  array
379
	 *
380
	 * @since   1.2
381
	 */
382
	public static function getPostedData()
383
	{
384
		$headers   = self::getHeaderVariablesFromGlobals();
385
		$input     = JFactory::getApplication()->input;
386
		$inputData = file_get_contents("php://input");
387
388
		// Is data is compressed we will fetch it through separate function
389
		if (isset($headers['CONTENT_ENCODING']))
390
		{
391
			if (strpos(strtolower($headers['CONTENT_ENCODING']), 'gzip') !== false)
392
			{
393
				$decompressed = gzdecode($inputData);
394
395
				if ($decompressed)
396
				{
397
					$inputData = $decompressed;
398
				}
399
			}
400
		}
401
402
		if (is_object($inputData))
0 ignored issues
show
introduced by
The condition is_object($inputData) is always false.
Loading history...
403
		{
404
			$inputData = ArrayHelper::fromObject($inputData);
405
		}
406
		elseif (is_string($inputData) && !empty($inputData))
407
		{
408
			$inputData  = trim($inputData);
409
			$parsedData = null;
410
411
			// We try to transform it into JSON
412
			if ($dataJson = @json_decode($inputData, true))
413
			{
414
				if (json_last_error() == JSON_ERROR_NONE)
415
				{
416
					$parsedData = (array) $dataJson;
417
				}
418
			}
419
420
			// We try to transform it into XML
421
			if (is_null($parsedData) && $xml = @simplexml_load_string($inputData))
422
			{
423
				$json       = json_encode((array) $xml);
424
				$parsedData = json_decode($json, true);
425
			}
426
427
			// We try to transform it into Array
428
			if (is_null($parsedData))
429
			{
430
				$parsedData = static::parsePut();
431
			}
432
433
			$inputData = $parsedData;
434
		}
435
		else
436
		{
437
			$inputData = $input->post->getArray();
438
		}
439
440
		$files = [];
441
442
		foreach ($_FILES as $fieldName => $keys)
443
		{
444
			$files[$fieldName] = [];
445
446
			foreach ($keys as $key => $list)
447
			{
448
				$files[$fieldName] = array_replace_recursive($files[$fieldName], static::rearrangeFiles($key, $list));
449
			}
450
		}
451
452
		$inputData = array_replace_recursive($inputData, $files);
453
454
		$filter = JFilterInput::getInstance(array(), array(), 1, 1);
455
456
		// Filter data with JInput default filter in blacklist mode
457
		$postedData = new JInput($inputData, array('filter' => $filter));
458
459
		if (version_compare(JVERSION, '3') >= 0)
460
		{
461
			return $postedData->getArray(array(), null, 'HTML');
462
		}
463
		elseif ($inputData)
0 ignored issues
show
Bug Best Practice introduced by
The expression $inputData of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
464
		{
465
			return $postedData->getArray(array(), $inputData, 'HTML');
466
		}
467
468
		return array();
469
	}
470
471
	/**
472
	 * Execute the Api operation.
473
	 *
474
	 * @return  mixed  RApi object with information on success, boolean false on failure.
475
	 *
476
	 * @since   1.2
477
	 * @throws  RuntimeException
478
	 */
479
	public function execute()
480
	{
481
		return null;
482
	}
483
484
	/**
485
	 * Method to render the api call output.
486
	 *
487
	 * @return  string  Api call output
488
	 *
489
	 * @since   1.2
490
	 */
491
	public function render()
492
	{
493
		return '';
494
	}
495
496
	/**
497
	 * Returns header variables from globals
498
	 *
499
	 * @return  array
500
	 */
501
	public static function getHeaderVariablesFromGlobals()
502
	{
503
		$headers = array();
504
505
		foreach ($_SERVER as $key => $value)
506
		{
507
			if (strpos($key, 'HTTP_') === 0)
508
			{
509
				$headers[substr($key, 5)] = $value;
510
			}
511
			// CONTENT_* are not prefixed with HTTP_
512
			elseif (in_array($key, array('CONTENT_LENGTH', 'CONTENT_MD5', 'CONTENT_TYPE')))
513
			{
514
				$headers[$key] = $value;
515
			}
516
		}
517
518
		if (isset($_SERVER['PHP_AUTH_USER']))
519
		{
520
			$headers['PHP_AUTH_USER'] = $_SERVER['PHP_AUTH_USER'];
521
			$headers['PHP_AUTH_PW'] = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : '';
522
		}
523
		else
524
		{
525
			/*
526
			 * php-cgi under Apache does not pass HTTP Basic user/pass to PHP by default
527
			 * For this workaround to work, add this line to your .htaccess file:
528
			 * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
529
			 *
530
			 * A sample .htaccess file:
531
			 * RewriteEngine On
532
			 * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
533
			 * RewriteCond %{REQUEST_FILENAME} !-f
534
			 * RewriteRule ^(.*)$ app.php [QSA,L]
535
			 */
536
537
			$authorizationHeader = null;
538
539
			if (isset($_SERVER['HTTP_AUTHORIZATION']))
540
			{
541
				$authorizationHeader = $_SERVER['HTTP_AUTHORIZATION'];
542
			}
543
			elseif (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']))
544
			{
545
				$authorizationHeader = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
546
			}
547
			elseif (function_exists('apache_request_headers'))
548
			{
549
				$requestHeaders = (array) apache_request_headers();
550
551
				// Server-side fix for bug in old Android versions (a nice side-effect of this fix means we don't care about capitalization for Authorization)
552
				$requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders));
553
554
				if (isset($requestHeaders['Authorization']))
555
				{
556
					$authorizationHeader = trim($requestHeaders['Authorization']);
557
				}
558
			}
559
560
			if (null !== $authorizationHeader)
561
			{
562
				$headers['AUTHORIZATION'] = $authorizationHeader;
563
564
				// Decode AUTHORIZATION header into PHP_AUTH_USER and PHP_AUTH_PW when authorization header is basic
565
				if (0 === stripos($authorizationHeader, 'basic'))
566
				{
567
					$exploded = explode(':', base64_decode(substr($authorizationHeader, 6)));
568
569
					if (count($exploded) == 2)
570
					{
571
						list($headers['PHP_AUTH_USER'], $headers['PHP_AUTH_PW']) = $exploded;
572
					}
573
				}
574
			}
575
		}
576
577
		// PHP_AUTH_USER/PHP_AUTH_PW
578
		if (isset($headers['PHP_AUTH_USER']))
579
		{
580
			$headers['AUTHORIZATION'] = 'Basic ' . base64_encode($headers['PHP_AUTH_USER'] . ':' . $headers['PHP_AUTH_PW']);
581
		}
582
583
		return $headers;
584
	}
585
586
	/**
587
	 * Method to clear any set response headers.
588
	 *
589
	 * @return  void
590
	 */
591
	public static function clearHeaders()
592
	{
593
		if (version_compare(JVERSION, '3') >= 0)
594
		{
595
			JFactory::getApplication()->clearHeaders();
596
		}
597
		else
598
		{
599
			JResponse::clearHeaders();
0 ignored issues
show
Deprecated Code introduced by
The function JResponse::clearHeaders() has been deprecated: 3.2 Use JApplicationWeb::clearHeaders() instead ( Ignorable by Annotation )

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

599
			/** @scrutinizer ignore-deprecated */ JResponse::clearHeaders();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
600
		}
601
	}
602
603
	/**
604
	 * Send the response headers.
605
	 *
606
	 * @return  void
607
	 */
608
	public static function sendHeaders()
609
	{
610
		if (version_compare(JVERSION, '3') >= 0)
611
		{
612
			JFactory::getApplication()->sendHeaders();
613
		}
614
		else
615
		{
616
			JResponse::sendHeaders();
0 ignored issues
show
Deprecated Code introduced by
The function JResponse::sendHeaders() has been deprecated: 3.2 Use JApplicationWeb::sendHeaders() instead ( Ignorable by Annotation )

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

616
			/** @scrutinizer ignore-deprecated */ JResponse::sendHeaders();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
617
		}
618
	}
619
620
	/**
621
	 * Method to set a response header.  If the replace flag is set then all headers
622
	 * with the given name will be replaced by the new one.  The headers are stored
623
	 * in an internal array to be sent when the site is sent to the browser.
624
	 *
625
	 * @param   string   $name     The name of the header to set.
626
	 * @param   string   $value    The value of the header to set.
627
	 * @param   boolean  $replace  True to replace any headers with the same name.
628
	 *
629
	 * @return  void
630
	 */
631
	public static function setHeader($name, $value, $replace = false)
632
	{
633
		if (version_compare(JVERSION, '3') >= 0)
634
		{
635
			JFactory::getApplication()->setHeader($name, $value, $replace);
636
		}
637
		else
638
		{
639
			JResponse::setHeader($name, $value, $replace);
0 ignored issues
show
Deprecated Code introduced by
The function JResponse::setHeader() has been deprecated: 3.2 Use JApplicationWeb::setHeader() instead ( Ignorable by Annotation )

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

639
			/** @scrutinizer ignore-deprecated */ JResponse::setHeader($name, $value, $replace);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
640
		}
641
	}
642
}
643