Seditio Source
Root |
./othercms/xenForo 2.2.8/src/XF/Service/Style/ArchiveValidator.php
<?php

namespace XF\Service\Style;

use
XF\Service\AbstractService;
use
XF\Util\File;

use function
count, in_array;

class
ArchiveValidator extends AbstractService
{
    protected
$filePath;
    protected
$archiveAction;

   
/**
     * @var array|null
     */
   
protected $knownFileHashes;

   
// all paths in lower case
   
const DIR_BLACKLIST = [
       
'data',
       
'install',
       
'internal_data',
       
'library',
       
'src',

       
'js/vendor',
       
'js/xf',
       
'js/xf*',
       
'styles/default/xenforo',
       
'styles/default/xf*',
       
'styles/fonts/fa'
   
];

   
// all extensions in lower case
   
const EXTENSION_WHITELIST = [
       
'avi', 'eot', 'gif',
       
'html', 'ico', 'jpg',
       
'jpeg', 'jpe', 'js',
       
'json', 'map', 'md',
       
'mov', 'mp4', 'png',
       
'svg', 'ttf', 'txt',
       
'woff', 'woff2', 'zip'
   
];

    public function
__construct(\XF\App $app, string $filePath, string $archiveAction)
    {
       
parent::__construct($app);

        if (!
file_exists($filePath) || !is_dir($filePath))
        {
            throw new \
InvalidArgumentException("Invalid file path passed in ($filePath)");
        }

       
$this->filePath = $filePath;

        switch (
$archiveAction)
        {
            case
'import':
            case
'export':
                break;
            default:
                throw new \
InvalidArgumentException('Only archive actions of \'import\' or \'export\' are valid.');
        }
       
$this->archiveAction = $archiveAction;
    }

    public function
validate(&$errors = []): bool
   
{
       
$errors = [];

       
$rootPath = $this->filePath;

       
$files = File::getRecursiveDirectoryIterator($rootPath);
        foreach (
$files AS $file)
        {
            if (
$file->isDir())
            {
                continue;
            }

           
$pathname = $file->getPathname();
           
$basename = $file->getBasename();

            if (
$basename === '' || $basename === false || $basename === null)
            {
                continue;
            }

            if (
$basename == '.DS_Store')
            {
               
// skip this but no error
               
continue;
            }

            if (
$basename[0] == '.' && $basename != '.htaccess')
            {
                if (!isset(
$errors['dot_file']))
                {
                   
$errors['dot_file'] = \XF::phrase('one_or_more_files_in_style_archive_disallowed_dot_files');
                }
                continue;
            }

           
$stdPath = File::stripRootPathPrefix($pathname, $rootPath);
           
$stdPath = $this->standardizePathForValidation($stdPath);

            if (
$this->isFileInRootDirectory($stdPath))
            {
                if (!isset(
$errors['root_dir']))
                {
                   
$errors['root_dir'] = \XF::phrase('one_or_more_files_in_style_archive_contained_within_root');
                }
                continue;
            }

            if (
$this->isFileInBlacklistedDirectory($stdPath))
            {
                if (!isset(
$errors['blacklisted_dir']))
                {
                   
$errors['blacklisted_dir'] = \XF::phrase('one_or_more_files_in_style_archive_within_unsupported_directory_x', ['disallowed' => implode(', ', self::DIR_BLACKLIST)]);
                }
                continue;
            }

           
$extension = $file->getExtension();
            if (!
$this->isFileWithWhitelistedExtension($extension))
            {
                if (!isset(
$errors['whitelisted_ext']))
                {
                   
$errors['whitelisted_ext'] = \XF::phrase('one_or_more_files_do_not_have_allowed_extension_x', ['allowed' => implode(', ', self::EXTENSION_WHITELIST)]);
                }
                continue;
            }

            if (
$this->isCoreFile($stdPath))
            {
                if (!isset(
$errors['core_file']))
                {
                   
$errors['core_file'] = \XF::phrase('one_or_more_files_not_permitted_as_they_belong_to_xenforo_or_add_on');
                }
                continue;
            }
        }

        return
count($errors) == 0;
    }

    protected function
standardizePathForValidation(string $path): string
   
{
       
// standardize on forward slashes for paths only
       
$path = str_replace('\\', '/', $path);

       
// avoid case sensitivity issues (as FS settings may vary)
       
$path = strtolower($path);

        return
$path;
    }

    protected function
isFileInRootDirectory(string $path): bool
   
{
        return
strpos($path, '/') === false;
    }

    protected function
isFileInBlacklistedDirectory(string $path): bool
   
{
        foreach (
self::DIR_BLACKLIST AS $dir)
        {
           
$suffix = '/';
            if (
substr($dir, -1) === '*')
            {
               
$suffix = '';
               
$dir = rtrim($dir, '*');
            }

            if (
strpos($path, $dir . $suffix) === 0)
            {
                return
true;
            }
        }

        return
false;
    }

    protected function
isFileWithWhitelistedExtension(string $extension): bool
   
{
        return (
           
$extension === ''
           
|| in_array(strtolower($extension), self::EXTENSION_WHITELIST)
        );
    }

    protected function
isCoreFile(string $path): bool
   
{
        switch (
$path)
        {
            case
'.htaccess':
            case
'htaccess.txt':
               
// core files that shouldn't be overwritten, as people may change them manually
               
return true;
        }

       
$hashes = $this->getKnownFileHashes();
        return isset(
$hashes[$path]);
    }

    protected function
getKnownFileHashes()
    {
        if (
$this->knownFileHashes === null)
        {
           
$jsonPath = \XF::getAddOnDirectory() . \XF::$DS . 'XF' . \XF::$DS . 'hashes.json';
            if (
file_exists($jsonPath))
            {
               
$hashes = json_decode(file_get_contents($jsonPath), true);
            }
            else
            {
               
$hashes = [];
            }

           
$addOns = $this->app->addOnManager()->getAllAddOns();
            foreach (
$addOns AS $addOn)
            {
               
$path = $addOn->getHashesPath();
                if (
$path && file_exists($path))
                {
                   
$decodedHashes = json_decode(file_get_contents($path), true) ?? [];
                   
$hashes += $decodedHashes;
                }
            }

           
$this->knownFileHashes = $hashes;
        }

        return
$this->knownFileHashes;
    }
}