From 44781aa3d77222addfe523790b2f3c97a7db7265 Mon Sep 17 00:00:00 2001 From: Volodymyr Shandak Date: Thu, 27 Oct 2022 15:15:35 +0300 Subject: [PATCH] Add support for multipart/form-data to API --- extensions/libraries/redcore/api/api.php | 211 +++++++++++++++++++++-- 1 file changed, 199 insertions(+), 12 deletions(-) diff --git a/extensions/libraries/redcore/api/api.php b/extensions/libraries/redcore/api/api.php index e478ba3e2..3d544c80b 100644 --- a/extensions/libraries/redcore/api/api.php +++ b/extensions/libraries/redcore/api/api.php @@ -9,6 +9,8 @@ defined('JPATH_BASE') or die; +use Joomla\Utilities\ArrayHelper; + /** * Interface to handle api calls * @@ -211,6 +213,151 @@ public function loadExtensionLanguage($option, $path = JPATH_SITE) return $this; } + /** + * @return array + * @since __DEPLOY_VERSION__ + */ + private static function parsePut(): array + { + $rawData = file_get_contents("php://input"); + + // Fetch content and determine boundary + $boundary = substr($rawData, 0, strpos($rawData, "\r\n")); + + if (empty($boundary)) + { + parse_str($rawData, $data); + + return $data; + } + + // Fetch each part + $parts = array_slice(explode($boundary, $rawData), 1); + $str = []; + $files = []; + + foreach ($parts as $part) + { + // If this is the last part, break + if ($part == "--\r\n") + { + break; + } + + // Separate content from headers + $part = ltrim($part, "\r\n"); + list($rawHeaders, $body) = explode("\r\n\r\n", $part, 2); + + // Parse the headers list + $rawHeaders = explode("\r\n", $rawHeaders); + $headers = []; + + foreach ($rawHeaders as $header) + { + list($name, $value) = explode(':', $header); + $headers[strtolower($name)] = ltrim($value, ' '); + } + + // Parse the Content-Disposition to get the field name, etc. + if (isset($headers['content-disposition'])) + { + $filename = null; + preg_match( + '/^(.+); *name="([^"]+)"(; *filename="([^"]+)")?/', + $headers['content-disposition'], + $matches + ); + list(, , $name) = $matches; + + // Parse File + if (isset($matches[4])) + { + // Get filename + $filename = $matches[4]; + + // Get tmp name + $tmpName = tempnam(ini_get('upload_tmp_dir'), rand()); + $values = [ + 'error' => 0, + 'name' => $filename, + 'tmp_name' => $tmpName, + 'size' => strlen($body), + 'type' => trim($value), + ]; + + $exploded = explode('[', $name); + + $first = array_shift($exploded); + + if (!empty($exploded)) + { + $last = '[' . implode('[', $exploded); + } + else + { + $last = ''; + } + + foreach ($values as $key => $val) + { + $files[] = $first . '[' . $key . ']' . $last . '=' . $val; + } + + // Place in temporary directory + file_put_contents($tmpName, $body); + + // Register a shutdown function to cleanup the temporary file + register_shutdown_function( + function () use ($tmpName) + { + unlink($tmpName); + } + ); + } + + // Parse Field + else + { + $str[] = $name . '=' . substr($body, 0, strlen($body) - 2); + } + } + } + + parse_str(implode('&', $str), $data); + parse_str(implode('&', $files), $list); + $_FILES = array_replace($_FILES, $list); + + return $data; + } + + /** + * @param string $group Group + * @param array|string $values Values + * + * @return array + * @since __DEPLOY_VERSION__ + */ + private static function rearrangeFiles(string $group, $values): array + { + if (is_array($values)) + { + $return = []; + + foreach ($values as $k => $v) + { + $return[$k] = static::rearrangeFiles($group, $v); + } + + return $return; + } + else + { + return [ + $group => $values, + ]; + } + } + /** * Returns posted data in array format * @@ -220,38 +367,53 @@ public function loadExtensionLanguage($option, $path = JPATH_SITE) */ public static function getPostedData() { - $input = JFactory::getApplication()->input; + $headers = self::getHeaderVariablesFromGlobals(); + $input = JFactory::getApplication()->input; $inputData = file_get_contents("php://input"); + // Is data is compressed we will fetch it through separate function + if (isset($headers['CONTENT_ENCODING'])) + { + if (strpos(strtolower($headers['CONTENT_ENCODING']), 'gzip') !== false) + { + $decompressed = gzdecode($inputData); + + if ($decompressed) + { + $inputData = $decompressed; + } + } + } + if (is_object($inputData)) { - $inputData = JArrayHelper::fromObject($inputData); + $inputData = ArrayHelper::fromObject($inputData); } - elseif (is_string($inputData)) + elseif (is_string($inputData) && !empty($inputData)) { - $inputData = trim($inputData); + $inputData = trim($inputData); $parsedData = null; // We try to transform it into JSON - if ($data_json = @json_decode($inputData, true)) + if ($dataJson = @json_decode($inputData, true)) { if (json_last_error() == JSON_ERROR_NONE) { - $parsedData = (array) $data_json; + $parsedData = (array) $dataJson; } } // We try to transform it into XML if (is_null($parsedData) && $xml = @simplexml_load_string($inputData)) { - $json = json_encode((array) $xml); + $json = json_encode((array) $xml); $parsedData = json_decode($json, true); } // We try to transform it into Array - if (is_null($parsedData) && !empty($inputData) && !is_array($inputData)) + if (is_null($parsedData)) { - parse_str($inputData, $parsedData); + $parsedData = static::parsePut(); } $inputData = $parsedData; @@ -261,10 +423,35 @@ public static function getPostedData() $inputData = $input->post->getArray(); } - // Filter data with JInput default filter - $postedData = new JInput($inputData); + $files = []; + + foreach ($_FILES as $fieldName => $keys) + { + $files[$fieldName] = []; + + foreach ($keys as $key => $list) + { + $files[$fieldName] = array_replace_recursive($files[$fieldName], static::rearrangeFiles($key, $list)); + } + } + + $inputData = array_replace_recursive($inputData, $files); + + $filter = JFilterInput::getInstance(array(), array(), 1, 1); + + // Filter data with JInput default filter in blacklist mode + $postedData = new JInput($inputData, array('filter' => $filter)); + + if (version_compare(JVERSION, '3') >= 0) + { + return $postedData->getArray(array(), null, 'HTML'); + } + elseif ($inputData) + { + return $postedData->getArray(array(), $inputData, 'HTML'); + } - return $postedData->getArray(); + return array(); } /**