2013-05-03 17:02:53 +02:00
|
|
|
<?php
|
2013-05-16 03:16:56 +02:00
|
|
|
/**
|
|
|
|
* PHPCI - Continuous Integration for PHP
|
|
|
|
*
|
2013-05-16 03:57:02 +02:00
|
|
|
* @copyright Copyright 2013, Block 8 Limited.
|
|
|
|
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
|
|
|
|
* @link http://www.phptesting.org/
|
2013-05-16 03:16:56 +02:00
|
|
|
*/
|
2013-05-03 17:02:53 +02:00
|
|
|
|
|
|
|
namespace PHPCI;
|
2013-05-16 03:57:02 +02:00
|
|
|
|
2013-05-03 17:02:53 +02:00
|
|
|
use PHPCI\Model\Build;
|
|
|
|
use b8\Store;
|
2013-07-30 03:55:29 +02:00
|
|
|
use b8\Config;
|
2013-10-26 17:15:29 +02:00
|
|
|
use Psr\Log\LoggerAwareInterface;
|
|
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
use Psr\Log\LogLevel;
|
2013-05-03 17:02:53 +02:00
|
|
|
|
2013-05-16 03:16:56 +02:00
|
|
|
/**
|
|
|
|
* PHPCI Build Runner
|
2013-05-16 03:57:02 +02:00
|
|
|
* @author Dan Cryer <dan@block8.co.uk>
|
2013-05-16 03:16:56 +02:00
|
|
|
*/
|
2013-10-26 17:15:29 +02:00
|
|
|
class Builder implements LoggerAwareInterface
|
2013-05-03 17:02:53 +02:00
|
|
|
{
|
2013-05-16 03:57:02 +02:00
|
|
|
/**
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
public $buildPath;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string[]
|
|
|
|
*/
|
|
|
|
public $ignore = array();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
protected $ciDir;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
protected $directory;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
protected $success = true;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
2013-10-10 02:01:06 +02:00
|
|
|
protected $verbose = true;
|
2013-05-16 03:57:02 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @var \PHPCI\Model\Build
|
|
|
|
*/
|
|
|
|
protected $build;
|
|
|
|
|
2013-10-26 17:15:29 +02:00
|
|
|
/**
|
|
|
|
* @var LoggerInterface
|
|
|
|
*/
|
|
|
|
protected $logger;
|
2013-05-16 03:57:02 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @var array
|
|
|
|
*/
|
|
|
|
protected $config;
|
2013-10-08 08:21:46 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
protected $lastOutput;
|
|
|
|
|
2013-05-22 20:17:33 +02:00
|
|
|
/**
|
|
|
|
* An array of key => value pairs that will be used for
|
|
|
|
* interpolation and environment variables
|
|
|
|
* @var array
|
|
|
|
* @see setInterpolationVars()
|
|
|
|
*/
|
|
|
|
protected $interpolation_vars = array();
|
2013-05-16 03:57:02 +02:00
|
|
|
|
2013-10-08 08:21:46 +02:00
|
|
|
/**
|
|
|
|
* @var \PHPCI\Store\BuildStore
|
|
|
|
*/
|
|
|
|
protected $store;
|
|
|
|
|
2013-10-10 02:01:06 +02:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
public $quiet = false;
|
|
|
|
|
2013-05-16 03:57:02 +02:00
|
|
|
/**
|
|
|
|
* Set up the builder.
|
2013-10-26 17:15:29 +02:00
|
|
|
* @param \PHPCI\Model\Build $build
|
|
|
|
* @param LoggerInterface $logger
|
2013-05-16 03:57:02 +02:00
|
|
|
*/
|
2013-10-26 17:15:29 +02:00
|
|
|
public function __construct(Build $build, $logger = null)
|
2013-05-16 03:57:02 +02:00
|
|
|
{
|
2013-10-26 17:15:29 +02:00
|
|
|
if ($logger) {
|
|
|
|
$this->setLogger($logger);
|
|
|
|
}
|
2013-05-16 03:57:02 +02:00
|
|
|
$this->build = $build;
|
|
|
|
$this->store = Store\Factory::getStore('Build');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the config array, as read from phpci.yml
|
|
|
|
* @param array
|
|
|
|
*/
|
|
|
|
public function setConfigArray(array $config)
|
|
|
|
{
|
|
|
|
$this->config = $config;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Access a variable from the phpci.yml file.
|
|
|
|
* @param string
|
|
|
|
*/
|
|
|
|
public function getConfig($key)
|
|
|
|
{
|
2013-10-10 02:01:06 +02:00
|
|
|
$rtn = null;
|
|
|
|
|
|
|
|
if (isset($this->config[$key])) {
|
|
|
|
$rtn = $this->config[$key];
|
|
|
|
}
|
|
|
|
|
|
|
|
return $rtn;
|
2013-05-16 03:57:02 +02:00
|
|
|
}
|
|
|
|
|
2013-06-01 14:28:42 +02:00
|
|
|
/**
|
|
|
|
* Access a variable from the config.yml
|
|
|
|
* @param $key
|
|
|
|
* @return mixed
|
|
|
|
*/
|
|
|
|
public function getSystemConfig($key)
|
|
|
|
{
|
2013-07-30 03:55:29 +02:00
|
|
|
return Config::getInstance()->get($key);
|
2013-06-01 14:28:42 +02:00
|
|
|
}
|
|
|
|
|
2013-06-04 22:47:45 +02:00
|
|
|
/**
|
|
|
|
* @return string The title of the project being built.
|
|
|
|
*/
|
2013-10-10 02:12:30 +02:00
|
|
|
public function getBuildProjectTitle()
|
|
|
|
{
|
2013-10-10 02:01:06 +02:00
|
|
|
return $this->build->getProject()->getTitle();
|
2013-06-01 14:56:09 +02:00
|
|
|
}
|
|
|
|
|
2013-05-16 03:57:02 +02:00
|
|
|
/**
|
|
|
|
* Run the active build.
|
|
|
|
*/
|
|
|
|
public function execute()
|
|
|
|
{
|
|
|
|
// Update the build in the database, ping any external services.
|
|
|
|
$this->build->setStatus(1);
|
|
|
|
$this->build->setStarted(new \DateTime());
|
|
|
|
$this->store->save($this->build);
|
|
|
|
$this->build->sendStatusPostback();
|
|
|
|
|
|
|
|
try {
|
2013-10-10 02:01:06 +02:00
|
|
|
// Set up the build:
|
|
|
|
$this->setupBuild();
|
2013-05-16 03:57:02 +02:00
|
|
|
|
2013-10-10 02:01:06 +02:00
|
|
|
// Run the core plugin stages:
|
|
|
|
foreach (array('setup', 'test', 'complete') as $stage) {
|
|
|
|
$this->executePlugins($stage);
|
|
|
|
}
|
2013-05-16 03:57:02 +02:00
|
|
|
|
2013-10-10 02:01:06 +02:00
|
|
|
// Failed build? Execute failure plugins and then mark the build as failed.
|
|
|
|
if (!$this->success) {
|
|
|
|
$this->executePlugins('failure');
|
|
|
|
throw new \Exception('BUILD FAILED!');
|
|
|
|
}
|
2013-05-16 03:57:02 +02:00
|
|
|
|
2013-10-10 02:01:06 +02:00
|
|
|
// If we got this far, the build was successful!
|
|
|
|
if ($this->success) {
|
|
|
|
$this->build->setStatus(2);
|
|
|
|
$this->executePlugins('success');
|
|
|
|
$this->logSuccess('BUILD SUCCESSFUL!');
|
2013-05-16 03:57:02 +02:00
|
|
|
}
|
2013-10-10 02:01:06 +02:00
|
|
|
|
2013-05-16 03:57:02 +02:00
|
|
|
} catch (\Exception $ex) {
|
|
|
|
$this->logFailure($ex->getMessage());
|
|
|
|
$this->build->setStatus(3);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Clean up:
|
2013-10-10 02:01:06 +02:00
|
|
|
$this->log('Removing build.');
|
|
|
|
shell_exec(sprintf('rm -Rf "%s"', $this->buildPath));
|
2013-05-16 03:57:02 +02:00
|
|
|
|
|
|
|
// Update the build in the database, ping any external services, etc.
|
|
|
|
$this->build->sendStatusPostback();
|
|
|
|
$this->build->setFinished(new \DateTime());
|
|
|
|
$this->store->save($this->build);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Used by this class, and plugins, to execute shell commands.
|
|
|
|
*/
|
|
|
|
public function executeCommand()
|
|
|
|
{
|
|
|
|
$command = call_user_func_array('sprintf', func_get_args());
|
2013-10-10 02:01:06 +02:00
|
|
|
|
|
|
|
if (!$this->quiet) {
|
2013-10-26 17:15:29 +02:00
|
|
|
$this->log('Executing: ' . $command);
|
2013-10-10 02:01:06 +02:00
|
|
|
}
|
2013-05-16 03:57:02 +02:00
|
|
|
|
|
|
|
$status = 0;
|
2013-10-08 08:21:46 +02:00
|
|
|
exec($command, $this->lastOutput, $status);
|
2013-05-16 03:57:02 +02:00
|
|
|
|
2013-10-08 08:21:46 +02:00
|
|
|
if (!empty($this->lastOutput) && ($this->verbose || $status != 0)) {
|
2013-10-26 17:15:29 +02:00
|
|
|
$this->log($this->lastOutput);
|
2013-05-16 03:57:02 +02:00
|
|
|
}
|
|
|
|
|
2013-10-10 02:01:06 +02:00
|
|
|
|
|
|
|
$rtn = false;
|
|
|
|
|
|
|
|
if ($status == 0) {
|
|
|
|
$rtn = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $rtn;
|
2013-05-16 03:57:02 +02:00
|
|
|
}
|
|
|
|
|
2013-10-08 08:21:46 +02:00
|
|
|
/**
|
|
|
|
* Returns the output from the last command run.
|
|
|
|
*/
|
|
|
|
public function getLastOutput()
|
|
|
|
{
|
|
|
|
return implode(PHP_EOL, $this->lastOutput);
|
|
|
|
}
|
|
|
|
|
2013-10-26 17:15:29 +02:00
|
|
|
/**
|
|
|
|
* 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())
|
2013-05-16 03:57:02 +02:00
|
|
|
{
|
2013-10-26 17:15:29 +02:00
|
|
|
// Skip if no logger has been loaded.
|
|
|
|
if (!$this->logger) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2013-10-10 02:01:06 +02:00
|
|
|
if (!is_array($message)) {
|
|
|
|
$message = array($message);
|
|
|
|
}
|
|
|
|
foreach ($message as $item) {
|
2013-10-26 17:15:29 +02:00
|
|
|
$this->logger->log($level, $item, $context);
|
2013-05-16 03:57:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
|
|
|
public function logFailure($message)
|
|
|
|
{
|
|
|
|
$this->log("\033[0;31m" . $message . "\033[0m");
|
|
|
|
}
|
2013-05-22 20:17:33 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Replace every occurance 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)
|
|
|
|
{
|
2013-10-10 02:01:06 +02:00
|
|
|
$keys = array_keys($this->interpolation_vars);
|
|
|
|
$values = array_values($this->interpolation_vars);
|
|
|
|
return str_replace($keys, $values, $input);
|
2013-05-22 20:17:33 +02:00
|
|
|
}
|
2013-05-16 03:57:02 +02:00
|
|
|
|
2013-05-22 20:17:33 +02:00
|
|
|
/**
|
|
|
|
* 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()
|
|
|
|
{
|
2013-10-10 02:01:06 +02:00
|
|
|
$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%']);
|
2013-05-22 20:17:33 +02:00
|
|
|
}
|
|
|
|
|
2013-05-16 03:57:02 +02:00
|
|
|
/**
|
|
|
|
* Set up a working copy of the project for building.
|
|
|
|
*/
|
|
|
|
protected function setupBuild()
|
|
|
|
{
|
|
|
|
$buildId = 'project' . $this->build->getProject()->getId() . '-build' . $this->build->getId();
|
2013-07-30 03:55:29 +02:00
|
|
|
$this->ciDir = dirname(__FILE__) . '/../';
|
2013-05-16 03:57:02 +02:00
|
|
|
$this->buildPath = $this->ciDir . 'build/' . $buildId . '/';
|
2013-05-19 07:47:41 +02:00
|
|
|
|
2013-05-22 20:17:33 +02:00
|
|
|
$this->setInterpolationVars();
|
|
|
|
|
2013-05-16 03:57:02 +02:00
|
|
|
// Create a working copy of the project:
|
|
|
|
if (!$this->build->createWorkingCopy($this, $this->buildPath)) {
|
2013-10-10 02:01:06 +02:00
|
|
|
throw new \Exception('Could not create a working copy.');
|
2013-05-16 03:57:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Does the project's phpci.yml request verbose mode?
|
|
|
|
if (!isset($this->config['build_settings']['verbose']) || !$this->config['build_settings']['verbose']) {
|
|
|
|
$this->verbose = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Does the project have any paths it wants plugins to ignore?
|
|
|
|
if (isset($this->config['build_settings']['ignore'])) {
|
|
|
|
$this->ignore = $this->config['build_settings']['ignore'];
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->logSuccess('Working copy created: ' . $this->buildPath);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Execute a the appropriate set of plugins for a given build stage.
|
|
|
|
*/
|
|
|
|
protected function executePlugins($stage)
|
|
|
|
{
|
|
|
|
// Ignore any stages for which we don't have plugins set:
|
|
|
|
if (!array_key_exists($stage, $this->config) || !is_array($this->config[$stage])) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($this->config[$stage] as $plugin => $options) {
|
|
|
|
$this->log('RUNNING PLUGIN: ' . $plugin);
|
|
|
|
|
|
|
|
// Is this plugin allowed to fail?
|
|
|
|
if ($stage == 'test' && !isset($options['allow_failures'])) {
|
|
|
|
$options['allow_failures'] = false;
|
|
|
|
}
|
|
|
|
|
2013-10-10 02:01:06 +02:00
|
|
|
// Try and execute it:
|
|
|
|
if ($this->executePlugin($plugin, $options)) {
|
2013-05-16 03:57:02 +02:00
|
|
|
|
2013-10-10 02:01:06 +02:00
|
|
|
// Execution was successful:
|
|
|
|
$this->logSuccess('PLUGIN STATUS: SUCCESS!');
|
2013-05-16 03:57:02 +02:00
|
|
|
|
2013-10-10 02:01:06 +02:00
|
|
|
} else {
|
2013-05-16 03:57:02 +02:00
|
|
|
|
2013-10-10 02:01:06 +02:00
|
|
|
// If we're in the "test" stage and the plugin is not allowed to fail,
|
|
|
|
// then mark the build as failed:
|
|
|
|
if ($stage == 'test' && !$options['allow_failures']) {
|
|
|
|
$this->success = false;
|
2013-05-16 03:57:02 +02:00
|
|
|
}
|
|
|
|
|
2013-10-10 02:01:06 +02:00
|
|
|
$this->logFailure('PLUGIN STATUS: FAILED');
|
2013-05-16 03:57:02 +02:00
|
|
|
}
|
2013-10-10 02:01:06 +02:00
|
|
|
}
|
|
|
|
}
|
2013-05-16 03:57:02 +02:00
|
|
|
|
2013-10-10 02:01:06 +02:00
|
|
|
/**
|
|
|
|
* Executes a given plugin, with options and returns the result.
|
|
|
|
*/
|
|
|
|
protected function executePlugin($plugin, $options)
|
|
|
|
{
|
|
|
|
// Figure out the class name and check the plugin exists:
|
|
|
|
$class = str_replace('_', ' ', $plugin);
|
|
|
|
$class = ucwords($class);
|
|
|
|
$class = 'PHPCI\\Plugin\\' . str_replace(' ', '', $class);
|
2013-05-16 03:57:02 +02:00
|
|
|
|
2013-10-10 02:01:06 +02:00
|
|
|
if (!class_exists($class)) {
|
|
|
|
$this->logFailure('Plugin does not exist: ' . $plugin);
|
|
|
|
return false;
|
|
|
|
}
|
2013-05-16 03:57:02 +02:00
|
|
|
|
2013-10-10 02:01:06 +02:00
|
|
|
$rtn = true;
|
2013-05-16 03:57:02 +02:00
|
|
|
|
2013-10-10 02:01:06 +02:00
|
|
|
// Try running it:
|
|
|
|
try {
|
|
|
|
$obj = new $class($this, $this->build, $options);
|
2013-05-16 03:57:02 +02:00
|
|
|
|
2013-10-10 02:01:06 +02:00
|
|
|
if (!$obj->execute()) {
|
|
|
|
$rtn = false;
|
2013-05-16 03:57:02 +02:00
|
|
|
}
|
2013-10-10 02:01:06 +02:00
|
|
|
} catch (\Exception $ex) {
|
|
|
|
$this->logFailure('EXCEPTION: ' . $ex->getMessage());
|
|
|
|
$rtn = false;
|
2013-05-16 03:57:02 +02:00
|
|
|
}
|
|
|
|
|
2013-10-10 02:01:06 +02:00
|
|
|
return $rtn;
|
2013-10-08 08:21:46 +02:00
|
|
|
}
|
2013-10-08 09:50:10 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Find a binary required by a plugin.
|
|
|
|
* @param $binary
|
|
|
|
* @return null|string
|
|
|
|
*/
|
|
|
|
public function findBinary($binary)
|
|
|
|
{
|
|
|
|
if (is_string($binary)) {
|
|
|
|
$binary = array($binary);
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($binary as $bin) {
|
|
|
|
// Check project root directory:
|
|
|
|
if (is_file(PHPCI_DIR . $bin)) {
|
|
|
|
return PHPCI_DIR . $bin;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check Composer bin dir:
|
|
|
|
if (is_file(PHPCI_DIR . 'vendor/bin/' . $bin)) {
|
|
|
|
return PHPCI_DIR . 'vendor/bin/' . $bin;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Use "which"
|
|
|
|
$which = trim(shell_exec('which ' . $bin));
|
|
|
|
|
|
|
|
if (!empty($which)) {
|
|
|
|
return $which;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
2013-10-26 17:15:29 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets a logger instance on the object
|
|
|
|
*
|
|
|
|
* @param LoggerInterface $logger
|
|
|
|
* @return null
|
|
|
|
*/
|
|
|
|
public function setLogger(LoggerInterface $logger) {
|
|
|
|
$this->logger = $logger;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* returns the logger attached to this builder.
|
|
|
|
*
|
|
|
|
* @return LoggerInterface
|
|
|
|
*/
|
|
|
|
public function getLogger() {
|
|
|
|
return $this->logger;
|
|
|
|
}
|
2013-05-16 03:57:02 +02:00
|
|
|
}
|