Файл: concrete5.7.5.6/concrete/src/Console/Command/PackPackageCommand.php
Строк: 668
<?php
namespace ConcreteCoreConsoleCommand;
use SymfonyComponentConsoleCommandCommand;
use SymfonyComponentConsoleInputInputOption;
use SymfonyComponentConsoleInputInputArgument;
use SymfonyComponentConsoleInputInputInterface;
use SymfonyComponentConsoleOutputOutputInterface;
use Core;
use Exception;
use Package;
use stdClass;
use ZipArchive;
use GettextTranslations;
final class PackPackageCommand extends Command
{
const PACKAGEFORMAT_LEGACY = 'legacy';
const PACKAGEFORMAT_CURRENT = 'current';
const SHORTTAGS_ALL = 'all';
const SHORTTAGS_KEEPECHO = 'keep-echo';
const SHORTTAGS_NO = 'no';
const KEEP_DOT = 'dot';
const KEEP_SOURCES = 'sources';
const YNA_YES = 'yes';
const YNA_AUTO = 'auto';
const YNA_NO = 'no';
const ZIPOUT_AUTO = '-';
protected function configure()
{
$zipAuto = static::ZIPOUT_AUTO;
$keepDot = static::KEEP_DOT;
$keepSources = static::KEEP_SOURCES;
$this
->setName('c5:package-pack')
->setDescription('Process a package (expand PHP short tags, compile icons and translations, create zip archive)')
->addArgument('package', InputArgument::REQUIRED, 'The handle of the package to work on (or the path to a directory containing a concrete5 package)')
->addOption('short-tags', 's', InputOption::VALUE_REQUIRED, 'Expand PHP short tags ['.static::SHORTTAGS_ALL.'|'.static::SHORTTAGS_KEEPECHO.'|'.static::SHORTTAGS_NO.']', static::SHORTTAGS_ALL)
->addOption('compile-icons', 'i', InputOption::VALUE_REQUIRED, 'Compile SVG icons to PNG icons ['.static::YNA_YES.'|'.static::YNA_NO.'|'.static::YNA_AUTO.']?', static::YNA_AUTO)
->addOption('compile-translations', 't', InputOption::VALUE_REQUIRED, 'Compile source .po translation files to the .mo binary format ['.static::YNA_YES.'|'.static::YNA_NO.'|'.static::YNA_AUTO.']?', static::YNA_AUTO)
->addOption('keep', 'k', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Which files should be stored in the zip archive ['.static::KEEP_DOT.'|'.static::KEEP_SOURCES.']')
->addOption('update-source-directory', 'u', InputOption::VALUE_NONE, 'Update the files of the source directory (otherwise it remains untouched)')
->addOption('zip', 'z', InputOption::VALUE_OPTIONAL, 'Create a zip archive of the package (and optionally set its path)', static::ZIPOUT_AUTO)
->setHelp(<<<EOT
You have to specify at least the -z option (to create a zip file) and/or the -u option (to update the package directory).
If the -u option is not specified, the package directory is not touched.
If the -z option is specified without a value (or with a value of "$zipAuto"), the zip file will be created in the directory containing the package folder.
If the -z option is set and the zip file already exists, it will be overwritten.
To include in the zip archive the files and directories starting with a dot, use the "-k $keepDot" option.
To include in the zip archive the source files for translations (.po files) and icons (.svg files) use the "-k $keepSources" option.
Please remark that this command can also parse legacy (pre-5.7) packages.
Returns codes:
0 operation completed successfully
1 errors occurred
EOT
)
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$rc = 0;
try {
$state = static::parseInput($input);
if ($state->updateSourceDirectory === false && $state->zipFilename === null) {
throw new Exception('No operation will be performed: neither a zip archive will be created nor the source directory will be updated');
}
$state->zip = null;
if ($state->zipFilename !== null) {
if (@file_exists($state->zipFilename)) {
@unlink($state->zipFilename);
if (@file_exists($state->zipFilename)) {
throw new Exception("Unable to delete the file {$state->zipFilename}");
}
}
$state->zip = new ZipArchive();
if (@$state->zip->open($state->zipFilename, ZipArchive::CREATE | ZipArchive::CHECKCONS) !== true) {
$err = @$state->zip->getStatusString();
if (!$err) {
$err = "Failed to create the file {$state->zipFilename}";
}
throw new Exception($err);
}
}
static::processDirectory($state, $output, $state->packageDirectory, '');
if ($state->zip !== null) {
$state->zip->close();
$state->zip = null;
}
if ($state->updateSourceDirectory) {
$output->writeln('Package folder has been updated');
}
if ($state->zipFilename !== null) {
$output->writeln('Zip archive created: '.$state->zipFilename);
}
} catch (Exception $x) {
if ($state->zip !== null) {
$state->zip->close();
$state->zip = null;
@unlink($state->zipFilename);
}
$output->writeln('<error>'.$x->getMessage().'</error>');
$rc = 1;
}
return $rc;
}
/**
* @param InputInterface $input
*
* @return stdClass {
*
* @var string $packageHandle The package handle
* @var string $packageDirectory The package directory
* @var string|null $packageVersion The package version (if retrieved)
* @var string|null $packageFormat The package format (if retrieved) - One of the PackPackageCommand::PACKAGEFORMAT_ constants
* @var string $shortTags One of the PackPackageCommand::SHORTTAGS_ constants
* @var string $compileIcons One of the PackPackageCommand::YNA_ constants
* @var string $compileTranslations One of the PackPackageCommand::YNA_ constants
* @var array $keep List of PackPackageCommand::KEEP_ constants
* @var bool $updateSourceDirectory
* @var string|null $zipFilename
* }
*/
protected static function parseInput(InputInterface $input)
{
$result = new stdClass();
$vsh = Core::make('helper/validation/strings');
/* @var ConcreteCoreUtilityServiceValidationStrings $vsh */
$fh = Core::make('helper/file');
/* @var ConcreteCoreFileServiceFile $fh */
$result->packageHandle = null;
$result->packageDirectory = null;
$result->packageVersion = null;
$result->packageFormat = null;
$p = $input->getArgument('package');
if (is_dir($p) || !$vsh->handle($p)) {
$result->packageDirectory = @realpath($p);
if ($result->packageDirectory === false) {
throw new Exception("Unable to find the directory '$p'");
}
$controllerFile = $result->packageDirectory.'/'.FILENAME_CONTROLLER;
if (!is_file($controllerFile)) {
throw new Exception("The directory '{$result->packageDirectory}' does not seems to contain a valid concrete5 package");
}
$controllerContents = $fh->getContents($controllerFile);
if ($controllerContents) {
$allTokens = @token_get_all($controllerContents);
if ($allTokens) {
$tokens = array_values(
array_filter(
$allTokens,
function ($token) {
$keep = true;
if (is_array($token)) {
switch ($token[0]) {
case T_DOC_COMMENT:
case T_WHITESPACE:
case T_COMMENT;
$keep = false;
break;
}
}
return $keep;
}
)
);
$packageAppVersionRequired = null;
// Look for package info
for ($i = 0; $i < count($tokens) - 2; ++$i) {
if (
$result->packageHandle === null
&& is_array($tokens[$i + 0]) && $tokens[$i + 0][0] === T_VARIABLE && $tokens[$i + 0][1] === '$pkgHandle'
&& is_string($tokens[$i + 1]) && $tokens[$i + 1] === '='
&& is_array($tokens[$i + 2]) && $tokens[$i + 2][0] === T_CONSTANT_ENCAPSED_STRING
) {
$result->packageHandle = @eval('return '.$tokens[$i + 2][1].';');
if (!is_string($result->packageHandle) || $result->packageHandle === '') {
$result->packageHandle = null;
}
}
if (
$result->packageVersion === null
&& is_array($tokens[$i + 0]) && $tokens[$i + 0][0] === T_VARIABLE && $tokens[$i + 0][1] === '$pkgVersion'
&& is_string($tokens[$i + 1]) && $tokens[$i + 1] === '='
&& is_array($tokens[$i + 2]) && $tokens[$i + 2][0] === T_CONSTANT_ENCAPSED_STRING
) {
$result->packageVersion = @eval('return '.$tokens[$i + 2][1].';');
if (!is_string($result->packageVersion) || $result->packageVersion === '') {
$result->packageVersion = null;
}
}
if (
$packageAppVersionRequired === null
&& is_array($tokens[$i + 0]) && $tokens[$i + 0][0] === T_VARIABLE && $tokens[$i + 0][1] === '$appVersionRequired'
&& is_string($tokens[$i + 1]) && $tokens[$i + 1] === '='
&& is_array($tokens[$i + 2]) && $tokens[$i + 2][0] === T_CONSTANT_ENCAPSED_STRING
) {
$packageAppVersionRequired = @eval('return '.$tokens[$i + 2][1].';');
if (!is_string($packageAppVersionRequired) || $packageAppVersionRequired === '') {
$packageAppVersionRequired = null;
}
}
}
if ($packageAppVersionRequired !== null) {
if (version_compare($packageAppVersionRequired, '5.7') < 0) {
$result->packageFormat = self::PACKAGEFORMAT_LEGACY;
} else {
$result->packageFormat = self::PACKAGEFORMAT_CURRENT;
}
}
}
}
if ($result->packageHandle === null) {
$result->packageHandle = basename($result->packageDirectory);
}
} else {
foreach (Package::getAvailablePackages(false) as $pkg) {
if (strcasecmp($p, $pkg->getPackageHandle()) === 0) {
$result->packageHandle = $pkg->getPackageHandle();
$result->packageDirectory = $pkg->getPackagePath();
$result->packageVersion = $pkg->getPackageVersion();
$result->packageFormat = self::PACKAGEFORMAT_CURRENT;
break;
}
}
if ($result->packageHandle === null) {
throw new Exception("Unable to find a package with handle '$p'");
}
}
$v = $input->getOption('short-tags');
switch ($v) {
case static::SHORTTAGS_ALL:
case static::SHORTTAGS_KEEPECHO:
case static::SHORTTAGS_NO:
$result->shortTags = $v;
break;
default:
throw new Exception('Invalid value of the --short-tags option: '.$v);
}
$v = $input->getOption('compile-icons');
switch ($v) {
case static::YNA_YES:
case static::YNA_AUTO:
case static::YNA_NO:
$result->compileIcons = $v;
break;
default:
throw new Exception('Invalid value of the --compile-icons option: '.$v);
}
$v = $input->getOption('compile-translations');
switch ($v) {
case static::YNA_YES:
case static::YNA_AUTO:
case static::YNA_NO:
$result->compileTranslations = $v;
break;
default:
throw new Exception('Invalid value of the --compile-translations option: '.$v);
}
$result->keep = array();
foreach ($input->getOption('keep') as $keep) {
if (!in_array($keep, $result->keep)) {
switch ($keep) {
case static::KEEP_DOT:
case static::KEEP_SOURCES:
$result->keep[] = $keep;
break;
default:
throw new Exception('Invalid value of the --keep option: '.$keep);
}
}
}
$result->updateSourceDirectory = (bool) $input->getOption('update-source-directory');
$result->zipFilename = null;
$zipOption = $input->getOption('zip');
if ($zipOption === static::ZIPOUT_AUTO) {
if ($input->getParameterOption(array('--zip', '-z')) === false) {
$zipOption = null;
}
}
if ($zipOption !== null) {
if ($zipOption === static::ZIPOUT_AUTO) {
$zipOption = dirname($result->packageDirectory);
}
if (is_dir($zipOption)) {
$dir = @realpath($zipOption);
if ($dir === false) {
throw new Exception('Unable to normalize the directory '.$zipOption);
}
$result->zipFilename = $dir.DIRECTORY_SEPARATOR.$result->packageHandle;
if ($result->packageVersion !== null) {
$result->zipFilename .= '-' . $result->packageVersion;
}
$result->zipFilename .= '.zip';
} else {
$result->zipFilename = $zipOption;
}
}
return $result;
}
private static function processDirectory($state, OutputInterface $output, $dirAbs, $dirRel)
{
$hDir = @opendir($dirAbs);
if ($hDir === false) {
throw new Exception("Failed to open directory $dirAbs");
}
try {
$storePrefix = $state->packageHandle;
if ($dirRel !== '') {
$storePrefix .= "/$dirRel";
}
if ($state->zip !== null) {
if (@$state->zip->addEmptyDir($storePrefix) === false) {
$err = "Failed to create directory entry $storePrefix in zip file";
$reason = $state->zip->getStatusString();
if ($reason) {
$err .= ": $reason";
}
throw new Exception($err);
}
}
$store = array();
$subDirs = array();
while (($item = readdir($hDir)) !== false) {
if (($item === '.') || ($item === '..')) {
continue;
}
if ($item[0] === '.' && !in_array(static::KEEP_DOT, $state->keep)) {
continue;
}
$itemAbs = $dirAbs.DIRECTORY_SEPARATOR.$item;
if (is_dir($itemAbs)) {
$subDirs[] = $item;
continue;
}
if (isset($store[$item])) {
continue;
}
$store[$item] = array('kind' => 'file', 'file' => $itemAbs);
$p = strrpos($item, '.');
$ext = ($p === false || $p === 0) ? '' : strtolower(substr($item, $p + 1));
switch ($ext) {
case 'php':
if ($state->shortTags !== static::SHORTTAGS_NO) {
$newContents = static::expandShortTags($itemAbs, $state->shortTags !== static::SHORTTAGS_KEEPECHO);
if ($newContents !== null) {
if ($state->updateSourceDirectory) {
if (@file_put_contents($itemAbs, $newContents) === false) {
throw new Exception("Failed to update PHP file $itemAbs");
}
} elseif ($state->zip !== null) {
$store[$item] = array('kind' => 'contents', 'contents' => $newContents);
}
$output->writeln("Expanded short tags in: $dirRel/$item");
}
}
break;
case 'pot':
if ($dirRel === 'languages') {
if ($state->zip !== null && !in_array(static::KEEP_SOURCES, $state->keep)) {
$output->writeln("Skipped source file: $dirRel/$item");
unset($store[$item]);
}
}
break;
case 'po':
if (preg_match('%^languages/[^/]+/LC_MESSAGES$%', $dirRel)) {
if ($state->zip !== null && !in_array(static::KEEP_SOURCES, $state->keep)) {
$output->writeln("Skipped source file: $dirRel/$item");
unset($store[$item]);
}
$compile = false;
$compiledAbs = substr($itemAbs, 0, -2).'mo';
switch ($state->compileTranslations) {
case static::YNA_YES:
$compile = true;
break;
case static::YNA_AUTO:
if (is_file($compiledAbs)) {
$sourceTime = @filemtime($itemAbs);
$compiledTime = @filemtime($compiledAbs);
if ($sourceTime && $compiledTime && $sourceTime > $compiledTime) {
$compile = true;
}
} else {
$compile = true;
}
break;
}
if ($compile) {
$compiledItem = substr($item, 0, -2).'mo';
$output->writeln("Compiling language : $dirRel/$item => $dirRel/$compiledItem");
$translations = Translations::fromPoFile($itemAbs);
if ($state->updateSourceDirectory) {
if ($translations->toMoFile($compiledAbs) === false) {
throw new Exception("Failed to write compiled translations file to $compiledAbs");
}
$store[$compiledItem] = array('kind' => 'file', 'file' => $compiledAbs);
} elseif ($state->zip !== null) {
$store[$compiledItem] = array('kind' => 'contents', 'contents' => $translations->toMoString());
}
}
}
break;
case 'svg':
if ($item === 'icon.svg') {
$iconSize = null;
if ($dirRel === '') {
// Package icon
$iconSize = ($state->packageFormat === static::PACKAGEFORMAT_CURRENT) ? array('width' => 200, 'height' => 200) : array('width' => 97, 'height' => 97);
} elseif (preg_match('%^blocks/[^/]+$%', $dirRel)) {
$iconSize = ($state->packageFormat === static::PACKAGEFORMAT_CURRENT) ? array('width' => 50, 'height' => 50) : array('width' => 16, 'height' => 16);
}
if ($iconSize !== null) {
if ($state->zip !== null && !in_array(static::KEEP_SOURCES, $state->keep)) {
$output->writeln("Skipped source file: $dirRel/$item");
unset($store[$item]);
}
$compile = false;
$compiledAbs = substr($itemAbs, 0, -3).'png';
switch ($state->compileIcons) {
case static::YNA_YES:
$compile = true;
break;
case static::YNA_AUTO:
if (is_file($compiledAbs)) {
$sourceTime = @filemtime($itemAbs);
$compiledTime = @filemtime($compiledAbs);
if ($sourceTime && $compiledTime && $sourceTime > $compiledTime) {
$compile = true;
}
} else {
$compile = true;
}
break;
}
if ($compile) {
$compiledItem = substr($item, 0, -3).'png';
$output->writeln("Generating png icon: $dirRel/$item => $dirRel/$compiledItem");
$tmpDir = Core::make('helper/file')->getTemporaryDirectory();
if (!is_dir($tmpDir)) {
throw new Exception('Failed to retrieve the temporary directory');
}
$tmpFile = @tempnam($tmpDir, 'c5p');
if ($tmpFile === false) {
throw new Exception('Failed to create a temporary file in directory '.$tmpDir);
}
try {
$cmd = 'inkscape';
$cmd .= ' --file=' . escapeshellarg($itemAbs);
$cmd .= ' --export-png=' . escapeshellarg($tmpFile);
$cmd .= ' --export-area-page';
$cmd .= ' --export-width='.$iconSize['width'];
$cmd .= ' --export-height='.$iconSize['height'];
$cmd .= ' 2>&1';
$execOutput = array();
@exec($cmd, $execOutput, $rc);
if (!is_int($rc)) {
$rc = -1;
}
if ($rc !== 0) {
throw new Exception(implode("n", $execOutput));
}
$pngData = @file_get_contents($tmpFile);
if ($pngData === false) {
throw new Exception('Failed to read the generated PNG file');
}
} catch (Exception $x) {
@unlink($tmpFile);
throw $x;
}
@unlink($tmpFile);
if ($pngData === '') {
throw new Exception("Inkscape failed to generate the PNG file:n".implode("n", $execOutput));
}
if ($state->updateSourceDirectory) {
if (@file_put_contents($compiledAbs, $pngData) === false) {
throw new Exception("Failed to write rendered SVN icon to $compiledAbs");
}
$store[$compiledItem] = array('kind' => 'file', 'file' => $compiledAbs);
} elseif ($state->zip !== null) {
$store[$compiledItem] = array('kind' => 'contents', 'contents' => $pngData);
}
}
}
}
break;
}
}
@closedir($hDir);
if ($state->zip !== null) {
foreach ($store as $storeItem => $storeWhat) {
$storeName = $storePrefix.'/'.$storeItem;
switch ($storeWhat['kind']) {
case 'file':
if ($state->zip->addFile($storeWhat['file'], $storeName) === false) {
$err = "Failed to store file $storeName to zip file";
$reason = $state->zip->getStatusString();
if ($reason) {
$err .= ": $reason";
}
throw new Exception($err);
}
break;
case 'contents':
if ($state->zip->addFromString($storeName, $storeWhat['contents']) === false) {
$err = "Failed to store file $storeName to zip file";
$reason = $state->zip->getStatusString();
if ($reason) {
$err .= ": $reason";
}
throw new Exception($err);
}
break;
default:
throw new Exception('?');
}
}
}
unset($store);
foreach ($subDirs as $subDir) {
static::processDirectory($state, $output, $dirAbs.DIRECTORY_SEPARATOR.$subDir, ($dirRel === '') ? $subDir : "$dirRel/$subDir");
}
} catch (Exception $x) {
@closedir($hDir);
throw $x;
}
}
/**
* @param string $file
* @param bool $shortEcho
*
* @return string|null
*/
protected static function expandShortTags($phpFile, $expandShortEcho)
{
$phpCode = @file_get_contents($phpFile);
if ($phpCode === false) {
throw new Exception("Failed to read contents of file $phpFile");
}
$result = '';
$tokens = @token_get_all($phpCode);
$numTokens = count($tokens);
$someExpanded = false;
for ($i = 0; $i < $numTokens; ++$i) {
$token = $tokens[$i];
if (is_array($token)) {
$expanded = null;
switch ($token[0]) {
case T_OPEN_TAG_WITH_ECHO:
if ($expandShortEcho) {
$expanded = '<?php echo';
}
break;
case T_OPEN_TAG:
$expanded = '<?php';
break;
}
if ($expanded === null) {
$result .= $token[1];
} else {
$someExpanded = true;
$result .= $expanded;
// Let's see if we have to add some white space after the expanded token
if (preg_match('/([ trn]+)$/', $token[1], $m)) {
$result .= $m[1];
} elseif ($i < $numTokens - 1 && (!is_array($tokens[$i + 1]) || $tokens[$i + 1][0] !== T_WHITESPACE)) {
$result .= ' ';
}
}
} else {
$result .= $token;
}
}
return ($someExpanded && $result !== $phpCode) ? $result : null;
}
}