app/resto/core/utils/RestoUtil.php

Summary

Maintainability
C
1 day
Test Coverage
<?php
/*
 * Copyright 2018 Jérôme Gasperi
 *
 * Licensed under the Apache License, version 2.0 (the "License");
 * You may not use this file except in compliance with the License.
 * You may obtain a copy of the License at:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */

/**
 * RESTo Utilities functions
 */
class RestoUtil
{
    /*
     * List of supported formats mimeTypes
     */
    public static $contentTypes = array(
        'atom' => 'application/atom+xml',
        'cog' => 'image/tiff; application=geotiff; profile=cloud-optimized',
        'csv' => 'text/csv',
        'geojson' => 'application/geo+json',
        'geopackage' => 'application/geopackage+sqlite3',
        'geotiff' => 'image/tiff; application=geotiff',
        'hdf' => 'application/x-hdf',
        'hdf5' => 'application/x-hdf5',
        'html' => 'text/html',
        'jpeg' => 'image/jpeg',
        'jpeg2000' => 'image/jp2',
        'jp2' => 'image/jp2',
        'json' => 'application/json',
        'jsonschema' => 'application/schema+json',
        'meta4' => 'application/metalink4+xml',
        'mvt' => 'application/vnd.mapbox-vector-tile',
        'openapi+json' => 'application/vnd.oai.openapi+json;version=3.0',
        'pbf' => 'application/vnd.mapbox-vector-tile',
        'png' => 'image/png',
        'text' => 'text/plain',
        'xml' => 'application/xml',
        'zip' => 'application/zip'
    );

    /**
     * Clean associative array i.e. remove empty or null keys
     *
     * @param array $associativeArray
     * @return array
     */
    public static function cleanAssociativeArray($associativeArray)
    {
        // Output
        $cleanArray = array();

        // Eventually unset all empty properties and array
        foreach (array_keys($associativeArray) as $key) {
            if (!isset($associativeArray[$key]) || (is_array($associativeArray[$key]) && count($associativeArray[$key]) === 0)) {
                continue;
            }
            $cleanArray[$key] = $associativeArray[$key];
        }
        
        return $cleanArray;
    }

    /**
     * Extract hashtags from a string (i.e. #something or -#something)
     *
     * @param string $str
     *
     * @return array
     */
    public static function extractHashtags($str)
    {
        preg_match_all("/(#|-#)([^ ]+)/u", $str, $matches);
        if ($matches && count($matches[0]) > 0) {
            return $matches[0];
        }
        return array();
    }

    /**
     * Clean a hashtag string i.e. disacard characters !, $, %, ^, ~, &, *, +, ., ", { ,}, /, \
     *
     * Example:
     *
     *    $str = #bad!hasHtag$%".veryBad
     *
     * returns:
     *
     *    #badhasHtagveryBad
     *
     * @param string $str
     *
     * @return string
     */
    public static function cleanHashtag($str)
    {
        $bad = array('!', '$', '%', '^', '~', '&', '*', '+', '.', '"', '{', '}', '/', '\\');
        return str_replace($bad, '', $str);
    }

    /**
     * Encrypt a string using $algorithm
     *
     * @param string $str
     */
    public static function encrypt($str)
    {
        return sha1($str);
    }

    /**
     * Generate v5 UUID
     *
     * Version 5 UUIDs are named based. They require a namespace (another
     * valid UUID) and a value (the name). Given the same namespace and
     * name, the output is always the same.
     *
     * @param string $str
     *
     * @author Andrew Moore
     * @link http://www.php.net/manual/en/function.uniqid.php#94959
     */
    public static function toUUID($str)
    {
        // Get hexadecimal components of namespace (Note: use a dummy uuid)
        $nhex = str_replace(array('-', '{', '}'), '', '92708059-2077-45a3-a4f3-1eb428789cff');

        // Binary Value
        $nstr = '';

        // Convert Namespace UUID to bits
        for ($i = 0, $ii = strlen($nhex); $i < $ii; $i+=2) {
            $nstr .= chr(hexdec($nhex[$i] . $nhex[$i + 1]));
        }

        // Calculate hash value
        $hash = sha1($nstr . $str);

        return sprintf(
            '%08s-%04s-%04x-%04x-%12s',
            // 32 bits for "time_low"
            substr($hash, 0, 8),
            // 16 bits for "time_mid"
            substr($hash, 8, 4),
            // 16 bits for "time_hi_and_version",
            // four most significant bits holds version number 5
            (hexdec(substr($hash, 12, 4)) & 0x0fff) | 0x5000,
            // 16 bits, 8 bits for "clk_seq_hi_res",
            // 8 bits for "clk_seq_low",
            // two most significant bits holds zero and one for variant DCE1.1
            (hexdec(substr($hash, 16, 4)) & 0x3fff) | 0x8000,
            // 48 bits for "node"
            substr($hash, 20, 12)
        );
    }

    /**
     * Rewrite URL with input query parameters
     *
     * @param string $url
     * @param array $newParams
     */
    public static function updateUrl($url, $newParams = array())
    {
        $existingParams = array();
        $exploded = parse_url($url);
        if (isset($exploded['query'])) {
            $existingParams = RestoUtil::queryStringToKvps($exploded['query']);
        }
        $queryString = RestoUtil::kvpsToQueryString(array_merge($existingParams, $newParams));
        return RestoUtil::baseUrl($exploded) . $exploded['path'] . ($queryString ? '?' . $queryString : '');
    }

    /**
     * Rewrite URL with new format
     *
     * @param string $url
     * @param string $format
     */
    public static function updateUrlFormat($url, $format)
    {
        $exploded = parse_url($url);
        $path = $exploded['path'];
        $splitted = explode('.', $path);
        if (count($splitted) > 1) {
            array_pop($splitted);
            $path = join('.', $splitted);
        }
        return RestoUtil::baseUrl($exploded) . $path . '.' . $format . (isset($exploded['query']) ? '?' . $exploded['query'] : '');
    }

    /**
     * Write a valid RESTo URL
     *
     * @param string $baseUrl
     * @param string $route
     * @param string $format
     */
    public static function restoUrl($baseUrl = '//', $route = '', $format = '')
    {
        return trim($baseUrl . $route, '/') . (isset($format) && $format !== '' ? '.' . $format : '');
    }

    /**
     *
     * Return true if input date string is ISO 8601 formatted
     * i.e. one in the following form :
     *
     *      YYYY
     *      YYYY-MM
     *      YYYY-MM-DD
     *      YYYY-MM-DDTHH:MM:SS
     *      YYYY-MM-DDTHH:MM:SSZ
     *      YYYY-MM-DDTHH:MM:SS.sssss
     *      YYYY-MM-DDTHH:MM:SS.sssssZ
     *      YYYY-MM-DDTHH:MM:SS+HHMM
     *      YYYY-MM-DDTHH:MM:SS-HHMM
     *      YYYY-MM-DDTHH:MM:SS.sssss+HHMM
     *      YYYY-MM-DDTHH:MM:SS.sssss-HHMM
     *
     * @param string $dateStr
     *
     */
    public static function isISO8601($dateStr)
    {
        /**
         * Construct the regex to match all ISO 8601 format date case
         * The regex is constructed as a combination of all pattern
         */
        return preg_match('/^' . join('$|^', array(
                    '\d{4}', // YYYY
                    '\d{4}-\d{2}', // YYYY-MM
                    '\d{4}-\d{2}-\d{2}', // YYYY-MM-DD
                    '\d{4}-\d{2}-\d{2}T\d{2}\:\d{2}\:\d{2}', // YYYY-MM-DDTHH:MM:SS
                    '\d{4}-\d{2}-\d{2}T\d{2}\:\d{2}\:\d{2}Z', // YYYY-MM-DDTHH:MM:SSZ
                    '\d{4}-\d{2}-\d{2}T\d{2}\:\d{2}\:\d{2}' . '' . '[\+|\-]\d{2}\:\d{2}', // YYYY-MM-DDTHH:MM:SS +HH:MM or -HH:MM
                    '\d{4}-\d{2}-\d{2}T\d{2}\:\d{2}\:\d{2}' . '' . '[,|\.]\d+', // YYYY-MM-DDTHH:MM:SS(. or ,)n
                    '\d{4}-\d{2}-\d{2}T\d{2}\:\d{2}\:\d{2}' . '' . '[,|\.]\d+' . 'Z', // YYYY-MM-DDTHH:MM:SS(. or ,)nZ
                    '\d{4}-\d{2}-\d{2}T\d{2}\:\d{2}\:\d{2}' . '' . '[,|\.]\d+' . '' . '[\+|\-]\d{2}\:\d{2}', // // YYYY-MM-DDTHH:MM:SS(. or ,)n +HH:MM or -HH:MM
                    '\d{4}\d{2}', // YYYYMM
                    '\d{4}\d{2}\d{2}', // YYYYMMDD
                    '\d{4}\d{2}\d{2}T\d{2}\d{2}\d{2}', // YYYYMMDDTHHMMSS
                    '\d{4}\d{2}\d{2}T\d{2}\d{2}\d{2}' . 'Z', // YYYYMMDDTHHMMSSZ
                    '\d{4}\d{2}\d{2}T\d{2}\d{2}\d{2}' . '' . '[\+|\-]\d{2}\d{2}', // YYYYMMDDTHHMMSSZ +HHMM or -HHMM
                    '\d{4}\d{2}\d{2}T\d{2}\d{2}\d{2}' . '' . '[\+|\-]\d{2}\d{2}' . 'Z', // // YYYYMMDDTHHMMSSZ(. or ,)nZ
                    '\d{4}\d{2}\d{2}T\d{2}\d{2}\d{2}' . '' . '[,|\.]\d+' . '' . '[\+|\-]\d{2}\d{2}' // YYYYMMDDTHHMMSSZ(. or ,)n +HHMM or -HHMM
                )) . '$/i', $dateStr);
    }

    /**
     * Split a string on space character into an array of words
     *
     * Note: if parts of the input string are inside quotes (i.e. " character"),
     * the content of the quotes is considered as a single word
     *
     *
     * @param string $str
     * @return array
     */
    public static function splitString($str)
    {
        $quotted = explode('"', $str);

        /*
         * Search for quotted (i.e. text within " ") parts
         */
        $count = count($quotted);
        if ($count > 1 && $count % 2 === 1) {
            $output = array();
            for ($i = 0; $i < $count; $i++) {
                if ($quotted[$i]) {
                    // Inside the quote
                    if ($i % 2 === 1) {
                        $output[] = $quotted[$i];
                    }
                    // Outside the quote - split on space character
                    else {
                        $exploded = explode(' ', $quotted[$i]);
                        for ($j = 0, $m = count($exploded); $j < $m; $j++) {
                            if ($exploded[$j]) {
                                $output[] = $exploded[$j];
                            }
                        }
                    }
                }
            }

            return $output;
        }

        return explode(' ', str_replace('"', '', $str));
    }

    /**
     * Check if string starts like an url i.e. http:// or https:// or //:
     *
     * @param string $str
     */
    public static function isUrl($str)
    {
        if (!isset($str)) {
            return false;
        }
        if (substr(trim($str), 0, 7) === 'http://' || substr(trim($str), 0, 8) === 'https://' || substr(trim($str), 0, 2) === '//') {
            return true;
        }
        return false;
    }

    /**
     * Check that input $uuid has a valid uuid syntax
     * @link http://tools.ietf.org/html/rfc4122
     *
     * @param string $uuid
     */
    public static function isValidUUID($uuid)
    {
        return preg_match('/^\{?[0-9a-f]{8}\-?[0-9a-f]{4}\-?[0-9a-f]{4}\-?[0-9a-f]{4}\-?[0-9a-f]{12}\}?$/i', $uuid) === 1;
    }

    /**
     * Sanitize input parameter to avoid code injection
     *   - remove html tags
     *
     * @param string|array $strOrArray
     */
    public static function sanitize($strOrArray)
    {
        if (!isset($strOrArray)) {
            return null;
        }

        if (is_array($strOrArray)) {
            $result = array();
            foreach ($strOrArray as $key => $value) {
                $result[$key] = RestoUtil::sanitizeString($value);
            }
            return $result;
        }
        
        return RestoUtil::sanitizeString($strOrArray);
    }

    /**
     * Format input Key/Value pairs array to query string
     *
     * @param array $kvps
     * @return string
     */
    public static function kvpsToQueryString($kvps)
    {
        $paramsStr = '';
        if (!is_array($kvps)) {
            return $paramsStr;
        }
        foreach ($kvps as $key => $value) {
            if ($value === null) {
                continue;
            }
            if (is_array($value)) {
                for ($i = count($value); $i--;) {
                    $paramsStr .= (isset($paramsStr) ? '&' : '') . rawurlencode($key) . '[]=' . rawurlencode($value[$i]);
                }
            } else {
                $paramsStr .= (isset($paramsStr) ? '&' : '') . rawurlencode($key) . '=' . rawurlencode($value);
            }
        }
        return $paramsStr;
    }

    /**
     * Explode query string to input Key/Value pairs array
     *
     * @param string $queryString
     * @return array
     */
    public static function queryStringToKvps($queryString)
    {
        $output = array();
        parse_str($queryString, $output);
        return $output;
    }

    /**
     * Replace all occurences of a string
     *
     *  Example :
     *
     *      replaceInTemplate('Hello. My name is {name}. I live in {location}', array('name' => 'Jérôme', 'location' => 'Toulouse'));
     *
     *  Will return
     *
     *      Hello. My name is Jérôme. I live in Toulouse
     *
     * [IMPORTANT]
     *
     *      
     *      {xxx} value without a xxx pair defined in pairs is replace by empty string
     *      In the previous example, if 'name' => 'Jérôme' is not provided, the return sentence
     *      would be
     *
     *      Hello. My name is . I live in Toulouse
     *
     *
     * @param string $sentence
     * @param array $pairs
     * @param string $pattern // Default is {}, other possible value is {::}
     *
     */
    public static function replaceInTemplate($sentence, $pairs, $pattern = '{}')
    {
        if (!isset($sentence)) {
            return null;
        }

        switch ($pattern) {
            case '{::}':
                $regex = "/{\:[^\\:}]*\:}/";
                break;
            default:
                $regex = "/{(.*?)}/";
        }
        
        /*
         * Extract pairs
         */
        preg_match_all($regex, $sentence, $matches);

        $replace = array();
        $size = strlen($pattern) / 2;
        for ($i = count($matches[0]); $i--;) {
            $replace[$matches[0][$i]] = $pairs[substr($matches[0][$i], $size, -$size)] ?? '';
        }
        if (count($replace) > 0) {
            return strtr($sentence, $replace);
        }

        return $sentence;
    }

    /**
     * Store file within outputDir
     *
     * @param string $data Base64 encoded file
     * @param string $outputDir Directory where to store the file
     * @param array $allowedTypes File types allowed to be uploaded
     * @return string The name of the stored file (without basename)
     */
    public static function storeBase64File($data, $outputDir, $allowedTypes = array())
    {
        try {
            if (preg_match('/^data:image\/(\w+);base64,/', $data, $type)) {
                $data = substr($data, strpos($data, ',') + 1);
                $type = strtolower($type[1]);
            
                if (!in_array($type, $allowedTypes)) {
                    throw new Exception('This file type is not allowed');
                }
            
                // Compute file md5
                $fileName = md5($data) . '.' . $type;

                // Decode data
                $data = base64_decode($data);
            
                if ($data === false) {
                    throw new Exception('File decoding failed');
                }
            } else {
                throw new Exception('Invalid file');
            }
            
            file_put_contents($outputDir . '/' . $fileName, $data);

            return $fileName;
        } catch (Exception $e) {
            RestoLogUtil::httpError(400, $e->getMessage());
        }
    }

    /**
     * Check that userid is the caller
     *
     * @param string userid
     */
    public static function checkUser($user, $userid)
    {
        if (!ctype_digit($userid)) {
            RestoLogUtil::httpError(400, 'Invalid userid');
        }
        if (! $user || $user->profile['id'] !== $userid) {
            RestoLogUtil::httpError(403);
        }
    }

    /**
     * Construct base url from parse_url fragments
     *
     * @param array $exploded
     */
    private static function baseUrl($exploded)
    {
        return (isset($exploded['scheme']) ? $exploded['scheme'] . ':' : '') . '//' .
               (isset($exploded['user']) ? $exploded['user'] . ':' . $exploded['pass'] . '@' : '') .
               $exploded['host'] . (isset($exploded['port']) ? ':' . $exploded['port'] : '');
    }

    /**
     * Sanitize string
     *
     * @param string $str
     * @return string
     */
    private static function sanitizeString($str)
    {
        /*
         * Remove html tags and NULL (i.e. \0)
         */
        if (is_string($str)) {
            /*
             * No Hexadecimal allowed i.e. nothing that starts with 0x
             */
            if (strlen($str) > 1 && substr($str, 0, 2) === '0x') {
                return null;
            }
            
            // Remove script tags
            return preg_replace('/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/i', '', str_replace(chr(0), '', $str));
        }
        
        /*
         * Let value untouched
         */
        return $str;
    }
}