Seditio Source
Root |
./othercms/xenForo 2.2.8/src/XF/Service/AddOn/ReleaseBuilder.php
<?php

namespace XF\Service\AddOn;

use
XF\AddOn\AddOn;
use
XF\Util\File;

use function
array_key_exists, is_array, is_scalar, strlen;

class
ReleaseBuilder extends \XF\Service\AbstractService
{
   
/**
     * @var AddOn
     */
   
protected $addOn;

    protected
$addOnRoot;
    protected
$buildRoot;
    protected
$uploadRoot;
    protected
$addOnBase;
    protected
$tempFile;

   
/**
     * @var \RecursiveIteratorIterator|\SplFileInfo[]
     */
   
protected $localFs;

   
/**
     * @var \ZipArchive
     */
   
protected $zipArchive;

    protected
$generateHashes = true;
    protected
$hashesGenerated;

    protected
$skipBuildTasks = false;
    protected
$buildTasksComplete;

    public function
__construct(\XF\App $app, AddOn $addOn)
    {
       
parent::__construct($app);

       
$this->addOn = $addOn;

       
$this->prepareDirectories();
       
$this->prepareFilesToCopy();
       
$this->prepareFsAdapters();
    }

    public function
setGenerateHashes($generate)
    {
       
$this->generateHashes = $generate;
    }

    public function
setSkipBuildTasks($skip)
    {
       
$this->skipBuildTasks = $skip;
    }

    protected function
prepareDirectories()
    {
       
$ds = \XF::$DS;

       
$addOn = $this->addOn;
       
$addOnDir = $addOn->getAddOnDirectory();
       
$buildDir = $addOn->getBuildDirectory();

       
$uploadDir = $buildDir . $ds . 'upload';
       
$addOnBase = $uploadDir . $ds . 'src' . $ds . 'addons' . $ds . $addOn->prepareAddOnIdForPath();

        if (
file_exists($buildDir))
        {
           
File::deleteDirectory($buildDir);
        }

       
File::createDirectory($buildDir, false);
       
File::createDirectory($uploadDir, false);
       
File::createDirectory($addOnBase, false);

       
$this->addOnRoot = $addOnDir;
       
$this->buildRoot = $buildDir;
       
$this->uploadRoot = $uploadDir;
       
$this->addOnBase = $addOnBase;
    }

    protected function
prepareFilesToCopy()
    {
       
$addOn = $this->addOn;
       
$addOnRoot = $this->addOnRoot;
       
$uploadRoot = $this->uploadRoot;
       
$addOnBase = $this->addOnBase;

       
$ds = \XF::$DS;

       
$exclude = [];

       
$files = File::getRecursiveDirectoryIterator($addOnRoot);
        foreach (
$files AS $file)
        {
           
$copyRoot = $addOnBase;

           
$path = File::stripRootPathPrefix($file->getPathname(), $addOnRoot);
            if (
$this->isPartOfExcludedDirectory($path))
            {
                if (
strpos($path, '_no_upload') !== 0)
                {
                    continue;
                }

               
$path = File::stripRootPathPrefix($file->getPathname(), $addOnRoot . $ds . '_no_upload');
                if (
$file->isDir() && $path == '_no_upload')
                {
                    continue;
                }

               
$copyRoot = $uploadRoot . $ds . '..'; // These need copying, but to a different path (outside upload)
           
}

            if (
$this->isExcludedFileName($file->getFilename()))
            {
               
$exclude[$file->getPathname()] = true;
                continue;
            }

            if (
array_key_exists($file->getPath(), $exclude))
            {
               
$exclude[$file->getPathname()] = true;
                continue;
            }

            if (!
$file->isDir())
            {
                if (
$path === 'build.json')
                {
                    continue;
                }

               
File::copyFile($file->getPathname(), $copyRoot . $ds . $path, false);
            }
        }

       
$rootPath = \XF::getRootDirectory();
       
$filesRoot = $addOn->getFilesDirectory();

       
$additionalFiles = $addOn->additional_files;
        foreach ((array)
$additionalFiles AS $additionalFile)
        {
           
$filePath = $filesRoot . $ds . $additionalFile;
            if (
file_exists($filePath))
            {
               
$root = $filesRoot;
            }
            else
            {
               
$filePath = $rootPath . $ds . $additionalFile;
                if (!
file_exists($filePath))
                {
                    continue;
                }
               
$root = $rootPath;
            }

            if (
is_dir($filePath))
            {
               
$files = File::getRecursiveDirectoryIterator($filePath);
                foreach (
$files AS $file)
                {
                    if (
$this->isExcludedFileName($file->getFilename()))
                    {
                       
$exclude[$file->getPathname()] = true;
                        continue;
                    }

                    if (
array_key_exists($file->getPath(), $exclude))
                    {
                       
$exclude[$file->getPathname()] = true;
                        continue;
                    }

                   
$stdPath = File::stripRootPathPrefix($file->getPathname(), $root);
                    if (!
$file->isDir())
                    {
                       
File::copyFile($file->getPathname(), $uploadRoot . $ds . $stdPath, false);
                    }
                }
            }
            else
            {
               
$stdPath = File::stripRootPathPrefix($filePath, $root);
               
File::copyFile($filePath, $uploadRoot . $ds . $stdPath, false);
            }
        }
    }

    protected function
prepareFsAdapters()
    {
       
$this->localFs = File::getRecursiveDirectoryIterator($this->buildRoot);
       
$this->tempFile = File::getTempFile();

       
$zipArchive = new \ZipArchive();
        if ((
$error = $zipArchive->open($this->tempFile, \ZipArchive::OVERWRITE)) !== true)
        {
            throw new \
RuntimeException(sprintf("Unable to create zip archive, error %d", $error));
        }
       
$this->zipArchive = $zipArchive;
    }

   
/**
     * @return array|null|string
     * @throws \ErrorException
     */
   
public function generateHashes()
    {
        if (
$this->hashesGenerated || !$this->generateHashes)
        {
            return
null;
        }

       
$ds = \XF::$DS;

       
/** @var HashGenerator $hashGenerator */
       
$hashGenerator = $this->service(
           
'XF:AddOn\HashGenerator', $this->addOn, $this->uploadRoot, $this->addOnBase . $ds . 'hashes.json'
       
);

       
$output = $hashGenerator->generate();

       
$this->hashesGenerated = true;

        return
$output;
    }

    public function
validateComposerPath()
    {
       
$addOn = $this->addOn;
       
$autoloadPath = $addOn->composer_autoload;

        if (!
$autoloadPath)
        {
            return;
        }

       
$addOnAutoload = $this->addOnBase . \XF::$DS . $autoloadPath;

        if (!
file_exists($addOnAutoload . \XF::$DS . 'installed.json'))
        {
           
File::deleteDirectory($this->buildRoot);
            throw new \
XF\PrintableException(
               
"Missing installed.json at " . \XF\Util\File::stripRootPathPrefix($addOnAutoload) . ". This may not be a valid composer directory."
           
);
        }
    }

   
/**
     * @throws \XF\PrintableException
     */
   
public function performBuildTasks()
    {
       
$addOn = $this->addOn;
       
$buildJsonPath = $addOn->getBuildJsonPath();

        if (
$this->buildTasksComplete || $this->skipBuildTasks || !file_exists($buildJsonPath))
        {
            return;
        }

        if (!
$this->testBuildJson($error))
        {
           
File::deleteDirectory($this->buildRoot);
            throw new \
XF\PrintableException('Cannot build add-on due to build.json error' . ($error ? ': ' . $error : '') . '.');
        }

       
$buildJson = $addOn->getBuildJson();

       
$this->minifyJs($buildJson['minify']);
       
$this->rollupJs($buildJson['rollup']);
       
$this->execCmds($buildJson['exec']);

       
$this->buildTasksComplete = true;
    }

    protected function
testBuildJson(&$error = null)
    {
       
$addOn = $this->addOn;

       
$baseBuildJson = @json_decode(file_get_contents($addOn->getBuildJsonPath()), true);
        if (!
is_array($baseBuildJson))
        {
           
$error = json_last_error_msg();
            return
false;
        }

        return
true;
    }

   
/**
     * @param $minify
     *
     * @throws \XF\PrintableException
     */
   
protected function minifyJs($minify)
    {
        if (!
$minify)
        {
            return;
        }

       
$uploadRoot = $this->uploadRoot;
       
$ds = \XF::$DS;

        if (!
is_array($minify) && $minify === '*')
        {
           
$minify = [];

           
$iterator = File::getRecursiveDirectoryIterator($uploadRoot . $ds . 'js');
            foreach (
$iterator AS $file)
            {
                if (
$file->isDir())
                {
                    continue;
                }

               
$fileName = $file->getBasename();

                if (
strpos($fileName, '.js') === false || strpos($fileName, '.min.js') !== false)
                {
                    continue;
                }
               
$minify[] = str_replace($uploadRoot . $ds, '', $file->getPathname());
            }
        }

        foreach (
$minify AS $file)
        {
           
/** @var JsMinifier $minifier */
           
$minifier = $this->service('XF:AddOn\JsMinifier', $uploadRoot . $ds . $file);

            try
            {
               
$minifier->minify();
            }
            catch (\
ErrorException $e)
            {
               
File::deleteDirectory($this->buildRoot);
                throw new \
XF\PrintableException('Unexpected error while minifying JS: ' . $e->getMessage());
            }
        }
    }

    protected function
rollupJs(array $rollup)
    {
        if (!
$rollup)
        {
            return;
        }

        foreach (
$rollup AS $rollupPath => $files)
        {
           
$output = '';
            foreach (
$files AS $file)
            {
               
$output .= file_get_contents($this->uploadRoot . \XF::$DS . $file);
               
$output .= "\n\n";
            }
           
File::writeFile($this->uploadRoot . \XF::$DS . $rollupPath, trim($output), false);
        }
    }

    protected function
execCmds(array $exec)
    {
        if (!
$exec)
        {
            return;
        }

       
$addOn = $this->addOn;

        foreach (
$exec AS $cmd)
        {
           
$cmd = preg_replace_callback('/({([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)})/', function($match) use ($addOn)
            {
               
$placeholder = $match[1];
               
$property = $match[2];

               
$value = $addOn->{$property};

                if (!
$value || !is_scalar($value))
                {
                    return
$placeholder;
                }

                return
escapeshellarg($value);
            },
$cmd);

           
chdir($this->addOnRoot);
           
passthru($cmd);
        }
    }

   
/**
     * @return bool
     *
     * @throws \ErrorException
     * @throws \XF\PrintableException
     */
   
public function build()
    {
       
$this->validateComposerPath();
       
$this->performBuildTasks();
       
$this->generateHashes();

       
$localFs = $this->localFs;
       
$zipArchive = $this->zipArchive;

       
// NOTE: any files skipped by generateHashes() won't appear in this loop...

       
foreach ($localFs AS $file)
        {
           
// skip hidden dot files, e.g. .DS_Store, .gitignore etc.
           
if ($this->isExcludedFileName($file->getBasename()))
            {
                continue;
            }

           
$localName = str_replace('\\', '/', substr($file->getPathname(), strlen($this->buildRoot) + 1));

            if (
$file->isDir())
            {
               
$localName .= '/';
               
$zipArchive->addEmptyDir($localName);
               
$perm = 040755 << 16; // dir: 0755
           
}
            else
            {
               
$zipArchive->addFile($file->getPathname(), $localName);
               
$perm = 0100644 << 16; // file: 0644
           
}

            if (
method_exists($zipArchive, 'setExternalAttributesName'))
            {
               
$zipArchive->setExternalAttributesName($localName, \ZipArchive::OPSYS_UNIX, $perm);
            }
        }

        if (!
$zipArchive->close())
        {
           
File::deleteDirectory($this->buildRoot);
            throw new \
ErrorException($zipArchive->getStatusString());
        }

        return
true;
    }

    public function
finalizeRelease()
    {
       
$releasePath = $this->addOn->getReleasePath();

       
File::createDirectory(dirname($releasePath), false);
       
File::renameFile($this->tempFile, $releasePath, false);

       
File::deleteDirectory($this->buildRoot);
    }

    protected function
isPartOfExcludedDirectory($path)
    {
        foreach (
$this->getExcludedDirectories() AS $dir)
        {
            if (
strpos($path, $dir) === 0)
            {
                return
true;
            }
        }
        return
false;
    }

    protected function
getExcludedDirectories()
    {
        return [
           
'_build',
           
'_files',
           
'_no_upload',
           
'_output',
           
'_releases',
           
'.git',
           
'.svn',
        ];
    }

    protected function
isExcludedFileName($fileName)
    {
        if (
$fileName === '' || $fileName === false || $fileName === null)
        {
            return
true;
        }

        if (
$fileName[0] == '.' && $fileName != '.htaccess')
        {
            return
true;
        }

        return
false;
    }
}