<?php
/**
 * Возвращает список директорий внутри указанной
 *
 * @param string $dir Полный путь к директории
 * @param boolean $asc_sort Сортировать по алфавиту, по умолчанию false
 * @return array
 */
function files_get_dirs_list($dir, $asc_sort = false) {

    if (!is_dir($dir)) {
        return [];
    }

    $sorting_order = $asc_sort ? SCANDIR_SORT_ASCENDING : SCANDIR_SORT_NONE;

    return array_values(array_filter(scandir($dir, $sorting_order), function ($entry) use ($dir) {

        return $entry !== '.' && $entry !== '..' &&
               is_dir($dir . '/' . $entry);
    }));
}

/**
 * Копирует директорию
 *
 * @param string $directory_from Полный путь к директории, которую копируем
 * @param string $directory_to Полный путь к директории, куда копируем
 * @return bool
 */
function files_copy_directory(string $directory_from, string $directory_to) {

    $directory_from = rtrim($directory_from, DIRECTORY_SEPARATOR);
    $directory_to   = rtrim($directory_to, DIRECTORY_SEPARATOR);

    if (!is_dir($directory_from)) {
        return false;
    }

    // Защита от recursive self copy
    if (
        $directory_from === $directory_to ||
        strpos($directory_to, $directory_from . DIRECTORY_SEPARATOR) === 0
    ) {
        return false;
    }

    if (!is_dir($directory_to)) {
        if (!mkdir($directory_to, 0755, true) && !is_dir($directory_to)) {
            return false;
        }
    }

    $items = new FilesystemIterator($directory_from);

    foreach ($items as $item) {

        // Запрещаем symlink
        if ($item->isLink()) {
            return false;
        }

        $target = $directory_to . DIRECTORY_SEPARATOR . $item->getBasename();

        if ($item->isDir()) {

            if (!files_copy_directory($item->getPathname(), $target)) {
                return false;
            }

            continue;
        }

        if (!copy($item->getPathname(), $target)) {
            return false;
        }

        @chmod($target, 0644);
    }

    return true;
}

/**
 * Рекурсивно удаляет директорию
 * @param string $directory
 * @param bool $is_clear Если TRUE, то директория будет очищена, но не удалена
 * @return bool
 */
function files_remove_directory(string $directory, $is_clear = false) {

    $directory = rtrim($directory, '/\\');

    if (
        !file_exists($directory) ||
        !is_dir($directory) ||
        !is_readable($directory)
    ) {
        return false;
    }

    $handle = opendir($directory);
    if ($handle === false) {
        return false;
    }

    while (($node = readdir($handle)) !== false) {

        if ($node === '.' || $node === '..') {
            continue;
        }

        $path = $directory . DIRECTORY_SEPARATOR . $node;

        // symlink удаляем как файл
        if (is_link($path)) {
            if (!@unlink($path)) {
                closedir($handle);
                return false;
            }
            continue;
        }

        if (is_dir($path)) {

            if (!files_remove_directory($path)) {
                closedir($handle);
                return false;
            }

        } else {

            if (!@unlink($path)) {
                closedir($handle);
                return false;
            }
        }
    }

    closedir($handle);

    if (!$is_clear) {
        if (!@rmdir($directory)) {
            return false;
        }
    }

    return true;
}

/**
 * Очищает директорию
 *
 * @param string $directory
 * @return bool
 */
function files_clear_directory(string $directory){
    return files_remove_directory($directory, true);
}

/**
 * Удаляет файл и его родительские директории
 * Работает до директории upload_path
 *
 * @param string $file_path Относительный (от upload_path без начального слеша) или полный путь к файлу
 * @param int $delete_parent_dir Количество родительских директорий, которые нужно также удалить, если они пустые
 * @return boolean
 */
function files_delete_file(string $file_path, $delete_parent_dir = 0) {

    $upload_root = cmsConfig::get('upload_path');

    if (!is_file($file_path)) {
        $file_path = $upload_root . $file_path;
    }

    $file_path = realpath($file_path);

    if (!$file_path || !is_file($file_path)) {
        return false;
    }

    if (strncmp($file_path, $upload_root, strlen($upload_root)) !== 0) {
        return false;
    }

    $success = @unlink($file_path);

    if ($delete_parent_dir && $success) {

        $parent_dir = dirname($file_path);

        for ($i = 1; $i <= $delete_parent_dir; $i++) {

            if (
                strncmp($parent_dir, $upload_root, strlen($upload_root)) !== 0 ||
                $parent_dir === $upload_root
            ) {
                break;
            }

            if (!@rmdir($parent_dir)) {
                break;
            }

            $parent_dir = dirname($parent_dir);
        }
    }

    return $success;
}

/**
 * Возвращает дерево каталогов и файлов по указанному пути в виде
 * рекурсивного массива
 * @param string $path
 * @return array
 */
function files_tree_to_array($path) {

    $data = [];

    $dir = new DirectoryIterator($path);

    foreach ($dir as $node) {
        if ($node->isDir() && !$node->isDot()) {
            $data[$node->getFilename()] = files_tree_to_array($node->getPathname());
        } else if ($node->isFile()) {
            $data[] = $node->getFilename();
        }
    }

    return $data;
}

/**
 * Нормализует путь к файлу, убирая все условные переходы.
 *
 * Например путь
 *      /path/to/../folder
 * будет преобразован в
 *      /path/folder
 *
 * @param string $path
 * @return string
 */
function files_normalize_path($path) {

    $parts = explode('/', $path);
    $safe  = [];

    foreach ($parts as $part) {
        if (empty($part) || $part === '.') {
            continue;
        }
        if ($part === '..') {
            array_pop($safe);
            continue;
        }
        $safe[] = $part;
    }

    return implode('/', $safe);
}

/**
 * Получает строку вида "8M" или "1024K" и возвращает значение в байтах
 * Полезно при получении max_upload_size из php.ini
 *
 * @param string $value
 * @return int
 */
function files_convert_bytes($value) {
    if ( is_numeric( $value ) ) {
        return $value;
    } else {
        $value_length = strlen( $value );
        $qty = substr( $value, 0, $value_length - 1 );
        $unit = strtolower( substr( $value, $value_length - 1 ) );
        switch ( $unit ) {
            case 'k':
                $qty *= 1024;
                break;
            case 'm':
                $qty *= 1048576;
                break;
            case 'g':
                $qty *= 1073741824;
                break;
        }
        return $qty;
    }
    return $value;
}

/**
 * Переводит байты в Гб, Мб или Кб и возвращает полученное число + единицу измерения
 * в виде единой строки
 * @param int $bytes
 * @return string
 */
function files_format_bytes($bytes) {

    $kb = 1024;
    $mb = 1048576;
    $gb = 1073741824;

    if (round($bytes / $gb) > 0) {
        return round(($bytes / $gb), 1, PHP_ROUND_HALF_UP) . ' ' . LANG_GB;
    }

    if (round($bytes / $mb) > 0) {
        return ceil($bytes / $mb) . ' ' . LANG_MB;
    }

    if (round($bytes / $kb) > 0) {
        return ceil($bytes / $kb) . ' ' . LANG_KB;
    }

    return $bytes . ' ' . LANG_B;
}

/**
 * Возвращает 32-х символьный хэш, привязанный к ip адресу
 * используется для защиты от хотлинка
 *
 * @param string $file_path Путь к файлу
 * @return string
 */
function files_user_file_hash($file_path = ''){
    return md5(cmsUser::getIp().md5($file_path.cmsConfig::get('root_path')));
}

/**
 * Нормализует имя файла:
 * - приводит к нижнему регистру
 * - удаляет недопустимые символы
 * - нормализует разделители
 * - очищает расширение
 * - ограничивает длину имени
 *
 * @param string $filename Имя файла
 * @param bool $convert_slug Транслитировать?
 * @return string
 */
function files_sanitize_name(string $filename, $convert_slug = true) {

    $filename = mb_strtolower($filename);

    $parts = pathinfo($filename);

    $name = $parts['filename'] ?? '';
    $ext  = $parts['extension'] ?? '';

    $name = $convert_slug ? lang_slug($name) : trim($name);

    $name = str_replace(
        ['&', '@', '#', ' ', '\''],
        ['-and-', '-at-', '-number-', '-', ''],
        $name
    );

    $name = preg_replace('/[^\w\-\.]+/u', '', $name);
    $name = preg_replace('/[-_]+/', '-', $name);
    $name = preg_replace('/\.+/', '.', $name);
    $name = trim($name, '.-');
    $name = mb_substr($name, 0, 200);

    if (!$name) {
        $name = 'file';
    }

    if ($ext) {

        $ext = preg_replace('/[^a-z0-9]+/i', '', $ext);

        if ($ext) {
            $name .= ".{$ext}";
        }
    }

    return $name;
}

/**
 * Возвращает/создаёт путь к директории хранения
 *
 * @param int $user_id
 * @return ?string
 */
function files_get_upload_dir($user_id = 0) {

    $upload_path = cmsConfig::get('upload_path');

    $user_group = sprintf('%03d', intdiv($user_id, 100));

    $hash = bin2hex(random_bytes(1));

    $dir1 = $hash[0];
    $dir2 = $hash[1];

    $dest_dir = sprintf(
        '%s%s/u%d/%s/%s/',
        $upload_path,
        $user_group,
        $user_id,
        $dir1,
        $dir2
    );

    if (!is_dir($dest_dir)
        && !mkdir($dest_dir, 0775, true)
        && !is_dir($dest_dir)) {
        return null;
    }

    return $dest_dir;
}

/**
 * Получает данные по заданному url
 * Не используйте эту функцию с URL, полученных от пользователя,
 * В таких случаях используйте cmsUploadremote()
 *
 * @param string $url URL, откуда нужно получить данные
 * @param int $timeout Таймаут соединения
 * @param bool $json_decode Преобразовывать JSON
 * @param array $params Дополнительные параметры
 * @return ?string
 */
function file_get_contents_from_url($url, $timeout = 5, $json_decode = false, $params = []) {

    $curl = curl_init();

    if (strpos($url, 'https') === 0) {
        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
    }
    $headers = ['User-Agent: InstantCMS/2.0'];
    if (!empty($params['cookie'])) {

        $cookie = [];
        foreach ($params['cookie'] as $k => $v) {
            $cookie[] = $k . '=' . $v;
        }

        $headers[] = 'Cookie: ' . implode('; ', $cookie);
        unset($params['cookie']);
    }
    if (!empty($params['proxy'])) {
        curl_setopt($curl, CURLOPT_PROXY, $params['proxy']['host'] . ':' . $params['proxy']['port']);
        curl_setopt($curl, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
        unset($params['proxy']);
    }
    if (!empty($params)) {
        foreach ($params as $key => $value) {
            $headers[] = $key . ': ' . $value;
        }
    }
    curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS | CURLPROTO_HTTP);
    curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($curl, CURLOPT_URL, $url);
    curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, $timeout);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($curl, CURLOPT_HEADER, false);
    curl_setopt($curl, CURLOPT_TIMEOUT, $timeout);
    $data = curl_exec($curl);

    if ($data === false) {
        return null;
    }

    if ($json_decode) {
        return json_decode($data, true);
    }

    return $data;
}

/**
 * Сохраняет удаленно расположенный файл
 * @param string $url url файла
 * @param string $destination Полный путь куда сохранить файл
 * @return boolean
 */
function file_save_from_url($url, $destination) {

    if (!function_exists('curl_init')) {
        return false;
    }

    $dest_file = @fopen($destination, "w");

    if ($dest_file === false) {
        return false;
    }

    $curl = curl_init();
    if (strpos($url, 'https') === 0) {
        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
    }
    curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS | CURLPROTO_HTTP);
    curl_setopt($curl, CURLOPT_URL, $url);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($curl, CURLOPT_FILE, $dest_file);
    curl_setopt($curl, CURLOPT_HEADER, false);
    curl_exec($curl);
    fclose($dest_file);

    return true;
}

/**
 * Накладывает ваттермарк на изображение
 *
 * @deprecated Используйте класс cmsImages
 *
 * @return boolean
 */
function img_add_watermark($src_file, $wm_file, $wm_origin, $wm_margin, $quality = 90) {
    return true;
}

/**
 * Изменяет размер изображения $src, сохраняя измененное в $dest
 *
 * @deprecated Используйте класс cmsImages
 *
 * @param string $src Полный путь к исходному изображению
 * @param string $dest Полный путь куда сохранять измененное изображение
 * @param int $maxwidth Максимальная ширина в px
 * @param int $maxheight Максимальная высота в px
 * @param bool $is_square Создавать квадратное изображение
 * @param int $quality Качество результирующего изображения от 1 до 100
 * @return boolean
 */
function img_resize($src, $dest, $maxwidth, $maxheight = 160, $is_square = false, $quality = 95) {

    if (!file_exists($src)) {
        return false;
    }

    $upload_dir = dirname($dest);

    if (!is_writable($upload_dir)) {

        @chmod($upload_dir, 0777);

        if (!is_writable($upload_dir)) {
            return false;
        }
    }

    try {
        $image = new cmsImages($src);
    } catch (Exception $exc) {
        return false;
    }

    if ($is_square) {

        $image->crop($maxwidth, $maxheight, false, cmsImages::CROPCENTER);

    } else {

        if (!$maxwidth || !$maxheight) {

            if (!$maxwidth) {
                $image->resizeToHeight($maxheight);
            } else {
                $image->resizeToWidth($maxwidth);
            }

        } else {
            $image->resizeToBestFit($maxwidth, $maxheight);
        }
    }

    $image->save($dest, null, $quality);

    return true;
}

/**
 * Возвращает параметры изображения
 *
 * @param string $path Полный путь к файлу
 * @return boolean|array
 */
function img_get_params($path) {

    if (!is_readable($path)) {
        return false;
    }

    // Получаем базовую информацию об изображении
    $s = getimagesize($path);
    if ($s === false) {
        return false;
    }

    $exif_data = [];
    // Получаем EXIF-данные, если это JPEG
    if (function_exists('exif_read_data') && $s['mime'] === 'image/jpeg') {

        $exif = @exif_read_data($path, null, true);

        if ($exif) {
            // Диафрагма
            $exif_data['aperturefnumber'] = $exif['COMPUTED']['ApertureFNumber'] ??
                (isset($exif['EXIF']['FNumber']) ? 'f/' . eval_fraction($exif['EXIF']['FNumber']) : null);

            // Выдержка
            $exif_data['exposuretime'] = $exif['EXIF']['ExposureTime'] ??
                (isset($exif['EXIF']['ExposureTime']) ? '1/' . round(eval_fraction($exif['EXIF']['ExposureTime'])) . 's' : null);

            // Камера
            $make = isset($exif['IFD0']['Make']) ? trim($exif['IFD0']['Make']) : null;
            $model = $exif['IFD0']['Model'] ?? null;
            if ($make && !in_array($make, ['NIKON CORPORATION', 'Canon', 'Lenovo'])) {
                $exif_data['camera'] = $model ? "$make $model" : $make;
            } elseif ($model) {
                $exif_data['camera'] = $model;
            }

            // Дата создания
            $exif_data['date'] = $exif['EXIF']['DateTimeOriginal'] ?? $exif['EXIF']['DateTimeDigitized'] ?? null;

            // ISO
            $iso = $exif['EXIF']['ISOSpeedRatings'] ?? null;
            $exif_data['isospeedratings'] = is_array($iso) ? current($iso) : $iso;

            // Фокусное расстояние
            $exif_data['focallength'] = isset($exif['EXIF']['FocalLength']) ? eval_fraction($exif['EXIF']['FocalLength']) . 'mm' : null;
            $exif_data['focallengthin35mmfilm'] = isset($exif['EXIF']['FocalLengthIn35mmFilm']) ? $exif['EXIF']['FocalLengthIn35mmFilm'] . 'mm' : null;

            // Ориентация
            $exif_data['orientation'] = $exif['IFD0']['Orientation'] ?? null;

            $exif_data = array_filter($exif_data);

            foreach ($exif_data as $key => $value) {
                $exif_data[$key] = is_string($value) ? strip_tags($value) : $value;
            }
        }
    }

    // Определение ориентации изображения
    $orientation = $s[0] === $s[1] ? 'square' : ($s[0] > $s[1] ? 'landscape' : 'portrait');

    return [
        'orientation' => $orientation,
        'width'       => $s[0],
        'height'      => $s[1],
        'mime'        => $s['mime'],
        'exif'        => array_filter($exif_data),
        'filesize'    => filesize($path)
    ];
}

/**
 * Вспомогательная функция для вычисления дробных значений EXIF
 *
 * @param string $fraction Строка дробного числа в формате 'числитель/знаменатель'
 * @return float
 */
function eval_fraction($fraction) {
    if (strpos($fraction, '/') !== false) {
        list($numerator, $denominator) = explode('/', $fraction);
        return $denominator == 0 ? 0 : $numerator / $denominator;
    }
    return (float)$fraction;
}

/**
 * Выполняет команду в shell и возвращает массив строк ответа
 *
 * @param string $command Команда
 * @param string $postfix Строка после команды
 * @return ?array
 */
function console_exec_command($command, $postfix = ' 2>&1') {

    if (!function_exists('exec')) {
        return null;
    }

    $buffer = [];
    $err    = '';

    $result = exec($command . $postfix, $buffer, $err);

    if ($err !== 127) {
        if (!isset($buffer[0])) {
            $buffer[0] = $result;
        }
        $b = strtolower($buffer[0]);
        if (strstr($b, 'error') || strstr($b, ' no ') || strstr($b, 'not found') || strstr($b, 'no such file or directory')) {
            return [];
        }
    } else {
        return [];
    }

    return $buffer;
}
