<?php
namespace XF\Cli\Command;
use Symfony\Component\Console\Helper\ProcessHelper;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\Process;
use XF\AddOn\AddOn;
use XF\AddOn\StepResult;
use XF\Phrase;
use function count;
trait AddOnActionTrait
{
protected function verifyAddOnAction(InputInterface $input, OutputInterface $output, AddOn $addOn)
{
if (!$input->getOption('force'))
{
$addOn->checkRequirements($errors, $warnings);
$addOn->passesHealthCheck($missing, $inconsistent);
if ($missing)
{
if (count($missing) > 5)
{
$errors[] = \XF::phrase('this_add_on_cannot_be_installed_because_x_files_are_missing', [
'missing' => count($missing)
]);
}
else
{
$errors[] = \XF::phrase('this_add_on_cannot_be_installed_because_following_files_are_missing_x', [
'missing' => implode(', ', $missing)
]);
}
}
if ($inconsistent)
{
if (count($inconsistent) > 5)
{
$warnings[] = \XF::phrase('this_add_on_contains_x_files_which_have_unexpected_contents', [
'inconsistent' => count($inconsistent)
]);
}
else
{
$warnings[] = \XF::phrase('this_add_on_contains_following_files_which_have_unexpected_contents_x', [
'inconsistent' => implode(', ', $inconsistent)
]);
}
}
if ($errors || $warnings)
{
if ($errors)
{
$output->writeln('<error>' . \XF::phrase('following_errors_must_be_resolved_before_continuing') . ':</error>');
foreach ($errors AS $error)
{
$output->writeln("<info>\t * " . (string)$error . '</info>');
}
}
if ($warnings)
{
$output->writeln('<warning>' . \XF::phrase('following_errors_must_be_resolved_before_continuing') . ':</warning>');
foreach ($warnings AS $warning)
{
$output->writeln("<info>\t * " . (string)$warning . '</info>');
}
if (!$errors)
{
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
$question = new ConfirmationQuestion("<question>" . \XF::phrase('i_have_reviewed_warnings_and_i_still_wish_to_proceed') . " (y/n)</question>");
if ($helper->ask($input, $output, $question))
{
return true;
}
}
}
return false;
}
}
return true;
}
protected function performAddOnAction(InputInterface $input, OutputInterface $output, AddOn $addOn, Phrase $actionPhrase, $methodName)
{
$setup = $addOn->getSetup();
$params = [];
$count = 0;
$output->write($actionPhrase->render());
if ($setup)
{
$setup->prepareForAction($methodName);
while (($result = $setup->$methodName($params)) !== null)
{
/** @var StepResult $result */
if (!$result)
{
$finished = true;
$params = [];
}
else
{
$count++;
$finished = false;
$params = $result->params;
$params['step'] = $result->step;
if ($result->version)
{
$params['version_id'] = $result->version;
}
$output->write(' .');
}
if ($finished)
{
$result = null;
}
}
}
$output->writeln(["", "Complete."]);
}
protected function importAddOnData(InputInterface $input, OutputInterface $output, AddOn $addOn)
{
$app = \XF::app();
$devOutput = $app->developmentOutput();
if ($devOutput->isAddOnOutputAvailable($addOn->addon_id))
{
$command = $this->getApplication()->find('xf-dev:import');
$childInput = new ArrayInput([
'command' => 'xf-dev:import',
'--addon' => $addOn->addon_id
]);
$command->run($childInput, $output);
$addOn->postDataImport();
}
else
{
$output->writeln(["", "Importing add-on data"]);
$this->setupAndRunJob(
'xfAddOnData-' . $addOn->addon_id,
'XF:AddOnData',
['addon_id' => $addOn->addon_id],
$output
);
// the job will trigger the post data import
}
}
public function checkInstalledAddOn($id, &$error = null)
{
$addOnManager = \XF::app()->addOnManager();
$addOn = $addOnManager->getById($id);
if (!$addOn || !$addOn->isInstalled())
{
$error = "No add-on with ID '$id' could be found.";
return null;
}
return $addOn;
}
public function checkEditableAddOn($id, &$error = null)
{
$addOn = $this->checkInstalledAddOn($id, $error);
if (!$addOn)
{
return null;
}
if (!$addOn->canEdit())
{
$error = "The add-on '$id' is not editable.";
return null;
}
return $addOn;
}
public function runSubAction(OutputInterface $output, AddOn $addOn, $action)
{
$execFinder = new PhpExecutableFinder();
$builderOptions = [
$execFinder->find(false),
\XF::getRootDirectory() . \XF::$DS .'cmd.php',
'xf:addon-sub-action',
$addOn->addon_id,
$action,
'--k=' . $this->getSubActionKey($addOn->addon_id, $action)
];
if ($verbosityOption = $this->getVerbosityOption($output->getVerbosity()))
{
$builderOptions[] = $verbosityOption;
}
$process = new Process($builderOptions);
$process->setTimeout(null);
/** @var ProcessHelper $processHelper */
$processHelper = $this->getHelper('process');
try
{
$processHelper->mustRun($output, $process, null, function($type, $data) use ($output)
{
if ($type == Process::OUT)
{
$output->write($data);
}
// Note that progress bar output is in Process::ERR/stderr, but they get streamed to this callback
// interleaved, so displaying both is difficult. Thus, we need to only display stuff sent stdout.
});
}
catch (ProcessFailedException $e)
{
$process = $e->getProcess();
if ($process->getExitCode() !== 0)
{
// This indicates that the sub-process threw an exception. It will have been printed and logged
// so don't trigger the normal exception handling. However, we can't continue so exit.
exit(1);
}
}
}
protected function getVerbosityOption($verbosity)
{
switch ($verbosity)
{
case OutputInterface::VERBOSITY_QUIET:
return '-q';
case OutputInterface::VERBOSITY_VERBOSE:
return '-v';
case OutputInterface::VERBOSITY_VERY_VERBOSE:
return '-vv';
case OutputInterface::VERBOSITY_DEBUG:
return '-vvv';
default:
return '';
}
}
public function getSubActionKey($addOnId, $action)
{
return hash_hmac('sha1', "$addOnId-$action", \XF::config('globalSalt'));
}
public function isAZipFile($pathToZip)
{
if (strtolower(pathinfo($pathToZip, PATHINFO_EXTENSION)) == 'zip'
&& file_exists($pathToZip) && is_readable($pathToZip)
)
{
return true;
}
return false;
}
public function validateAndExtractAddOnZip($zipFile, &$error = null)
{
/** @var \XF\Repository\AddOn $addOnRepo */
$addOnRepo = \XF::repository('XF:AddOn');
if (!$addOnRepo->canInstallFromArchive($error, true))
{
return null;
}
/** @var \XF\Service\AddOnArchive\Validator $validator */
$validator = \XF::service('XF:AddOnArchive\Validator', $zipFile);
if (!$validator->validate($error))
{
return null;
}
$addOnId = $validator->getAddOnId();
/** @var \XF\Service\AddOnArchive\Extractor $extractor */
$extractor = \XF::service('XF:AddOnArchive\Extractor', $addOnId, $zipFile);
$result = $extractor->copyFiles();
switch ($result['status'])
{
case 'error':
$error = $result['error'];
return null;
case 'complete':
\XF\Util\Php::resetOpcache();
break;
default:
throw new \LogicException("Unknown result when copying add-on files '$result[status]'");
}
return $addOnId;
}
}