diff --git a/PHPCI/Application.php b/PHPCI/Application.php index 77bd7170..3d889967 100644 --- a/PHPCI/Application.php +++ b/PHPCI/Application.php @@ -10,6 +10,7 @@ namespace PHPCI; use b8; +use b8\Http\Response; use b8\Http\Response\RedirectResponse; use b8\View; @@ -19,52 +20,37 @@ use b8\View; */ class Application extends b8\Application { + public function init() + { + $request =& $this->request; + $route = '/:controller/:action'; + $opts = array('controller' => 'Home', 'action' => 'index'); + + $this->router->clearRoutes(); + $this->router->register($route, $opts, function (&$route, Response &$response) use (&$request) { + $skipValidation = in_array($route['controller'], array('session', 'webhook', 'build-status')); + + if (!$skipValidation && !$this->validateSession()) { + if ($request->isAjax()) { + $response->setResponseCode(401); + $response->setContent(''); + } else { + $response = new RedirectResponse($response); + $response->setHeader('Location', PHPCI_URL.'session/login'); + } + + return false; + } + + return true; + }); + } /** * Handle an incoming web request. */ public function handleRequest() { - try { - $this->initRequest(); - - // Validate the user's session unless it is a login/logout action or a web hook: - $sessionAction = ($this->controllerName == 'Session' && in_array($this->action, array('login', 'logout'))); - $externalAction = in_array($this->controllerName, array('Bitbucket', 'Github', 'Gitlab', 'BuildStatus', 'Git')); - $skipValidation = ($externalAction || $sessionAction); - - if ($skipValidation || $this->validateSession()) { - parent::handleRequest(); - } - } catch (\Exception $ex) { - $content = '
Please paste the details below into a - new bug report - so that we can investigate and fix it.
'; - - ob_start(); - var_dump(array( - 'message' => $ex->getMessage(), - 'file' => $ex->getFile(), - 'line' => $ex->getLine(), - 'trace' => $ex->getTraceAsString() - )); - var_dump(array( - 'PATH_INFO' => $_SERVER['PATH_INFO'], - 'REDIRECT_PATH_INFO' => $_SERVER['REDIRECT_PATH_INFO'], - 'REQUEST_URI' => $_SERVER['REQUEST_URI'], - 'PHP_SELF' => $_SERVER['PHP_SELF'], - 'SCRIPT_NAME' => $_SERVER['SCRIPT_NAME'], - 'DOCUMENT_ROOT' => $_SERVER['DOCUMENT_ROOT'], - 'SCRIPT_FILENAME' => $_SERVER['SCRIPT_FILENAME'], - 'SERVER_SOFTWARE' => $_SERVER['SERVER_SOFTWARE'], - )); - $content .= ob_get_contents(); - ob_end_clean(); - - $this->response->setContent($content); - $this->response->disableLayout(); - } - + $this->response = parent::handleRequest(); if (View::exists('layout') && $this->response->hasLayout()) { $view = new View('layout'); @@ -91,14 +77,6 @@ class Application extends b8\Application unset($_SESSION['user_id']); } - if ($this->request->isAjax()) { - $this->response->setResponseCode(401); - $this->response->setContent(''); - } else { - $this->response = new RedirectResponse($this->response); - $this->response->setHeader('Location', PHPCI_URL.'session/login'); - } - return false; } } diff --git a/PHPCI/BuildFactory.php b/PHPCI/BuildFactory.php index 0dd53058..82ebc221 100644 --- a/PHPCI/BuildFactory.php +++ b/PHPCI/BuildFactory.php @@ -9,10 +9,8 @@ namespace PHPCI; +use b8\Store\Factory; use PHPCI\Model\Build; -use PHPCI\Model\Build\LocalBuild; -use PHPCI\Model\Build\GithubBuild; -use PHPCI\Model\Build\BitbucketBuild; /** * PHPCI Build Factory - Takes in a generic "Build" and returns a type-specific build model. @@ -20,9 +18,21 @@ use PHPCI\Model\Build\BitbucketBuild; */ class BuildFactory { + /** + * @param $buildId + * @return Build + */ + public static function getBuildById($buildId) + { + $build = Factory::getStore('Build')->getById($buildId); + + return self::getBuild($build); + } + /** * Takes a generic build and returns a type-specific build model. - * @return \PHPCI\Model\Build\LocalBuild|\PHPCI\Model\Build\GithubBuild|\PHPCI\Model\Build\BitbucketBuild + * @param Build $base The build from which to get a more specific build type. + * @return Build */ public static function getBuild(Build $base) { diff --git a/PHPCI/BuildLogger.php b/PHPCI/BuildLogger.php deleted file mode 100644 index ccb6288a..00000000 --- a/PHPCI/BuildLogger.php +++ /dev/null @@ -1,31 +0,0 @@ - */ -class Builder implements LoggerAwareInterface, BuildLogger +class Builder implements LoggerAwareInterface { /** * @var string @@ -76,12 +78,9 @@ class Builder implements LoggerAwareInterface, BuildLogger protected $lastOutput; /** - * An array of key => value pairs that will be used for - * interpolation and environment variables - * @var array - * @see setInterpolationVars() + * @var BuildInterpolator */ - protected $interpolation_vars = array(); + protected $interpolator; /** * @var \PHPCI\Store\BuildStore @@ -103,24 +102,36 @@ class Builder implements LoggerAwareInterface, BuildLogger */ protected $commandExecutor; + /** + * @var Logging\BuildLogger + */ + protected $buildLogger; + /** * Set up the builder. * @param \PHPCI\Model\Build $build * @param LoggerInterface $logger */ - public function __construct(Build $build, $logger = null) + public function __construct(Build $build, LoggerInterface $logger = null) { - if ($logger) { - $this->setLogger($logger); - } $this->build = $build; - $this->store = Store\Factory::getStore('Build'); + $this->store = Factory::getStore('Build'); + + $this->buildLogger = new BuildLogger($logger, $build); $pluginFactory = $this->buildPluginFactory($build); $pluginFactory->addConfigFromFile(PHPCI_DIR . "/pluginconfig.php"); $this->pluginExecutor = new Plugin\Util\Executor($pluginFactory, $this); - $this->commandExecutor = new CommandExecutor($this, PHPCI_DIR, $this->quiet, $this->verbose); + $this->commandExecutor = new CommandExecutor( + $this->buildLogger, + PHPCI_DIR, + $this->quiet, + $this->verbose + ); + + $this->interpolator = new BuildInterpolator(); + } /** @@ -189,8 +200,7 @@ class Builder implements LoggerAwareInterface, BuildLogger // stages. if ($this->success) { $this->build->setStatus(Build::STATUS_SUCCESS); - } - else { + } else { $this->build->setStatus(Build::STATUS_FAILED); } @@ -199,15 +209,14 @@ class Builder implements LoggerAwareInterface, BuildLogger if ($this->success) { $this->pluginExecutor->executePlugins($this->config, 'success'); - $this->logSuccess('BUILD SUCCESSFUL!'); - } - else { + $this->buildLogger->logSuccess('BUILD SUCCESSFUL!'); + } else { $this->pluginExecutor->executePlugins($this->config, 'failure'); - $this->logFailure("BUILD FAILURE"); + $this->buildLogger->logFailure("BUILD FAILURE"); } // Clean up: - $this->log('Removing build.'); + $this->buildLogger->log('Removing build.'); shell_exec(sprintf('rm -Rf "%s"', $this->buildPath)); // Update the build in the database, ping any external services, etc. @@ -243,107 +252,14 @@ class Builder implements LoggerAwareInterface, BuildLogger } /** - * Add an entry to the build log. - * @param string|string[] $message - * @param string $level - * @param mixed[] $context - */ - public function log($message, $level = LogLevel::INFO, $context = array()) - { - // Skip if no logger has been loaded. - if (!$this->logger) { - return; - } - - if (!is_array($message)) { - $message = array($message); - } - - // The build is added to the context so the logger can use - // details from it if required. - $context['build'] = $this->build; - - foreach ($message as $item) { - $this->logger->log($level, $item, $context); - } - } - - /** - * Add a success-coloured message to the log. - * @param string - */ - public function logSuccess($message) - { - $this->log("\033[0;32m" . $message . "\033[0m"); - } - - /** - * Add a failure-coloured message to the log. - * @param string $message - * @param \Exception $exception The exception that caused the error. - */ - public function logFailure($message, \Exception $exception = null) - { - $context = array(); - - // The psr3 log interface stipulates that exceptions should be passed - // as the exception key in the context array. - if ($exception) { - $context['exception'] = $exception; - } - - $this->log( - "\033[0;31m" . $message . "\033[0m", - LogLevel::ERROR, - $context - ); - } - - /** - * Replace every occurance of the interpolation vars in the given string + * Replace every occurrence of the interpolation vars in the given string * Example: "This is build %PHPCI_BUILD%" => "This is build 182" * @param string $input * @return string */ public function interpolate($input) { - $keys = array_keys($this->interpolation_vars); - $values = array_values($this->interpolation_vars); - return str_replace($keys, $values, $input); - } - - /** - * Sets the variables that will be used for interpolation. This must be run - * from setupBuild() because prior to that, we don't know the buildPath - */ - protected function setInterpolationVars() - { - $this->interpolation_vars = array(); - $this->interpolation_vars['%PHPCI%'] = 1; - $this->interpolation_vars['%COMMIT%'] = $this->build->getCommitId(); - $this->interpolation_vars['%PROJECT%'] = $this->build->getProjectId(); - $this->interpolation_vars['%BUILD%'] = $this->build->getId(); - $this->interpolation_vars['%PROJECT_TITLE%'] = $this->getBuildProjectTitle( - ); - $this->interpolation_vars['%BUILD_PATH%'] = $this->buildPath; - $this->interpolation_vars['%BUILD_URI%'] = PHPCI_URL . "build/view/" . $this->build->getId( - ); - $this->interpolation_vars['%PHPCI_COMMIT%'] = $this->interpolation_vars['%COMMIT%']; - $this->interpolation_vars['%PHPCI_PROJECT%'] = $this->interpolation_vars['%PROJECT%']; - $this->interpolation_vars['%PHPCI_BUILD%'] = $this->interpolation_vars['%BUILD%']; - $this->interpolation_vars['%PHPCI_PROJECT_TITLE%'] = $this->interpolation_vars['%PROJECT_TITLE%']; - $this->interpolation_vars['%PHPCI_BUILD_PATH%'] = $this->interpolation_vars['%BUILD_PATH%']; - $this->interpolation_vars['%PHPCI_BUILD_URI%'] = $this->interpolation_vars['%BUILD_URI%']; - - putenv('PHPCI=1'); - putenv('PHPCI_COMMIT=' . $this->interpolation_vars['%COMMIT%']); - putenv('PHPCI_PROJECT=' . $this->interpolation_vars['%PROJECT%']); - putenv('PHPCI_BUILD=' . $this->interpolation_vars['%BUILD%']); - putenv( - 'PHPCI_PROJECT_TITLE=' . $this->interpolation_vars['%PROJECT_TITLE%'] - ); - putenv('PHPCI_BUILD_PATH=' . $this->interpolation_vars['%BUILD_PATH%']); - putenv('PHPCI_BUILD_URI=' . $this->interpolation_vars['%BUILD_URI%']); + return $this->interpolator->interpolate($input); } /** @@ -351,12 +267,17 @@ class Builder implements LoggerAwareInterface, BuildLogger */ protected function setupBuild() { - $buildId = 'project' . $this->build->getProject()->getId( - ) . '-build' . $this->build->getId(); - $this->ciDir = dirname(__FILE__) . '/../'; + $buildId = 'project' . $this->build->getProject()->getId() + . '-build' . $this->build->getId(); + $this->ciDir = dirname(dirname(__FILE__) . '/../') . '/'; $this->buildPath = $this->ciDir . 'build/' . $buildId . '/'; + $this->build->currentBuildPath = $this->buildPath; - $this->setInterpolationVars(); + $this->interpolator->setupInterpolationVars( + $this->build, + $this->buildPath, + PHPCI_URL + ); // Create a working copy of the project: if (!$this->build->createWorkingCopy($this, $this->buildPath)) { @@ -373,7 +294,7 @@ class Builder implements LoggerAwareInterface, BuildLogger $this->ignore = $this->config['build_settings']['ignore']; } - $this->logSuccess('Working copy created: ' . $this->buildPath); + $this->buildLogger->logSuccess('Working copy created: ' . $this->buildPath); return true; } @@ -385,32 +306,45 @@ class Builder implements LoggerAwareInterface, BuildLogger */ public function setLogger(LoggerInterface $logger) { - $this->logger = $logger; + $this->buildLogger->setLogger($logger); + } + + public function log($message, $level = LogLevel::INFO, $context = array()) + { + $this->buildLogger->log($message, $level, $context); + } + + /** + * Add a success-coloured message to the log. + * @param string + */ + public function logSuccess($message) + { + $this->buildLogger->logSuccess($message); } /** - * returns the logger attached to this builder. - * - * @return LoggerInterface + * Add a failure-coloured message to the log. + * @param string $message + * @param \Exception $exception The exception that caused the error. */ - public function getLogger() + public function logFailure($message, \Exception $exception = null) { - return $this->logger; + $this->buildLogger->logFailure($message, $exception); } - /** * Returns a configured instance of the plugin factory. * * @param Build $build - * @return Factory + * @return PluginFactory */ private function buildPluginFactory(Build $build) { - $pluginFactory = new Factory(); + $pluginFactory = new PluginFactory(); $self = $this; $pluginFactory->registerResource( - function () use($self) { + function () use ($self) { return $self; }, null, @@ -418,7 +352,7 @@ class Builder implements LoggerAwareInterface, BuildLogger ); $pluginFactory->registerResource( - function () use($build) { + function () use ($build) { return $build; }, null, diff --git a/PHPCI/Command/CreateAdminCommand.php b/PHPCI/Command/CreateAdminCommand.php new file mode 100644 index 00000000..647dffca --- /dev/null +++ b/PHPCI/Command/CreateAdminCommand.php @@ -0,0 +1,130 @@ +setName('phpci:create-admin') + ->setDescription('Create an admin user'); + } + + /** + * Creates an admin user in the existing PHPCI database + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + + require(PHPCI_DIR . 'bootstrap.php'); + + // Try to create a user account: + $adminEmail = $this->ask('Admin email address: ', true, FILTER_VALIDATE_EMAIL); + + if (empty($adminEmail)) { + return; + } + + $adminPass = $this->ask('Admin password: '); + $adminName = $this->ask('Admin name: '); + + try { + $user = new \PHPCI\Model\User(); + $user->setEmail($adminEmail); + $user->setName($adminName); + $user->setIsAdmin(1); + $user->setHash(password_hash($adminPass, PASSWORD_DEFAULT)); + + $store = \b8\Store\Factory::getStore('User'); + $store->save($user); + + print 'User account created!' . PHP_EOL; + } catch (\Exception $ex) { + print 'There was a problem creating your account. :(' . PHP_EOL; + print $ex->getMessage(); + print PHP_EOL; + } + } + + protected function ask($question, $emptyOk = false, $validationFilter = null) + { + print $question . ' '; + + $rtn = ''; + $stdin = fopen('php://stdin', 'r'); + $rtn = fgets($stdin); + fclose($stdin); + + $rtn = trim($rtn); + + if (!$emptyOk && empty($rtn)) { + $rtn = $this->ask($question, $emptyOk, $validationFilter); + } elseif ($validationFilter != null && ! empty($rtn)) { + if (! $this -> controlFormat($rtn, $validationFilter, $statusMessage)) { + print $statusMessage; + $rtn = $this->ask($question, $emptyOk, $validationFilter); + } + } + + return $rtn; + } + protected function controlFormat($valueToInspect, $filter, &$statusMessage) + { + $filters = !(is_array($filter))? array($filter) : $filter; + $statusMessage = ''; + $status = true; + $options = array(); + + foreach ($filters as $filter) { + if (! is_int($filter)) { + $regexp = $filter; + $filter = FILTER_VALIDATE_REGEXP; + $options = array( + 'options' => array( + 'regexp' => $regexp, + ) + ); + } + if (! filter_var($valueToInspect, $filter, $options)) { + $status = false; + + switch ($filter) + { + case FILTER_VALIDATE_URL: + $statusMessage = 'Incorrect url format.' . PHP_EOL; + break; + case FILTER_VALIDATE_EMAIL: + $statusMessage = 'Incorrect e-mail format.' . PHP_EOL; + break; + case FILTER_VALIDATE_REGEXP: + $statusMessage = 'Incorrect format.' . PHP_EOL; + break; + } + } + } + + return $status; + } +} diff --git a/PHPCI/Command/DaemoniseCommand.php b/PHPCI/Command/DaemoniseCommand.php index 3c490d75..f8cbc45b 100644 --- a/PHPCI/Command/DaemoniseCommand.php +++ b/PHPCI/Command/DaemoniseCommand.php @@ -10,14 +10,13 @@ namespace PHPCI\Command; +use Monolog\Logger; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use b8\Store\Factory; -use PHPCI\Builder; -use PHPCI\BuildFactory; /** * Daemon that loops and call the run-command. @@ -27,6 +26,36 @@ use PHPCI\BuildFactory; */ class DaemoniseCommand extends Command { + /** + * @var Logger + */ + protected $logger; + + /** + * @var OutputInterface + */ + protected $output; + + /** + * @var boolean + */ + protected $run; + + /** + * @var int + */ + protected $sleep; + + /** + * @param \Monolog\Logger $logger + * @param string $name + */ + public function __construct(Logger $logger, $name = null) + { + parent::__construct($name); + $this->logger = $logger; + } + protected function configure() { $this @@ -43,14 +72,19 @@ class DaemoniseCommand extends Command $command = sprintf($cmd, getmypid(), PHPCI_DIR); exec($command); + $this->output = $output; $this->run = true; $this->sleep = 0; - $runner = new RunCommand; + $runner = new RunCommand($this->logger); + + $emptyInput = new ArgvInput(array()); while ($this->run) { + $buildCount = 0; + try { - $buildCount = $runner->execute($input, $output); + $buildCount = $runner->run($emptyInput, $output); } catch (\Exception $e) { var_dump($e); } diff --git a/PHPCI/Command/GenerateCommand.php b/PHPCI/Command/GenerateCommand.php index 3a97f70c..44a499de 100644 --- a/PHPCI/Command/GenerateCommand.php +++ b/PHPCI/Command/GenerateCommand.php @@ -37,7 +37,7 @@ class GenerateCommand extends Command */ protected function execute(InputInterface $input, OutputInterface $output) { - $gen = new CodeGenerator(Database::getConnection(), 'PHPCI', PHPCI_DIR . '/PHPCI/', false); + $gen = new CodeGenerator(Database::getConnection(), ['default' => 'PHPCI'], ['default' => PHPCI_DIR], false); $gen->generateModels(); $gen->generateStores(); } diff --git a/PHPCI/Command/PollCommand.php b/PHPCI/Command/PollCommand.php index 8bbe83cf..503e83a8 100644 --- a/PHPCI/Command/PollCommand.php +++ b/PHPCI/Command/PollCommand.php @@ -80,7 +80,9 @@ class PollCommand extends Command $this->logger->info("Last commit to github for " . $project->getTitle() . " is " . $last_commit); if ($project->getLastCommit() != $last_commit && $last_commit != "") { - $this->logger->info("Last commit is different from database, adding new build for " . $project->getTitle()); + $this->logger->info( + "Last commit is different from database, adding new build for " . $project->getTitle() + ); $build = new Build(); $build->setProjectId($project->getId()); @@ -101,4 +103,3 @@ class PollCommand extends Command $this->logger->addInfo("Finished processing builds"); } } - diff --git a/PHPCI/Command/RunCommand.php b/PHPCI/Command/RunCommand.php index d635b8a2..318f9247 100644 --- a/PHPCI/Command/RunCommand.php +++ b/PHPCI/Command/RunCommand.php @@ -10,9 +10,9 @@ namespace PHPCI\Command; use Monolog\Logger; -use PHPCI\Helper\BuildDBLogHandler; -use PHPCI\Helper\LoggedBuildContextTidier; -use PHPCI\Helper\OutputLogHandler; +use PHPCI\Logging\BuildDBLogHandler; +use PHPCI\Logging\LoggedBuildContextTidier; +use PHPCI\Logging\OutputLogHandler; use Psr\Log\LoggerAwareInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -22,6 +22,7 @@ use Symfony\Component\Console\Output\OutputInterface; use b8\Store\Factory; use PHPCI\Builder; use PHPCI\BuildFactory; +use PHPCI\Model\Build; /** * Run console command - Runs any pending builds. @@ -88,17 +89,24 @@ class RunCommand extends Command $build = BuildFactory::getBuild($build); - // Logging relevant to this build should be stored - // against the build itself. - $buildDbLog = new BuildDBLogHandler($build, Logger::INFO); - $this->logger->pushHandler($buildDbLog); + try { + // Logging relevant to this build should be stored + // against the build itself. + $buildDbLog = new BuildDBLogHandler($build, Logger::INFO); + $this->logger->pushHandler($buildDbLog); - $builder = new Builder($build, $this->logger); - $builder->execute(); + $builder = new Builder($build, $this->logger); + $builder->execute(); + + // After execution we no longer want to record the information + // back to this specific build so the handler should be removed. + $this->logger->popHandler($buildDbLog); + } catch (\Exception $ex) { + $build->setStatus(Build::STATUS_FAILED); + $build->setLog($build->getLog() . PHP_EOL . PHP_EOL . $ex->getMessage()); + $store->save($build); + } - // After execution we no longer want to record the information - // back to this specific build so the handler should be removed. - $this->logger->popHandler($buildDbLog); } $this->logger->addInfo("Finished processing builds"); diff --git a/PHPCI/Controller.php b/PHPCI/Controller.php index d95c24e0..442d9248 100644 --- a/PHPCI/Controller.php +++ b/PHPCI/Controller.php @@ -38,7 +38,7 @@ class Controller extends \b8\Controller if (View::exists($this->className)) { $this->controllerView = new View($this->className); } else { - $this->controllerView = new View\UserView('{@content}'); + $this->controllerView = new View\Template('{@content}'); } } diff --git a/PHPCI/Controller/BitbucketController.php b/PHPCI/Controller/BitbucketController.php deleted file mode 100644 index 783421a3..00000000 --- a/PHPCI/Controller/BitbucketController.php +++ /dev/null @@ -1,67 +0,0 @@ - -* @package PHPCI -* @subpackage Web -*/ -class BitbucketController extends \PHPCI\Controller -{ - /** - * @var \PHPCI\Store\BuildStore - */ - protected $buildStore; - - public function init() - { - $this->buildStore = Store\Factory::getStore('Build'); - } - - /** - * Called by Bitbucket POST service. - */ - public function webhook($project) - { - $payload = json_decode($this->getParam('payload'), true); - $branches = array(); - $commits = array(); - - foreach ($payload['commits'] as $commit) { - if (!in_array($commit['branch'], $branches)) { - $branches[] = $commit['branch']; - $commits[$commit['branch']] = $commit['raw_node']; - } - } - - foreach ($branches as $branch) { - try { - - $build = new Build(); - $build->setProjectId($project); - $build->setCommitId($commits[$branch]); - $build->setStatus(Build::STATUS_NEW); - $build->setLog(''); - $build->setCreated(new \DateTime()); - $build->setBranch($branch); - $this->buildStore->save($build); - } catch (\Exception $ex) { - } - } - - die('OK'); - } -} diff --git a/PHPCI/Controller/BuildController.php b/PHPCI/Controller/BuildController.php index c80cb7c8..032ce118 100644 --- a/PHPCI/Controller/BuildController.php +++ b/PHPCI/Controller/BuildController.php @@ -10,6 +10,7 @@ namespace PHPCI\Controller; use b8; +use PHPCI\BuildFactory; use PHPCI\Model\Build; /** @@ -35,7 +36,7 @@ class BuildController extends \PHPCI\Controller */ public function view($buildId) { - $build = $this->buildStore->getById($buildId); + $build = BuildFactory::getBuildById($buildId); $this->view->plugins = $this->getUiPlugins(); $this->view->build = $build; $this->view->data = $this->getBuildData($build); @@ -63,7 +64,7 @@ class BuildController extends \PHPCI\Controller */ public function data($buildId) { - die($this->getBuildData($this->buildStore->getById($buildId))); + die($this->getBuildData(BuildFactory::getBuildById($buildId))); } /** @@ -71,7 +72,7 @@ class BuildController extends \PHPCI\Controller */ public function meta($buildId) { - $build = $this->buildStore->getById($buildId); + $build = BuildFactory::getBuildById($buildId); $key = $this->getParam('key', null); $numBuilds = $this->getParam('num_builds', 1); $data = null; @@ -104,7 +105,7 @@ class BuildController extends \PHPCI\Controller */ public function rebuild($buildId) { - $copy = $this->buildStore->getById($buildId); + $copy = BuildFactory::getBuildById($buildId); $build = new Build(); $build->setProjectId($copy->getProjectId()); @@ -112,6 +113,8 @@ class BuildController extends \PHPCI\Controller $build->setStatus(Build::STATUS_NEW); $build->setBranch($copy->getBranch()); $build->setCreated(new \DateTime()); + $build->setCommitterEmail($copy->getCommitterEmail()); + $build->setCommitMessage($copy->getCommitMessage()); $build = $this->buildStore->save($build); @@ -128,7 +131,7 @@ class BuildController extends \PHPCI\Controller throw new \Exception('You do not have permission to do that.'); } - $build = $this->buildStore->getById($buildId); + $build = BuildFactory::getBuildById($buildId); if (!$build) { $this->response->setResponseCode(404); diff --git a/PHPCI/Controller/GitController.php b/PHPCI/Controller/GitController.php deleted file mode 100644 index b5a6cf87..00000000 --- a/PHPCI/Controller/GitController.php +++ /dev/null @@ -1,69 +0,0 @@ - - */ -class GitController extends \PHPCI\Controller -{ - public function init() - { - $this->_buildStore = Store\Factory::getStore('Build'); - } - - /** - * Called by POSTing to /git/webhook/%s", $logText) ); } else { + $view = new View('Email/failed'); + $view->build = $this->build; + $view->project = $this->build->getProject(); + + $emailHtml = $view->render(); + $sendFailures = $this->sendSeparateEmails( $addresses, sprintf($subjectTemplate, $projectName, "Failing Build"), - sprintf("Log Output:
%s", $logText) + $emailHtml ); } @@ -99,18 +106,24 @@ class Email implements \PHPCI\Plugin } /** - * @param array|string $toAddresses Array or single address to send to - * @param string $subject Email subject - * @param string $body Email body + * @param string[]|string $toAddresses Array or single address to send to + * @param string[] $ccList + * @param string $subject Email subject + * @param string $body Email body * @return array Array of failed addresses */ - public function sendEmail($toAddresses, $subject, $body) + public function sendEmail($toAddresses, $ccList, $subject, $body) { $message = \Swift_Message::newInstance($subject) ->setFrom($this->fromAddress) ->setTo($toAddresses) ->setBody($body) ->setContentType("text/html"); + + if (is_array($ccList) && count($ccList)) { + $message->setCc($ccList); + } + $failedAddresses = array(); $this->mailer->send($message, $failedAddresses); @@ -120,8 +133,10 @@ class Email implements \PHPCI\Plugin public function sendSeparateEmails(array $toAddresses, $subject, $body) { $failures = array(); + $ccList = $this->getCcAddresses(); + foreach ($toAddresses as $address) { - $newFailures = $this->sendEmail($address, $subject, $body); + $newFailures = $this->sendEmail($address, $ccList, $subject, $body); foreach ($newFailures as $failure) { $failures[] = $failure; } @@ -150,4 +165,17 @@ class Email implements \PHPCI\Plugin } return $addresses; } -} \ No newline at end of file + + protected function getCcAddresses() + { + $ccAddresses = array(); + + if (isset($this->options['cc'])) { + foreach ($this->options['cc'] as $address) { + $ccAddresses[] = $address; + } + } + + return $ccAddresses; + } +} diff --git a/PHPCI/Plugin/Util/ComposerPluginInformation.php b/PHPCI/Plugin/Util/ComposerPluginInformation.php new file mode 100644 index 00000000..c870514e --- /dev/null +++ b/PHPCI/Plugin/Util/ComposerPluginInformation.php @@ -0,0 +1,145 @@ +composerPackages = $composerPackages; + } + + /** + * Returns an array of objects. Each one represents an available plugin + * and will have the following properties: + * name - The friendly name of the plugin (may be an empty string) + * class - The class of the plugin (will include namespace) + * @return \stdClass[] + */ + public function getInstalledPlugins() + { + $this->loadPluginInfo(); + return $this->pluginInfo; + } + + /** + * Returns an array of all the class names of plugins that have been + * loaded. + * + * @return string[] + */ + public function getPluginClasses() + { + return array_map( + function ($plugin) { + return $plugin->class; + }, + $this->getInstalledPlugins() + ); + } + + protected function loadPluginInfo() + { + if ($this->pluginInfo !== null) { + return; + } + $this->pluginInfo = array(); + foreach ($this->composerPackages as $package) { + $this->addPluginsFromPackage($package); + } + } + + /** + * @param \stdClass $package + */ + protected function addPluginsFromPackage($package) + { + if (isset($package->extra->phpci)) { + $phpciData = $package->extra->phpci; + + if (isset($phpciData->pluginNamespace)) { + $rootNamespace = $phpciData->pluginNamespace; + } else { + $rootNamespace = ""; + } + + if (is_array($phpciData->suppliedPlugins)) { + $this->addPlugins( + $phpciData->suppliedPlugins, + $package->name, + $rootNamespace + ); + } + } + } + + /** + * @param \stdClass[] $plugins + * @param string $sourcePackageName + * @param string $rootNamespace + */ + protected function addPlugins( + array $plugins, + $sourcePackageName, + $rootNamespace = "" + ) { + foreach ($plugins as $plugin) { + if (!isset($plugin->class)) { + continue; + } + $this->addPlugin($plugin, $sourcePackageName, $rootNamespace); + } + } + + /** + * @param \stdClass $plugin + * @param string $sourcePackageName + * @param string $rootNamespace + */ + protected function addPlugin( + $plugin, + $sourcePackageName, + $rootNamespace = "" + ) { + $newPlugin = clone $plugin; + + $newPlugin->class = $rootNamespace . $newPlugin->class; + + if (!isset($newPlugin->name)) { + $newPlugin->name = ""; + } + + $newPlugin->source = $sourcePackageName; + + $this->pluginInfo[] = $newPlugin; + } +} diff --git a/PHPCI/Plugin/Util/Executor.php b/PHPCI/Plugin/Util/Executor.php index 5bec1820..39a5d35e 100644 --- a/PHPCI/Plugin/Util/Executor.php +++ b/PHPCI/Plugin/Util/Executor.php @@ -2,7 +2,7 @@ namespace PHPCI\Plugin\Util; -use PHPCI\BuildLogger; +use \PHPCI\Logging\BuildLogger; class Executor { @@ -17,7 +17,7 @@ class Executor */ protected $pluginFactory; - function __construct(Factory $pluginFactory, BuildLogger $logger) + public function __construct(Factory $pluginFactory, BuildLogger $logger) { $this->pluginFactory = $pluginFactory; $this->logger = $logger; @@ -78,8 +78,7 @@ class Executor $class = str_replace('_', ' ', $plugin); $class = ucwords($class); $class = 'PHPCI\\Plugin\\' . str_replace(' ', '', $class); - } - else { + } else { $class = $plugin; } @@ -105,4 +104,4 @@ class Executor return $rtn; } -} \ No newline at end of file +} diff --git a/PHPCI/Plugin/Util/Factory.php b/PHPCI/Plugin/Util/Factory.php index 8adfd99d..9740516f 100644 --- a/PHPCI/Plugin/Util/Factory.php +++ b/PHPCI/Plugin/Util/Factory.php @@ -36,30 +36,10 @@ class Factory { 'array' ); } + - /** - * Trys to get a function from the file path specified. If the - * file returns a function then $this will be passed to it. - * This enables the config file to call any public methods. - * - * @param $configPath - * @return bool - true if the function exists else false. - */ - public function addConfigFromFile($configPath) + public function getLastOptions() { - // The file is expected to return a function which can - // act on the pluginFactory to register any resources needed. - if (file_exists($configPath)) { - $configFunction = require($configPath); - if (is_callable($configFunction)) { - $configFunction($this); - return true; - } - } - return false; - } - - public function getLastOptions() { return $this->currentPluginOptions; } diff --git a/PHPCI/Plugin/Util/FilesPluginInformation.php b/PHPCI/Plugin/Util/FilesPluginInformation.php new file mode 100644 index 00000000..8366c4cb --- /dev/null +++ b/PHPCI/Plugin/Util/FilesPluginInformation.php @@ -0,0 +1,104 @@ +files = $files; + } + + /** + * Returns an array of objects. Each one represents an available plugin + * and will have the following properties: + * name - The friendly name of the plugin (may be an empty string) + * class - The class of the plugin (will include namespace) + * @return \stdClass[] + */ + public function getInstalledPlugins() + { + if ($this->pluginInfo === null) { + $this->loadPluginInfo(); + } + return $this->pluginInfo; + } + + /** + * Returns an array of all the class names of plugins that have been + * loaded. + * + * @return string[] + */ + public function getPluginClasses() + { + return array_map( + function ($plugin) { + return $plugin->class; + }, + $this->getInstalledPlugins() + ); + } + + protected function loadPluginInfo() + { + $this->pluginInfo = array(); + foreach ($this->files as $fileInfo) { + if ($fileInfo instanceof \SplFileInfo) { + if ($fileInfo->isFile()) { + $this->addPluginFromFile($fileInfo); + } + } + } + } + + protected function addPluginFromFile(\SplFileInfo $fileInfo) + { + $newPlugin = new \stdClass(); + $newPlugin->class = $this->getFullClassFromFile($fileInfo); + $newPlugin->source = "core"; + $parts = explode('\\', $newPlugin->class); + $newPlugin->name = end($parts); + + $this->pluginInfo[] = $newPlugin; + } + + protected function getFullClassFromFile(\SplFileInfo $fileInfo) + { + //TODO: Something less horrible than a regular expression + // on the contents of a file + $contents = file_get_contents($fileInfo->getRealPath()); + + $matches = array(); + preg_match('#class +([A-Za-z]+) +implements#i', $contents, $matches); + $className = $matches[1]; + + $matches = array(); + preg_match('#namespace +([A-Za-z\\\\]+);#i', $contents, $matches); + $namespace = $matches[1]; + + return $namespace . '\\' . $className; + } +} diff --git a/PHPCI/Plugin/Util/InstalledPluginInformation.php b/PHPCI/Plugin/Util/InstalledPluginInformation.php new file mode 100644 index 00000000..133b8de8 --- /dev/null +++ b/PHPCI/Plugin/Util/InstalledPluginInformation.php @@ -0,0 +1,23 @@ +pluginInformations[] = $information; + } + + /** + * Returns an array of objects. Each one represents an available plugin + * and will have the following properties: + * name - The friendly name of the plugin (may be an empty string) + * class - The class of the plugin (will include namespace) + * @return \stdClass[] + */ + public function getInstalledPlugins() + { + $arr = array(); + foreach ($this->pluginInformations as $single) { + $arr = array_merge($arr, $single->getInstalledPlugins()); + } + return $arr; + } + + /** + * Returns an array of all the class names of plugins that have been + * loaded. + * + * @return string[] + */ + public function getPluginClasses() + { + $arr = array(); + foreach ($this->pluginInformations as $single) { + $arr = array_merge($arr, $single->getPluginClasses()); + } + return $arr; + } + +} diff --git a/PHPCI/Store.php b/PHPCI/Store.php new file mode 100644 index 00000000..6806a097 --- /dev/null +++ b/PHPCI/Store.php @@ -0,0 +1,7 @@ +prepare($query); $stmt->bindValue(':id', $value); @@ -58,7 +58,7 @@ class BuildMetaStoreBase extends Store $count = null; - $query = 'SELECT * FROM build_meta WHERE build_id = :build_id' . $add; + $query = 'SELECT * FROM `build_meta` WHERE `build_id` = :build_id' . $add; $stmt = Database::getConnection($useConnection)->prepare($query); $stmt->bindValue(':build_id', $value); diff --git a/PHPCI/Store/Base/BuildStoreBase.php b/PHPCI/Store/Base/BuildStoreBase.php index f0e66085..b67d5f73 100644 --- a/PHPCI/Store/Base/BuildStoreBase.php +++ b/PHPCI/Store/Base/BuildStoreBase.php @@ -8,7 +8,7 @@ namespace PHPCI\Store\Base; use b8\Database; use b8\Exception\HttpException; -use b8\Store; +use PHPCI\Store; use PHPCI\Model\Build; /** @@ -31,7 +31,7 @@ class BuildStoreBase extends Store throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); } - $query = 'SELECT * FROM build WHERE id = :id LIMIT 1'; + $query = 'SELECT * FROM `build` WHERE `id` = :id LIMIT 1'; $stmt = Database::getConnection($useConnection)->prepare($query); $stmt->bindValue(':id', $value); @@ -58,7 +58,7 @@ class BuildStoreBase extends Store $count = null; - $query = 'SELECT * FROM build WHERE project_id = :project_id' . $add; + $query = 'SELECT * FROM `build` WHERE `project_id` = :project_id' . $add; $stmt = Database::getConnection($useConnection)->prepare($query); $stmt->bindValue(':project_id', $value); @@ -90,7 +90,7 @@ class BuildStoreBase extends Store $count = null; - $query = 'SELECT * FROM build WHERE status = :status' . $add; + $query = 'SELECT * FROM `build` WHERE `status` = :status' . $add; $stmt = Database::getConnection($useConnection)->prepare($query); $stmt->bindValue(':status', $value); diff --git a/PHPCI/Store/Base/ProjectStoreBase.php b/PHPCI/Store/Base/ProjectStoreBase.php index 8b78c055..410a305e 100644 --- a/PHPCI/Store/Base/ProjectStoreBase.php +++ b/PHPCI/Store/Base/ProjectStoreBase.php @@ -8,7 +8,7 @@ namespace PHPCI\Store\Base; use b8\Database; use b8\Exception\HttpException; -use b8\Store; +use PHPCI\Store; use PHPCI\Model\Project; /** @@ -31,7 +31,7 @@ class ProjectStoreBase extends Store throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); } - $query = 'SELECT * FROM project WHERE id = :id LIMIT 1'; + $query = 'SELECT * FROM `project` WHERE `id` = :id LIMIT 1'; $stmt = Database::getConnection($useConnection)->prepare($query); $stmt->bindValue(':id', $value); @@ -58,7 +58,7 @@ class ProjectStoreBase extends Store $count = null; - $query = 'SELECT * FROM project WHERE title = :title' . $add; + $query = 'SELECT * FROM `project` WHERE `title` = :title' . $add; $stmt = Database::getConnection($useConnection)->prepare($query); $stmt->bindValue(':title', $value); diff --git a/PHPCI/Store/Base/UserStoreBase.php b/PHPCI/Store/Base/UserStoreBase.php index c4268804..fd903d8e 100644 --- a/PHPCI/Store/Base/UserStoreBase.php +++ b/PHPCI/Store/Base/UserStoreBase.php @@ -8,7 +8,7 @@ namespace PHPCI\Store\Base; use b8\Database; use b8\Exception\HttpException; -use b8\Store; +use PHPCI\Store; use PHPCI\Model\User; /** @@ -31,7 +31,7 @@ class UserStoreBase extends Store throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); } - $query = 'SELECT * FROM user WHERE id = :id LIMIT 1'; + $query = 'SELECT * FROM `user` WHERE `id` = :id LIMIT 1'; $stmt = Database::getConnection($useConnection)->prepare($query); $stmt->bindValue(':id', $value); @@ -50,7 +50,7 @@ class UserStoreBase extends Store throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); } - $query = 'SELECT * FROM user WHERE email = :email LIMIT 1'; + $query = 'SELECT * FROM `user` WHERE `email` = :email LIMIT 1'; $stmt = Database::getConnection($useConnection)->prepare($query); $stmt->bindValue(':email', $value); diff --git a/PHPCI/View/Build/view.phtml b/PHPCI/View/Build/view.phtml index aa18e591..6e181444 100644 --- a/PHPCI/View/Build/view.phtml +++ b/PHPCI/View/Build/view.phtml @@ -1,17 +1,23 @@