diff --git a/PHPCI/Application.php b/PHPCI/Application.php index c0048adf..3378a050 100644 --- a/PHPCI/Application.php +++ b/PHPCI/Application.php @@ -10,6 +10,7 @@ namespace PHPCI; use b8; +use b8\Registry; use b8\Http\Response\RedirectResponse; use b8\View; @@ -33,8 +34,11 @@ class Application extends b8\Application $sessionAction = ($this->controllerName == 'Session' && in_array($this->action, array('login', 'logout'))); $externalAction = in_array($this->controllerName, array('Bitbucket', 'Github', 'BuildStatus')); $skipValidation = ($externalAction || $sessionAction); - + if($skipValidation || $this->validateSession()) { + if ( !empty($_SESSION['user']) ) { + Registry::getInstance()->set('user', $_SESSION['user']); + } parent::handleRequest(); } @@ -43,7 +47,7 @@ class Application extends b8\Application $view->content = $this->response->getContent(); $this->response->setContent($view->render()); } - + return $this->response; } diff --git a/PHPCI/Builder.php b/PHPCI/Builder.php index a7b60bc1..f943a092 100644 --- a/PHPCI/Builder.php +++ b/PHPCI/Builder.php @@ -116,6 +116,16 @@ class Builder return isset($this->config[$key]) ? $this->config[$key] : null; } + /** + * Access a variable from the config.yml + * @param $key + * @return mixed + */ + public function getSystemConfig($key) + { + return \b8\Registry::getInstance()->get($key); + } + /** * Access the build. * @param Build @@ -125,6 +135,22 @@ class Builder return $this->build; } + /** + * @return string The title of the project being built. + */ + public function getBuildProjectTitle() { + return $this->getBuild()->getProject()->getTitle(); + } + + /** + * Indicates if the build has passed or failed. + * @return bool + */ + public function getSuccessStatus() + { + return $this->success; + } + /** * Run the active build. */ diff --git a/PHPCI/Command/InstallCommand.php b/PHPCI/Command/InstallCommand.php index 35459aec..ad9cbeb7 100644 --- a/PHPCI/Command/InstallCommand.php +++ b/PHPCI/Command/InstallCommand.php @@ -48,6 +48,13 @@ class InstallCommand extends Command $conf['phpci']['github']['id'] = $this->ask('(Optional) Github Application ID: ', true); $conf['phpci']['github']['secret'] = $this->ask('(Optional) Github Application Secret: ', true); + $conf['phpci']['email_settings']['smtp_address'] = $this->ask('(Optional) Smtp server address: ', true); + $conf['phpci']['email_settings']['smtp_port'] = $this->ask('(Optional) Smtp port: ', true); + $conf['phpci']['email_settings']['smtp_username'] = $this->ask('(Optional) Smtp Username: ', true); + $conf['phpci']['email_settings']['smtp_password'] = $this->ask('(Optional) Smtp Password: ', true); + $conf['phpci']['email_settings']['from_address'] = $this->ask('(Optional) Email address to send from: ', true); + $conf['phpci']['email_settings']['default_mailto_address'] = $this->ask('(Optional) Default address to email notifications to: ', true); + $dbUser = $conf['b8']['database']['username']; $dbPass = $conf['b8']['database']['password']; $dbHost = $conf['b8']['database']['servers']['write']; diff --git a/PHPCI/Controller/BuildController.php b/PHPCI/Controller/BuildController.php index 21de59c9..b41287aa 100644 --- a/PHPCI/Controller/BuildController.php +++ b/PHPCI/Controller/BuildController.php @@ -86,10 +86,10 @@ class BuildController extends \PHPCI\Controller */ public function delete($buildId) { - if (!Registry::getInstance()->get('user')->getIsAdmin()) { + if (empty($_SESSION['user']) || !$_SESSION['user']->getIsAdmin()) { throw new \Exception('You do not have permission to do that.'); } - + $build = $this->_buildStore->getById($buildId); $this->_buildStore->delete($build); diff --git a/PHPCI/Plugin/Email.php b/PHPCI/Plugin/Email.php new file mode 100644 index 00000000..ae7ced05 --- /dev/null +++ b/PHPCI/Plugin/Email.php @@ -0,0 +1,191 @@ + +* @package PHPCI +* @subpackage Plugins +*/ +class Email implements \PHPCI\Plugin +{ + + /** + * @var \PHPCI\Builder + */ + protected $phpci; + + /** + * @var array + */ + protected $options; + + /** + * @var array + */ + protected $emailConfig; + + /** + * @var \Swift_Mailer + */ + protected $mailer; + + public function __construct(\PHPCI\Builder $phpci, + array $options = array(), + \Swift_Mailer $mailer = null) + { + $phpCiSettings = $phpci->getSystemConfig('phpci'); + $this->phpci = $phpci; + $this->options = $options; + $this->emailConfig = isset($phpCiSettings['email_settings']) ? $phpCiSettings['email_settings'] : array(); + + // Either a mailer will have been passed in or we load from the + // config. + if ($mailer === null) { + $this->loadSwiftMailerFromConfig(); + } + else { + $this->mailer = $mailer; + } + } + + /** + * Connects to MySQL and runs a specified set of queries. + */ + public function execute() + { + $addresses = $this->getEmailAddresses(); + + // Without some email addresses in the yml file then we + // can't do anything. + if (count($addresses) == 0) { + return false; + } + + $sendFailures = array(); + + $subjectTemplate = "PHPCI - %s - %s"; + $projectName = $this->phpci->getBuildProjectTitle(); + $logText = $this->phpci->getBuild()->getLog(); + + if($this->phpci->getSuccessStatus()) { + $sendFailures = $this->sendSeparateEmails( + $addresses, + sprintf($subjectTemplate, $projectName, "Passing Build"), + sprintf("Log Output:
%s
", $logText) + ); + } + else { + $sendFailures = $this->sendSeparateEmails( + $addresses, + sprintf($subjectTemplate, $projectName, "Failing Build"), + sprintf("Log Output:
%s
", $logText) + ); + } + + // This is a success if we've not failed to send anything. + $this->phpci->log(sprintf( + "%d emails sent", + (count($addresses) - count($sendFailures))) + ); + $this->phpci->log(sprintf( + "%d emails failed to send", + count($sendFailures)) + ); + return (count($sendFailures) == 0); + } + + /** + * @param array|string $toAddresses Array or single address to send to + * @param string $subject Email subject + * @param string $body Email body + * @return array Array of failed addresses + */ + public function sendEmail($toAddresses, $subject, $body) + { + $message = \Swift_Message::newInstance($subject) + ->setFrom($this->getMailConfig('from_address')) + ->setTo($toAddresses) + ->setBody($body) + ->setContentType("text/html"); + $failedAddresses = array(); + $this->mailer->send($message, $failedAddresses); + + return $failedAddresses; + } + + public function sendSeparateEmails(array $toAddresses, $subject, $body) + { + $failures = array(); + foreach($toAddresses as $address) { + $newFailures = $this->sendEmail($address, $subject, $body); + foreach($newFailures as $failure) { + $failures[] = $failure; + } + } + return $failures; + } + + protected function loadSwiftMailerFromConfig() + { + /** @var \Swift_SmtpTransport $transport */ + $transport = \Swift_SmtpTransport::newInstance( + $this->getMailConfig('smtp_address'), + $this->getMailConfig('smtp_port') + ); + $transport->setUsername($this->getMailConfig('smtp_username')); + $transport->setPassword($this->getMailConfig('smtp_password')); + + $this->mailer = \Swift_Mailer::newInstance($transport); + } + + protected function getMailConfig($configName) + { + if (isset($this->emailConfig[$configName]) + && $this->emailConfig[$configName] != "") + { + return $this->emailConfig[$configName]; + } + // Check defaults + else { + switch($configName) { + case 'smtp_address': + return "localhost"; + case 'default_mailto_address': + return null; + case 'smtp_port': + return '25'; + case 'from_address': + return "notifications-ci@phptesting.org"; + default: + return ""; + } + } + } + + protected function getEmailAddresses() + { + $addresses = array(); + + if (isset($this->options['addresses'])) { + foreach ($this->options['addresses'] as $address) { + $addresses[] = $address; + } + } + + if (isset($this->options['default_mailto_address'])) { + $addresses[] = $this->options['default_mailto_address']; + return $addresses; + } + return $addresses; + } +} \ No newline at end of file diff --git a/PHPCI/View/BuildsTable.phtml b/PHPCI/View/BuildsTable.phtml index 8ced123f..34a5ca72 100644 --- a/PHPCI/View/BuildsTable.phtml +++ b/PHPCI/View/BuildsTable.phtml @@ -11,21 +11,26 @@ switch($build->getStatus()) { case 0: $cls = 'info'; + $subcls = 'info'; $status = 'Pending'; + break; case 1: $cls = 'warning'; + $subcls = 'warning'; $status = 'Running'; break; case 2: $cls = 'success'; + $subcls = 'success'; $status = 'Success'; break; case 3: $cls = 'error'; + $subcls = 'important'; $status = 'Failed'; break; } @@ -35,7 +40,31 @@ switch($build->getStatus()) getProject()->getTitle(); ?> getCommitId(); ?> getBranch(); ?> - + + getPlugins(), true); + + if ( !is_array($plugins) ) { + $plugins = array(); + } + if ( 0 === count($plugins) ) { + ?> + + + + + $pluginstatus): + $subcls = $pluginstatus?'label label-success':'label label-important'; + ?> + + + + +
+
View diff --git a/PHPCI/View/Project/view.phtml b/PHPCI/View/Project/view.phtml index 59d07ced..17b0a1ab 100644 --- a/PHPCI/View/Project/view.phtml +++ b/PHPCI/View/Project/view.phtml @@ -61,14 +61,14 @@ $pages = ceil($total / 10); $pages = $pages == 0 ? 1 : $pages; - print '
  • «
  • '; + print '
  • «
  • '; for($i = 1; $i <= $pages; $i++) { - print '
  • ' . $i . '
  • '; + print '
  • ' . $i . '
  • '; } - print '
  • »
  • '; + print '
  • »
  • '; print '
    '; diff --git a/Tests/PHPCI/Plugin/Email.php b/Tests/PHPCI/Plugin/Email.php new file mode 100644 index 00000000..8000da7e --- /dev/null +++ b/Tests/PHPCI/Plugin/Email.php @@ -0,0 +1,256 @@ +mockBuild = $this->getMock( + '\PHPCI\Model\Build', + array('getLog'), + array(), + "mockBuild", + false + ); + + $this->mockBuild->expects($this->any()) + ->method('getLog') + ->will($this->returnValue("Build Log")); + + $this->mockCiBuilder = $this->getMock( + '\PHPCI\Builder', + array('getSystemConfig', + 'getBuildProjectTitle', + 'getBuild', + 'log'), + array(), + "mockBuilder", + false + ); + + $this->mockCiBuilder->buildPath = "/"; + + $this->mockCiBuilder->expects($this->any()) + ->method('getSystemConfig') + ->with('phpci') + ->will($this->returnValue(array( + 'email_settings' => array( + 'from_address' => "test-from-address@example.com" + ) + ))); + $this->mockCiBuilder->expects($this->any()) + ->method('getBuildProjectTitle') + ->will($this->returnValue('Test-Project')); + $this->mockCiBuilder->expects($this->any()) + ->method('getBuild') + ->will($this->returnValue($this->mockBuild)); + + $this->mockMailer = $this->getMock( + '\Swift_Mailer', + array('send'), + array(), + "mockMailer", + false + ); + + $this->loadEmailPluginWithOptions(); + } + + protected function loadEmailPluginWithOptions($arrOptions = array()) + { + $this->testedEmailPlugin = new EmailPlugin( + $this->mockCiBuilder, + $arrOptions, + $this->mockMailer + ); + } + + /** + * @covers PHPUnit::execute + */ + public function testExecute_ReturnsFalseWithoutArgs() + { + $returnValue = $this->testedEmailPlugin->execute(); + // As no addresses will have been mailed as non are configured. + $expectedReturn = false; + + $this->assertEquals($expectedReturn, $returnValue); + } + + /** + * @covers PHPUnit::execute + */ + public function testExecute_BuildsBasicEmails() + { + $this->loadEmailPluginWithOptions(array( + 'addresses' => array('test-receiver@example.com') + )); + + /** @var \Swift_Message $actualMail */ + $actualMail = null; + $this->catchMailPassedToSend($actualMail); + + $returnValue = $this->testedEmailPlugin->execute(); + $expectedReturn = true; + + $this->assertSystemMail( + 'test-receiver@example.com', + 'test-from-address@example.com', + "Log Output:
    Build Log
    ", + "PHPCI - Test-Project - Passing Build", + $actualMail + ); + + $this->assertEquals($expectedReturn, $returnValue); + + + } + + /** + * @covers PHPUnit::sendEmail + */ + public function testSendEmail_CallsMailerSend() + { + $this->mockMailer->expects($this->once()) + ->method('send'); + $this->testedEmailPlugin->sendEmail("test@email.com", "hello", "body"); + } + + /** + * @covers PHPUnit::sendEmail + */ + public function testSendEmail_BuildsAMessageObject() + { + $subject = "Test mail"; + $body = "Message Body"; + $toAddress = "test@example.com"; + + $this->mockMailer->expects($this->once()) + ->method('send') + ->with($this->isInstanceOf('\Swift_Message'), $this->anything()); + $this->testedEmailPlugin->sendEmail($toAddress, $subject, $body); + } + + /** + * @covers PHPUnit::sendEmail + */ + public function testSendEmail_BuildsExpectedMessage() + { + $subject = "Test mail"; + $body = "Message Body"; + $toAddress = "test@example.com"; + $expectedMessage = \Swift_Message::newInstance($subject) + ->setFrom('test-from-address@example.com') + ->setTo($toAddress) + ->setBody($body); + + /** @var \Swift_Message $actualMail */ + $actualMail = null; + $this->catchMailPassedToSend($actualMail); + + $this->testedEmailPlugin->sendEmail($toAddress, $subject, $body); + + $this->assertSystemMail( + $toAddress, + 'test-from-address@example.com', + $body, + $subject, + $actualMail + ); + } + + /** + * @param \Swift_Message $actualMail passed by ref and populated with + * the message object the mock mailer + * receives. + */ + protected function catchMailPassedToSend(&$actualMail) + { + $this->mockMailer->expects($this->once()) + ->method('send') + ->will( + $this->returnCallback( + function ($passedMail) use (&$actualMail) { + $actualMail = $passedMail; + return array(); + } + ) + ); + } + + /** + * Asserts that the actual mail object is populated as expected. + * + * @param string $expectedToAddress + * @param $expectedFromAddress + * @param string $expectedBody + * @param string $expectedSubject + * @param \Swift_Message $actualMail + */ + protected function assertSystemMail($expectedToAddress, + $expectedFromAddress, + $expectedBody, + $expectedSubject, + $actualMail) + { + if (! ($actualMail instanceof \Swift_Message)) { + $type = is_object($actualMail) ? get_class($actualMail) : gettype($actualMail); + throw new \Exception("Expected Swift_Message got " . $type); + } + $this->assertEquals( + array($expectedFromAddress => null), + $actualMail->getFrom() + ); + + $this->assertEquals( + array($expectedToAddress => null), + $actualMail->getTo() + ); + + $this->assertEquals( + $expectedBody, + $actualMail->getBody() + ); + + $this->assertEquals( + $expectedSubject, + $actualMail->getSubject() + ); + } +} \ No newline at end of file diff --git a/assets/css/phpci.css b/assets/css/phpci.css index 931c4b0a..cfb886d4 100644 --- a/assets/css/phpci.css +++ b/assets/css/phpci.css @@ -14,6 +14,20 @@ body padding: 10px; } +td .label { margin-right: 5px; } + +.success-message { + background-color: #4F8A10; +} + +.error-message { + background-color: #FF4747; +} + +#latest-builds td { + vertical-align: middle; +} + .widget-title, .modal-header, .table th, div.dataTables_wrapper .ui-widget-header, .ui-dialog .ui-dialog-titlebar { background-color: #efefef; background-image: -webkit-gradient(linear, 0 0%, 0 100%, from(#fdfdfd), to(#eaeaea)); @@ -61,7 +75,7 @@ body background: url('/assets/img/icon-build-running.png') no-repeat top left; } -h3 +h3 { border-bottom: 1px solid #f0f0f0; margin-top: 0; diff --git a/composer.json b/composer.json index 7b8c37b3..a4424830 100644 --- a/composer.json +++ b/composer.json @@ -32,6 +32,7 @@ "phpspec/phpspec" : "2.*", "symfony/yaml" : "2.2.x-dev", "symfony/console" : "2.2.*", - "fabpot/php-cs-fixer" : "0.3.*@dev" + "fabpot/php-cs-fixer" : "0.3.*@dev", + "swiftmailer/swiftmailer" : "v5.0.0" } }