TikiWiki/tiki-manager

View on GitHub
src/Application/Tiki/Handler/SystemConfigurationFile.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php
/**
 * @copyright (c) Copyright by authors of the Tiki Manager Project. All Rights Reserved.
 *     See copyright.txt for details and a complete list of authors.
 * @licence Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See LICENSE for details.
 */

namespace TikiManager\Application\Tiki\Handler;

use Exception;

class SystemConfigurationFile
{
    protected array $defaultDangerousDirectives = [
        'preference.cookie_domain',
        'preference.fgal_use_dir',
        'preference.gal_batch_dir',
        'preference.gal_use_dir',
        'preference.fallbackBaseUrl',
        'preference.tmpDir',
        'preference.memcache_enabled',
        'preference.memcache_servers',
        'preference.memcache_prefix',
        'preference.redis_enabled',
        'preference.redis_host',
        'preference.redis_prefix',
        'preference.tikimanager_storage_path',
        'preference.unified_elastic_url',
        'preference.unified_elastic_index_prefix',
        'preference.unified_manticore_url',
        'preference.unified_manticore_index_prefix',
    ];

    protected array $dangerousDirectives = [];

    public const ENV_KEY = 'SYSTEM_CONFIG_DANGER_DIRECTIVES';

    /**
     * Constructor, allows to set the directives to use as parameter or env variable.
     * It falls back to use the default list if another is not provided as env or parameter.
     *
     * @param array|null $directives
     */
    public function __construct(?array $directives = null)
    {
        if ($directives !== null) {
            $this->dangerousDirectives = $directives;
        } elseif (! empty($_ENV[self::ENV_KEY])) {
            $directives = array_map('trim', explode(',', $_ENV[self::ENV_KEY]));
            $this->dangerousDirectives = $directives;
        } else {
            $this->dangerousDirectives = $this->defaultDangerousDirectives;
        }
    }

    /**
     * Return the default list of directives that can exist in the system config file, that may cause clobber issues
     * when the system configuration file is restored to a different (from the original) instance.
     * Example: elastic search address and prefix, php session name, etc.
     *
     * @return array with the directive names
     */
    public function getDefaultDangerousDirectives(): array
    {
        return $this->defaultDangerousDirectives;
    }

    /**
     * Return the current list of directives that can exist in the system config file, that may cause clobber issues
     * when the system configuration file is restored to a different (from the original) instance.
     * Example: elastic search address and prefix, php session name, etc.
     *
     * @return array with the directive names
     */
    public function getDangerousDirectives(): array
    {
        return $this->dangerousDirectives;
    }

    /**
     * Allows to override the default list of directives
     *
     * @param array $directives
     * @return void
     */
    public function setDangerousDirectives(array $directives): void
    {
        $this->dangerousDirectives = $directives;
    }

    /**
     * Attempts to read a file and return true if the file contains any (potentially) dangerous directives
     *
     * @return bool return true if a dangerous directive is found
     */
    public function hasDangerousDirectives(string $filename): bool
    {
        // we expect $file to be accessible in the local filesystem, if that's not the case, you should localize it first
        if (! file_exists($filename)) {
            return false;
        }

        $directives = $this->getDangerousDirectives();

        try {
            $hasDirectives = $this->checkDirectivesExistsInIniFile($filename, $directives);
        } catch (Exception $e) {
            $hasDirectives = $this->checkDirectivesExistsInPlainFile($filename, $directives);
        }

        return $hasDirectives;
    }

    /**
     * Parses an INI file and check if any entry match one of the directives
     *
     * @param string $filename
     * @param array $directives
     * @return bool
     * @throws Exception
     */
    protected function checkDirectivesExistsInIniFile(string $filename, array $directives): bool
    {
        // Tiki uses Laminas to load the ini file, that uses parse_ini_{file,string} under the hood,
        // this will avoid false positives in comments, etc

        if ('.ini.php' === substr($filename, -8)) { // handle .ini.php files
            $initString = $this->retrieveIniPhpFileContents($filename);
            $ini = parse_ini_string($initString, true, INI_SCANNER_NORMAL);
        } else { // handle .ini files (actually assume any other file name will be an ini file)
            $ini = parse_ini_file($filename, true, INI_SCANNER_NORMAL);
        }

        if ($ini === false) {
            throw new Exception('Ini file could not be parsed.');
        }

        $hasDirectives = false;
        array_walk_recursive($ini, function ($item, $key) use ($directives, &$hasDirectives) {
            if (in_array($key, $directives)) {
                $hasDirectives = true;
            }
        });

        return $hasDirectives;
    }

    /**
     * Retrieves INI information from an .ini.php file
     *
     * @see https://gitlab.com/tikiwiki/tiki/-/blob/92228a15b5e9053e17abeaee029ca014fcf81b9a/db/tiki-db.php#L114-125
     * @param string $filename
     * @return false|string
     */
    protected function retrieveIniPhpFileContents(string $filename)
    {
        ob_start();
        include($filename);
        $systemConfigurationFileContent = ob_get_contents();
        ob_end_clean();

        return $systemConfigurationFileContent;
    }

    /**
     * Treat the file as plain text, and search the directive text anywhere in the file
     *
     * @param string $filename
     * @param array $directives
     * @return bool
     */
    protected function checkDirectivesExistsInPlainFile(string $filename, array $directives): bool
    {
        $content = file_get_contents($filename);
        if ($content === false) {
            return false;
        }

        foreach ($directives as $directive) {
            if (strpos($content, $directive) !== false) {
                return true;
            }
        }

        return false;
    }
}