Completed
Pull Request — master (#489)
by Helpful
03:51
created
code/backends/PackageGenerator.php 1 patch
Indentation   +51 added lines, -51 removed lines patch added patch discarded remove patch
@@ -9,61 +9,61 @@
 block discarded – undo
9 9
 abstract class PackageGenerator
10 10
 {
11 11
 
12
-    protected $cache;
12
+	protected $cache;
13 13
 
14
-    /**
15
-     * Generate the package file, saving to the given location
16
-     *
17
-     * @param string $baseDir string The base directory of the project, checked out from git.
18
-     * @param $outputFilename string The filename to write to.
19
-     * @param string $sha
20
-     *
21
-     * @return boolean True on success
22
-     */
23
-    abstract public function generatePackage($sha, $baseDir, $outputFilename, DeploynautLogFile $log);
14
+	/**
15
+	 * Generate the package file, saving to the given location
16
+	 *
17
+	 * @param string $baseDir string The base directory of the project, checked out from git.
18
+	 * @param $outputFilename string The filename to write to.
19
+	 * @param string $sha
20
+	 *
21
+	 * @return boolean True on success
22
+	 */
23
+	abstract public function generatePackage($sha, $baseDir, $outputFilename, DeploynautLogFile $log);
24 24
 
25
-    /**
26
-     * Return a string that uniquely identifies this package generator.
27
-     *
28
-     * This will be used as part of cache keys; if meaningful changes to the operation of the generator are
29
-     * made, then the identifier should change. Note there is no need to include the classname in the
30
-     * identifier; callers to getIdentifier() should be responsible for disambiguating based on class.
31
-     */
32
-    abstract public function getIdentifier();
25
+	/**
26
+	 * Return a string that uniquely identifies this package generator.
27
+	 *
28
+	 * This will be used as part of cache keys; if meaningful changes to the operation of the generator are
29
+	 * made, then the identifier should change. Note there is no need to include the classname in the
30
+	 * identifier; callers to getIdentifier() should be responsible for disambiguating based on class.
31
+	 */
32
+	abstract public function getIdentifier();
33 33
 
34
-    public function getCache()
35
-    {
36
-        return $this->cache;
37
-    }
34
+	public function getCache()
35
+	{
36
+		return $this->cache;
37
+	}
38 38
 
39
-    public function setCache(PackageCache $cache)
40
-    {
41
-        $this->cache = $cache;
42
-    }
39
+	public function setCache(PackageCache $cache)
40
+	{
41
+		$this->cache = $cache;
42
+	}
43 43
 
44
-    /**
45
-     * Generate or retrieve a package from the cache
46
-     *
47
-     * @param string $identifier A unique identifier for the generator; used to partition the cache
48
-     * @param string $sha The SHA of the commit to be deployed
49
-     * @param string $repositoryDir The directory where the repository resides
50
-     * @param DeploynautLogFile $log The log to write status output to, including package-generation commands
51
-     *
52
-     * @return string
53
-     */
54
-    public function getPackageFilename($identifier, $sha, $repositoryDir, DeploynautLogFile $log)
55
-    {
56
-        // Fetch through the cache
57
-        if ($this->cache) {
58
-            $identifier .= '-' . get_class($this) . '-' . $this->getIdentifier();
59
-            return $this->cache->getPackageFilename($this, $identifier, $sha, $repositoryDir, $log);
44
+	/**
45
+	 * Generate or retrieve a package from the cache
46
+	 *
47
+	 * @param string $identifier A unique identifier for the generator; used to partition the cache
48
+	 * @param string $sha The SHA of the commit to be deployed
49
+	 * @param string $repositoryDir The directory where the repository resides
50
+	 * @param DeploynautLogFile $log The log to write status output to, including package-generation commands
51
+	 *
52
+	 * @return string
53
+	 */
54
+	public function getPackageFilename($identifier, $sha, $repositoryDir, DeploynautLogFile $log)
55
+	{
56
+		// Fetch through the cache
57
+		if ($this->cache) {
58
+			$identifier .= '-' . get_class($this) . '-' . $this->getIdentifier();
59
+			return $this->cache->getPackageFilename($this, $identifier, $sha, $repositoryDir, $log);
60 60
 
61
-        // Default, cacheless implementation
62
-        } else {
63
-            $filename = TEMP_FOLDER . '/' . $sha . '.tar.gz';
64
-            if ($this->generatePackage($sha, $repositoryDir, $filename, $log)) {
65
-                return $filename;
66
-            }
67
-        }
68
-    }
61
+		// Default, cacheless implementation
62
+		} else {
63
+			$filename = TEMP_FOLDER . '/' . $sha . '.tar.gz';
64
+			if ($this->generatePackage($sha, $repositoryDir, $filename, $log)) {
65
+				return $filename;
66
+			}
67
+		}
68
+	}
69 69
 }
Please login to merge, or discard this patch.
code/backends/SimplePackageGenerator.php 1 patch
Indentation   +71 added lines, -71 removed lines patch added patch discarded remove patch
@@ -8,86 +8,86 @@
 block discarded – undo
8 8
 class SimplePackageGenerator extends PackageGenerator
9 9
 {
10 10
 
11
-    protected $buildScript = "composer install --prefer-dist --no-dev";
11
+	protected $buildScript = "composer install --prefer-dist --no-dev";
12 12
 
13
-    public function getParamMetadata()
14
-    {
15
-        return array(
16
-            'BuildScript' => array('title' => 'Build script'),
17
-        );
18
-    }
13
+	public function getParamMetadata()
14
+	{
15
+		return array(
16
+			'BuildScript' => array('title' => 'Build script'),
17
+		);
18
+	}
19 19
 
20
-    public function getBuildScript()
21
-    {
22
-        return $this->buildScript;
23
-    }
20
+	public function getBuildScript()
21
+	{
22
+		return $this->buildScript;
23
+	}
24 24
 
25
-    public function setBuildScript($buildScript)
26
-    {
27
-        $this->buildScript = $buildScript;
28
-    }
25
+	public function setBuildScript($buildScript)
26
+	{
27
+		$this->buildScript = $buildScript;
28
+	}
29 29
 
30
-    public function getIdentifier()
31
-    {
32
-        // If the build script changes, don't re-use cached items
33
-        return substr(sha1($this->buildScript), 0, 8);
34
-    }
30
+	public function getIdentifier()
31
+	{
32
+		// If the build script changes, don't re-use cached items
33
+		return substr(sha1($this->buildScript), 0, 8);
34
+	}
35 35
 
36
-    /**
37
-     * Generate the package
38
-     */
39
-    public function generatePackage($sha, $baseDir, $outputFilename, DeploynautLogFile $log)
40
-    {
41
-        $tempPath = TEMP_FOLDER . "/" . str_replace(".tar.gz", "", basename($outputFilename));
42
-        if (!file_exists($tempPath)) {
43
-            mkdir($tempPath);
44
-        }
36
+	/**
37
+	 * Generate the package
38
+	 */
39
+	public function generatePackage($sha, $baseDir, $outputFilename, DeploynautLogFile $log)
40
+	{
41
+		$tempPath = TEMP_FOLDER . "/" . str_replace(".tar.gz", "", basename($outputFilename));
42
+		if (!file_exists($tempPath)) {
43
+			mkdir($tempPath);
44
+		}
45 45
 
46
-        $escapedTempPath = escapeshellarg($tempPath);
47
-        $escapedOutputFile = escapeshellarg($outputFilename);
48
-        $escapedTempDir = escapeshellarg(basename($tempPath));
46
+		$escapedTempPath = escapeshellarg($tempPath);
47
+		$escapedOutputFile = escapeshellarg($outputFilename);
48
+		$escapedTempDir = escapeshellarg(basename($tempPath));
49 49
 
50
-        // Execute these in sequence until there's a failure
51
-        $processes = array(
52
-            // Export the relevant SHA into a temp folder
53
-            new Process("git archive $sha | tar -x -C " . $escapedTempPath, $baseDir),
54
-            // Run build script
55
-            new Process($this->buildScript, $tempPath, null, null, 3600),
56
-            // Compress the result
57
-            new Process("tar -czf " . $escapedOutputFile . " " . $escapedTempDir, dirname($tempPath)),
58
-        );
50
+		// Execute these in sequence until there's a failure
51
+		$processes = array(
52
+			// Export the relevant SHA into a temp folder
53
+			new Process("git archive $sha | tar -x -C " . $escapedTempPath, $baseDir),
54
+			// Run build script
55
+			new Process($this->buildScript, $tempPath, null, null, 3600),
56
+			// Compress the result
57
+			new Process("tar -czf " . $escapedOutputFile . " " . $escapedTempDir, dirname($tempPath)),
58
+		);
59 59
 
60
-        // Call at the end, regardless of success or failure
61
-        $cleanup = array(
62
-            // Delete the temporary staging folder
63
-            new Process("rm -rf " . $escapedTempPath),
64
-        );
60
+		// Call at the end, regardless of success or failure
61
+		$cleanup = array(
62
+			// Delete the temporary staging folder
63
+			new Process("rm -rf " . $escapedTempPath),
64
+		);
65 65
 
66
-        try {
67
-            $this->executeProcesses($processes, $log);
68
-        } catch (Exception $e) {
69
-            // Execute cleanup on failure
70
-            $this->executeProcesses($cleanup, $log);
71
-            throw $e;
72
-        }
66
+		try {
67
+			$this->executeProcesses($processes, $log);
68
+		} catch (Exception $e) {
69
+			// Execute cleanup on failure
70
+			$this->executeProcesses($cleanup, $log);
71
+			throw $e;
72
+		}
73 73
 
74
-        // Execute cleanup on success
75
-        $this->executeProcesses($cleanup, $log);
76
-        return true;
77
-    }
74
+		// Execute cleanup on success
75
+		$this->executeProcesses($cleanup, $log);
76
+		return true;
77
+	}
78 78
 
79
-    /**
80
-     * Execute an array of processes, one after the other, throwing an exception on the first failure.
81
-     *
82
-     * @param array $processes An array of Symfony\Component\Process\Process objects
83
-     * @param DeploynautLogFile $log The log to send output to
84
-     */
85
-    protected function executeProcesses($processes, DeploynautLogFile $log)
86
-    {
87
-        foreach ($processes as $process) {
88
-            $process->mustRun(function ($type, $buffer) use ($log) {
89
-                $log->write($buffer);
90
-            });
91
-        }
92
-    }
79
+	/**
80
+	 * Execute an array of processes, one after the other, throwing an exception on the first failure.
81
+	 *
82
+	 * @param array $processes An array of Symfony\Component\Process\Process objects
83
+	 * @param DeploynautLogFile $log The log to send output to
84
+	 */
85
+	protected function executeProcesses($processes, DeploynautLogFile $log)
86
+	{
87
+		foreach ($processes as $process) {
88
+			$process->mustRun(function ($type, $buffer) use ($log) {
89
+				$log->write($buffer);
90
+			});
91
+		}
92
+	}
93 93
 }
Please login to merge, or discard this patch.
code/backends/SizeRestrictedPackageCache.php 1 patch
Indentation   +103 added lines, -103 removed lines patch added patch discarded remove patch
@@ -6,117 +6,117 @@
 block discarded – undo
6 6
 class SizeRestrictedPackageCache implements PackageCache
7 7
 {
8 8
 
9
-    protected $cacheSize;
10
-    protected $baseDir;
9
+	protected $cacheSize;
10
+	protected $baseDir;
11 11
 
12
-    /**
13
-     * Set the maximum number of items that will be stored in the package cache.
14
-     *
15
-     * If null, items will never be deleted. If set, cache entries will be touched whenever they are accessed,
16
-     * and the least-recently-access items will be deleted.
17
-     *
18
-     * @param int $cacheSize the number of package files to keep.
19
-     */
20
-    public function setCacheSize($cacheSize)
21
-    {
22
-        $this->cacheSize = $cacheSize;
23
-    }
12
+	/**
13
+	 * Set the maximum number of items that will be stored in the package cache.
14
+	 *
15
+	 * If null, items will never be deleted. If set, cache entries will be touched whenever they are accessed,
16
+	 * and the least-recently-access items will be deleted.
17
+	 *
18
+	 * @param int $cacheSize the number of package files to keep.
19
+	 */
20
+	public function setCacheSize($cacheSize)
21
+	{
22
+		$this->cacheSize = $cacheSize;
23
+	}
24 24
 
25
-    /**
26
-     * The base directory to store cached packages in.
27
-     * The files will be stored in a subdirectory named after the identifier.
28
-     * If base directory isn't set, the getPackageFilename() won't run.
29
-     *
30
-     * @param string $baseDir The base directory
31
-     */
32
-    public function setBaseDir($baseDir)
33
-    {
34
-        $this->baseDir = realpath($baseDir);
35
-    }
25
+	/**
26
+	 * The base directory to store cached packages in.
27
+	 * The files will be stored in a subdirectory named after the identifier.
28
+	 * If base directory isn't set, the getPackageFilename() won't run.
29
+	 *
30
+	 * @param string $baseDir The base directory
31
+	 */
32
+	public function setBaseDir($baseDir)
33
+	{
34
+		$this->baseDir = realpath($baseDir);
35
+	}
36 36
 
37
-    /**
38
-     * Return the filename of the generated package, retrieving from cache or generating as necessary
39
-     *
40
-     * @param PackageGenerator $generator The generator to use to create cache entries.
41
-     * @param string $identifier A unique identifier for the generator; used to partition the cache
42
-     * @param string $sha The SHA of the commit to be deployed
43
-     * @param string $repositoryDir The directory where the repository resides
44
-     * @param DeploynautLogFile $log The log to write status output to, including package-generation commands
45
-     *
46
-     * @return string
47
-     */
48
-    public function getPackageFilename(
49
-        PackageGenerator $generator,
50
-        $identifier, $sha,
51
-        $repositoryDir,
52
-        DeploynautLogFile $log
53
-    ) {
54
-        if (!$this->baseDir) {
55
-            throw new \LogicException("Can't use PackageCache without setting BaseDir");
56
-        }
37
+	/**
38
+	 * Return the filename of the generated package, retrieving from cache or generating as necessary
39
+	 *
40
+	 * @param PackageGenerator $generator The generator to use to create cache entries.
41
+	 * @param string $identifier A unique identifier for the generator; used to partition the cache
42
+	 * @param string $sha The SHA of the commit to be deployed
43
+	 * @param string $repositoryDir The directory where the repository resides
44
+	 * @param DeploynautLogFile $log The log to write status output to, including package-generation commands
45
+	 *
46
+	 * @return string
47
+	 */
48
+	public function getPackageFilename(
49
+		PackageGenerator $generator,
50
+		$identifier, $sha,
51
+		$repositoryDir,
52
+		DeploynautLogFile $log
53
+	) {
54
+		if (!$this->baseDir) {
55
+			throw new \LogicException("Can't use PackageCache without setting BaseDir");
56
+		}
57 57
 
58
-        $buildPath = $this->baseDir . '/' . $this->sanitiseDirName($identifier);
59
-        $filename = "$buildPath/$sha.tar.gz";
58
+		$buildPath = $this->baseDir . '/' . $this->sanitiseDirName($identifier);
59
+		$filename = "$buildPath/$sha.tar.gz";
60 60
 
61
-        if (!file_exists($this->baseDir)) {
62
-            if (!mkdir($this->baseDir)) {
63
-                throw new \LogicException("Can't create base dir {$this->baseDir}");
64
-            }
65
-        }
66
-        if (!file_exists($buildPath)) {
67
-            if (!mkdir($buildPath)) {
68
-                throw new \LogicException("Can't create build path $buildPath");
69
-            }
70
-        }
61
+		if (!file_exists($this->baseDir)) {
62
+			if (!mkdir($this->baseDir)) {
63
+				throw new \LogicException("Can't create base dir {$this->baseDir}");
64
+			}
65
+		}
66
+		if (!file_exists($buildPath)) {
67
+			if (!mkdir($buildPath)) {
68
+				throw new \LogicException("Can't create build path $buildPath");
69
+			}
70
+		}
71 71
 
72
-        if (file_exists($filename)) {
73
-            $log->write("Using previously generated package $filename");
74
-            // This will ensure that our cache garbage collection will remove least-recently-accessed,
75
-            // rather than oldest.
76
-            touch($filename);
77
-            return $filename;
78
-        } else {
79
-            if ($this->cacheSize) {
80
-                $this->reduceDirSizeTo($buildPath, $this->cacheSize - 1, $log);
81
-            }
72
+		if (file_exists($filename)) {
73
+			$log->write("Using previously generated package $filename");
74
+			// This will ensure that our cache garbage collection will remove least-recently-accessed,
75
+			// rather than oldest.
76
+			touch($filename);
77
+			return $filename;
78
+		} else {
79
+			if ($this->cacheSize) {
80
+				$this->reduceDirSizeTo($buildPath, $this->cacheSize - 1, $log);
81
+			}
82 82
 
83
-            if ($generator->generatePackage($sha, $repositoryDir, $filename, $log)) {
84
-                return $filename;
85
-            }
86
-        }
87
-    }
83
+			if ($generator->generatePackage($sha, $repositoryDir, $filename, $log)) {
84
+				return $filename;
85
+			}
86
+		}
87
+	}
88 88
 
89
-    /**
90
-     * Take the identifier an make it safe to use as a directory name.
91
-     *
92
-     * @param string $identifier The unsanitised directory name.
93
-     */
94
-    protected function sanitiseDirName($identifier)
95
-    {
96
-        $safe = preg_replace('/[^A-Za-z0-9_-]/', '', $identifier);
97
-        return $safe ? $safe : 'null';
98
-    }
89
+	/**
90
+	 * Take the identifier an make it safe to use as a directory name.
91
+	 *
92
+	 * @param string $identifier The unsanitised directory name.
93
+	 */
94
+	protected function sanitiseDirName($identifier)
95
+	{
96
+		$safe = preg_replace('/[^A-Za-z0-9_-]/', '', $identifier);
97
+		return $safe ? $safe : 'null';
98
+	}
99 99
 
100
-    /**
101
-     * Delete items in this directory until the number of items is <= $count.
102
-     * Delete the oldest files first.
103
-     *
104
-     * @param string $dir The directory to remove items from
105
-     * @param int $count The maximum number of .tar.gz files that can appear in that directory
106
-     * @param DeploynautLogFile $log The log to send removal status messages to
107
-     */
108
-    protected function reduceDirSizeTo($dir, $count, DeploynautLogFile $log)
109
-    {
110
-        $files = glob($dir . '/*.tar.gz');
111
-        if (sizeof($files) > $count) {
112
-            usort($files, function ($a, $b) {
113
-                return filemtime($a) > filemtime($b);
114
-            });
100
+	/**
101
+	 * Delete items in this directory until the number of items is <= $count.
102
+	 * Delete the oldest files first.
103
+	 *
104
+	 * @param string $dir The directory to remove items from
105
+	 * @param int $count The maximum number of .tar.gz files that can appear in that directory
106
+	 * @param DeploynautLogFile $log The log to send removal status messages to
107
+	 */
108
+	protected function reduceDirSizeTo($dir, $count, DeploynautLogFile $log)
109
+	{
110
+		$files = glob($dir . '/*.tar.gz');
111
+		if (sizeof($files) > $count) {
112
+			usort($files, function ($a, $b) {
113
+				return filemtime($a) > filemtime($b);
114
+			});
115 115
 
116
-            for ($i = 0; $i < sizeof($files) - $count; $i++) {
117
-                $log->write("Removing " . $files[$i] . " from package cache");
118
-                unlink($files[$i]);
119
-            }
120
-        }
121
-    }
116
+			for ($i = 0; $i < sizeof($files) - $count; $i++) {
117
+				$log->write("Removing " . $files[$i] . " from package cache");
118
+				unlink($files[$i]);
119
+			}
120
+		}
121
+	}
122 122
 }
Please login to merge, or discard this patch.
code/control/CheckPipelineStatus.php 1 patch
Indentation   +22 added lines, -22 removed lines patch added patch discarded remove patch
@@ -10,31 +10,31 @@
 block discarded – undo
10 10
  */
11 11
 class CheckPipelineStatus extends Controller
12 12
 {
13
-    public function index()
14
-    {
15
-        // @TODO Check that we have been called via the daemon
16
-        if (php_sapi_name() != "cli") {
17
-            throw new Exception("CheckPipelineStatus must be run from the command-line.");
18
-        }
13
+	public function index()
14
+	{
15
+		// @TODO Check that we have been called via the daemon
16
+		if (php_sapi_name() != "cli") {
17
+			throw new Exception("CheckPipelineStatus must be run from the command-line.");
18
+		}
19 19
 
20
-        $this->run();
21
-    }
20
+		$this->run();
21
+	}
22 22
 
23
-    private function run()
24
-    {
25
-        // Note that a pipeline must be started prior to being picked up by this task
26
-        $runningPipelines = Pipeline::get()->filter('Status', array('Running', 'Rollback'));
23
+	private function run()
24
+	{
25
+		// Note that a pipeline must be started prior to being picked up by this task
26
+		$runningPipelines = Pipeline::get()->filter('Status', array('Running', 'Rollback'));
27 27
 
28
-        printf(
29
-            "%s Checking status of %d pipelines... ",
30
-            "[" . date("Y-m-d H:i:s") . "]",
31
-            $runningPipelines->count()
32
-        );
28
+		printf(
29
+			"%s Checking status of %d pipelines... ",
30
+			"[" . date("Y-m-d H:i:s") . "]",
31
+			$runningPipelines->count()
32
+		);
33 33
 
34
-        foreach ($runningPipelines as $pipeline) {
35
-            $pipeline->checkPipelineStatus();
36
-        }
34
+		foreach ($runningPipelines as $pipeline) {
35
+			$pipeline->checkPipelineStatus();
36
+		}
37 37
 
38
-        echo "done!" . PHP_EOL;
39
-    }
38
+		echo "done!" . PHP_EOL;
39
+	}
40 40
 }
Please login to merge, or discard this patch.
code/control/ConfirmationMessagingService.php 1 patch
Indentation   +10 added lines, -10 removed lines patch added patch discarded remove patch
@@ -7,14 +7,14 @@
 block discarded – undo
7 7
 interface ConfirmationMessagingService
8 8
 {
9 9
 
10
-    /**
11
-     * Sends a message to the specified user
12
-     *
13
-     * @param PipelineData $source Source client step
14
-     * @param string $message Plain text message
15
-     * @param mixed $recipients Either a Member object, string, or array of strings or Member objects
16
-     * @param array $arguments Additional arguments that may be configured
17
-     * @return boolean True if success
18
-     */
19
-    public function sendMessage($source, $message, $recipients, $arguments = array());
10
+	/**
11
+	 * Sends a message to the specified user
12
+	 *
13
+	 * @param PipelineData $source Source client step
14
+	 * @param string $message Plain text message
15
+	 * @param mixed $recipients Either a Member object, string, or array of strings or Member objects
16
+	 * @param array $arguments Additional arguments that may be configured
17
+	 * @return boolean True if success
18
+	 */
19
+	public function sendMessage($source, $message, $recipients, $arguments = array());
20 20
 }
Please login to merge, or discard this patch.
code/control/DNAdmin.php 1 patch
Indentation   +20 added lines, -20 removed lines patch added patch discarded remove patch
@@ -3,27 +3,27 @@
 block discarded – undo
3 3
 class DNAdmin extends ModelAdmin
4 4
 {
5 5
 
6
-    /**
7
-     * @var string
8
-     */
9
-    public static $menu_title = "Deploynaut Projects";
6
+	/**
7
+	 * @var string
8
+	 */
9
+	public static $menu_title = "Deploynaut Projects";
10 10
 
11
-    /**
12
-     * @var string
13
-     */
14
-    public static $url_segment = "naut";
11
+	/**
12
+	 * @var string
13
+	 */
14
+	public static $url_segment = "naut";
15 15
 
16
-    /**
17
-     * @var array
18
-     */
19
-    public static $managed_models = array(
20
-        'DNProject' => array('title' => 'Projects'),
21
-        'DNDataTransfer' => array('title' => 'Transfers'),
22
-        'DNDataArchive' => array('title' => 'Archives'),
23
-    );
16
+	/**
17
+	 * @var array
18
+	 */
19
+	public static $managed_models = array(
20
+		'DNProject' => array('title' => 'Projects'),
21
+		'DNDataTransfer' => array('title' => 'Transfers'),
22
+		'DNDataArchive' => array('title' => 'Archives'),
23
+	);
24 24
 
25
-    /**
26
-     * @var int
27
-     */
28
-    private static $menu_priority = 100;
25
+	/**
26
+	 * @var int
27
+	 */
28
+	private static $menu_priority = 100;
29 29
 }
Please login to merge, or discard this patch.
code/control/DNRoot.php 1 patch
Indentation   +2607 added lines, -2607 removed lines patch added patch discarded remove patch
@@ -10,2611 +10,2611 @@
 block discarded – undo
10 10
 class DNRoot extends Controller implements PermissionProvider, TemplateGlobalProvider
11 11
 {
12 12
 
13
-    /**
14
-     * @const string - action type for actions that perform deployments
15
-     */
16
-    const ACTION_DEPLOY = 'deploy';
17
-
18
-    /**
19
-     * @const string - action type for actions that manipulate snapshots
20
-     */
21
-    const ACTION_SNAPSHOT = 'snapshot';
22
-
23
-    const ACTION_ENVIRONMENTS = 'createenv';
24
-
25
-    /**
26
-     * @var string
27
-     */
28
-    private $actionType = self::ACTION_DEPLOY;
29
-
30
-    /**
31
-     * Bypass pipeline permission code
32
-     */
33
-    const DEPLOYNAUT_BYPASS_PIPELINE = 'DEPLOYNAUT_BYPASS_PIPELINE';
34
-
35
-    /**
36
-     * Allow dryrun of pipelines
37
-     */
38
-    const DEPLOYNAUT_DRYRUN_PIPELINE = 'DEPLOYNAUT_DRYRUN_PIPELINE';
39
-
40
-    /**
41
-     * Allow advanced options on deployments
42
-     */
43
-    const DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS = 'DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS';
44
-
45
-    const ALLOW_PROD_DEPLOYMENT = 'ALLOW_PROD_DEPLOYMENT';
46
-    const ALLOW_NON_PROD_DEPLOYMENT = 'ALLOW_NON_PROD_DEPLOYMENT';
47
-    const ALLOW_PROD_SNAPSHOT = 'ALLOW_PROD_SNAPSHOT';
48
-    const ALLOW_NON_PROD_SNAPSHOT = 'ALLOW_NON_PROD_SNAPSHOT';
49
-    const ALLOW_CREATE_ENVIRONMENT = 'ALLOW_CREATE_ENVIRONMENT';
50
-
51
-    /**
52
-     * @var array
53
-     */
54
-    private static $allowed_actions = array(
55
-        'projects',
56
-        'nav',
57
-        'update',
58
-        'project',
59
-        'toggleprojectstar',
60
-        'branch',
61
-        'environment',
62
-        'abortpipeline',
63
-        'pipeline',
64
-        'pipelinelog',
65
-        'metrics',
66
-        'createenvlog',
67
-        'createenv',
68
-        'getDeployForm',
69
-        'doDeploy',
70
-        'deploy',
71
-        'deploylog',
72
-        'getDataTransferForm',
73
-        'transfer',
74
-        'transferlog',
75
-        'snapshots',
76
-        'createsnapshot',
77
-        'snapshotslog',
78
-        'uploadsnapshot',
79
-        'getCreateEnvironmentForm',
80
-        'getUploadSnapshotForm',
81
-        'getPostSnapshotForm',
82
-        'getDataTransferRestoreForm',
83
-        'getDeleteForm',
84
-        'getMoveForm',
85
-        'restoresnapshot',
86
-        'deletesnapshot',
87
-        'movesnapshot',
88
-        'postsnapshotsuccess',
89
-        'gitRevisions',
90
-        'deploySummary',
91
-        'startDeploy',
92
-        'createproject',
93
-        'CreateProjectForm',
94
-        'createprojectprogress',
95
-        'checkrepoaccess',
96
-    );
97
-
98
-    /**
99
-     * URL handlers pretending that we have a deep URL structure.
100
-     */
101
-    private static $url_handlers = array(
102
-        'project/$Project/environment/$Environment/DeployForm' => 'getDeployForm',
103
-        'project/$Project/createsnapshot/DataTransferForm' => 'getDataTransferForm',
104
-        'project/$Project/DataTransferForm' => 'getDataTransferForm',
105
-        'project/$Project/DataTransferRestoreForm' => 'getDataTransferRestoreForm',
106
-        'project/$Project/DeleteForm' => 'getDeleteForm',
107
-        'project/$Project/MoveForm' => 'getMoveForm',
108
-        'project/$Project/UploadSnapshotForm' => 'getUploadSnapshotForm',
109
-        'project/$Project/PostSnapshotForm' => 'getPostSnapshotForm',
110
-        'project/$Project/environment/$Environment/metrics' => 'metrics',
111
-        'project/$Project/environment/$Environment/pipeline/$Identifier//$Action/$ID/$OtherID' => 'pipeline',
112
-        'project/$Project/environment/$Environment/deploy_summary' => 'deploySummary',
113
-        'project/$Project/environment/$Environment/git_revisions' => 'gitRevisions',
114
-        'project/$Project/environment/$Environment/start-deploy' => 'startDeploy',
115
-        'project/$Project/environment/$Environment/deploy/$Identifier/log' => 'deploylog',
116
-        'project/$Project/environment/$Environment/deploy/$Identifier' => 'deploy',
117
-        'project/$Project/transfer/$Identifier/log' => 'transferlog',
118
-        'project/$Project/transfer/$Identifier' => 'transfer',
119
-        'project/$Project/environment/$Environment' => 'environment',
120
-        'project/$Project/createenv/$Identifier/log' => 'createenvlog',
121
-        'project/$Project/createenv/$Identifier' => 'createenv',
122
-        'project/$Project/CreateEnvironmentForm' => 'getCreateEnvironmentForm',
123
-        'project/$Project/branch' => 'branch',
124
-        'project/$Project/build/$Build' => 'build',
125
-        'project/$Project/restoresnapshot/$DataArchiveID' => 'restoresnapshot',
126
-        'project/$Project/deletesnapshot/$DataArchiveID' => 'deletesnapshot',
127
-        'project/$Project/movesnapshot/$DataArchiveID' => 'movesnapshot',
128
-        'project/$Project/update' => 'update',
129
-        'project/$Project/snapshots' => 'snapshots',
130
-        'project/$Project/createsnapshot' => 'createsnapshot',
131
-        'project/$Project/uploadsnapshot' => 'uploadsnapshot',
132
-        'project/$Project/snapshotslog' => 'snapshotslog',
133
-        'project/$Project/postsnapshotsuccess/$DataArchiveID' => 'postsnapshotsuccess',
134
-        'project/$Project/star' => 'toggleprojectstar',
135
-        'project/$Project/createprojectprogress' => 'createprojectprogress',
136
-        'project/$Project/checkrepoaccess' => 'checkrepoaccess',
137
-        'project/$Project' => 'project',
138
-        'nav/$Project' => 'nav',
139
-        'projects' => 'projects',
140
-    );
141
-
142
-    /**
143
-     * @var array
144
-     */
145
-    protected static $_project_cache = array();
146
-
147
-    /**
148
-     * @var array
149
-     */
150
-    private static $support_links = array();
151
-
152
-    /**
153
-     * @var array
154
-     */
155
-    private static $platform_specific_strings = array();
156
-
157
-    /**
158
-     * @var array
159
-     */
160
-    private static $action_types = array(
161
-        self::ACTION_DEPLOY,
162
-        self::ACTION_SNAPSHOT
163
-    );
164
-
165
-    /**
166
-     * @var DNData
167
-     */
168
-    protected $data;
169
-
170
-    /**
171
-     * Include requirements that deploynaut needs, such as javascript.
172
-     */
173
-    public static function include_requirements()
174
-    {
175
-
176
-        // JS should always go to the bottom, otherwise there's the risk that Requirements
177
-        // puts them halfway through the page to the nearest <script> tag. We don't want that.
178
-        Requirements::set_force_js_to_bottom(true);
179
-
180
-        // todo these should be bundled into the same JS as the others in "static" below.
181
-        // We've deliberately not used combined_files as it can mess with some of the JS used
182
-        // here and cause sporadic errors.
183
-        Requirements::javascript('deploynaut/javascript/jquery.js');
184
-        Requirements::javascript('deploynaut/javascript/bootstrap.js');
185
-        Requirements::javascript('deploynaut/javascript/q.js');
186
-        Requirements::javascript('deploynaut/javascript/tablefilter.js');
187
-        Requirements::javascript('deploynaut/javascript/deploynaut.js');
188
-        Requirements::javascript('deploynaut/javascript/react-with-addons.js');
189
-        Requirements::javascript('deploynaut/javascript/bootstrap.file-input.js');
190
-        Requirements::javascript('deploynaut/thirdparty/select2/dist/js/select2.min.js');
191
-        Requirements::javascript('deploynaut/javascript/material.js');
192
-
193
-        // Load the buildable dependencies only if not loaded centrally.
194
-        if (!is_dir(BASE_PATH . DIRECTORY_SEPARATOR . 'static')) {
195
-            if (\Director::isDev()) {
196
-                \Requirements::javascript('deploynaut/static/bundle-debug.js');
197
-            } else {
198
-                \Requirements::javascript('deploynaut/static/bundle.js');
199
-            }
200
-        }
201
-
202
-        Requirements::css('deploynaut/static/style.css');
203
-    }
204
-
205
-    /**
206
-     * Check for feature flags:
207
-     * - FLAG_SNAPSHOTS_ENABLED: set to true to enable globally
208
-     * - FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS: set to semicolon-separated list of email addresses of allowed users.
209
-     *
210
-     * @return boolean
211
-     */
212
-    public static function FlagSnapshotsEnabled()
213
-    {
214
-        if (defined('FLAG_SNAPSHOTS_ENABLED') && FLAG_SNAPSHOTS_ENABLED) {
215
-            return true;
216
-        }
217
-        if (defined('FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS') && FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS) {
218
-            $allowedMembers = explode(';', FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS);
219
-            $member = Member::currentUser();
220
-            if ($allowedMembers && $member && in_array($member->Email, $allowedMembers)) {
221
-                return true;
222
-            }
223
-        }
224
-        return false;
225
-    }
226
-
227
-    /**
228
-     * @return ArrayList
229
-     */
230
-    public static function get_support_links()
231
-    {
232
-        $supportLinks = self::config()->support_links;
233
-        if ($supportLinks) {
234
-            return new ArrayList($supportLinks);
235
-        }
236
-    }
237
-
238
-    /**
239
-     * @return array
240
-     */
241
-    public static function get_template_global_variables()
242
-    {
243
-        return array(
244
-            'RedisUnavailable' => 'RedisUnavailable',
245
-            'RedisWorkersCount' => 'RedisWorkersCount',
246
-            'SidebarLinks' => 'SidebarLinks',
247
-            "SupportLinks" => 'get_support_links'
248
-        );
249
-    }
250
-
251
-    /**
252
-     */
253
-    public function init()
254
-    {
255
-        parent::init();
256
-
257
-        if (!Member::currentUser() && !Session::get('AutoLoginHash')) {
258
-            return Security::permissionFailure();
259
-        }
260
-
261
-        // Block framework jquery
262
-        Requirements::block(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
263
-
264
-        self::include_requirements();
265
-    }
266
-
267
-    /**
268
-     * @return string
269
-     */
270
-    public function Link()
271
-    {
272
-        return "naut/";
273
-    }
274
-
275
-    /**
276
-     * Actions
277
-     *
278
-     * @param SS_HTTPRequest $request
279
-     * @return \SS_HTTPResponse
280
-     */
281
-    public function index(SS_HTTPRequest $request)
282
-    {
283
-        return $this->redirect($this->Link() . 'projects/');
284
-    }
285
-
286
-    /**
287
-     * Action
288
-     *
289
-     * @param SS_HTTPRequest $request
290
-     * @return string - HTML
291
-     */
292
-    public function projects(SS_HTTPRequest $request)
293
-    {
294
-        // Performs canView permission check by limiting visible projects in DNProjectsList() call.
295
-        return $this->customise(array(
296
-            'Title' => 'Projects',
297
-        ))->render();
298
-    }
299
-
300
-    /**
301
-     * @param SS_HTTPRequest $request
302
-     * @return HTMLText
303
-     */
304
-    public function nav(SS_HTTPRequest $request)
305
-    {
306
-        return $this->renderWith('Nav');
307
-    }
308
-
309
-    /**
310
-     * Return a link to the navigation template used for AJAX requests.
311
-     * @return string
312
-     */
313
-    public function NavLink()
314
-    {
315
-        $currentProject = $this->getCurrentProject();
316
-        $projectName = $currentProject ? $currentProject->Name : null;
317
-        return Controller::join_links(Director::absoluteBaseURL(), 'naut', 'nav', $projectName);
318
-    }
319
-
320
-    /**
321
-     * Action
322
-     *
323
-     * @param SS_HTTPRequest $request
324
-     * @return SS_HTTPResponse - HTML
325
-     */
326
-    public function snapshots(SS_HTTPRequest $request)
327
-    {
328
-        $this->setCurrentActionType(self::ACTION_SNAPSHOT);
329
-        return $this->getCustomisedViewSection('SnapshotsSection', 'Data Snapshots');
330
-    }
331
-
332
-    /**
333
-     * Action
334
-     *
335
-     * @param SS_HTTPRequest $request
336
-     * @return string - HTML
337
-     */
338
-    public function createsnapshot(SS_HTTPRequest $request)
339
-    {
340
-        $this->setCurrentActionType(self::ACTION_SNAPSHOT);
341
-
342
-        // Performs canView permission check by limiting visible projects
343
-        $project = $this->getCurrentProject();
344
-        if (!$project) {
345
-            return $this->project404Response();
346
-        }
347
-
348
-        if (!$project->canBackup()) {
349
-            return new SS_HTTPResponse("Not allowed to create snapshots on any environments", 401);
350
-        }
351
-
352
-        return $this->customise(array(
353
-            'Title' => 'Create Data Snapshot',
354
-            'SnapshotsSection' => 1,
355
-            'DataTransferForm' => $this->getDataTransferForm($request)
356
-        ))->render();
357
-    }
358
-
359
-    /**
360
-     * Action
361
-     *
362
-     * @param SS_HTTPRequest $request
363
-     * @return string - HTML
364
-     */
365
-    public function uploadsnapshot(SS_HTTPRequest $request)
366
-    {
367
-        $this->setCurrentActionType(self::ACTION_SNAPSHOT);
368
-
369
-        // Performs canView permission check by limiting visible projects
370
-        $project = $this->getCurrentProject();
371
-        if (!$project) {
372
-            return $this->project404Response();
373
-        }
374
-
375
-        if (!$project->canUploadArchive()) {
376
-            return new SS_HTTPResponse("Not allowed to upload", 401);
377
-        }
378
-
379
-        return $this->customise(array(
380
-            'SnapshotsSection' => 1,
381
-            'UploadSnapshotForm' => $this->getUploadSnapshotForm($request),
382
-            'PostSnapshotForm' => $this->getPostSnapshotForm($request)
383
-        ))->render();
384
-    }
385
-
386
-    /**
387
-     * Return the upload limit for snapshot uploads
388
-     * @return string
389
-     */
390
-    public function UploadLimit()
391
-    {
392
-        return File::format_size(min(
393
-            File::ini2bytes(ini_get('upload_max_filesize')),
394
-            File::ini2bytes(ini_get('post_max_size'))
395
-        ));
396
-    }
397
-
398
-    /**
399
-     * Construct the upload form.
400
-     *
401
-     * @param SS_HTTPRequest $request
402
-     * @return Form
403
-     */
404
-    public function getUploadSnapshotForm(SS_HTTPRequest $request)
405
-    {
406
-        // Performs canView permission check by limiting visible projects
407
-        $project = $this->getCurrentProject();
408
-        if (!$project) {
409
-            return $this->project404Response();
410
-        }
411
-
412
-        if (!$project->canUploadArchive()) {
413
-            return new SS_HTTPResponse("Not allowed to upload", 401);
414
-        }
415
-
416
-        // Framing an environment as a "group of people with download access"
417
-        // makes more sense to the user here, while still allowing us to enforce
418
-        // environment specific restrictions on downloading the file later on.
419
-        $envs = $project->DNEnvironmentList()->filterByCallback(function ($item) {
420
-            return $item->canUploadArchive();
421
-        });
422
-        $envsMap = array();
423
-        foreach ($envs as $env) {
424
-            $envsMap[$env->ID] = $env->Name;
425
-        }
426
-
427
-        $maxSize = min(File::ini2bytes(ini_get('upload_max_filesize')), File::ini2bytes(ini_get('post_max_size')));
428
-        $fileField = DataArchiveFileField::create('ArchiveFile', 'File');
429
-        $fileField->getValidator()->setAllowedExtensions(array('sspak'));
430
-        $fileField->getValidator()->setAllowedMaxFileSize(array('*' => $maxSize));
431
-
432
-        $form = Form::create(
433
-            $this,
434
-            'UploadSnapshotForm',
435
-            FieldList::create(
436
-                $fileField,
437
-                DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
438
-                DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
439
-                    ->setEmptyString('Select an environment')
440
-            ),
441
-            FieldList::create(
442
-                FormAction::create('doUploadSnapshot', 'Upload File')
443
-                    ->addExtraClass('btn')
444
-            ),
445
-            RequiredFields::create('ArchiveFile')
446
-        );
447
-
448
-        $form->disableSecurityToken();
449
-        $form->addExtraClass('fields-wide');
450
-        // Tweak the action so it plays well with our fake URL structure.
451
-        $form->setFormAction($project->Link() . '/UploadSnapshotForm');
452
-
453
-        return $form;
454
-    }
455
-
456
-    /**
457
-     * @param array $data
458
-     * @param Form $form
459
-     *
460
-     * @return bool|HTMLText|SS_HTTPResponse
461
-     */
462
-    public function doUploadSnapshot($data, Form $form)
463
-    {
464
-        $this->setCurrentActionType(self::ACTION_SNAPSHOT);
465
-
466
-        // Performs canView permission check by limiting visible projects
467
-        $project = $this->getCurrentProject();
468
-        if (!$project) {
469
-            return $this->project404Response();
470
-        }
471
-
472
-        $validEnvs = $project->DNEnvironmentList()
473
-            ->filterByCallback(function ($item) {
474
-                return $item->canUploadArchive();
475
-            });
476
-
477
-        // Validate $data['EnvironmentID'] by checking against $validEnvs.
478
-        $environment = $validEnvs->find('ID', $data['EnvironmentID']);
479
-        if (!$environment) {
480
-            throw new LogicException('Invalid environment');
481
-        }
482
-
483
-        $this->validateSnapshotMode($data['Mode']);
484
-
485
-        $dataArchive = DNDataArchive::create(array(
486
-            'AuthorID' => Member::currentUserID(),
487
-            'EnvironmentID' => $data['EnvironmentID'],
488
-            'IsManualUpload' => true,
489
-        ));
490
-        // needs an ID and transfer to determine upload path
491
-        $dataArchive->write();
492
-        $dataTransfer = DNDataTransfer::create(array(
493
-            'AuthorID' => Member::currentUserID(),
494
-            'Mode' => $data['Mode'],
495
-            'Origin' => 'ManualUpload',
496
-            'EnvironmentID' => $data['EnvironmentID']
497
-        ));
498
-        $dataTransfer->write();
499
-        $dataArchive->DataTransfers()->add($dataTransfer);
500
-        $form->saveInto($dataArchive);
501
-        $dataArchive->write();
502
-        $workingDir = TEMP_FOLDER . DIRECTORY_SEPARATOR . 'deploynaut-transfer-' . $dataTransfer->ID;
503
-
504
-        $cleanupFn = function () use ($workingDir, $dataTransfer, $dataArchive) {
505
-            $process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir)));
506
-            $process->run();
507
-            $dataTransfer->delete();
508
-            $dataArchive->delete();
509
-        };
510
-
511
-        // extract the sspak contents so we can inspect them
512
-        try {
513
-            $dataArchive->extractArchive($workingDir);
514
-        } catch (Exception $e) {
515
-            $cleanupFn();
516
-            $form->sessionMessage(
517
-                'There was a problem trying to open your snapshot for processing. Please try uploading again',
518
-                'bad'
519
-            );
520
-            return $this->redirectBack();
521
-        }
522
-
523
-        // validate that the sspak contents match the declared contents
524
-        $result = $dataArchive->validateArchiveContents();
525
-        if (!$result->valid()) {
526
-            $cleanupFn();
527
-            $form->sessionMessage($result->message(), 'bad');
528
-            return $this->redirectBack();
529
-        }
530
-
531
-        // fix file permissions of extracted sspak files then re-build the sspak
532
-        try {
533
-            $dataArchive->fixArchivePermissions($workingDir);
534
-            $dataArchive->setArchiveFromFiles($workingDir);
535
-        } catch (Exception $e) {
536
-            $cleanupFn();
537
-            $form->sessionMessage(
538
-                'There was a problem processing your snapshot. Please try uploading again',
539
-                'bad'
540
-            );
541
-            return $this->redirectBack();
542
-        }
543
-
544
-        // cleanup any extracted sspak contents lying around
545
-        $process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir)));
546
-        $process->run();
547
-
548
-        return $this->customise(array(
549
-            'Project' => $project,
550
-            'CurrentProject' => $project,
551
-            'SnapshotsSection' => 1,
552
-            'DataArchive' => $dataArchive,
553
-            'DataTransferRestoreForm' => $this->getDataTransferRestoreForm($this->request, $dataArchive),
554
-            'BackURL' => $project->Link('snapshots')
555
-        ))->renderWith(array('DNRoot_uploadsnapshot', 'DNRoot'));
556
-    }
557
-
558
-    /**
559
-     * @param SS_HTTPRequest $request
560
-     * @return Form
561
-     */
562
-    public function getPostSnapshotForm(SS_HTTPRequest $request)
563
-    {
564
-        // Performs canView permission check by limiting visible projects
565
-        $project = $this->getCurrentProject();
566
-        if (!$project) {
567
-            return $this->project404Response();
568
-        }
569
-
570
-        if (!$project->canUploadArchive()) {
571
-            return new SS_HTTPResponse("Not allowed to upload", 401);
572
-        }
573
-
574
-        // Framing an environment as a "group of people with download access"
575
-        // makes more sense to the user here, while still allowing us to enforce
576
-        // environment specific restrictions on downloading the file later on.
577
-        $envs = $project->DNEnvironmentList()->filterByCallback(function ($item) {
578
-            return $item->canUploadArchive();
579
-        });
580
-        $envsMap = array();
581
-        foreach ($envs as $env) {
582
-            $envsMap[$env->ID] = $env->Name;
583
-        }
584
-
585
-        $form = Form::create(
586
-            $this,
587
-            'PostSnapshotForm',
588
-            FieldList::create(
589
-                DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
590
-                DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
591
-                    ->setEmptyString('Select an environment')
592
-            ),
593
-            FieldList::create(
594
-                FormAction::create('doPostSnapshot', 'Submit request')
595
-                    ->addExtraClass('btn')
596
-            ),
597
-            RequiredFields::create('File')
598
-        );
599
-
600
-        $form->disableSecurityToken();
601
-        $form->addExtraClass('fields-wide');
602
-        // Tweak the action so it plays well with our fake URL structure.
603
-        $form->setFormAction($project->Link() . '/PostSnapshotForm');
604
-
605
-        return $form;
606
-    }
607
-
608
-    /**
609
-     * @param array $data
610
-     * @param Form $form
611
-     *
612
-     * @return SS_HTTPResponse
613
-     */
614
-    public function doPostSnapshot($data, $form)
615
-    {
616
-        $this->setCurrentActionType(self::ACTION_SNAPSHOT);
617
-
618
-        $project = $this->getCurrentProject();
619
-        if (!$project) {
620
-            return $this->project404Response();
621
-        }
622
-
623
-        $validEnvs = $project->DNEnvironmentList()->filterByCallback(function ($item) {
624
-                return $item->canUploadArchive();
625
-        });
626
-
627
-        // Validate $data['EnvironmentID'] by checking against $validEnvs.
628
-        $environment = $validEnvs->find('ID', $data['EnvironmentID']);
629
-        if (!$environment) {
630
-            throw new LogicException('Invalid environment');
631
-        }
632
-
633
-        $dataArchive = DNDataArchive::create(array(
634
-            'UploadToken' => DNDataArchive::generate_upload_token(),
635
-        ));
636
-        $form->saveInto($dataArchive);
637
-        $dataArchive->write();
638
-
639
-        return $this->redirect(Controller::join_links(
640
-            $project->Link(),
641
-            'postsnapshotsuccess',
642
-            $dataArchive->ID
643
-        ));
644
-    }
645
-
646
-    /**
647
-     * Action
648
-     *
649
-     * @param SS_HTTPRequest $request
650
-     * @return SS_HTTPResponse - HTML
651
-     */
652
-    public function snapshotslog(SS_HTTPRequest $request)
653
-    {
654
-        $this->setCurrentActionType(self::ACTION_SNAPSHOT);
655
-        return $this->getCustomisedViewSection('SnapshotsSection', 'Data Snapshots Log');
656
-    }
657
-
658
-    /**
659
-     * @param SS_HTTPRequest $request
660
-     * @return SS_HTTPResponse|string
661
-     * @throws SS_HTTPResponse_Exception
662
-     */
663
-    public function postsnapshotsuccess(SS_HTTPRequest $request)
664
-    {
665
-        $this->setCurrentActionType(self::ACTION_SNAPSHOT);
666
-
667
-        // Performs canView permission check by limiting visible projects
668
-        $project = $this->getCurrentProject();
669
-        if (!$project) {
670
-            return $this->project404Response();
671
-        }
672
-
673
-        if (!$project->canUploadArchive()) {
674
-            return new SS_HTTPResponse("Not allowed to upload", 401);
675
-        }
676
-
677
-        $dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
678
-        if (!$dataArchive) {
679
-            return new SS_HTTPResponse("Archive not found.", 404);
680
-        }
681
-
682
-        if (!$dataArchive->canRestore()) {
683
-            throw new SS_HTTPResponse_Exception('Not allowed to restore archive', 403);
684
-        }
685
-
686
-        return $this->render(array(
687
-                'Title' => 'How to send us your Data Snapshot by post',
688
-                'DataArchive' => $dataArchive,
689
-                'Address' => Config::inst()->get('Deploynaut', 'snapshot_post_address'),
690
-                'BackURL' => $project->Link(),
691
-            ));
692
-    }
693
-
694
-    /**
695
-     * @param SS_HTTPRequest $request
696
-     * @return \SS_HTTPResponse
697
-     */
698
-    public function project(SS_HTTPRequest $request)
699
-    {
700
-        return $this->getCustomisedViewSection('ProjectOverview', '', array('IsAdmin' => Permission::check('ADMIN')));
701
-    }
702
-
703
-    /**
704
-     * This action will star / unstar a project for the current member
705
-     *
706
-     * @param SS_HTTPRequest $request
707
-     *
708
-     * @return SS_HTTPResponse
709
-     */
710
-    public function toggleprojectstar(SS_HTTPRequest $request)
711
-    {
712
-        $project = $this->getCurrentProject();
713
-        if (!$project) {
714
-            return $this->project404Response();
715
-        }
716
-
717
-        $member = Member::currentUser();
718
-        if ($member === null) {
719
-            return $this->project404Response();
720
-        }
721
-        $favProject = $member->StarredProjects()
722
-            ->filter('DNProjectID', $project->ID)
723
-            ->first();
724
-
725
-        if ($favProject) {
726
-            $member->StarredProjects()->remove($favProject);
727
-        } else {
728
-            $member->StarredProjects()->add($project);
729
-        }
730
-        return $this->redirectBack();
731
-    }
732
-
733
-    /**
734
-     * @param SS_HTTPRequest $request
735
-     * @return \SS_HTTPResponse
736
-     */
737
-    public function branch(SS_HTTPRequest $request)
738
-    {
739
-        $project = $this->getCurrentProject();
740
-        if (!$project) {
741
-            return $this->project404Response();
742
-        }
743
-
744
-        $branchName = $request->getVar('name');
745
-        $branch = $project->DNBranchList()->byName($branchName);
746
-        if (!$branch) {
747
-            return new SS_HTTPResponse("Branch '" . Convert::raw2xml($branchName) . "' not found.", 404);
748
-        }
749
-
750
-        return $this->render(array(
751
-            'CurrentBranch' => $branch,
752
-        ));
753
-    }
754
-
755
-    /**
756
-     * @param SS_HTTPRequest $request
757
-     * @return \SS_HTTPResponse
758
-     */
759
-    public function environment(SS_HTTPRequest $request)
760
-    {
761
-        // Performs canView permission check by limiting visible projects
762
-        $project = $this->getCurrentProject();
763
-        if (!$project) {
764
-            return $this->project404Response();
765
-        }
766
-
767
-        // Performs canView permission check by limiting visible projects
768
-        $env = $this->getCurrentEnvironment($project);
769
-        if (!$env) {
770
-            return $this->environment404Response();
771
-        }
772
-
773
-        return $this->render(array(
774
-            'DNEnvironmentList' => $this->getCurrentProject()->DNEnvironmentList(),
775
-            'FlagSnapshotsEnabled' => $this->FlagSnapshotsEnabled(),
776
-        ));
777
-    }
778
-
779
-
780
-    /**
781
-     * Initiate a pipeline dry run
782
-     *
783
-     * @param array $data
784
-     * @param DeployForm $form
785
-     *
786
-     * @return SS_HTTPResponse
787
-     */
788
-    public function doDryRun($data, DeployForm $form)
789
-    {
790
-        return $this->beginPipeline($data, $form, true);
791
-    }
792
-
793
-    /**
794
-     * Initiate a pipeline
795
-     *
796
-     * @param array $data
797
-     * @param DeployForm $form
798
-     * @return \SS_HTTPResponse
799
-     */
800
-    public function startPipeline($data, $form)
801
-    {
802
-        return $this->beginPipeline($data, $form);
803
-    }
804
-
805
-    /**
806
-     * Start a pipeline
807
-     *
808
-     * @param array $data
809
-     * @param DeployForm $form
810
-     * @param bool $isDryRun
811
-     * @return \SS_HTTPResponse
812
-     */
813
-    protected function beginPipeline($data, DeployForm $form, $isDryRun = false)
814
-    {
815
-        $buildName = $form->getSelectedBuild($data);
816
-
817
-        // Performs canView permission check by limiting visible projects
818
-        $project = $this->getCurrentProject();
819
-        if (!$project) {
820
-            return $this->project404Response();
821
-        }
822
-
823
-        // Performs canView permission check by limiting visible projects
824
-        $environment = $this->getCurrentEnvironment($project);
825
-        if (!$environment) {
826
-            return $this->environment404Response();
827
-        }
828
-
829
-        if (!$environment->DryRunEnabled && $isDryRun) {
830
-            return new SS_HTTPResponse("Dry-run for pipelines is not enabled for this environment", 404);
831
-        }
832
-
833
-        // Initiate the pipeline
834
-        $sha = $project->DNBuildList()->byName($buildName);
835
-        $pipeline = Pipeline::create();
836
-        $pipeline->DryRun = $isDryRun;
837
-        $pipeline->EnvironmentID = $environment->ID;
838
-        $pipeline->AuthorID = Member::currentUserID();
839
-        $pipeline->SHA = $sha->FullName();
840
-        // Record build at time of execution
841
-        if ($currentBuild = $environment->CurrentBuild()) {
842
-            $pipeline->PreviousDeploymentID = $currentBuild->ID;
843
-        }
844
-        $pipeline->start(); // start() will call write(), so no need to do it here as well.
845
-        return $this->redirect($environment->Link());
846
-    }
847
-
848
-    /**
849
-     * @param SS_HTTPRequest $request
850
-     *
851
-     * @return SS_HTTPResponse
852
-     * @throws SS_HTTPResponse_Exception
853
-     */
854
-    public function pipeline(SS_HTTPRequest $request)
855
-    {
856
-        $params = $request->params();
857
-        $pipeline = Pipeline::get()->byID($params['Identifier']);
858
-
859
-        if (!$pipeline || !$pipeline->ID || !$pipeline->Environment()) {
860
-            throw new SS_HTTPResponse_Exception('Pipeline not found', 404);
861
-        }
862
-        if (!$pipeline->Environment()->canView()) {
863
-            return Security::permissionFailure();
864
-        }
865
-
866
-        $environment = $pipeline->Environment();
867
-        $project = $pipeline->Environment()->Project();
868
-
869
-        if ($environment->Name != $params['Environment']) {
870
-            throw new LogicException("Environment in URL doesn't match this pipeline");
871
-        }
872
-        if ($project->Name != $params['Project']) {
873
-            throw new LogicException("Project in URL doesn't match this pipeline");
874
-        }
875
-
876
-        // Delegate to sub-requesthandler
877
-        return PipelineController::create($this, $pipeline);
878
-    }
879
-
880
-    /**
881
-     * Shows the creation log.
882
-     *
883
-     * @param SS_HTTPRequest $request
884
-     * @return string
885
-     */
886
-    public function createenv(SS_HTTPRequest $request)
887
-    {
888
-        $params = $request->params();
889
-        if ($params['Identifier']) {
890
-            $record = DNCreateEnvironment::get()->byId($params['Identifier']);
891
-
892
-            if (!$record || !$record->ID) {
893
-                throw new SS_HTTPResponse_Exception('Create environment not found', 404);
894
-            }
895
-            if (!$record->canView()) {
896
-                return Security::permissionFailure();
897
-            }
898
-
899
-            $project = $this->getCurrentProject();
900
-            if (!$project) {
901
-                return $this->project404Response();
902
-            }
903
-
904
-            if ($project->Name != $params['Project']) {
905
-                throw new LogicException("Project in URL doesn't match this creation");
906
-            }
907
-
908
-            return $this->render(array(
909
-                'CreateEnvironment' => $record,
910
-            ));
911
-        }
912
-        return $this->render(array('CurrentTitle' => 'Create an environment'));
913
-    }
914
-
915
-
916
-    public function createenvlog(SS_HTTPRequest $request)
917
-    {
918
-        $this->setCurrentActionType(self::ACTION_SNAPSHOT);
919
-
920
-        $params = $request->params();
921
-        $env = DNCreateEnvironment::get()->byId($params['Identifier']);
922
-
923
-        if (!$env || !$env->ID) {
924
-            throw new SS_HTTPResponse_Exception('Log not found', 404);
925
-        }
926
-        if (!$env->canView()) {
927
-            return Security::permissionFailure();
928
-        }
929
-
930
-        $project = $env->Project();
931
-
932
-        if ($project->Name != $params['Project']) {
933
-            throw new LogicException("Project in URL doesn't match this deploy");
934
-        }
935
-
936
-        $log = $env->log();
937
-        if ($log->exists()) {
938
-            $content = $log->content();
939
-        } else {
940
-            $content = 'Waiting for action to start';
941
-        }
942
-
943
-        return $this->sendResponse($env->ResqueStatus(), $content);
944
-    }
945
-
946
-    /**
947
-     * @param SS_HTTPRequest $request
948
-     * @return Form
949
-     */
950
-    public function getCreateEnvironmentForm(SS_HTTPRequest $request)
951
-    {
952
-        $this->setCurrentActionType(self::ACTION_ENVIRONMENTS);
953
-
954
-        $project = $this->getCurrentProject();
955
-        if (!$project) {
956
-            return $this->project404Response();
957
-        }
958
-
959
-        $envType = $project->AllowedEnvironmentType;
960
-        if (!$envType || !class_exists($envType)) {
961
-            return null;
962
-        }
963
-
964
-        $backend = Injector::inst()->get($envType);
965
-        if (!($backend instanceof EnvironmentCreateBackend)) {
966
-            // Only allow this for supported backends.
967
-            return null;
968
-        }
969
-
970
-        $fields = $backend->getCreateEnvironmentFields($project);
971
-        if (!$fields) {
972
-            return null;
973
-        }
974
-
975
-        if (!$project->canCreateEnvironments()) {
976
-            return new SS_HTTPResponse('Not allowed to create environments for this project', 401);
977
-        }
978
-
979
-        $form = Form::create(
980
-            $this,
981
-            'CreateEnvironmentForm',
982
-            $fields,
983
-            FieldList::create(
984
-                FormAction::create('doCreateEnvironment', 'Create')
985
-                    ->addExtraClass('btn')
986
-            ),
987
-            $backend->getCreateEnvironmentValidator()
988
-        );
989
-
990
-        // Tweak the action so it plays well with our fake URL structure.
991
-        $form->setFormAction($project->Link() . '/CreateEnvironmentForm');
992
-
993
-        return $form;
994
-    }
995
-
996
-    /**
997
-     * @param array $data
998
-     * @param Form $form
999
-     *
1000
-     * @return bool|HTMLText|SS_HTTPResponse
1001
-     */
1002
-    public function doCreateEnvironment($data, Form $form)
1003
-    {
1004
-        $this->setCurrentActionType(self::ACTION_ENVIRONMENTS);
1005
-
1006
-        $project = $this->getCurrentProject();
1007
-        if (!$project) {
1008
-            return $this->project404Response();
1009
-        }
1010
-
1011
-        if (!$project->canCreateEnvironments()) {
1012
-            return new SS_HTTPResponse('Not allowed to create environments for this project', 401);
1013
-        }
1014
-
1015
-        // Set the environment type so we know what we're creating.
1016
-        $data['EnvironmentType'] = $project->AllowedEnvironmentType;
1017
-
1018
-        $job = DNCreateEnvironment::create();
1019
-
1020
-        $job->Data = serialize($data);
1021
-        $job->ProjectID = $project->ID;
1022
-        $job->write();
1023
-        $job->start();
1024
-
1025
-        return $this->redirect($project->Link('createenv') . '/' . $job->ID);
1026
-    }
1027
-
1028
-    /**
1029
-     *
1030
-     * @param SS_HTTPRequest $request
1031
-     * @return \SS_HTTPResponse
1032
-     */
1033
-    public function metrics(SS_HTTPRequest $request)
1034
-    {
1035
-        // Performs canView permission check by limiting visible projects
1036
-        $project = $this->getCurrentProject();
1037
-        if (!$project) {
1038
-            return $this->project404Response();
1039
-        }
1040
-
1041
-        // Performs canView permission check by limiting visible projects
1042
-        $env = $this->getCurrentEnvironment($project);
1043
-        if (!$env) {
1044
-            return $this->environment404Response();
1045
-        }
1046
-
1047
-        return $this->render();
1048
-    }
1049
-
1050
-    /**
1051
-     * Get the DNData object.
1052
-     *
1053
-     * @return DNData
1054
-     */
1055
-    public function DNData()
1056
-    {
1057
-        return DNData::inst();
1058
-    }
1059
-
1060
-    /**
1061
-     * Provide a list of all projects.
1062
-     *
1063
-     * @return SS_List
1064
-     */
1065
-    public function DNProjectList()
1066
-    {
1067
-        $memberId = Member::currentUserID();
1068
-        if (!$memberId) {
1069
-            return new ArrayList();
1070
-        }
1071
-
1072
-        if (Permission::check('ADMIN')) {
1073
-            return DNProject::get();
1074
-        }
1075
-
1076
-        return Member::get()->filter('ID', $memberId)
1077
-            ->relation('Groups')
1078
-            ->relation('Projects');
1079
-    }
1080
-
1081
-    /**
1082
-     * @return ArrayList
1083
-     */
1084
-    public function getPlatformSpecificStrings()
1085
-    {
1086
-        $strings = $this->config()->platform_specific_strings;
1087
-        if ($strings) {
1088
-            return new ArrayList($strings);
1089
-        }
1090
-    }
1091
-
1092
-    /**
1093
-     * Provide a list of all starred projects for the currently logged in member
1094
-     *
1095
-     * @return SS_List
1096
-     */
1097
-    public function getStarredProjects()
1098
-    {
1099
-        $member = Member::currentUser();
1100
-        if ($member === null) {
1101
-            return new ArrayList();
1102
-        }
1103
-
1104
-        $favProjects = $member->StarredProjects();
1105
-
1106
-        $list = new ArrayList();
1107
-        foreach ($favProjects as $project) {
1108
-            if ($project->canView($member)) {
1109
-                $list->add($project);
1110
-            }
1111
-        }
1112
-        return $list;
1113
-    }
1114
-
1115
-    /**
1116
-     * Returns top level navigation of projects.
1117
-     *
1118
-     * @param int $limit
1119
-     *
1120
-     * @return ArrayList
1121
-     */
1122
-    public function Navigation($limit = 5)
1123
-    {
1124
-        $navigation = new ArrayList();
1125
-
1126
-        $currentProject = $this->getCurrentProject();
1127
-
1128
-        $projects = $this->getStarredProjects();
1129
-        if ($projects->count() < 1) {
1130
-            $projects = $this->DNProjectList();
1131
-        } else {
1132
-            $limit = -1;
1133
-        }
1134
-
1135
-        if ($projects->count() > 0) {
1136
-            $activeProject = false;
1137
-
1138
-            if ($limit > 0) {
1139
-                $limitedProjects = $projects->limit($limit);
1140
-            } else {
1141
-                $limitedProjects = $projects;
1142
-            }
1143
-
1144
-            foreach ($limitedProjects as $project) {
1145
-                $isActive = $currentProject && $currentProject->ID == $project->ID;
1146
-                if ($isActive) {
1147
-                    $activeProject = true;
1148
-                }
1149
-
1150
-                $navigation->push(array(
1151
-                    'Project' => $project,
1152
-                    'IsActive' => $currentProject && $currentProject->ID == $project->ID,
1153
-                ));
1154
-            }
1155
-
1156
-            // Ensure the current project is in the list
1157
-            if (!$activeProject && $currentProject) {
1158
-                $navigation->unshift(array(
1159
-                    'Project' => $currentProject,
1160
-                    'IsActive' => true,
1161
-                ));
1162
-                if ($limit > 0 && $navigation->count() > $limit) {
1163
-                    $navigation->pop();
1164
-                }
1165
-            }
1166
-        }
1167
-
1168
-        return $navigation;
1169
-    }
1170
-
1171
-    /**
1172
-     * Construct the deployment form
1173
-     *
1174
-     * @return Form
1175
-     */
1176
-    public function getDeployForm($request = null)
1177
-    {
1178
-
1179
-        // Performs canView permission check by limiting visible projects
1180
-        $project = $this->getCurrentProject();
1181
-        if (!$project) {
1182
-            return $this->project404Response();
1183
-        }
1184
-
1185
-        // Performs canView permission check by limiting visible projects
1186
-        $environment = $this->getCurrentEnvironment($project);
1187
-        if (!$environment) {
1188
-            return $this->environment404Response();
1189
-        }
1190
-
1191
-        if (!$environment->canDeploy()) {
1192
-            return new SS_HTTPResponse("Not allowed to deploy", 401);
1193
-        }
1194
-
1195
-        // Generate the form
1196
-        $form = new DeployForm($this, 'DeployForm', $environment, $project);
1197
-
1198
-        // If this is an ajax request we don't want to submit the form - we just want to retrieve the markup.
1199
-        if (
1200
-            $request &&
1201
-            !$request->requestVar('action_showDeploySummary') &&
1202
-            $this->getRequest()->isAjax() &&
1203
-            $this->getRequest()->isGET()
1204
-        ) {
1205
-            // We can just use the URL we're accessing
1206
-            $form->setFormAction($this->getRequest()->getURL());
1207
-
1208
-            $body = json_encode(array('Content' => $form->forAjaxTemplate()->forTemplate()));
1209
-            $this->getResponse()->addHeader('Content-Type', 'application/json');
1210
-            $this->getResponse()->setBody($body);
1211
-            return $body;
1212
-        }
1213
-
1214
-        $form->setFormAction($this->getRequest()->getURL() . '/DeployForm');
1215
-        return $form;
1216
-    }
1217
-
1218
-    /**
1219
-     * @param SS_HTTPRequest $request
1220
-     *
1221
-     * @return SS_HTTPResponse|string
1222
-     */
1223
-    public function gitRevisions(SS_HTTPRequest $request)
1224
-    {
1225
-
1226
-        // Performs canView permission check by limiting visible projects
1227
-        $project = $this->getCurrentProject();
1228
-        if (!$project) {
1229
-            return $this->project404Response();
1230
-        }
1231
-
1232
-        // Performs canView permission check by limiting visible projects
1233
-        $env = $this->getCurrentEnvironment($project);
1234
-        if (!$env) {
1235
-            return $this->environment404Response();
1236
-        }
1237
-
1238
-        // For now only permit advanced options on one environment type, because we hacked the "full-deploy"
1239
-        // checkbox in. Other environments such as the fast or capistrano one wouldn't know what to do with it.
1240
-        if (get_class($env) === 'RainforestEnvironment') {
1241
-            $advanced = Permission::check('DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS') ? 'true' : 'false';
1242
-        } else {
1243
-            $advanced = 'false';
1244
-        }
1245
-
1246
-        $tabs = array();
1247
-        $id = 0;
1248
-        $data = array(
1249
-            'id' => ++$id,
1250
-            'name' => 'Deploy the latest version of a branch',
1251
-            'field_type' => 'dropdown',
1252
-            'field_label' => 'Choose a branch',
1253
-            'field_id' => 'branch',
1254
-            'field_data' => array(),
1255
-            'advanced_opts' => $advanced
1256
-        );
1257
-        foreach ($project->DNBranchList() as $branch) {
1258
-            $sha = $branch->SHA();
1259
-            $name = $branch->Name();
1260
-            $branchValue = sprintf("%s (%s, %s old)",
1261
-                $name,
1262
-                substr($sha, 0, 8),
1263
-                $branch->LastUpdated()->TimeDiff()
1264
-            );
1265
-            $data['field_data'][] = array(
1266
-                'id' => $sha,
1267
-                'text' => $branchValue
1268
-            );
1269
-        }
1270
-        $tabs[] = $data;
1271
-
1272
-        $data = array(
1273
-            'id' => ++$id,
1274
-            'name' => 'Deploy a tagged release',
1275
-            'field_type' => 'dropdown',
1276
-            'field_label' => 'Choose a tag',
1277
-            'field_id' => 'tag',
1278
-            'field_data' => array(),
1279
-            'advanced_opts' => $advanced
1280
-        );
1281
-
1282
-        foreach ($project->DNTagList()->setLimit(null) as $tag) {
1283
-            $name = $tag->Name();
1284
-            $data['field_data'][] = array(
1285
-                'id' => $tag->SHA(),
1286
-                'text' => sprintf("%s", $name)
1287
-            );
1288
-        }
1289
-
1290
-        // show newest tags first.
1291
-        $data['field_data'] = array_reverse($data['field_data']);
1292
-
1293
-        $tabs[] = $data;
1294
-
1295
-        // Past deployments
1296
-        $data = array(
1297
-            'id' => ++$id,
1298
-            'name' => 'Redeploy a release that was previously deployed (to any environment)',
1299
-            'field_type' => 'dropdown',
1300
-            'field_label' => 'Choose a previously deployed release',
1301
-            'field_id' => 'release',
1302
-            'field_data' => array(),
1303
-            'advanced_opts' => $advanced
1304
-        );
1305
-        // We are aiming at the format:
1306
-        // [{text: 'optgroup text', children: [{id: '<sha>', text: '<inner text>'}]}]
1307
-        $redeploy = array();
1308
-        foreach ($project->DNEnvironmentList() as $dnEnvironment) {
1309
-            $envName = $dnEnvironment->Name;
1310
-            $perEnvDeploys = array();
1311
-
1312
-            foreach ($dnEnvironment->DeployHistory() as $deploy) {
1313
-                $sha = $deploy->SHA;
1314
-
1315
-                // Check if exists to make sure the newest deployment date is used.
1316
-                if (!isset($perEnvDeploys[$sha])) {
1317
-                    $pastValue = sprintf("%s (deployed %s)",
1318
-                        substr($sha, 0, 8),
1319
-                        $deploy->obj('LastEdited')->Ago()
1320
-                    );
1321
-                    $perEnvDeploys[$sha] = array(
1322
-                        'id' => $sha,
1323
-                        'text' => $pastValue
1324
-                    );
1325
-                }
1326
-            }
1327
-
1328
-            if (!empty($perEnvDeploys)) {
1329
-                $redeploy[$envName] = array_values($perEnvDeploys);
1330
-            }
1331
-        }
1332
-        // Convert the array to the frontend format (i.e. keyed to regular array)
1333
-        foreach ($redeploy as $env => $descr) {
1334
-            $data['field_data'][] = array('text'=>$env, 'children'=>$descr);
1335
-        }
1336
-        $tabs[] = $data;
1337
-
1338
-        $data = array(
1339
-            'id' => ++$id,
1340
-            'name' => 'Deploy a specific SHA',
1341
-            'field_type' => 'textfield',
1342
-            'field_label' => 'Choose a SHA',
1343
-            'field_id' => 'SHA',
1344
-            'field_data' => array(),
1345
-            'advanced_opts' => $advanced
1346
-        );
1347
-        $tabs[] = $data;
1348
-
1349
-        // get the last time git fetch was run
1350
-        $lastFetched = 'never';
1351
-        $fetch = DNGitFetch::get()
1352
-            ->filter('ProjectID', $project->ID)
1353
-            ->sort('LastEdited', 'DESC')
1354
-            ->first();
1355
-        if ($fetch) {
1356
-            $lastFetched = $fetch->dbObject('LastEdited')->Ago();
1357
-        }
1358
-
1359
-        $data = array(
1360
-            'Tabs' => $tabs,
1361
-            'last_fetched' => $lastFetched
1362
-        );
1363
-
1364
-        return json_encode($data, JSON_PRETTY_PRINT);
1365
-    }
1366
-
1367
-    /**
1368
-     * Check and regenerate a global CSRF token
1369
-     *
1370
-     * @param SS_HTTPRequest $request
1371
-     * @param bool $resetToken
1372
-     *
1373
-     * @return bool
1374
-     */
1375
-    protected function checkCsrfToken(SS_HTTPRequest $request, $resetToken = true)
1376
-    {
1377
-        $token = SecurityToken::inst();
1378
-
1379
-        // Ensure the submitted token has a value
1380
-        $submittedToken = $request->postVar('SecurityID');
1381
-        if (!$submittedToken) {
1382
-            return false;
1383
-        }
1384
-
1385
-        // Do the actual check.
1386
-        $check = $token->check($submittedToken);
1387
-
1388
-        // Reset the token after we've checked the existing token
1389
-        if ($resetToken) {
1390
-            $token->reset();
1391
-        }
1392
-
1393
-        // Return whether the token was correct or not
1394
-        return $check;
1395
-    }
1396
-
1397
-    /**
1398
-     * @param SS_HTTPRequest $request
1399
-     *
1400
-     * @return string
1401
-     */
1402
-    public function deploySummary(SS_HTTPRequest $request)
1403
-    {
1404
-
1405
-        // Performs canView permission check by limiting visible projects
1406
-        $project = $this->getCurrentProject();
1407
-        if (!$project) {
1408
-            return $this->project404Response();
1409
-        }
1410
-
1411
-        // Performs canView permission check by limiting visible projects
1412
-        $environment = $this->getCurrentEnvironment($project);
1413
-        if (!$environment) {
1414
-            return $this->environment404Response();
1415
-        }
1416
-
1417
-        // Plan the deployment.
1418
-        $strategy = $environment->Backend()->planDeploy(
1419
-            $environment,
1420
-            $request->requestVars()
1421
-        );
1422
-        $data = $strategy->toArray();
1423
-
1424
-        // Add in a URL for comparing from->to code changes. Ensure that we have
1425
-        // two proper 40 character SHAs, otherwise we can't show the compare link.
1426
-        $interface = $project->getRepositoryInterface();
1427
-        if (
1428
-            !empty($interface) && !empty($interface->URL)
1429
-            && !empty($data['changes']['Code version']['from'])
1430
-            && strlen($data['changes']['Code version']['from']) == '40'
1431
-            && !empty($data['changes']['Code version']['to'])
1432
-            && strlen($data['changes']['Code version']['to']) == '40'
1433
-        ) {
1434
-            $compareurl = sprintf(
1435
-                '%s/compare/%s...%s',
1436
-                $interface->URL,
1437
-                $data['changes']['Code version']['from'],
1438
-                $data['changes']['Code version']['to']
1439
-            );
1440
-            $data['changes']['Code version']['compareUrl'] = $compareurl;
1441
-        }
1442
-
1443
-        // Append json to response
1444
-        $token = SecurityToken::inst();
1445
-        $data['SecurityID'] = $token->getValue();
1446
-
1447
-        return json_encode($data);
1448
-    }
1449
-
1450
-    /**
1451
-     * Deployment form submission handler.
1452
-     *
1453
-     * Initiate a DNDeployment record and redirect to it for status polling
1454
-     *
1455
-     * @param SS_HTTPRequest $request
1456
-     *
1457
-     * @return SS_HTTPResponse
1458
-     * @throws ValidationException
1459
-     * @throws null
1460
-     */
1461
-    public function startDeploy(SS_HTTPRequest $request)
1462
-    {
1463
-
1464
-        // Ensure the CSRF Token is correct
1465
-        if (!$this->checkCsrfToken($request)) {
1466
-            // CSRF token didn't match
1467
-            return $this->httpError(400, 'Bad Request');
1468
-        }
1469
-
1470
-        // Performs canView permission check by limiting visible projects
1471
-        $project = $this->getCurrentProject();
1472
-        if (!$project) {
1473
-            return $this->project404Response();
1474
-        }
1475
-
1476
-        // Performs canView permission check by limiting visible projects
1477
-        $environment = $this->getCurrentEnvironment($project);
1478
-        if (!$environment) {
1479
-            return $this->environment404Response();
1480
-        }
1481
-
1482
-        // Initiate the deployment
1483
-        // The extension point should pass in: Project, Environment, SelectRelease, buildName
1484
-        $this->extend('doDeploy', $project, $environment, $buildName, $data);
1485
-
1486
-        // Start the deployment based on the approved strategy.
1487
-        $strategy = new DeploymentStrategy($environment);
1488
-        $strategy->fromArray($request->requestVar('strategy'));
1489
-        $deployment = $strategy->createDeployment();
1490
-        $deployment->start();
1491
-
1492
-        return json_encode(array(
1493
-            'url' => Director::absoluteBaseURL() . $deployment->Link()
1494
-        ), JSON_PRETTY_PRINT);
1495
-    }
1496
-
1497
-    /**
1498
-     * Action - Do the actual deploy
1499
-     *
1500
-     * @param SS_HTTPRequest $request
1501
-     *
1502
-     * @return SS_HTTPResponse|string
1503
-     * @throws SS_HTTPResponse_Exception
1504
-     */
1505
-    public function deploy(SS_HTTPRequest $request)
1506
-    {
1507
-        $params = $request->params();
1508
-        $deployment = DNDeployment::get()->byId($params['Identifier']);
1509
-
1510
-        if (!$deployment || !$deployment->ID) {
1511
-            throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1512
-        }
1513
-        if (!$deployment->canView()) {
1514
-            return Security::permissionFailure();
1515
-        }
1516
-
1517
-        $environment = $deployment->Environment();
1518
-        $project = $environment->Project();
1519
-
1520
-        if ($environment->Name != $params['Environment']) {
1521
-            throw new LogicException("Environment in URL doesn't match this deploy");
1522
-        }
1523
-        if ($project->Name != $params['Project']) {
1524
-            throw new LogicException("Project in URL doesn't match this deploy");
1525
-        }
1526
-
1527
-        return $this->render(array(
1528
-            'Deployment' => $deployment,
1529
-        ));
1530
-    }
1531
-
1532
-
1533
-    /**
1534
-     * Action - Get the latest deploy log
1535
-     *
1536
-     * @param SS_HTTPRequest $request
1537
-     *
1538
-     * @return string
1539
-     * @throws SS_HTTPResponse_Exception
1540
-     */
1541
-    public function deploylog(SS_HTTPRequest $request)
1542
-    {
1543
-        $params = $request->params();
1544
-        $deployment = DNDeployment::get()->byId($params['Identifier']);
1545
-
1546
-        if (!$deployment || !$deployment->ID) {
1547
-            throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1548
-        }
1549
-        if (!$deployment->canView()) {
1550
-            return Security::permissionFailure();
1551
-        }
1552
-
1553
-        $environment = $deployment->Environment();
1554
-        $project = $environment->Project();
1555
-
1556
-        if ($environment->Name != $params['Environment']) {
1557
-            throw new LogicException("Environment in URL doesn't match this deploy");
1558
-        }
1559
-        if ($project->Name != $params['Project']) {
1560
-            throw new LogicException("Project in URL doesn't match this deploy");
1561
-        }
1562
-
1563
-        $log = $deployment->log();
1564
-        if ($log->exists()) {
1565
-            $content = $log->content();
1566
-        } else {
1567
-            $content = 'Waiting for action to start';
1568
-        }
1569
-
1570
-        return $this->sendResponse($deployment->ResqueStatus(), $content);
1571
-    }
1572
-
1573
-    /**
1574
-     * @param SS_HTTPRequest|null $request
1575
-     *
1576
-     * @return Form
1577
-     */
1578
-    public function getDataTransferForm(SS_HTTPRequest $request = null)
1579
-    {
1580
-        // Performs canView permission check by limiting visible projects
1581
-        $envs = $this->getCurrentProject()->DNEnvironmentList()->filterByCallback(function ($item) {
1582
-            return $item->canBackup();
1583
-        });
1584
-
1585
-        if (!$envs) {
1586
-            return $this->environment404Response();
1587
-        }
1588
-
1589
-        $form = Form::create(
1590
-            $this,
1591
-            'DataTransferForm',
1592
-            FieldList::create(
1593
-                HiddenField::create('Direction', null, 'get'),
1594
-                DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1595
-                    ->setEmptyString('Select an environment'),
1596
-                DropdownField::create('Mode', 'Transfer', DNDataArchive::get_mode_map())
1597
-            ),
1598
-            FieldList::create(
1599
-                FormAction::create('doDataTransfer', 'Create')
1600
-                    ->addExtraClass('btn')
1601
-            )
1602
-        );
1603
-        $form->setFormAction($this->getRequest()->getURL() . '/DataTransferForm');
1604
-
1605
-        return $form;
1606
-    }
1607
-
1608
-    /**
1609
-     * @param array $data
1610
-     * @param Form $form
1611
-     *
1612
-     * @return SS_HTTPResponse
1613
-     * @throws SS_HTTPResponse_Exception
1614
-     */
1615
-    public function doDataTransfer($data, Form $form)
1616
-    {
1617
-        $this->setCurrentActionType(self::ACTION_SNAPSHOT);
1618
-
1619
-        // Performs canView permission check by limiting visible projects
1620
-        $project = $this->getCurrentProject();
1621
-        if (!$project) {
1622
-            return $this->project404Response();
1623
-        }
1624
-
1625
-        $dataArchive = null;
1626
-
1627
-        // Validate direction.
1628
-        if ($data['Direction'] == 'get') {
1629
-            $validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1630
-                ->filterByCallback(function ($item) {
1631
-                    return $item->canBackup();
1632
-                });
1633
-        } elseif ($data['Direction'] == 'push') {
1634
-            $validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1635
-                ->filterByCallback(function ($item) {
1636
-                    return $item->canRestore();
1637
-                });
1638
-        } else {
1639
-            throw new LogicException('Invalid direction');
1640
-        }
1641
-
1642
-        // Validate $data['EnvironmentID'] by checking against $validEnvs.
1643
-        $environment = $validEnvs->find('ID', $data['EnvironmentID']);
1644
-        if (!$environment) {
1645
-            throw new LogicException('Invalid environment');
1646
-        }
1647
-
1648
-        $this->validateSnapshotMode($data['Mode']);
1649
-
1650
-
1651
-        // Only 'push' direction is allowed an association with an existing archive.
1652
-        if (
1653
-            $data['Direction'] == 'push'
1654
-            && isset($data['DataArchiveID'])
1655
-            && is_numeric($data['DataArchiveID'])
1656
-        ) {
1657
-            $dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1658
-            if (!$dataArchive) {
1659
-                throw new LogicException('Invalid data archive');
1660
-            }
1661
-
1662
-            if (!$dataArchive->canDownload()) {
1663
-                throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1664
-            }
1665
-        }
1666
-
1667
-        $transfer = DNDataTransfer::create();
1668
-        $transfer->EnvironmentID = $environment->ID;
1669
-        $transfer->Direction = $data['Direction'];
1670
-        $transfer->Mode = $data['Mode'];
1671
-        $transfer->DataArchiveID = $dataArchive ? $dataArchive->ID : null;
1672
-        if ($data['Direction'] == 'push') {
1673
-            $transfer->setBackupBeforePush(!empty($data['BackupBeforePush']));
1674
-        }
1675
-        $transfer->write();
1676
-        $transfer->start();
1677
-
1678
-        return $this->redirect($transfer->Link());
1679
-    }
1680
-
1681
-    /**
1682
-     * View into the log for a {@link DNDataTransfer}.
1683
-     *
1684
-     * @param SS_HTTPRequest $request
1685
-     *
1686
-     * @return SS_HTTPResponse|string
1687
-     * @throws SS_HTTPResponse_Exception
1688
-     */
1689
-    public function transfer(SS_HTTPRequest $request)
1690
-    {
1691
-        $this->setCurrentActionType(self::ACTION_SNAPSHOT);
1692
-
1693
-        $params = $request->params();
1694
-        $transfer = DNDataTransfer::get()->byId($params['Identifier']);
1695
-
1696
-        if (!$transfer || !$transfer->ID) {
1697
-            throw new SS_HTTPResponse_Exception('Transfer not found', 404);
1698
-        }
1699
-        if (!$transfer->canView()) {
1700
-            return Security::permissionFailure();
1701
-        }
1702
-
1703
-        $environment = $transfer->Environment();
1704
-        $project = $environment->Project();
1705
-
1706
-        if ($project->Name != $params['Project']) {
1707
-            throw new LogicException("Project in URL doesn't match this deploy");
1708
-        }
1709
-
1710
-        return $this->render(array(
1711
-            'CurrentTransfer' => $transfer,
1712
-            'SnapshotsSection' => 1,
1713
-        ));
1714
-    }
1715
-
1716
-    /**
1717
-     * Action - Get the latest deploy log
1718
-     *
1719
-     * @param SS_HTTPRequest $request
1720
-     *
1721
-     * @return string
1722
-     * @throws SS_HTTPResponse_Exception
1723
-     */
1724
-    public function transferlog(SS_HTTPRequest $request)
1725
-    {
1726
-        $this->setCurrentActionType(self::ACTION_SNAPSHOT);
1727
-
1728
-        $params = $request->params();
1729
-        $transfer = DNDataTransfer::get()->byId($params['Identifier']);
1730
-
1731
-        if (!$transfer || !$transfer->ID) {
1732
-            throw new SS_HTTPResponse_Exception('Transfer not found', 404);
1733
-        }
1734
-        if (!$transfer->canView()) {
1735
-            return Security::permissionFailure();
1736
-        }
1737
-
1738
-        $environment = $transfer->Environment();
1739
-        $project = $environment->Project();
1740
-
1741
-        if ($project->Name != $params['Project']) {
1742
-            throw new LogicException("Project in URL doesn't match this deploy");
1743
-        }
1744
-
1745
-        $log = $transfer->log();
1746
-        if ($log->exists()) {
1747
-            $content = $log->content();
1748
-        } else {
1749
-            $content = 'Waiting for action to start';
1750
-        }
1751
-
1752
-        return $this->sendResponse($transfer->ResqueStatus(), $content);
1753
-    }
1754
-
1755
-    /**
1756
-     * Note: Submits to the same action as {@link getDataTransferForm()},
1757
-     * but with a Direction=push and an archive reference.
1758
-     *
1759
-     * @param SS_HTTPRequest $request
1760
-     * @param DNDataArchive|null $dataArchive Only set when method is called manually in {@link restore()},
1761
-     *                            otherwise the state is inferred from the request data.
1762
-     * @return Form
1763
-     */
1764
-    public function getDataTransferRestoreForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null)
1765
-    {
1766
-        $dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1767
-
1768
-        // Performs canView permission check by limiting visible projects
1769
-        $project = $this->getCurrentProject();
1770
-        $envs = $project->DNEnvironmentList()->filterByCallback(function ($item) {
1771
-            return $item->canRestore();
1772
-        });
1773
-
1774
-        if (!$envs) {
1775
-            return $this->environment404Response();
1776
-        }
1777
-
1778
-        $modesMap = array();
1779
-        if (in_array($dataArchive->Mode, array('all'))) {
1780
-            $modesMap['all'] = 'Database and Assets';
1781
-        };
1782
-        if (in_array($dataArchive->Mode, array('all', 'db'))) {
1783
-            $modesMap['db'] = 'Database only';
1784
-        };
1785
-        if (in_array($dataArchive->Mode, array('all', 'assets'))) {
1786
-            $modesMap['assets'] = 'Assets only';
1787
-        };
1788
-
1789
-        $alertMessage = '<div class="alert alert-warning"><strong>Warning:</strong> '
1790
-            . 'This restore will overwrite the data on the chosen environment below</div>';
1791
-
1792
-        $form = Form::create(
1793
-            $this,
1794
-            'DataTransferRestoreForm',
1795
-            FieldList::create(
1796
-                HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1797
-                HiddenField::create('Direction', null, 'push'),
1798
-                LiteralField::create('Warning', $alertMessage),
1799
-                DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1800
-                    ->setEmptyString('Select an environment'),
1801
-                DropdownField::create('Mode', 'Transfer', $modesMap),
1802
-                CheckboxField::create('BackupBeforePush', 'Backup existing data', '1')
1803
-            ),
1804
-            FieldList::create(
1805
-                FormAction::create('doDataTransfer', 'Restore Data')
1806
-                    ->addExtraClass('btn')
1807
-            )
1808
-        );
1809
-        $form->setFormAction($project->Link() . '/DataTransferRestoreForm');
1810
-
1811
-        return $form;
1812
-    }
1813
-
1814
-    /**
1815
-     * View a form to restore a specific {@link DataArchive}.
1816
-     * Permission checks are handled in {@link DataArchives()}.
1817
-     * Submissions are handled through {@link doDataTransfer()}, same as backup operations.
1818
-     *
1819
-     * @param SS_HTTPRequest $request
1820
-     *
1821
-     * @return HTMLText
1822
-     * @throws SS_HTTPResponse_Exception
1823
-     */
1824
-    public function restoresnapshot(SS_HTTPRequest $request)
1825
-    {
1826
-        $this->setCurrentActionType(self::ACTION_SNAPSHOT);
1827
-
1828
-        /** @var DNDataArchive $dataArchive */
1829
-        $dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1830
-
1831
-        if (!$dataArchive) {
1832
-            throw new SS_HTTPResponse_Exception('Archive not found', 404);
1833
-        }
1834
-
1835
-        // We check for canDownload because that implies access to the data.
1836
-        // canRestore is later checked on the actual restore action per environment.
1837
-        if (!$dataArchive->canDownload()) {
1838
-            throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1839
-        }
1840
-
1841
-        $form = $this->getDataTransferRestoreForm($this->request, $dataArchive);
1842
-
1843
-        // View currently only available via ajax
1844
-        return $form->forTemplate();
1845
-    }
1846
-
1847
-    /**
1848
-     * View a form to delete a specific {@link DataArchive}.
1849
-     * Permission checks are handled in {@link DataArchives()}.
1850
-     * Submissions are handled through {@link doDelete()}.
1851
-     *
1852
-     * @param SS_HTTPRequest $request
1853
-     *
1854
-     * @return HTMLText
1855
-     * @throws SS_HTTPResponse_Exception
1856
-     */
1857
-    public function deletesnapshot(SS_HTTPRequest $request)
1858
-    {
1859
-        $this->setCurrentActionType(self::ACTION_SNAPSHOT);
1860
-
1861
-        /** @var DNDataArchive $dataArchive */
1862
-        $dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1863
-
1864
-        if (!$dataArchive) {
1865
-            throw new SS_HTTPResponse_Exception('Archive not found', 404);
1866
-        }
1867
-
1868
-        if (!$dataArchive->canDelete()) {
1869
-            throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403);
1870
-        }
1871
-
1872
-        $form = $this->getDeleteForm($this->request, $dataArchive);
1873
-
1874
-        // View currently only available via ajax
1875
-        return $form->forTemplate();
1876
-    }
1877
-
1878
-    /**
1879
-     * @param SS_HTTPRequest $request
1880
-     * @param DNDataArchive|null $dataArchive Only set when method is called manually, otherwise the state is inferred
1881
-     *        from the request data.
1882
-     * @return Form
1883
-     */
1884
-    public function getDeleteForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null)
1885
-    {
1886
-        $dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1887
-
1888
-        // Performs canView permission check by limiting visible projects
1889
-        $project = $this->getCurrentProject();
1890
-        if (!$project) {
1891
-            return $this->project404Response();
1892
-        }
1893
-
1894
-        $snapshotDeleteWarning = '<div class="alert alert-warning">'
1895
-            . 'Are you sure you want to permanently delete this snapshot from this archive area?'
1896
-            . '</div>';
1897
-
1898
-        $form = Form::create(
1899
-            $this,
1900
-            'DeleteForm',
1901
-            FieldList::create(
1902
-                HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1903
-                LiteralField::create('Warning', $snapshotDeleteWarning)
1904
-            ),
1905
-            FieldList::create(
1906
-                FormAction::create('doDelete', 'Delete')
1907
-                    ->addExtraClass('btn')
1908
-            )
1909
-        );
1910
-        $form->setFormAction($project->Link() . '/DeleteForm');
1911
-
1912
-        return $form;
1913
-    }
1914
-
1915
-    /**
1916
-     * @param array $data
1917
-     * @param Form $form
1918
-     *
1919
-     * @return bool|SS_HTTPResponse
1920
-     * @throws SS_HTTPResponse_Exception
1921
-     */
1922
-    public function doDelete($data, Form $form)
1923
-    {
1924
-        $this->setCurrentActionType(self::ACTION_SNAPSHOT);
1925
-
1926
-        // Performs canView permission check by limiting visible projects
1927
-        $project = $this->getCurrentProject();
1928
-        if (!$project) {
1929
-            return $this->project404Response();
1930
-        }
1931
-
1932
-        $dataArchive = null;
1933
-
1934
-        if (
1935
-            isset($data['DataArchiveID'])
1936
-            && is_numeric($data['DataArchiveID'])
1937
-        ) {
1938
-            $dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1939
-        }
1940
-
1941
-        if (!$dataArchive) {
1942
-            throw new LogicException('Invalid data archive');
1943
-        }
1944
-
1945
-        if (!$dataArchive->canDelete()) {
1946
-            throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403);
1947
-        }
1948
-
1949
-        $dataArchive->delete();
1950
-
1951
-        return $this->redirectBack();
1952
-    }
1953
-
1954
-    /**
1955
-     * View a form to move a specific {@link DataArchive}.
1956
-     *
1957
-     * @param SS_HTTPRequest $request
1958
-     *
1959
-     * @return HTMLText
1960
-     * @throws SS_HTTPResponse_Exception
1961
-     */
1962
-    public function movesnapshot(SS_HTTPRequest $request)
1963
-    {
1964
-        $this->setCurrentActionType(self::ACTION_SNAPSHOT);
1965
-
1966
-        /** @var DNDataArchive $dataArchive */
1967
-        $dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1968
-
1969
-        if (!$dataArchive) {
1970
-            throw new SS_HTTPResponse_Exception('Archive not found', 404);
1971
-        }
1972
-
1973
-        // We check for canDownload because that implies access to the data.
1974
-        if (!$dataArchive->canDownload()) {
1975
-            throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1976
-        }
1977
-
1978
-        $form = $this->getMoveForm($this->request, $dataArchive);
1979
-
1980
-        // View currently only available via ajax
1981
-        return $form->forTemplate();
1982
-    }
1983
-
1984
-    /**
1985
-     * Build snapshot move form.
1986
-     *
1987
-     * @param SS_HTTPRequest $request
1988
-     * @param DNDataArchive|null $dataArchive
1989
-     *
1990
-     * @return Form|SS_HTTPResponse
1991
-     */
1992
-    public function getMoveForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null)
1993
-    {
1994
-        $dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1995
-
1996
-        $envs = $dataArchive->validTargetEnvironments();
1997
-        if (!$envs) {
1998
-            return $this->environment404Response();
1999
-        }
2000
-
2001
-        $warningMessage = '<div class="alert alert-warning"><strong>Warning:</strong> This will make the snapshot '
2002
-            . 'available to people with access to the target environment.<br>By pressing "Change ownership" you '
2003
-            . 'confirm that you have considered data confidentiality regulations.</div>';
2004
-
2005
-        $form = Form::create(
2006
-            $this,
2007
-            'MoveForm',
2008
-            FieldList::create(
2009
-                HiddenField::create('DataArchiveID', null, $dataArchive->ID),
2010
-                LiteralField::create('Warning', $warningMessage),
2011
-                DropdownField::create('EnvironmentID', 'Environment', $envs->map())
2012
-                    ->setEmptyString('Select an environment')
2013
-            ),
2014
-            FieldList::create(
2015
-                FormAction::create('doMove', 'Change ownership')
2016
-                    ->addExtraClass('btn')
2017
-            )
2018
-        );
2019
-        $form->setFormAction($this->getCurrentProject()->Link() . '/MoveForm');
2020
-
2021
-        return $form;
2022
-    }
2023
-
2024
-    /**
2025
-     * @param array $data
2026
-     * @param Form $form
2027
-     *
2028
-     * @return bool|SS_HTTPResponse
2029
-     * @throws SS_HTTPResponse_Exception
2030
-     * @throws ValidationException
2031
-     * @throws null
2032
-     */
2033
-    public function doMove($data, Form $form)
2034
-    {
2035
-        $this->setCurrentActionType(self::ACTION_SNAPSHOT);
2036
-
2037
-        // Performs canView permission check by limiting visible projects
2038
-        $project = $this->getCurrentProject();
2039
-        if (!$project) {
2040
-            return $this->project404Response();
2041
-        }
2042
-
2043
-        /** @var DNDataArchive $dataArchive */
2044
-        $dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
2045
-        if (!$dataArchive) {
2046
-            throw new LogicException('Invalid data archive');
2047
-        }
2048
-
2049
-        // We check for canDownload because that implies access to the data.
2050
-        if (!$dataArchive->canDownload()) {
2051
-            throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
2052
-        }
2053
-
2054
-        // Validate $data['EnvironmentID'] by checking against $validEnvs.
2055
-        $validEnvs = $dataArchive->validTargetEnvironments();
2056
-        $environment = $validEnvs->find('ID', $data['EnvironmentID']);
2057
-        if (!$environment) {
2058
-            throw new LogicException('Invalid environment');
2059
-        }
2060
-
2061
-        $dataArchive->EnvironmentID = $environment->ID;
2062
-        $dataArchive->write();
2063
-
2064
-        return $this->redirectBack();
2065
-    }
2066
-
2067
-    /**
2068
-     * Returns an error message if redis is unavailable
2069
-     *
2070
-     * @return string
2071
-     */
2072
-    public static function RedisUnavailable()
2073
-    {
2074
-        try {
2075
-            Resque::queues();
2076
-        } catch (Exception $e) {
2077
-            return $e->getMessage();
2078
-        }
2079
-        return '';
2080
-    }
2081
-
2082
-    /**
2083
-     * Returns the number of connected Redis workers
2084
-     *
2085
-     * @return int
2086
-     */
2087
-    public static function RedisWorkersCount()
2088
-    {
2089
-        return count(Resque_Worker::all());
2090
-    }
2091
-
2092
-    /**
2093
-     * @return array
2094
-     */
2095
-    public function providePermissions()
2096
-    {
2097
-        return array(
2098
-            self::DEPLOYNAUT_BYPASS_PIPELINE => array(
2099
-                'name' => "Bypass Pipeline",
2100
-                'category' => "Deploynaut",
2101
-                'help' => "Enables users to directly initiate deployments, bypassing any pipeline",
2102
-            ),
2103
-            self::DEPLOYNAUT_DRYRUN_PIPELINE => array(
2104
-                'name' => 'Dry-run Pipeline',
2105
-                'category' => 'Deploynaut',
2106
-                'help' => 'Enable dry-run execution of pipelines for testing'
2107
-            ),
2108
-            self::DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS => array(
2109
-                'name' => "Access to advanced deploy options",
2110
-                'category' => "Deploynaut",
2111
-            ),
2112
-
2113
-            // Permissions that are intended to be added to the roles.
2114
-            self::ALLOW_PROD_DEPLOYMENT => array(
2115
-                'name' => "Ability to deploy to production environments",
2116
-                'category' => "Deploynaut",
2117
-            ),
2118
-            self::ALLOW_NON_PROD_DEPLOYMENT => array(
2119
-                'name' => "Ability to deploy to non-production environments",
2120
-                'category' => "Deploynaut",
2121
-            ),
2122
-            self::ALLOW_PROD_SNAPSHOT => array(
2123
-                'name' => "Ability to make production snapshots",
2124
-                'category' => "Deploynaut",
2125
-            ),
2126
-            self::ALLOW_NON_PROD_SNAPSHOT => array(
2127
-                'name' => "Ability to make non-production snapshots",
2128
-                'category' => "Deploynaut",
2129
-            ),
2130
-            self::ALLOW_CREATE_ENVIRONMENT => array(
2131
-                'name' => "Ability to create environments",
2132
-                'category' => "Deploynaut",
2133
-            ),
2134
-        );
2135
-    }
2136
-
2137
-    /**
2138
-     * @return DNProject|null
2139
-     */
2140
-    public function getCurrentProject()
2141
-    {
2142
-        $projectName = trim($this->getRequest()->param('Project'));
2143
-        if (!$projectName) {
2144
-            return null;
2145
-        }
2146
-        if (empty(self::$_project_cache[$projectName])) {
2147
-            self::$_project_cache[$projectName] = $this->DNProjectList()->filter('Name', $projectName)->First();
2148
-        }
2149
-        return self::$_project_cache[$projectName];
2150
-    }
2151
-
2152
-    /**
2153
-     * @param DNProject|null $project
2154
-     * @return DNEnvironment|null
2155
-     */
2156
-    public function getCurrentEnvironment(DNProject $project = null)
2157
-    {
2158
-        if ($this->getRequest()->param('Environment') === null) {
2159
-            return null;
2160
-        }
2161
-        if ($project === null) {
2162
-            $project = $this->getCurrentProject();
2163
-        }
2164
-        // project can still be null
2165
-        if ($project === null) {
2166
-            return null;
2167
-        }
2168
-        return $project->DNEnvironmentList()->filter('Name', $this->getRequest()->param('Environment'))->First();
2169
-    }
2170
-
2171
-    /**
2172
-     * This will return a const that indicates the class of action currently being performed
2173
-     *
2174
-     * Until DNRoot is de-godded, it does a bunch of different actions all in the same class.
2175
-     * So we just have each action handler calll setCurrentActionType to define what sort of
2176
-     * action it is.
2177
-     *
2178
-     * @return string - one of the consts from self::$action_types
2179
-     */
2180
-    public function getCurrentActionType()
2181
-    {
2182
-        return $this->actionType;
2183
-    }
2184
-
2185
-    /**
2186
-     * Sets the current action type
2187
-     *
2188
-     * @param string $actionType string - one of the consts from self::$action_types
2189
-     */
2190
-    public function setCurrentActionType($actionType)
2191
-    {
2192
-        $this->actionType = $actionType;
2193
-    }
2194
-
2195
-    /**
2196
-     * Helper method to allow templates to know whether they should show the 'Archive List' include or not.
2197
-     * The actual permissions are set on a per-environment level, so we need to find out if this $member can upload to
2198
-     * or download from *any* {@link DNEnvironment} that (s)he has access to.
2199
-     *
2200
-     * TODO To be replaced with a method that just returns the list of archives this {@link Member} has access to.
2201
-     *
2202
-     * @param Member|null $member The {@link Member} to check (or null to check the currently logged in Member)
2203
-     * @return boolean|null true if $member has access to upload or download to at least one {@link DNEnvironment}.
2204
-     */
2205
-    public function CanViewArchives(Member $member = null)
2206
-    {
2207
-        if ($member === null) {
2208
-            $member = Member::currentUser();
2209
-        }
2210
-
2211
-        if (Permission::checkMember($member, 'ADMIN')) {
2212
-            return true;
2213
-        }
2214
-
2215
-        $allProjects = $this->DNProjectList();
2216
-        if (!$allProjects) {
2217
-            return false;
2218
-        }
2219
-
2220
-        foreach ($allProjects as $project) {
2221
-            if ($project->Environments()) {
2222
-                foreach ($project->Environments() as $environment) {
2223
-                    if (
2224
-                        $environment->canRestore($member) ||
2225
-                        $environment->canBackup($member) ||
2226
-                        $environment->canUploadArchive($member) ||
2227
-                        $environment->canDownloadArchive($member)
2228
-                    ) {
2229
-                        // We can return early as we only need to know that we can access one environment
2230
-                        return true;
2231
-                    }
2232
-                }
2233
-            }
2234
-        }
2235
-    }
2236
-
2237
-    /**
2238
-     * Returns a list of attempted environment creations.
2239
-     *
2240
-     * @return PaginatedList
2241
-     */
2242
-    public function CreateEnvironmentList()
2243
-    {
2244
-        $project = $this->getCurrentProject();
2245
-        if ($project) {
2246
-            return new PaginatedList($project->CreateEnvironments()->sort("Created DESC"), $this->request);
2247
-        }
2248
-        return new PaginatedList(new ArrayList(), $this->request);
2249
-    }
2250
-
2251
-    /**
2252
-     * Returns a list of all archive files that can be accessed by the currently logged-in {@link Member}
2253
-     *
2254
-     * @return PaginatedList
2255
-     */
2256
-    public function CompleteDataArchives()
2257
-    {
2258
-        $project = $this->getCurrentProject();
2259
-        $archives = new ArrayList();
2260
-
2261
-        $archiveList = $project->Environments()->relation("DataArchives");
2262
-        if ($archiveList->count() > 0) {
2263
-            foreach ($archiveList as $archive) {
2264
-                if ($archive->canView() && !$archive->isPending()) {
2265
-                    $archives->push($archive);
2266
-                }
2267
-            }
2268
-        }
2269
-        return new PaginatedList($archives->sort("Created", "DESC"), $this->request);
2270
-    }
2271
-
2272
-    /**
2273
-     * @return PaginatedList The list of "pending" data archives which are waiting for a file
2274
-     * to be delivered offline by post, and manually uploaded into the system.
2275
-     */
2276
-    public function PendingDataArchives()
2277
-    {
2278
-        $project = $this->getCurrentProject();
2279
-        $archives = new ArrayList();
2280
-        foreach ($project->DNEnvironmentList() as $env) {
2281
-            foreach ($env->DataArchives() as $archive) {
2282
-                if ($archive->canView() && $archive->isPending()) {
2283
-                    $archives->push($archive);
2284
-                }
2285
-            }
2286
-        }
2287
-        return new PaginatedList($archives->sort("Created", "DESC"), $this->request);
2288
-    }
2289
-
2290
-    /**
2291
-     * @return PaginatedList
2292
-     */
2293
-    public function DataTransferLogs()
2294
-    {
2295
-        $project = $this->getCurrentProject();
2296
-
2297
-        $transfers = DNDataTransfer::get()->filterByCallback(function ($record) use ($project) {
2298
-            return
2299
-                $record->Environment()->Project()->ID == $project->ID && // Ensure only the current Project is shown
2300
-                (
2301
-                    $record->Environment()->canRestore() || // Ensure member can perform an action on the transfers env
2302
-                    $record->Environment()->canBackup() ||
2303
-                    $record->Environment()->canUploadArchive() ||
2304
-                    $record->Environment()->canDownloadArchive()
2305
-                );
2306
-        });
2307
-
2308
-        return new PaginatedList($transfers->sort("Created", "DESC"), $this->request);
2309
-    }
2310
-
2311
-    /**
2312
-     * @return null|PaginatedList
2313
-     */
2314
-    public function DeployHistory()
2315
-    {
2316
-        if ($env = $this->getCurrentEnvironment()) {
2317
-            $history = $env->DeployHistory();
2318
-            if ($history->count() > 0) {
2319
-                $pagination = new PaginatedList($history, $this->getRequest());
2320
-                $pagination->setPageLength(8);
2321
-                return $pagination;
2322
-            }
2323
-        }
2324
-        return null;
2325
-    }
2326
-
2327
-    /**
2328
-     * @return SS_HTTPResponse
2329
-     */
2330
-    protected function project404Response()
2331
-    {
2332
-        return new SS_HTTPResponse(
2333
-            "Project '" . Convert::raw2xml($this->getRequest()->param('Project')) . "' not found.",
2334
-            404
2335
-        );
2336
-    }
2337
-
2338
-    /**
2339
-     * @return SS_HTTPResponse
2340
-     */
2341
-    protected function environment404Response()
2342
-    {
2343
-        $envName = Convert::raw2xml($this->getRequest()->param('Environment'));
2344
-        return new SS_HTTPResponse("Environment '" . $envName . "' not found.", 404);
2345
-    }
2346
-
2347
-    /**
2348
-     * @param string $status
2349
-     * @param string $content
2350
-     *
2351
-     * @return string
2352
-     */
2353
-    protected function sendResponse($status, $content)
2354
-    {
2355
-        // strip excessive newlines
2356
-        $content = preg_replace('/(?:(?:\r\n|\r|\n)\s*){2}/s', "\n", $content);
2357
-
2358
-        $sendJSON = (strpos($this->getRequest()->getHeader('Accept'), 'application/json') !== false)
2359
-            || $this->getRequest()->getExtension() == 'json';
2360
-
2361
-        if (!$sendJSON) {
2362
-            $this->response->addHeader("Content-type", "text/plain");
2363
-            return $content;
2364
-        }
2365
-        $this->response->addHeader("Content-type", "application/json");
2366
-        return json_encode(array(
2367
-            'status' => $status,
2368
-            'content' => $content,
2369
-        ));
2370
-    }
2371
-
2372
-    /**
2373
-     * Validate the snapshot mode
2374
-     *
2375
-     * @param string $mode
2376
-     */
2377
-    protected function validateSnapshotMode($mode)
2378
-    {
2379
-        if (!in_array($mode, array('all', 'assets', 'db'))) {
2380
-            throw new LogicException('Invalid mode');
2381
-        }
2382
-    }
2383
-
2384
-    /**
2385
-     * @param string $sectionName
2386
-     * @param string $title
2387
-     *
2388
-     * @return SS_HTTPResponse
2389
-     */
2390
-    protected function getCustomisedViewSection($sectionName, $title = '', $data = array())
2391
-    {
2392
-        // Performs canView permission check by limiting visible projects
2393
-        $project = $this->getCurrentProject();
2394
-        if (!$project) {
2395
-            return $this->project404Response();
2396
-        }
2397
-        $data[$sectionName] = 1;
2398
-
2399
-        if ($this !== '') {
2400
-            $data['Title'] = $title;
2401
-        }
2402
-
2403
-        return $this->render($data);
2404
-    }
2405
-
2406
-    /**
2407
-     * Get items for the ambient menu that should be accessible from all pages.
2408
-     *
2409
-     * @return ArrayList
2410
-     */
2411
-    public function AmbientMenu()
2412
-    {
2413
-        $list = new ArrayList();
2414
-
2415
-        if (Member::currentUserID()) {
2416
-            $list->push(new ArrayData(array(
2417
-                'Classes' => 'logout',
2418
-                'FaIcon' => 'sign-out',
2419
-                'Link' => 'Security/logout',
2420
-                'Title' => 'Log out',
2421
-                'IsCurrent' => false,
2422
-                'IsSection' => false
2423
-            )));
2424
-        }
2425
-
2426
-        $this->extend('updateAmbientMenu', $list);
2427
-        return $list;
2428
-    }
2429
-
2430
-    /**
2431
-     * Create project action.
2432
-     *
2433
-     * @return SS_HTTPResponse
2434
-     */
2435
-    public function createproject(SS_HTTPRequest $request)
2436
-    {
2437
-        if ($this->canCreateProjects()) {
2438
-            return $this->render(['CurrentTitle' => 'Create Stack']);
2439
-        }
2440
-        return $this->httpError(403);
2441
-    }
2442
-
2443
-    /**
2444
-     * Checks whether the user can create a project.
2445
-     *
2446
-     * @return bool
2447
-     */
2448
-    public function canCreateProjects($member = null)
2449
-    {
2450
-        if (!$member) {
2451
-            $member = Member::currentUser();
2452
-        }
2453
-        if (!$member) {
2454
-            return false;
2455
-        }
2456
-
2457
-        return singleton('DNProject')->canCreate($member);
2458
-    }
2459
-
2460
-    /**
2461
-     * @return Form
2462
-     */
2463
-    public function CreateProjectForm()
2464
-    {
2465
-        $form = Form::create(
2466
-            $this,
2467
-            __FUNCTION__,
2468
-            $this->getCreateProjectFormFields(),
2469
-            $this->getCreateProjectFormActions(),
2470
-            new RequiredFields('Name', 'CVSPath')
2471
-        );
2472
-        $this->extend('updateCreateProjectForm', $form);
2473
-        return $form;
2474
-    }
2475
-
2476
-    /**
2477
-     * @return FieldList
2478
-     */
2479
-    protected function getCreateProjectFormFields()
2480
-    {
2481
-        $fields = FieldList::create();
2482
-        $fields->merge([
2483
-            TextField::create('Name', 'Title')->setDescription('Limited to alphanumeric characters, underscores and hyphens.'),
2484
-            TextField::create('CVSPath', 'Git URL')->setDescription('Your repository URL so we can clone your code (eg. [email protected]:silverstripe/silverstripe-installer.git)')
2485
-        ]);
2486
-        $this->extend('updateCreateProjectFormFields', $fields);
2487
-        return $fields;
2488
-    }
2489
-
2490
-    /**
2491
-     * @return FieldList
2492
-     */
2493
-    protected function getCreateProjectFormActions()
2494
-    {
2495
-        $fields = FieldList::create(
2496
-            FormAction::create('doCreateProject', 'Create Stack')
2497
-        );
2498
-        $this->extend('updateCreateProjectFormActions', $fields);
2499
-        return $fields;
2500
-    }
2501
-
2502
-    /**
2503
-     * Does the actual project creation.
2504
-     *
2505
-     * @param $data array
2506
-     * @param $form Form
2507
-     *
2508
-     * @return SS_HTTPResponse
2509
-     */
2510
-    public function doCreateProject($data, $form)
2511
-    {
2512
-        $form->loadDataFrom($data);
2513
-        $project = DNProject::create();
2514
-
2515
-        $form->saveInto($project);
2516
-        $this->extend('onBeforeCreateProject', $project, $data, $form);
2517
-        try {
2518
-            if ($project->write() > 0) {
2519
-                $this->extend('onAfterCreateProject', $project, $data, $form);
2520
-
2521
-                // If an extension hasn't redirected us, we'll redirect to the project.
2522
-                if (!$this->redirectedTo()) {
2523
-                    return $this->redirect($project->Link());
2524
-                } else {
2525
-                    return $this->response;
2526
-                }
2527
-            } else {
2528
-                $form->sessionMessage('Unable to write the stack to the database.', 'bad');
2529
-            }
2530
-        } catch (ValidationException $e) {
2531
-            $form->sessionMessage($e->getMessage(), 'bad');
2532
-        }
2533
-        return $this->redirectBack();
2534
-    }
2535
-
2536
-    /**
2537
-     * Returns the state of a current project build.
2538
-     *
2539
-     * @param SS_HTTPRequest $request
2540
-     *
2541
-     * @return SS_HTTPResponse
2542
-     */
2543
-    public function createprojectprogress(SS_HTTPRequest $request)
2544
-    {
2545
-        $project = $this->getCurrentProject();
2546
-        if (!$project) {
2547
-            return $this->httpError(404);
2548
-        }
2549
-
2550
-        $envCreations = $project->getInitialEnvironmentCreations();
2551
-        $complete = array();
2552
-        $inProgress = array();
2553
-        $failed = array();
2554
-        if ($envCreations->count() > 0) {
2555
-            foreach ($envCreations as $env) {
2556
-                $data = unserialize($env->Data);
2557
-                if (!isset($data['Name'])) {
2558
-                    $data['Name'] = 'Unknown';
2559
-                }
2560
-                switch ($env->ResqueStatus()) {
2561
-                    case "Queued":
2562
-                    case "Running":
2563
-                        $inProgress[$env->ID] = Convert::raw2xml($env->ResqueStatus());
2564
-                        break;
2565
-                    case "Complete":
2566
-                        $complete[$env->ID] = Convert::raw2xml($data['Name']);
2567
-                        break;
2568
-                    case "Failed":
2569
-                    case "Invalid":
2570
-                    default:
2571
-                        $failed[$env->ID] = Convert::raw2xml($data['Name']);
2572
-                }
2573
-            }
2574
-        }
2575
-
2576
-        $data = [
2577
-            'complete' => $project->isProjectReady(),
2578
-            'progress' => [
2579
-                'environments' => [
2580
-                    'complete' => $complete,
2581
-                    'inProgress' => $inProgress,
2582
-                    'failed' => $failed,
2583
-                ]
2584
-            ]
2585
-        ];
2586
-        $this->extend('updateCreateProjectProgressData', $data);
2587
-
2588
-        $response = $this->getResponse();
2589
-        $response->addHeader('Content-Type', 'application/json');
2590
-        $response->setBody(json_encode($data));
2591
-        return $response;
2592
-    }
2593
-
2594
-    public function checkrepoaccess(SS_HTTPRequest $request)
2595
-    {
2596
-        $project = $this->getCurrentProject();
2597
-        if (!$project) {
2598
-            return $this->httpError(404);
2599
-        }
2600
-
2601
-        if ($project->CVSPath) {
2602
-            $fetch = new FetchJob();
2603
-            $fetch->args = array('projectID' => $project->ID);
2604
-            try {
2605
-                $fetch->perform();
2606
-                $canAccessRepo = true;
2607
-            } catch (RuntimeException $e) {
2608
-                $canAccessRepo = false;
2609
-            }
2610
-            $data = ['canAccessRepo' => $canAccessRepo];
2611
-        } else {
2612
-            $data = ['canAccessRepo' => false];
2613
-        }
2614
-
2615
-        $response = $this->getResponse();
2616
-        $response->addHeader("Content-Type", "application/json");
2617
-        $response->setBody(json_encode($data));
2618
-        return $response;
2619
-    }
13
+	/**
14
+	 * @const string - action type for actions that perform deployments
15
+	 */
16
+	const ACTION_DEPLOY = 'deploy';
17
+
18
+	/**
19
+	 * @const string - action type for actions that manipulate snapshots
20
+	 */
21
+	const ACTION_SNAPSHOT = 'snapshot';
22
+
23
+	const ACTION_ENVIRONMENTS = 'createenv';
24
+
25
+	/**
26
+	 * @var string
27
+	 */
28
+	private $actionType = self::ACTION_DEPLOY;
29
+
30
+	/**
31
+	 * Bypass pipeline permission code
32
+	 */
33
+	const DEPLOYNAUT_BYPASS_PIPELINE = 'DEPLOYNAUT_BYPASS_PIPELINE';
34
+
35
+	/**
36
+	 * Allow dryrun of pipelines
37
+	 */
38
+	const DEPLOYNAUT_DRYRUN_PIPELINE = 'DEPLOYNAUT_DRYRUN_PIPELINE';
39
+
40
+	/**
41
+	 * Allow advanced options on deployments
42
+	 */
43
+	const DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS = 'DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS';
44
+
45
+	const ALLOW_PROD_DEPLOYMENT = 'ALLOW_PROD_DEPLOYMENT';
46
+	const ALLOW_NON_PROD_DEPLOYMENT = 'ALLOW_NON_PROD_DEPLOYMENT';
47
+	const ALLOW_PROD_SNAPSHOT = 'ALLOW_PROD_SNAPSHOT';
48
+	const ALLOW_NON_PROD_SNAPSHOT = 'ALLOW_NON_PROD_SNAPSHOT';
49
+	const ALLOW_CREATE_ENVIRONMENT = 'ALLOW_CREATE_ENVIRONMENT';
50
+
51
+	/**
52
+	 * @var array
53
+	 */
54
+	private static $allowed_actions = array(
55
+		'projects',
56
+		'nav',
57
+		'update',
58
+		'project',
59
+		'toggleprojectstar',
60
+		'branch',
61
+		'environment',
62
+		'abortpipeline',
63
+		'pipeline',
64
+		'pipelinelog',
65
+		'metrics',
66
+		'createenvlog',
67
+		'createenv',
68
+		'getDeployForm',
69
+		'doDeploy',
70
+		'deploy',
71
+		'deploylog',
72
+		'getDataTransferForm',
73
+		'transfer',
74
+		'transferlog',
75
+		'snapshots',
76
+		'createsnapshot',
77
+		'snapshotslog',
78
+		'uploadsnapshot',
79
+		'getCreateEnvironmentForm',
80
+		'getUploadSnapshotForm',
81
+		'getPostSnapshotForm',
82
+		'getDataTransferRestoreForm',
83
+		'getDeleteForm',
84
+		'getMoveForm',
85
+		'restoresnapshot',
86
+		'deletesnapshot',
87
+		'movesnapshot',
88
+		'postsnapshotsuccess',
89
+		'gitRevisions',
90
+		'deploySummary',
91
+		'startDeploy',
92
+		'createproject',
93
+		'CreateProjectForm',
94
+		'createprojectprogress',
95
+		'checkrepoaccess',
96
+	);
97
+
98
+	/**
99
+	 * URL handlers pretending that we have a deep URL structure.
100
+	 */
101
+	private static $url_handlers = array(
102
+		'project/$Project/environment/$Environment/DeployForm' => 'getDeployForm',
103
+		'project/$Project/createsnapshot/DataTransferForm' => 'getDataTransferForm',
104
+		'project/$Project/DataTransferForm' => 'getDataTransferForm',
105
+		'project/$Project/DataTransferRestoreForm' => 'getDataTransferRestoreForm',
106
+		'project/$Project/DeleteForm' => 'getDeleteForm',
107
+		'project/$Project/MoveForm' => 'getMoveForm',
108
+		'project/$Project/UploadSnapshotForm' => 'getUploadSnapshotForm',
109
+		'project/$Project/PostSnapshotForm' => 'getPostSnapshotForm',
110
+		'project/$Project/environment/$Environment/metrics' => 'metrics',
111
+		'project/$Project/environment/$Environment/pipeline/$Identifier//$Action/$ID/$OtherID' => 'pipeline',
112
+		'project/$Project/environment/$Environment/deploy_summary' => 'deploySummary',
113
+		'project/$Project/environment/$Environment/git_revisions' => 'gitRevisions',
114
+		'project/$Project/environment/$Environment/start-deploy' => 'startDeploy',
115
+		'project/$Project/environment/$Environment/deploy/$Identifier/log' => 'deploylog',
116
+		'project/$Project/environment/$Environment/deploy/$Identifier' => 'deploy',
117
+		'project/$Project/transfer/$Identifier/log' => 'transferlog',
118
+		'project/$Project/transfer/$Identifier' => 'transfer',
119
+		'project/$Project/environment/$Environment' => 'environment',
120
+		'project/$Project/createenv/$Identifier/log' => 'createenvlog',
121
+		'project/$Project/createenv/$Identifier' => 'createenv',
122
+		'project/$Project/CreateEnvironmentForm' => 'getCreateEnvironmentForm',
123
+		'project/$Project/branch' => 'branch',
124
+		'project/$Project/build/$Build' => 'build',
125
+		'project/$Project/restoresnapshot/$DataArchiveID' => 'restoresnapshot',
126
+		'project/$Project/deletesnapshot/$DataArchiveID' => 'deletesnapshot',
127
+		'project/$Project/movesnapshot/$DataArchiveID' => 'movesnapshot',
128
+		'project/$Project/update' => 'update',
129
+		'project/$Project/snapshots' => 'snapshots',
130
+		'project/$Project/createsnapshot' => 'createsnapshot',
131
+		'project/$Project/uploadsnapshot' => 'uploadsnapshot',
132
+		'project/$Project/snapshotslog' => 'snapshotslog',
133
+		'project/$Project/postsnapshotsuccess/$DataArchiveID' => 'postsnapshotsuccess',
134
+		'project/$Project/star' => 'toggleprojectstar',
135
+		'project/$Project/createprojectprogress' => 'createprojectprogress',
136
+		'project/$Project/checkrepoaccess' => 'checkrepoaccess',
137
+		'project/$Project' => 'project',
138
+		'nav/$Project' => 'nav',
139
+		'projects' => 'projects',
140
+	);
141
+
142
+	/**
143
+	 * @var array
144
+	 */
145
+	protected static $_project_cache = array();
146
+
147
+	/**
148
+	 * @var array
149
+	 */
150
+	private static $support_links = array();
151
+
152
+	/**
153
+	 * @var array
154
+	 */
155
+	private static $platform_specific_strings = array();
156
+
157
+	/**
158
+	 * @var array
159
+	 */
160
+	private static $action_types = array(
161
+		self::ACTION_DEPLOY,
162
+		self::ACTION_SNAPSHOT
163
+	);
164
+
165
+	/**
166
+	 * @var DNData
167
+	 */
168
+	protected $data;
169
+
170
+	/**
171
+	 * Include requirements that deploynaut needs, such as javascript.
172
+	 */
173
+	public static function include_requirements()
174
+	{
175
+
176
+		// JS should always go to the bottom, otherwise there's the risk that Requirements
177
+		// puts them halfway through the page to the nearest <script> tag. We don't want that.
178
+		Requirements::set_force_js_to_bottom(true);
179
+
180
+		// todo these should be bundled into the same JS as the others in "static" below.
181
+		// We've deliberately not used combined_files as it can mess with some of the JS used
182
+		// here and cause sporadic errors.
183
+		Requirements::javascript('deploynaut/javascript/jquery.js');
184
+		Requirements::javascript('deploynaut/javascript/bootstrap.js');
185
+		Requirements::javascript('deploynaut/javascript/q.js');
186
+		Requirements::javascript('deploynaut/javascript/tablefilter.js');
187
+		Requirements::javascript('deploynaut/javascript/deploynaut.js');
188
+		Requirements::javascript('deploynaut/javascript/react-with-addons.js');
189
+		Requirements::javascript('deploynaut/javascript/bootstrap.file-input.js');
190
+		Requirements::javascript('deploynaut/thirdparty/select2/dist/js/select2.min.js');
191
+		Requirements::javascript('deploynaut/javascript/material.js');
192
+
193
+		// Load the buildable dependencies only if not loaded centrally.
194
+		if (!is_dir(BASE_PATH . DIRECTORY_SEPARATOR . 'static')) {
195
+			if (\Director::isDev()) {
196
+				\Requirements::javascript('deploynaut/static/bundle-debug.js');
197
+			} else {
198
+				\Requirements::javascript('deploynaut/static/bundle.js');
199
+			}
200
+		}
201
+
202
+		Requirements::css('deploynaut/static/style.css');
203
+	}
204
+
205
+	/**
206
+	 * Check for feature flags:
207
+	 * - FLAG_SNAPSHOTS_ENABLED: set to true to enable globally
208
+	 * - FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS: set to semicolon-separated list of email addresses of allowed users.
209
+	 *
210
+	 * @return boolean
211
+	 */
212
+	public static function FlagSnapshotsEnabled()
213
+	{
214
+		if (defined('FLAG_SNAPSHOTS_ENABLED') && FLAG_SNAPSHOTS_ENABLED) {
215
+			return true;
216
+		}
217
+		if (defined('FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS') && FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS) {
218
+			$allowedMembers = explode(';', FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS);
219
+			$member = Member::currentUser();
220
+			if ($allowedMembers && $member && in_array($member->Email, $allowedMembers)) {
221
+				return true;
222
+			}
223
+		}
224
+		return false;
225
+	}
226
+
227
+	/**
228
+	 * @return ArrayList
229
+	 */
230
+	public static function get_support_links()
231
+	{
232
+		$supportLinks = self::config()->support_links;
233
+		if ($supportLinks) {
234
+			return new ArrayList($supportLinks);
235
+		}
236
+	}
237
+
238
+	/**
239
+	 * @return array
240
+	 */
241
+	public static function get_template_global_variables()
242
+	{
243
+		return array(
244
+			'RedisUnavailable' => 'RedisUnavailable',
245
+			'RedisWorkersCount' => 'RedisWorkersCount',
246
+			'SidebarLinks' => 'SidebarLinks',
247
+			"SupportLinks" => 'get_support_links'
248
+		);
249
+	}
250
+
251
+	/**
252
+	 */
253
+	public function init()
254
+	{
255
+		parent::init();
256
+
257
+		if (!Member::currentUser() && !Session::get('AutoLoginHash')) {
258
+			return Security::permissionFailure();
259
+		}
260
+
261
+		// Block framework jquery
262
+		Requirements::block(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
263
+
264
+		self::include_requirements();
265
+	}
266
+
267
+	/**
268
+	 * @return string
269
+	 */
270
+	public function Link()
271
+	{
272
+		return "naut/";
273
+	}
274
+
275
+	/**
276
+	 * Actions
277
+	 *
278
+	 * @param SS_HTTPRequest $request
279
+	 * @return \SS_HTTPResponse
280
+	 */
281
+	public function index(SS_HTTPRequest $request)
282
+	{
283
+		return $this->redirect($this->Link() . 'projects/');
284
+	}
285
+
286
+	/**
287
+	 * Action
288
+	 *
289
+	 * @param SS_HTTPRequest $request
290
+	 * @return string - HTML
291
+	 */
292
+	public function projects(SS_HTTPRequest $request)
293
+	{
294
+		// Performs canView permission check by limiting visible projects in DNProjectsList() call.
295
+		return $this->customise(array(
296
+			'Title' => 'Projects',
297
+		))->render();
298
+	}
299
+
300
+	/**
301
+	 * @param SS_HTTPRequest $request
302
+	 * @return HTMLText
303
+	 */
304
+	public function nav(SS_HTTPRequest $request)
305
+	{
306
+		return $this->renderWith('Nav');
307
+	}
308
+
309
+	/**
310
+	 * Return a link to the navigation template used for AJAX requests.
311
+	 * @return string
312
+	 */
313
+	public function NavLink()
314
+	{
315
+		$currentProject = $this->getCurrentProject();
316
+		$projectName = $currentProject ? $currentProject->Name : null;
317
+		return Controller::join_links(Director::absoluteBaseURL(), 'naut', 'nav', $projectName);
318
+	}
319
+
320
+	/**
321
+	 * Action
322
+	 *
323
+	 * @param SS_HTTPRequest $request
324
+	 * @return SS_HTTPResponse - HTML
325
+	 */
326
+	public function snapshots(SS_HTTPRequest $request)
327
+	{
328
+		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
329
+		return $this->getCustomisedViewSection('SnapshotsSection', 'Data Snapshots');
330
+	}
331
+
332
+	/**
333
+	 * Action
334
+	 *
335
+	 * @param SS_HTTPRequest $request
336
+	 * @return string - HTML
337
+	 */
338
+	public function createsnapshot(SS_HTTPRequest $request)
339
+	{
340
+		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
341
+
342
+		// Performs canView permission check by limiting visible projects
343
+		$project = $this->getCurrentProject();
344
+		if (!$project) {
345
+			return $this->project404Response();
346
+		}
347
+
348
+		if (!$project->canBackup()) {
349
+			return new SS_HTTPResponse("Not allowed to create snapshots on any environments", 401);
350
+		}
351
+
352
+		return $this->customise(array(
353
+			'Title' => 'Create Data Snapshot',
354
+			'SnapshotsSection' => 1,
355
+			'DataTransferForm' => $this->getDataTransferForm($request)
356
+		))->render();
357
+	}
358
+
359
+	/**
360
+	 * Action
361
+	 *
362
+	 * @param SS_HTTPRequest $request
363
+	 * @return string - HTML
364
+	 */
365
+	public function uploadsnapshot(SS_HTTPRequest $request)
366
+	{
367
+		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
368
+
369
+		// Performs canView permission check by limiting visible projects
370
+		$project = $this->getCurrentProject();
371
+		if (!$project) {
372
+			return $this->project404Response();
373
+		}
374
+
375
+		if (!$project->canUploadArchive()) {
376
+			return new SS_HTTPResponse("Not allowed to upload", 401);
377
+		}
378
+
379
+		return $this->customise(array(
380
+			'SnapshotsSection' => 1,
381
+			'UploadSnapshotForm' => $this->getUploadSnapshotForm($request),
382
+			'PostSnapshotForm' => $this->getPostSnapshotForm($request)
383
+		))->render();
384
+	}
385
+
386
+	/**
387
+	 * Return the upload limit for snapshot uploads
388
+	 * @return string
389
+	 */
390
+	public function UploadLimit()
391
+	{
392
+		return File::format_size(min(
393
+			File::ini2bytes(ini_get('upload_max_filesize')),
394
+			File::ini2bytes(ini_get('post_max_size'))
395
+		));
396
+	}
397
+
398
+	/**
399
+	 * Construct the upload form.
400
+	 *
401
+	 * @param SS_HTTPRequest $request
402
+	 * @return Form
403
+	 */
404
+	public function getUploadSnapshotForm(SS_HTTPRequest $request)
405
+	{
406
+		// Performs canView permission check by limiting visible projects
407
+		$project = $this->getCurrentProject();
408
+		if (!$project) {
409
+			return $this->project404Response();
410
+		}
411
+
412
+		if (!$project->canUploadArchive()) {
413
+			return new SS_HTTPResponse("Not allowed to upload", 401);
414
+		}
415
+
416
+		// Framing an environment as a "group of people with download access"
417
+		// makes more sense to the user here, while still allowing us to enforce
418
+		// environment specific restrictions on downloading the file later on.
419
+		$envs = $project->DNEnvironmentList()->filterByCallback(function ($item) {
420
+			return $item->canUploadArchive();
421
+		});
422
+		$envsMap = array();
423
+		foreach ($envs as $env) {
424
+			$envsMap[$env->ID] = $env->Name;
425
+		}
426
+
427
+		$maxSize = min(File::ini2bytes(ini_get('upload_max_filesize')), File::ini2bytes(ini_get('post_max_size')));
428
+		$fileField = DataArchiveFileField::create('ArchiveFile', 'File');
429
+		$fileField->getValidator()->setAllowedExtensions(array('sspak'));
430
+		$fileField->getValidator()->setAllowedMaxFileSize(array('*' => $maxSize));
431
+
432
+		$form = Form::create(
433
+			$this,
434
+			'UploadSnapshotForm',
435
+			FieldList::create(
436
+				$fileField,
437
+				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
438
+				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
439
+					->setEmptyString('Select an environment')
440
+			),
441
+			FieldList::create(
442
+				FormAction::create('doUploadSnapshot', 'Upload File')
443
+					->addExtraClass('btn')
444
+			),
445
+			RequiredFields::create('ArchiveFile')
446
+		);
447
+
448
+		$form->disableSecurityToken();
449
+		$form->addExtraClass('fields-wide');
450
+		// Tweak the action so it plays well with our fake URL structure.
451
+		$form->setFormAction($project->Link() . '/UploadSnapshotForm');
452
+
453
+		return $form;
454
+	}
455
+
456
+	/**
457
+	 * @param array $data
458
+	 * @param Form $form
459
+	 *
460
+	 * @return bool|HTMLText|SS_HTTPResponse
461
+	 */
462
+	public function doUploadSnapshot($data, Form $form)
463
+	{
464
+		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
465
+
466
+		// Performs canView permission check by limiting visible projects
467
+		$project = $this->getCurrentProject();
468
+		if (!$project) {
469
+			return $this->project404Response();
470
+		}
471
+
472
+		$validEnvs = $project->DNEnvironmentList()
473
+			->filterByCallback(function ($item) {
474
+				return $item->canUploadArchive();
475
+			});
476
+
477
+		// Validate $data['EnvironmentID'] by checking against $validEnvs.
478
+		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
479
+		if (!$environment) {
480
+			throw new LogicException('Invalid environment');
481
+		}
482
+
483
+		$this->validateSnapshotMode($data['Mode']);
484
+
485
+		$dataArchive = DNDataArchive::create(array(
486
+			'AuthorID' => Member::currentUserID(),
487
+			'EnvironmentID' => $data['EnvironmentID'],
488
+			'IsManualUpload' => true,
489
+		));
490
+		// needs an ID and transfer to determine upload path
491
+		$dataArchive->write();
492
+		$dataTransfer = DNDataTransfer::create(array(
493
+			'AuthorID' => Member::currentUserID(),
494
+			'Mode' => $data['Mode'],
495
+			'Origin' => 'ManualUpload',
496
+			'EnvironmentID' => $data['EnvironmentID']
497
+		));
498
+		$dataTransfer->write();
499
+		$dataArchive->DataTransfers()->add($dataTransfer);
500
+		$form->saveInto($dataArchive);
501
+		$dataArchive->write();
502
+		$workingDir = TEMP_FOLDER . DIRECTORY_SEPARATOR . 'deploynaut-transfer-' . $dataTransfer->ID;
503
+
504
+		$cleanupFn = function () use ($workingDir, $dataTransfer, $dataArchive) {
505
+			$process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir)));
506
+			$process->run();
507
+			$dataTransfer->delete();
508
+			$dataArchive->delete();
509
+		};
510
+
511
+		// extract the sspak contents so we can inspect them
512
+		try {
513
+			$dataArchive->extractArchive($workingDir);
514
+		} catch (Exception $e) {
515
+			$cleanupFn();
516
+			$form->sessionMessage(
517
+				'There was a problem trying to open your snapshot for processing. Please try uploading again',
518
+				'bad'
519
+			);
520
+			return $this->redirectBack();
521
+		}
522
+
523
+		// validate that the sspak contents match the declared contents
524
+		$result = $dataArchive->validateArchiveContents();
525
+		if (!$result->valid()) {
526
+			$cleanupFn();
527
+			$form->sessionMessage($result->message(), 'bad');
528
+			return $this->redirectBack();
529
+		}
530
+
531
+		// fix file permissions of extracted sspak files then re-build the sspak
532
+		try {
533
+			$dataArchive->fixArchivePermissions($workingDir);
534
+			$dataArchive->setArchiveFromFiles($workingDir);
535
+		} catch (Exception $e) {
536
+			$cleanupFn();
537
+			$form->sessionMessage(
538
+				'There was a problem processing your snapshot. Please try uploading again',
539
+				'bad'
540
+			);
541
+			return $this->redirectBack();
542
+		}
543
+
544
+		// cleanup any extracted sspak contents lying around
545
+		$process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir)));
546
+		$process->run();
547
+
548
+		return $this->customise(array(
549
+			'Project' => $project,
550
+			'CurrentProject' => $project,
551
+			'SnapshotsSection' => 1,
552
+			'DataArchive' => $dataArchive,
553
+			'DataTransferRestoreForm' => $this->getDataTransferRestoreForm($this->request, $dataArchive),
554
+			'BackURL' => $project->Link('snapshots')
555
+		))->renderWith(array('DNRoot_uploadsnapshot', 'DNRoot'));
556
+	}
557
+
558
+	/**
559
+	 * @param SS_HTTPRequest $request
560
+	 * @return Form
561
+	 */
562
+	public function getPostSnapshotForm(SS_HTTPRequest $request)
563
+	{
564
+		// Performs canView permission check by limiting visible projects
565
+		$project = $this->getCurrentProject();
566
+		if (!$project) {
567
+			return $this->project404Response();
568
+		}
569
+
570
+		if (!$project->canUploadArchive()) {
571
+			return new SS_HTTPResponse("Not allowed to upload", 401);
572
+		}
573
+
574
+		// Framing an environment as a "group of people with download access"
575
+		// makes more sense to the user here, while still allowing us to enforce
576
+		// environment specific restrictions on downloading the file later on.
577
+		$envs = $project->DNEnvironmentList()->filterByCallback(function ($item) {
578
+			return $item->canUploadArchive();
579
+		});
580
+		$envsMap = array();
581
+		foreach ($envs as $env) {
582
+			$envsMap[$env->ID] = $env->Name;
583
+		}
584
+
585
+		$form = Form::create(
586
+			$this,
587
+			'PostSnapshotForm',
588
+			FieldList::create(
589
+				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
590
+				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
591
+					->setEmptyString('Select an environment')
592
+			),
593
+			FieldList::create(
594
+				FormAction::create('doPostSnapshot', 'Submit request')
595
+					->addExtraClass('btn')
596
+			),
597
+			RequiredFields::create('File')
598
+		);
599
+
600
+		$form->disableSecurityToken();
601
+		$form->addExtraClass('fields-wide');
602
+		// Tweak the action so it plays well with our fake URL structure.
603
+		$form->setFormAction($project->Link() . '/PostSnapshotForm');
604
+
605
+		return $form;
606
+	}
607
+
608
+	/**
609
+	 * @param array $data
610
+	 * @param Form $form
611
+	 *
612
+	 * @return SS_HTTPResponse
613
+	 */
614
+	public function doPostSnapshot($data, $form)
615
+	{
616
+		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
617
+
618
+		$project = $this->getCurrentProject();
619
+		if (!$project) {
620
+			return $this->project404Response();
621
+		}
622
+
623
+		$validEnvs = $project->DNEnvironmentList()->filterByCallback(function ($item) {
624
+				return $item->canUploadArchive();
625
+		});
626
+
627
+		// Validate $data['EnvironmentID'] by checking against $validEnvs.
628
+		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
629
+		if (!$environment) {
630
+			throw new LogicException('Invalid environment');
631
+		}
632
+
633
+		$dataArchive = DNDataArchive::create(array(
634
+			'UploadToken' => DNDataArchive::generate_upload_token(),
635
+		));
636
+		$form->saveInto($dataArchive);
637
+		$dataArchive->write();
638
+
639
+		return $this->redirect(Controller::join_links(
640
+			$project->Link(),
641
+			'postsnapshotsuccess',
642
+			$dataArchive->ID
643
+		));
644
+	}
645
+
646
+	/**
647
+	 * Action
648
+	 *
649
+	 * @param SS_HTTPRequest $request
650
+	 * @return SS_HTTPResponse - HTML
651
+	 */
652
+	public function snapshotslog(SS_HTTPRequest $request)
653
+	{
654
+		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
655
+		return $this->getCustomisedViewSection('SnapshotsSection', 'Data Snapshots Log');
656
+	}
657
+
658
+	/**
659
+	 * @param SS_HTTPRequest $request
660
+	 * @return SS_HTTPResponse|string
661
+	 * @throws SS_HTTPResponse_Exception
662
+	 */
663
+	public function postsnapshotsuccess(SS_HTTPRequest $request)
664
+	{
665
+		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
666
+
667
+		// Performs canView permission check by limiting visible projects
668
+		$project = $this->getCurrentProject();
669
+		if (!$project) {
670
+			return $this->project404Response();
671
+		}
672
+
673
+		if (!$project->canUploadArchive()) {
674
+			return new SS_HTTPResponse("Not allowed to upload", 401);
675
+		}
676
+
677
+		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
678
+		if (!$dataArchive) {
679
+			return new SS_HTTPResponse("Archive not found.", 404);
680
+		}
681
+
682
+		if (!$dataArchive->canRestore()) {
683
+			throw new SS_HTTPResponse_Exception('Not allowed to restore archive', 403);
684
+		}
685
+
686
+		return $this->render(array(
687
+				'Title' => 'How to send us your Data Snapshot by post',
688
+				'DataArchive' => $dataArchive,
689
+				'Address' => Config::inst()->get('Deploynaut', 'snapshot_post_address'),
690
+				'BackURL' => $project->Link(),
691
+			));
692
+	}
693
+
694
+	/**
695
+	 * @param SS_HTTPRequest $request
696
+	 * @return \SS_HTTPResponse
697
+	 */
698
+	public function project(SS_HTTPRequest $request)
699
+	{
700
+		return $this->getCustomisedViewSection('ProjectOverview', '', array('IsAdmin' => Permission::check('ADMIN')));
701
+	}
702
+
703
+	/**
704
+	 * This action will star / unstar a project for the current member
705
+	 *
706
+	 * @param SS_HTTPRequest $request
707
+	 *
708
+	 * @return SS_HTTPResponse
709
+	 */
710
+	public function toggleprojectstar(SS_HTTPRequest $request)
711
+	{
712
+		$project = $this->getCurrentProject();
713
+		if (!$project) {
714
+			return $this->project404Response();
715
+		}
716
+
717
+		$member = Member::currentUser();
718
+		if ($member === null) {
719
+			return $this->project404Response();
720
+		}
721
+		$favProject = $member->StarredProjects()
722
+			->filter('DNProjectID', $project->ID)
723
+			->first();
724
+
725
+		if ($favProject) {
726
+			$member->StarredProjects()->remove($favProject);
727
+		} else {
728
+			$member->StarredProjects()->add($project);
729
+		}
730
+		return $this->redirectBack();
731
+	}
732
+
733
+	/**
734
+	 * @param SS_HTTPRequest $request
735
+	 * @return \SS_HTTPResponse
736
+	 */
737
+	public function branch(SS_HTTPRequest $request)
738
+	{
739
+		$project = $this->getCurrentProject();
740
+		if (!$project) {
741
+			return $this->project404Response();
742
+		}
743
+
744
+		$branchName = $request->getVar('name');
745
+		$branch = $project->DNBranchList()->byName($branchName);
746
+		if (!$branch) {
747
+			return new SS_HTTPResponse("Branch '" . Convert::raw2xml($branchName) . "' not found.", 404);
748
+		}
749
+
750
+		return $this->render(array(
751
+			'CurrentBranch' => $branch,
752
+		));
753
+	}
754
+
755
+	/**
756
+	 * @param SS_HTTPRequest $request
757
+	 * @return \SS_HTTPResponse
758
+	 */
759
+	public function environment(SS_HTTPRequest $request)
760
+	{
761
+		// Performs canView permission check by limiting visible projects
762
+		$project = $this->getCurrentProject();
763
+		if (!$project) {
764
+			return $this->project404Response();
765
+		}
766
+
767
+		// Performs canView permission check by limiting visible projects
768
+		$env = $this->getCurrentEnvironment($project);
769
+		if (!$env) {
770
+			return $this->environment404Response();
771
+		}
772
+
773
+		return $this->render(array(
774
+			'DNEnvironmentList' => $this->getCurrentProject()->DNEnvironmentList(),
775
+			'FlagSnapshotsEnabled' => $this->FlagSnapshotsEnabled(),
776
+		));
777
+	}
778
+
779
+
780
+	/**
781
+	 * Initiate a pipeline dry run
782
+	 *
783
+	 * @param array $data
784
+	 * @param DeployForm $form
785
+	 *
786
+	 * @return SS_HTTPResponse
787
+	 */
788
+	public function doDryRun($data, DeployForm $form)
789
+	{
790
+		return $this->beginPipeline($data, $form, true);
791
+	}
792
+
793
+	/**
794
+	 * Initiate a pipeline
795
+	 *
796
+	 * @param array $data
797
+	 * @param DeployForm $form
798
+	 * @return \SS_HTTPResponse
799
+	 */
800
+	public function startPipeline($data, $form)
801
+	{
802
+		return $this->beginPipeline($data, $form);
803
+	}
804
+
805
+	/**
806
+	 * Start a pipeline
807
+	 *
808
+	 * @param array $data
809
+	 * @param DeployForm $form
810
+	 * @param bool $isDryRun
811
+	 * @return \SS_HTTPResponse
812
+	 */
813
+	protected function beginPipeline($data, DeployForm $form, $isDryRun = false)
814
+	{
815
+		$buildName = $form->getSelectedBuild($data);
816
+
817
+		// Performs canView permission check by limiting visible projects
818
+		$project = $this->getCurrentProject();
819
+		if (!$project) {
820
+			return $this->project404Response();
821
+		}
822
+
823
+		// Performs canView permission check by limiting visible projects
824
+		$environment = $this->getCurrentEnvironment($project);
825
+		if (!$environment) {
826
+			return $this->environment404Response();
827
+		}
828
+
829
+		if (!$environment->DryRunEnabled && $isDryRun) {
830
+			return new SS_HTTPResponse("Dry-run for pipelines is not enabled for this environment", 404);
831
+		}
832
+
833
+		// Initiate the pipeline
834
+		$sha = $project->DNBuildList()->byName($buildName);
835
+		$pipeline = Pipeline::create();
836
+		$pipeline->DryRun = $isDryRun;
837
+		$pipeline->EnvironmentID = $environment->ID;
838
+		$pipeline->AuthorID = Member::currentUserID();
839
+		$pipeline->SHA = $sha->FullName();
840
+		// Record build at time of execution
841
+		if ($currentBuild = $environment->CurrentBuild()) {
842
+			$pipeline->PreviousDeploymentID = $currentBuild->ID;
843
+		}
844
+		$pipeline->start(); // start() will call write(), so no need to do it here as well.
845
+		return $this->redirect($environment->Link());
846
+	}
847
+
848
+	/**
849
+	 * @param SS_HTTPRequest $request
850
+	 *
851
+	 * @return SS_HTTPResponse
852
+	 * @throws SS_HTTPResponse_Exception
853
+	 */
854
+	public function pipeline(SS_HTTPRequest $request)
855
+	{
856
+		$params = $request->params();
857
+		$pipeline = Pipeline::get()->byID($params['Identifier']);
858
+
859
+		if (!$pipeline || !$pipeline->ID || !$pipeline->Environment()) {
860
+			throw new SS_HTTPResponse_Exception('Pipeline not found', 404);
861
+		}
862
+		if (!$pipeline->Environment()->canView()) {
863
+			return Security::permissionFailure();
864
+		}
865
+
866
+		$environment = $pipeline->Environment();
867
+		$project = $pipeline->Environment()->Project();
868
+
869
+		if ($environment->Name != $params['Environment']) {
870
+			throw new LogicException("Environment in URL doesn't match this pipeline");
871
+		}
872
+		if ($project->Name != $params['Project']) {
873
+			throw new LogicException("Project in URL doesn't match this pipeline");
874
+		}
875
+
876
+		// Delegate to sub-requesthandler
877
+		return PipelineController::create($this, $pipeline);
878
+	}
879
+
880
+	/**
881
+	 * Shows the creation log.
882
+	 *
883
+	 * @param SS_HTTPRequest $request
884
+	 * @return string
885
+	 */
886
+	public function createenv(SS_HTTPRequest $request)
887
+	{
888
+		$params = $request->params();
889
+		if ($params['Identifier']) {
890
+			$record = DNCreateEnvironment::get()->byId($params['Identifier']);
891
+
892
+			if (!$record || !$record->ID) {
893
+				throw new SS_HTTPResponse_Exception('Create environment not found', 404);
894
+			}
895
+			if (!$record->canView()) {
896
+				return Security::permissionFailure();
897
+			}
898
+
899
+			$project = $this->getCurrentProject();
900
+			if (!$project) {
901
+				return $this->project404Response();
902
+			}
903
+
904
+			if ($project->Name != $params['Project']) {
905
+				throw new LogicException("Project in URL doesn't match this creation");
906
+			}
907
+
908
+			return $this->render(array(
909
+				'CreateEnvironment' => $record,
910
+			));
911
+		}
912
+		return $this->render(array('CurrentTitle' => 'Create an environment'));
913
+	}
914
+
915
+
916
+	public function createenvlog(SS_HTTPRequest $request)
917
+	{
918
+		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
919
+
920
+		$params = $request->params();
921
+		$env = DNCreateEnvironment::get()->byId($params['Identifier']);
922
+
923
+		if (!$env || !$env->ID) {
924
+			throw new SS_HTTPResponse_Exception('Log not found', 404);
925
+		}
926
+		if (!$env->canView()) {
927
+			return Security::permissionFailure();
928
+		}
929
+
930
+		$project = $env->Project();
931
+
932
+		if ($project->Name != $params['Project']) {
933
+			throw new LogicException("Project in URL doesn't match this deploy");
934
+		}
935
+
936
+		$log = $env->log();
937
+		if ($log->exists()) {
938
+			$content = $log->content();
939
+		} else {
940
+			$content = 'Waiting for action to start';
941
+		}
942
+
943
+		return $this->sendResponse($env->ResqueStatus(), $content);
944
+	}
945
+
946
+	/**
947
+	 * @param SS_HTTPRequest $request
948
+	 * @return Form
949
+	 */
950
+	public function getCreateEnvironmentForm(SS_HTTPRequest $request)
951
+	{
952
+		$this->setCurrentActionType(self::ACTION_ENVIRONMENTS);
953
+
954
+		$project = $this->getCurrentProject();
955
+		if (!$project) {
956
+			return $this->project404Response();
957
+		}
958
+
959
+		$envType = $project->AllowedEnvironmentType;
960
+		if (!$envType || !class_exists($envType)) {
961
+			return null;
962
+		}
963
+
964
+		$backend = Injector::inst()->get($envType);
965
+		if (!($backend instanceof EnvironmentCreateBackend)) {
966
+			// Only allow this for supported backends.
967
+			return null;
968
+		}
969
+
970
+		$fields = $backend->getCreateEnvironmentFields($project);
971
+		if (!$fields) {
972
+			return null;
973
+		}
974
+
975
+		if (!$project->canCreateEnvironments()) {
976
+			return new SS_HTTPResponse('Not allowed to create environments for this project', 401);
977
+		}
978
+
979
+		$form = Form::create(
980
+			$this,
981
+			'CreateEnvironmentForm',
982
+			$fields,
983
+			FieldList::create(
984
+				FormAction::create('doCreateEnvironment', 'Create')
985
+					->addExtraClass('btn')
986
+			),
987
+			$backend->getCreateEnvironmentValidator()
988
+		);
989
+
990
+		// Tweak the action so it plays well with our fake URL structure.
991
+		$form->setFormAction($project->Link() . '/CreateEnvironmentForm');
992
+
993
+		return $form;
994
+	}
995
+
996
+	/**
997
+	 * @param array $data
998
+	 * @param Form $form
999
+	 *
1000
+	 * @return bool|HTMLText|SS_HTTPResponse
1001
+	 */
1002
+	public function doCreateEnvironment($data, Form $form)
1003
+	{
1004
+		$this->setCurrentActionType(self::ACTION_ENVIRONMENTS);
1005
+
1006
+		$project = $this->getCurrentProject();
1007
+		if (!$project) {
1008
+			return $this->project404Response();
1009
+		}
1010
+
1011
+		if (!$project->canCreateEnvironments()) {
1012
+			return new SS_HTTPResponse('Not allowed to create environments for this project', 401);
1013
+		}
1014
+
1015
+		// Set the environment type so we know what we're creating.
1016
+		$data['EnvironmentType'] = $project->AllowedEnvironmentType;
1017
+
1018
+		$job = DNCreateEnvironment::create();
1019
+
1020
+		$job->Data = serialize($data);
1021
+		$job->ProjectID = $project->ID;
1022
+		$job->write();
1023
+		$job->start();
1024
+
1025
+		return $this->redirect($project->Link('createenv') . '/' . $job->ID);
1026
+	}
1027
+
1028
+	/**
1029
+	 *
1030
+	 * @param SS_HTTPRequest $request
1031
+	 * @return \SS_HTTPResponse
1032
+	 */
1033
+	public function metrics(SS_HTTPRequest $request)
1034
+	{
1035
+		// Performs canView permission check by limiting visible projects
1036
+		$project = $this->getCurrentProject();
1037
+		if (!$project) {
1038
+			return $this->project404Response();
1039
+		}
1040
+
1041
+		// Performs canView permission check by limiting visible projects
1042
+		$env = $this->getCurrentEnvironment($project);
1043
+		if (!$env) {
1044
+			return $this->environment404Response();
1045
+		}
1046
+
1047
+		return $this->render();
1048
+	}
1049
+
1050
+	/**
1051
+	 * Get the DNData object.
1052
+	 *
1053
+	 * @return DNData
1054
+	 */
1055
+	public function DNData()
1056
+	{
1057
+		return DNData::inst();
1058
+	}
1059
+
1060
+	/**
1061
+	 * Provide a list of all projects.
1062
+	 *
1063
+	 * @return SS_List
1064
+	 */
1065
+	public function DNProjectList()
1066
+	{
1067
+		$memberId = Member::currentUserID();
1068
+		if (!$memberId) {
1069
+			return new ArrayList();
1070
+		}
1071
+
1072
+		if (Permission::check('ADMIN')) {
1073
+			return DNProject::get();
1074
+		}
1075
+
1076
+		return Member::get()->filter('ID', $memberId)
1077
+			->relation('Groups')
1078
+			->relation('Projects');
1079
+	}
1080
+
1081
+	/**
1082
+	 * @return ArrayList
1083
+	 */
1084
+	public function getPlatformSpecificStrings()
1085
+	{
1086
+		$strings = $this->config()->platform_specific_strings;
1087
+		if ($strings) {
1088
+			return new ArrayList($strings);
1089
+		}
1090
+	}
1091
+
1092
+	/**
1093
+	 * Provide a list of all starred projects for the currently logged in member
1094
+	 *
1095
+	 * @return SS_List
1096
+	 */
1097
+	public function getStarredProjects()
1098
+	{
1099
+		$member = Member::currentUser();
1100
+		if ($member === null) {
1101
+			return new ArrayList();
1102
+		}
1103
+
1104
+		$favProjects = $member->StarredProjects();
1105
+
1106
+		$list = new ArrayList();
1107
+		foreach ($favProjects as $project) {
1108
+			if ($project->canView($member)) {
1109
+				$list->add($project);
1110
+			}
1111
+		}
1112
+		return $list;
1113
+	}
1114
+
1115
+	/**
1116
+	 * Returns top level navigation of projects.
1117
+	 *
1118
+	 * @param int $limit
1119
+	 *
1120
+	 * @return ArrayList
1121
+	 */
1122
+	public function Navigation($limit = 5)
1123
+	{
1124
+		$navigation = new ArrayList();
1125
+
1126
+		$currentProject = $this->getCurrentProject();
1127
+
1128
+		$projects = $this->getStarredProjects();
1129
+		if ($projects->count() < 1) {
1130
+			$projects = $this->DNProjectList();
1131
+		} else {
1132
+			$limit = -1;
1133
+		}
1134
+
1135
+		if ($projects->count() > 0) {
1136
+			$activeProject = false;
1137
+
1138
+			if ($limit > 0) {
1139
+				$limitedProjects = $projects->limit($limit);
1140
+			} else {
1141
+				$limitedProjects = $projects;
1142
+			}
1143
+
1144
+			foreach ($limitedProjects as $project) {
1145
+				$isActive = $currentProject && $currentProject->ID == $project->ID;
1146
+				if ($isActive) {
1147
+					$activeProject = true;
1148
+				}
1149
+
1150
+				$navigation->push(array(
1151
+					'Project' => $project,
1152
+					'IsActive' => $currentProject && $currentProject->ID == $project->ID,
1153
+				));
1154
+			}
1155
+
1156
+			// Ensure the current project is in the list
1157
+			if (!$activeProject && $currentProject) {
1158
+				$navigation->unshift(array(
1159
+					'Project' => $currentProject,
1160
+					'IsActive' => true,
1161
+				));
1162
+				if ($limit > 0 && $navigation->count() > $limit) {
1163
+					$navigation->pop();
1164
+				}
1165
+			}
1166
+		}
1167
+
1168
+		return $navigation;
1169
+	}
1170
+
1171
+	/**
1172
+	 * Construct the deployment form
1173
+	 *
1174
+	 * @return Form
1175
+	 */
1176
+	public function getDeployForm($request = null)
1177
+	{
1178
+
1179
+		// Performs canView permission check by limiting visible projects
1180
+		$project = $this->getCurrentProject();
1181
+		if (!$project) {
1182
+			return $this->project404Response();
1183
+		}
1184
+
1185
+		// Performs canView permission check by limiting visible projects
1186
+		$environment = $this->getCurrentEnvironment($project);
1187
+		if (!$environment) {
1188
+			return $this->environment404Response();
1189
+		}
1190
+
1191
+		if (!$environment->canDeploy()) {
1192
+			return new SS_HTTPResponse("Not allowed to deploy", 401);
1193
+		}
1194
+
1195
+		// Generate the form
1196
+		$form = new DeployForm($this, 'DeployForm', $environment, $project);
1197
+
1198
+		// If this is an ajax request we don't want to submit the form - we just want to retrieve the markup.
1199
+		if (
1200
+			$request &&
1201
+			!$request->requestVar('action_showDeploySummary') &&
1202
+			$this->getRequest()->isAjax() &&
1203
+			$this->getRequest()->isGET()
1204
+		) {
1205
+			// We can just use the URL we're accessing
1206
+			$form->setFormAction($this->getRequest()->getURL());
1207
+
1208
+			$body = json_encode(array('Content' => $form->forAjaxTemplate()->forTemplate()));
1209
+			$this->getResponse()->addHeader('Content-Type', 'application/json');
1210
+			$this->getResponse()->setBody($body);
1211
+			return $body;
1212
+		}
1213
+
1214
+		$form->setFormAction($this->getRequest()->getURL() . '/DeployForm');
1215
+		return $form;
1216
+	}
1217
+
1218
+	/**
1219
+	 * @param SS_HTTPRequest $request
1220
+	 *
1221
+	 * @return SS_HTTPResponse|string
1222
+	 */
1223
+	public function gitRevisions(SS_HTTPRequest $request)
1224
+	{
1225
+
1226
+		// Performs canView permission check by limiting visible projects
1227
+		$project = $this->getCurrentProject();
1228
+		if (!$project) {
1229
+			return $this->project404Response();
1230
+		}
1231
+
1232
+		// Performs canView permission check by limiting visible projects
1233
+		$env = $this->getCurrentEnvironment($project);
1234
+		if (!$env) {
1235
+			return $this->environment404Response();
1236
+		}
1237
+
1238
+		// For now only permit advanced options on one environment type, because we hacked the "full-deploy"
1239
+		// checkbox in. Other environments such as the fast or capistrano one wouldn't know what to do with it.
1240
+		if (get_class($env) === 'RainforestEnvironment') {
1241
+			$advanced = Permission::check('DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS') ? 'true' : 'false';
1242
+		} else {
1243
+			$advanced = 'false';
1244
+		}
1245
+
1246
+		$tabs = array();
1247
+		$id = 0;
1248
+		$data = array(
1249
+			'id' => ++$id,
1250
+			'name' => 'Deploy the latest version of a branch',
1251
+			'field_type' => 'dropdown',
1252
+			'field_label' => 'Choose a branch',
1253
+			'field_id' => 'branch',
1254
+			'field_data' => array(),
1255
+			'advanced_opts' => $advanced
1256
+		);
1257
+		foreach ($project->DNBranchList() as $branch) {
1258
+			$sha = $branch->SHA();
1259
+			$name = $branch->Name();
1260
+			$branchValue = sprintf("%s (%s, %s old)",
1261
+				$name,
1262
+				substr($sha, 0, 8),
1263
+				$branch->LastUpdated()->TimeDiff()
1264
+			);
1265
+			$data['field_data'][] = array(
1266
+				'id' => $sha,
1267
+				'text' => $branchValue
1268
+			);
1269
+		}
1270
+		$tabs[] = $data;
1271
+
1272
+		$data = array(
1273
+			'id' => ++$id,
1274
+			'name' => 'Deploy a tagged release',
1275
+			'field_type' => 'dropdown',
1276
+			'field_label' => 'Choose a tag',
1277
+			'field_id' => 'tag',
1278
+			'field_data' => array(),
1279
+			'advanced_opts' => $advanced
1280
+		);
1281
+
1282
+		foreach ($project->DNTagList()->setLimit(null) as $tag) {
1283
+			$name = $tag->Name();
1284
+			$data['field_data'][] = array(
1285
+				'id' => $tag->SHA(),
1286
+				'text' => sprintf("%s", $name)
1287
+			);
1288
+		}
1289
+
1290
+		// show newest tags first.
1291
+		$data['field_data'] = array_reverse($data['field_data']);
1292
+
1293
+		$tabs[] = $data;
1294
+
1295
+		// Past deployments
1296
+		$data = array(
1297
+			'id' => ++$id,
1298
+			'name' => 'Redeploy a release that was previously deployed (to any environment)',
1299
+			'field_type' => 'dropdown',
1300
+			'field_label' => 'Choose a previously deployed release',
1301
+			'field_id' => 'release',
1302
+			'field_data' => array(),
1303
+			'advanced_opts' => $advanced
1304
+		);
1305
+		// We are aiming at the format:
1306
+		// [{text: 'optgroup text', children: [{id: '<sha>', text: '<inner text>'}]}]
1307
+		$redeploy = array();
1308
+		foreach ($project->DNEnvironmentList() as $dnEnvironment) {
1309
+			$envName = $dnEnvironment->Name;
1310
+			$perEnvDeploys = array();
1311
+
1312
+			foreach ($dnEnvironment->DeployHistory() as $deploy) {
1313
+				$sha = $deploy->SHA;
1314
+
1315
+				// Check if exists to make sure the newest deployment date is used.
1316
+				if (!isset($perEnvDeploys[$sha])) {
1317
+					$pastValue = sprintf("%s (deployed %s)",
1318
+						substr($sha, 0, 8),
1319
+						$deploy->obj('LastEdited')->Ago()
1320
+					);
1321
+					$perEnvDeploys[$sha] = array(
1322
+						'id' => $sha,
1323
+						'text' => $pastValue
1324
+					);
1325
+				}
1326
+			}
1327
+
1328
+			if (!empty($perEnvDeploys)) {
1329
+				$redeploy[$envName] = array_values($perEnvDeploys);
1330
+			}
1331
+		}
1332
+		// Convert the array to the frontend format (i.e. keyed to regular array)
1333
+		foreach ($redeploy as $env => $descr) {
1334
+			$data['field_data'][] = array('text'=>$env, 'children'=>$descr);
1335
+		}
1336
+		$tabs[] = $data;
1337
+
1338
+		$data = array(
1339
+			'id' => ++$id,
1340
+			'name' => 'Deploy a specific SHA',
1341
+			'field_type' => 'textfield',
1342
+			'field_label' => 'Choose a SHA',
1343
+			'field_id' => 'SHA',
1344
+			'field_data' => array(),
1345
+			'advanced_opts' => $advanced
1346
+		);
1347
+		$tabs[] = $data;
1348
+
1349
+		// get the last time git fetch was run
1350
+		$lastFetched = 'never';
1351
+		$fetch = DNGitFetch::get()
1352
+			->filter('ProjectID', $project->ID)
1353
+			->sort('LastEdited', 'DESC')
1354
+			->first();
1355
+		if ($fetch) {
1356
+			$lastFetched = $fetch->dbObject('LastEdited')->Ago();
1357
+		}
1358
+
1359
+		$data = array(
1360
+			'Tabs' => $tabs,
1361
+			'last_fetched' => $lastFetched
1362
+		);
1363
+
1364
+		return json_encode($data, JSON_PRETTY_PRINT);
1365
+	}
1366
+
1367
+	/**
1368
+	 * Check and regenerate a global CSRF token
1369
+	 *
1370
+	 * @param SS_HTTPRequest $request
1371
+	 * @param bool $resetToken
1372
+	 *
1373
+	 * @return bool
1374
+	 */
1375
+	protected function checkCsrfToken(SS_HTTPRequest $request, $resetToken = true)
1376
+	{
1377
+		$token = SecurityToken::inst();
1378
+
1379
+		// Ensure the submitted token has a value
1380
+		$submittedToken = $request->postVar('SecurityID');
1381
+		if (!$submittedToken) {
1382
+			return false;
1383
+		}
1384
+
1385
+		// Do the actual check.
1386
+		$check = $token->check($submittedToken);
1387
+
1388
+		// Reset the token after we've checked the existing token
1389
+		if ($resetToken) {
1390
+			$token->reset();
1391
+		}
1392
+
1393
+		// Return whether the token was correct or not
1394
+		return $check;
1395
+	}
1396
+
1397
+	/**
1398
+	 * @param SS_HTTPRequest $request
1399
+	 *
1400
+	 * @return string
1401
+	 */
1402
+	public function deploySummary(SS_HTTPRequest $request)
1403
+	{
1404
+
1405
+		// Performs canView permission check by limiting visible projects
1406
+		$project = $this->getCurrentProject();
1407
+		if (!$project) {
1408
+			return $this->project404Response();
1409
+		}
1410
+
1411
+		// Performs canView permission check by limiting visible projects
1412
+		$environment = $this->getCurrentEnvironment($project);
1413
+		if (!$environment) {
1414
+			return $this->environment404Response();
1415
+		}
1416
+
1417
+		// Plan the deployment.
1418
+		$strategy = $environment->Backend()->planDeploy(
1419
+			$environment,
1420
+			$request->requestVars()
1421
+		);
1422
+		$data = $strategy->toArray();
1423
+
1424
+		// Add in a URL for comparing from->to code changes. Ensure that we have
1425
+		// two proper 40 character SHAs, otherwise we can't show the compare link.
1426
+		$interface = $project->getRepositoryInterface();
1427
+		if (
1428
+			!empty($interface) && !empty($interface->URL)
1429
+			&& !empty($data['changes']['Code version']['from'])
1430
+			&& strlen($data['changes']['Code version']['from']) == '40'
1431
+			&& !empty($data['changes']['Code version']['to'])
1432
+			&& strlen($data['changes']['Code version']['to']) == '40'
1433
+		) {
1434
+			$compareurl = sprintf(
1435
+				'%s/compare/%s...%s',
1436
+				$interface->URL,
1437
+				$data['changes']['Code version']['from'],
1438
+				$data['changes']['Code version']['to']
1439
+			);
1440
+			$data['changes']['Code version']['compareUrl'] = $compareurl;
1441
+		}
1442
+
1443
+		// Append json to response
1444
+		$token = SecurityToken::inst();
1445
+		$data['SecurityID'] = $token->getValue();
1446
+
1447
+		return json_encode($data);
1448
+	}
1449
+
1450
+	/**
1451
+	 * Deployment form submission handler.
1452
+	 *
1453
+	 * Initiate a DNDeployment record and redirect to it for status polling
1454
+	 *
1455
+	 * @param SS_HTTPRequest $request
1456
+	 *
1457
+	 * @return SS_HTTPResponse
1458
+	 * @throws ValidationException
1459
+	 * @throws null
1460
+	 */
1461
+	public function startDeploy(SS_HTTPRequest $request)
1462
+	{
1463
+
1464
+		// Ensure the CSRF Token is correct
1465
+		if (!$this->checkCsrfToken($request)) {
1466
+			// CSRF token didn't match
1467
+			return $this->httpError(400, 'Bad Request');
1468
+		}
1469
+
1470
+		// Performs canView permission check by limiting visible projects
1471
+		$project = $this->getCurrentProject();
1472
+		if (!$project) {
1473
+			return $this->project404Response();
1474
+		}
1475
+
1476
+		// Performs canView permission check by limiting visible projects
1477
+		$environment = $this->getCurrentEnvironment($project);
1478
+		if (!$environment) {
1479
+			return $this->environment404Response();
1480
+		}
1481
+
1482
+		// Initiate the deployment
1483
+		// The extension point should pass in: Project, Environment, SelectRelease, buildName
1484
+		$this->extend('doDeploy', $project, $environment, $buildName, $data);
1485
+
1486
+		// Start the deployment based on the approved strategy.
1487
+		$strategy = new DeploymentStrategy($environment);
1488
+		$strategy->fromArray($request->requestVar('strategy'));
1489
+		$deployment = $strategy->createDeployment();
1490
+		$deployment->start();
1491
+
1492
+		return json_encode(array(
1493
+			'url' => Director::absoluteBaseURL() . $deployment->Link()
1494
+		), JSON_PRETTY_PRINT);
1495
+	}
1496
+
1497
+	/**
1498
+	 * Action - Do the actual deploy
1499
+	 *
1500
+	 * @param SS_HTTPRequest $request
1501
+	 *
1502
+	 * @return SS_HTTPResponse|string
1503
+	 * @throws SS_HTTPResponse_Exception
1504
+	 */
1505
+	public function deploy(SS_HTTPRequest $request)
1506
+	{
1507
+		$params = $request->params();
1508
+		$deployment = DNDeployment::get()->byId($params['Identifier']);
1509
+
1510
+		if (!$deployment || !$deployment->ID) {
1511
+			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1512
+		}
1513
+		if (!$deployment->canView()) {
1514
+			return Security::permissionFailure();
1515
+		}
1516
+
1517
+		$environment = $deployment->Environment();
1518
+		$project = $environment->Project();
1519
+
1520
+		if ($environment->Name != $params['Environment']) {
1521
+			throw new LogicException("Environment in URL doesn't match this deploy");
1522
+		}
1523
+		if ($project->Name != $params['Project']) {
1524
+			throw new LogicException("Project in URL doesn't match this deploy");
1525
+		}
1526
+
1527
+		return $this->render(array(
1528
+			'Deployment' => $deployment,
1529
+		));
1530
+	}
1531
+
1532
+
1533
+	/**
1534
+	 * Action - Get the latest deploy log
1535
+	 *
1536
+	 * @param SS_HTTPRequest $request
1537
+	 *
1538
+	 * @return string
1539
+	 * @throws SS_HTTPResponse_Exception
1540
+	 */
1541
+	public function deploylog(SS_HTTPRequest $request)
1542
+	{
1543
+		$params = $request->params();
1544
+		$deployment = DNDeployment::get()->byId($params['Identifier']);
1545
+
1546
+		if (!$deployment || !$deployment->ID) {
1547
+			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1548
+		}
1549
+		if (!$deployment->canView()) {
1550
+			return Security::permissionFailure();
1551
+		}
1552
+
1553
+		$environment = $deployment->Environment();
1554
+		$project = $environment->Project();
1555
+
1556
+		if ($environment->Name != $params['Environment']) {
1557
+			throw new LogicException("Environment in URL doesn't match this deploy");
1558
+		}
1559
+		if ($project->Name != $params['Project']) {
1560
+			throw new LogicException("Project in URL doesn't match this deploy");
1561
+		}
1562
+
1563
+		$log = $deployment->log();
1564
+		if ($log->exists()) {
1565
+			$content = $log->content();
1566
+		} else {
1567
+			$content = 'Waiting for action to start';
1568
+		}
1569
+
1570
+		return $this->sendResponse($deployment->ResqueStatus(), $content);
1571
+	}
1572
+
1573
+	/**
1574
+	 * @param SS_HTTPRequest|null $request
1575
+	 *
1576
+	 * @return Form
1577
+	 */
1578
+	public function getDataTransferForm(SS_HTTPRequest $request = null)
1579
+	{
1580
+		// Performs canView permission check by limiting visible projects
1581
+		$envs = $this->getCurrentProject()->DNEnvironmentList()->filterByCallback(function ($item) {
1582
+			return $item->canBackup();
1583
+		});
1584
+
1585
+		if (!$envs) {
1586
+			return $this->environment404Response();
1587
+		}
1588
+
1589
+		$form = Form::create(
1590
+			$this,
1591
+			'DataTransferForm',
1592
+			FieldList::create(
1593
+				HiddenField::create('Direction', null, 'get'),
1594
+				DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1595
+					->setEmptyString('Select an environment'),
1596
+				DropdownField::create('Mode', 'Transfer', DNDataArchive::get_mode_map())
1597
+			),
1598
+			FieldList::create(
1599
+				FormAction::create('doDataTransfer', 'Create')
1600
+					->addExtraClass('btn')
1601
+			)
1602
+		);
1603
+		$form->setFormAction($this->getRequest()->getURL() . '/DataTransferForm');
1604
+
1605
+		return $form;
1606
+	}
1607
+
1608
+	/**
1609
+	 * @param array $data
1610
+	 * @param Form $form
1611
+	 *
1612
+	 * @return SS_HTTPResponse
1613
+	 * @throws SS_HTTPResponse_Exception
1614
+	 */
1615
+	public function doDataTransfer($data, Form $form)
1616
+	{
1617
+		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1618
+
1619
+		// Performs canView permission check by limiting visible projects
1620
+		$project = $this->getCurrentProject();
1621
+		if (!$project) {
1622
+			return $this->project404Response();
1623
+		}
1624
+
1625
+		$dataArchive = null;
1626
+
1627
+		// Validate direction.
1628
+		if ($data['Direction'] == 'get') {
1629
+			$validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1630
+				->filterByCallback(function ($item) {
1631
+					return $item->canBackup();
1632
+				});
1633
+		} elseif ($data['Direction'] == 'push') {
1634
+			$validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1635
+				->filterByCallback(function ($item) {
1636
+					return $item->canRestore();
1637
+				});
1638
+		} else {
1639
+			throw new LogicException('Invalid direction');
1640
+		}
1641
+
1642
+		// Validate $data['EnvironmentID'] by checking against $validEnvs.
1643
+		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
1644
+		if (!$environment) {
1645
+			throw new LogicException('Invalid environment');
1646
+		}
1647
+
1648
+		$this->validateSnapshotMode($data['Mode']);
1649
+
1650
+
1651
+		// Only 'push' direction is allowed an association with an existing archive.
1652
+		if (
1653
+			$data['Direction'] == 'push'
1654
+			&& isset($data['DataArchiveID'])
1655
+			&& is_numeric($data['DataArchiveID'])
1656
+		) {
1657
+			$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1658
+			if (!$dataArchive) {
1659
+				throw new LogicException('Invalid data archive');
1660
+			}
1661
+
1662
+			if (!$dataArchive->canDownload()) {
1663
+				throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1664
+			}
1665
+		}
1666
+
1667
+		$transfer = DNDataTransfer::create();
1668
+		$transfer->EnvironmentID = $environment->ID;
1669
+		$transfer->Direction = $data['Direction'];
1670
+		$transfer->Mode = $data['Mode'];
1671
+		$transfer->DataArchiveID = $dataArchive ? $dataArchive->ID : null;
1672
+		if ($data['Direction'] == 'push') {
1673
+			$transfer->setBackupBeforePush(!empty($data['BackupBeforePush']));
1674
+		}
1675
+		$transfer->write();
1676
+		$transfer->start();
1677
+
1678
+		return $this->redirect($transfer->Link());
1679
+	}
1680
+
1681
+	/**
1682
+	 * View into the log for a {@link DNDataTransfer}.
1683
+	 *
1684
+	 * @param SS_HTTPRequest $request
1685
+	 *
1686
+	 * @return SS_HTTPResponse|string
1687
+	 * @throws SS_HTTPResponse_Exception
1688
+	 */
1689
+	public function transfer(SS_HTTPRequest $request)
1690
+	{
1691
+		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1692
+
1693
+		$params = $request->params();
1694
+		$transfer = DNDataTransfer::get()->byId($params['Identifier']);
1695
+
1696
+		if (!$transfer || !$transfer->ID) {
1697
+			throw new SS_HTTPResponse_Exception('Transfer not found', 404);
1698
+		}
1699
+		if (!$transfer->canView()) {
1700
+			return Security::permissionFailure();
1701
+		}
1702
+
1703
+		$environment = $transfer->Environment();
1704
+		$project = $environment->Project();
1705
+
1706
+		if ($project->Name != $params['Project']) {
1707
+			throw new LogicException("Project in URL doesn't match this deploy");
1708
+		}
1709
+
1710
+		return $this->render(array(
1711
+			'CurrentTransfer' => $transfer,
1712
+			'SnapshotsSection' => 1,
1713
+		));
1714
+	}
1715
+
1716
+	/**
1717
+	 * Action - Get the latest deploy log
1718
+	 *
1719
+	 * @param SS_HTTPRequest $request
1720
+	 *
1721
+	 * @return string
1722
+	 * @throws SS_HTTPResponse_Exception
1723
+	 */
1724
+	public function transferlog(SS_HTTPRequest $request)
1725
+	{
1726
+		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1727
+
1728
+		$params = $request->params();
1729
+		$transfer = DNDataTransfer::get()->byId($params['Identifier']);
1730
+
1731
+		if (!$transfer || !$transfer->ID) {
1732
+			throw new SS_HTTPResponse_Exception('Transfer not found', 404);
1733
+		}
1734
+		if (!$transfer->canView()) {
1735
+			return Security::permissionFailure();
1736
+		}
1737
+
1738
+		$environment = $transfer->Environment();
1739
+		$project = $environment->Project();
1740
+
1741
+		if ($project->Name != $params['Project']) {
1742
+			throw new LogicException("Project in URL doesn't match this deploy");
1743
+		}
1744
+
1745
+		$log = $transfer->log();
1746
+		if ($log->exists()) {
1747
+			$content = $log->content();
1748
+		} else {
1749
+			$content = 'Waiting for action to start';
1750
+		}
1751
+
1752
+		return $this->sendResponse($transfer->ResqueStatus(), $content);
1753
+	}
1754
+
1755
+	/**
1756
+	 * Note: Submits to the same action as {@link getDataTransferForm()},
1757
+	 * but with a Direction=push and an archive reference.
1758
+	 *
1759
+	 * @param SS_HTTPRequest $request
1760
+	 * @param DNDataArchive|null $dataArchive Only set when method is called manually in {@link restore()},
1761
+	 *                            otherwise the state is inferred from the request data.
1762
+	 * @return Form
1763
+	 */
1764
+	public function getDataTransferRestoreForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null)
1765
+	{
1766
+		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1767
+
1768
+		// Performs canView permission check by limiting visible projects
1769
+		$project = $this->getCurrentProject();
1770
+		$envs = $project->DNEnvironmentList()->filterByCallback(function ($item) {
1771
+			return $item->canRestore();
1772
+		});
1773
+
1774
+		if (!$envs) {
1775
+			return $this->environment404Response();
1776
+		}
1777
+
1778
+		$modesMap = array();
1779
+		if (in_array($dataArchive->Mode, array('all'))) {
1780
+			$modesMap['all'] = 'Database and Assets';
1781
+		};
1782
+		if (in_array($dataArchive->Mode, array('all', 'db'))) {
1783
+			$modesMap['db'] = 'Database only';
1784
+		};
1785
+		if (in_array($dataArchive->Mode, array('all', 'assets'))) {
1786
+			$modesMap['assets'] = 'Assets only';
1787
+		};
1788
+
1789
+		$alertMessage = '<div class="alert alert-warning"><strong>Warning:</strong> '
1790
+			. 'This restore will overwrite the data on the chosen environment below</div>';
1791
+
1792
+		$form = Form::create(
1793
+			$this,
1794
+			'DataTransferRestoreForm',
1795
+			FieldList::create(
1796
+				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1797
+				HiddenField::create('Direction', null, 'push'),
1798
+				LiteralField::create('Warning', $alertMessage),
1799
+				DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1800
+					->setEmptyString('Select an environment'),
1801
+				DropdownField::create('Mode', 'Transfer', $modesMap),
1802
+				CheckboxField::create('BackupBeforePush', 'Backup existing data', '1')
1803
+			),
1804
+			FieldList::create(
1805
+				FormAction::create('doDataTransfer', 'Restore Data')
1806
+					->addExtraClass('btn')
1807
+			)
1808
+		);
1809
+		$form->setFormAction($project->Link() . '/DataTransferRestoreForm');
1810
+
1811
+		return $form;
1812
+	}
1813
+
1814
+	/**
1815
+	 * View a form to restore a specific {@link DataArchive}.
1816
+	 * Permission checks are handled in {@link DataArchives()}.
1817
+	 * Submissions are handled through {@link doDataTransfer()}, same as backup operations.
1818
+	 *
1819
+	 * @param SS_HTTPRequest $request
1820
+	 *
1821
+	 * @return HTMLText
1822
+	 * @throws SS_HTTPResponse_Exception
1823
+	 */
1824
+	public function restoresnapshot(SS_HTTPRequest $request)
1825
+	{
1826
+		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1827
+
1828
+		/** @var DNDataArchive $dataArchive */
1829
+		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1830
+
1831
+		if (!$dataArchive) {
1832
+			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1833
+		}
1834
+
1835
+		// We check for canDownload because that implies access to the data.
1836
+		// canRestore is later checked on the actual restore action per environment.
1837
+		if (!$dataArchive->canDownload()) {
1838
+			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1839
+		}
1840
+
1841
+		$form = $this->getDataTransferRestoreForm($this->request, $dataArchive);
1842
+
1843
+		// View currently only available via ajax
1844
+		return $form->forTemplate();
1845
+	}
1846
+
1847
+	/**
1848
+	 * View a form to delete a specific {@link DataArchive}.
1849
+	 * Permission checks are handled in {@link DataArchives()}.
1850
+	 * Submissions are handled through {@link doDelete()}.
1851
+	 *
1852
+	 * @param SS_HTTPRequest $request
1853
+	 *
1854
+	 * @return HTMLText
1855
+	 * @throws SS_HTTPResponse_Exception
1856
+	 */
1857
+	public function deletesnapshot(SS_HTTPRequest $request)
1858
+	{
1859
+		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1860
+
1861
+		/** @var DNDataArchive $dataArchive */
1862
+		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1863
+
1864
+		if (!$dataArchive) {
1865
+			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1866
+		}
1867
+
1868
+		if (!$dataArchive->canDelete()) {
1869
+			throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403);
1870
+		}
1871
+
1872
+		$form = $this->getDeleteForm($this->request, $dataArchive);
1873
+
1874
+		// View currently only available via ajax
1875
+		return $form->forTemplate();
1876
+	}
1877
+
1878
+	/**
1879
+	 * @param SS_HTTPRequest $request
1880
+	 * @param DNDataArchive|null $dataArchive Only set when method is called manually, otherwise the state is inferred
1881
+	 *        from the request data.
1882
+	 * @return Form
1883
+	 */
1884
+	public function getDeleteForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null)
1885
+	{
1886
+		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1887
+
1888
+		// Performs canView permission check by limiting visible projects
1889
+		$project = $this->getCurrentProject();
1890
+		if (!$project) {
1891
+			return $this->project404Response();
1892
+		}
1893
+
1894
+		$snapshotDeleteWarning = '<div class="alert alert-warning">'
1895
+			. 'Are you sure you want to permanently delete this snapshot from this archive area?'
1896
+			. '</div>';
1897
+
1898
+		$form = Form::create(
1899
+			$this,
1900
+			'DeleteForm',
1901
+			FieldList::create(
1902
+				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1903
+				LiteralField::create('Warning', $snapshotDeleteWarning)
1904
+			),
1905
+			FieldList::create(
1906
+				FormAction::create('doDelete', 'Delete')
1907
+					->addExtraClass('btn')
1908
+			)
1909
+		);
1910
+		$form->setFormAction($project->Link() . '/DeleteForm');
1911
+
1912
+		return $form;
1913
+	}
1914
+
1915
+	/**
1916
+	 * @param array $data
1917
+	 * @param Form $form
1918
+	 *
1919
+	 * @return bool|SS_HTTPResponse
1920
+	 * @throws SS_HTTPResponse_Exception
1921
+	 */
1922
+	public function doDelete($data, Form $form)
1923
+	{
1924
+		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1925
+
1926
+		// Performs canView permission check by limiting visible projects
1927
+		$project = $this->getCurrentProject();
1928
+		if (!$project) {
1929
+			return $this->project404Response();
1930
+		}
1931
+
1932
+		$dataArchive = null;
1933
+
1934
+		if (
1935
+			isset($data['DataArchiveID'])
1936
+			&& is_numeric($data['DataArchiveID'])
1937
+		) {
1938
+			$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1939
+		}
1940
+
1941
+		if (!$dataArchive) {
1942
+			throw new LogicException('Invalid data archive');
1943
+		}
1944
+
1945
+		if (!$dataArchive->canDelete()) {
1946
+			throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403);
1947
+		}
1948
+
1949
+		$dataArchive->delete();
1950
+
1951
+		return $this->redirectBack();
1952
+	}
1953
+
1954
+	/**
1955
+	 * View a form to move a specific {@link DataArchive}.
1956
+	 *
1957
+	 * @param SS_HTTPRequest $request
1958
+	 *
1959
+	 * @return HTMLText
1960
+	 * @throws SS_HTTPResponse_Exception
1961
+	 */
1962
+	public function movesnapshot(SS_HTTPRequest $request)
1963
+	{
1964
+		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1965
+
1966
+		/** @var DNDataArchive $dataArchive */
1967
+		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1968
+
1969
+		if (!$dataArchive) {
1970
+			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1971
+		}
1972
+
1973
+		// We check for canDownload because that implies access to the data.
1974
+		if (!$dataArchive->canDownload()) {
1975
+			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1976
+		}
1977
+
1978
+		$form = $this->getMoveForm($this->request, $dataArchive);
1979
+
1980
+		// View currently only available via ajax
1981
+		return $form->forTemplate();
1982
+	}
1983
+
1984
+	/**
1985
+	 * Build snapshot move form.
1986
+	 *
1987
+	 * @param SS_HTTPRequest $request
1988
+	 * @param DNDataArchive|null $dataArchive
1989
+	 *
1990
+	 * @return Form|SS_HTTPResponse
1991
+	 */
1992
+	public function getMoveForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null)
1993
+	{
1994
+		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1995
+
1996
+		$envs = $dataArchive->validTargetEnvironments();
1997
+		if (!$envs) {
1998
+			return $this->environment404Response();
1999
+		}
2000
+
2001
+		$warningMessage = '<div class="alert alert-warning"><strong>Warning:</strong> This will make the snapshot '
2002
+			. 'available to people with access to the target environment.<br>By pressing "Change ownership" you '
2003
+			. 'confirm that you have considered data confidentiality regulations.</div>';
2004
+
2005
+		$form = Form::create(
2006
+			$this,
2007
+			'MoveForm',
2008
+			FieldList::create(
2009
+				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
2010
+				LiteralField::create('Warning', $warningMessage),
2011
+				DropdownField::create('EnvironmentID', 'Environment', $envs->map())
2012
+					->setEmptyString('Select an environment')
2013
+			),
2014
+			FieldList::create(
2015
+				FormAction::create('doMove', 'Change ownership')
2016
+					->addExtraClass('btn')
2017
+			)
2018
+		);
2019
+		$form->setFormAction($this->getCurrentProject()->Link() . '/MoveForm');
2020
+
2021
+		return $form;
2022
+	}
2023
+
2024
+	/**
2025
+	 * @param array $data
2026
+	 * @param Form $form
2027
+	 *
2028
+	 * @return bool|SS_HTTPResponse
2029
+	 * @throws SS_HTTPResponse_Exception
2030
+	 * @throws ValidationException
2031
+	 * @throws null
2032
+	 */
2033
+	public function doMove($data, Form $form)
2034
+	{
2035
+		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
2036
+
2037
+		// Performs canView permission check by limiting visible projects
2038
+		$project = $this->getCurrentProject();
2039
+		if (!$project) {
2040
+			return $this->project404Response();
2041
+		}
2042
+
2043
+		/** @var DNDataArchive $dataArchive */
2044
+		$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
2045
+		if (!$dataArchive) {
2046
+			throw new LogicException('Invalid data archive');
2047
+		}
2048
+
2049
+		// We check for canDownload because that implies access to the data.
2050
+		if (!$dataArchive->canDownload()) {
2051
+			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
2052
+		}
2053
+
2054
+		// Validate $data['EnvironmentID'] by checking against $validEnvs.
2055
+		$validEnvs = $dataArchive->validTargetEnvironments();
2056
+		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
2057
+		if (!$environment) {
2058
+			throw new LogicException('Invalid environment');
2059
+		}
2060
+
2061
+		$dataArchive->EnvironmentID = $environment->ID;
2062
+		$dataArchive->write();
2063
+
2064
+		return $this->redirectBack();
2065
+	}
2066
+
2067
+	/**
2068
+	 * Returns an error message if redis is unavailable
2069
+	 *
2070
+	 * @return string
2071
+	 */
2072
+	public static function RedisUnavailable()
2073
+	{
2074
+		try {
2075
+			Resque::queues();
2076
+		} catch (Exception $e) {
2077
+			return $e->getMessage();
2078
+		}
2079
+		return '';
2080
+	}
2081
+
2082
+	/**
2083
+	 * Returns the number of connected Redis workers
2084
+	 *
2085
+	 * @return int
2086
+	 */
2087
+	public static function RedisWorkersCount()
2088
+	{
2089
+		return count(Resque_Worker::all());
2090
+	}
2091
+
2092
+	/**
2093
+	 * @return array
2094
+	 */
2095
+	public function providePermissions()
2096
+	{
2097
+		return array(
2098
+			self::DEPLOYNAUT_BYPASS_PIPELINE => array(
2099
+				'name' => "Bypass Pipeline",
2100
+				'category' => "Deploynaut",
2101
+				'help' => "Enables users to directly initiate deployments, bypassing any pipeline",
2102
+			),
2103
+			self::DEPLOYNAUT_DRYRUN_PIPELINE => array(
2104
+				'name' => 'Dry-run Pipeline',
2105
+				'category' => 'Deploynaut',
2106
+				'help' => 'Enable dry-run execution of pipelines for testing'
2107
+			),
2108
+			self::DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS => array(
2109
+				'name' => "Access to advanced deploy options",
2110
+				'category' => "Deploynaut",
2111
+			),
2112
+
2113
+			// Permissions that are intended to be added to the roles.
2114
+			self::ALLOW_PROD_DEPLOYMENT => array(
2115
+				'name' => "Ability to deploy to production environments",
2116
+				'category' => "Deploynaut",
2117
+			),
2118
+			self::ALLOW_NON_PROD_DEPLOYMENT => array(
2119
+				'name' => "Ability to deploy to non-production environments",
2120
+				'category' => "Deploynaut",
2121
+			),
2122
+			self::ALLOW_PROD_SNAPSHOT => array(
2123
+				'name' => "Ability to make production snapshots",
2124
+				'category' => "Deploynaut",
2125
+			),
2126
+			self::ALLOW_NON_PROD_SNAPSHOT => array(
2127
+				'name' => "Ability to make non-production snapshots",
2128
+				'category' => "Deploynaut",
2129
+			),
2130
+			self::ALLOW_CREATE_ENVIRONMENT => array(
2131
+				'name' => "Ability to create environments",
2132
+				'category' => "Deploynaut",
2133
+			),
2134
+		);
2135
+	}
2136
+
2137
+	/**
2138
+	 * @return DNProject|null
2139
+	 */
2140
+	public function getCurrentProject()
2141
+	{
2142
+		$projectName = trim($this->getRequest()->param('Project'));
2143
+		if (!$projectName) {
2144
+			return null;
2145
+		}
2146
+		if (empty(self::$_project_cache[$projectName])) {
2147
+			self::$_project_cache[$projectName] = $this->DNProjectList()->filter('Name', $projectName)->First();
2148
+		}
2149
+		return self::$_project_cache[$projectName];
2150
+	}
2151
+
2152
+	/**
2153
+	 * @param DNProject|null $project
2154
+	 * @return DNEnvironment|null
2155
+	 */
2156
+	public function getCurrentEnvironment(DNProject $project = null)
2157
+	{
2158
+		if ($this->getRequest()->param('Environment') === null) {
2159
+			return null;
2160
+		}
2161
+		if ($project === null) {
2162
+			$project = $this->getCurrentProject();
2163
+		}
2164
+		// project can still be null
2165
+		if ($project === null) {
2166
+			return null;
2167
+		}
2168
+		return $project->DNEnvironmentList()->filter('Name', $this->getRequest()->param('Environment'))->First();
2169
+	}
2170
+
2171
+	/**
2172
+	 * This will return a const that indicates the class of action currently being performed
2173
+	 *
2174
+	 * Until DNRoot is de-godded, it does a bunch of different actions all in the same class.
2175
+	 * So we just have each action handler calll setCurrentActionType to define what sort of
2176
+	 * action it is.
2177
+	 *
2178
+	 * @return string - one of the consts from self::$action_types
2179
+	 */
2180
+	public function getCurrentActionType()
2181
+	{
2182
+		return $this->actionType;
2183
+	}
2184
+
2185
+	/**
2186
+	 * Sets the current action type
2187
+	 *
2188
+	 * @param string $actionType string - one of the consts from self::$action_types
2189
+	 */
2190
+	public function setCurrentActionType($actionType)
2191
+	{
2192
+		$this->actionType = $actionType;
2193
+	}
2194
+
2195
+	/**
2196
+	 * Helper method to allow templates to know whether they should show the 'Archive List' include or not.
2197
+	 * The actual permissions are set on a per-environment level, so we need to find out if this $member can upload to
2198
+	 * or download from *any* {@link DNEnvironment} that (s)he has access to.
2199
+	 *
2200
+	 * TODO To be replaced with a method that just returns the list of archives this {@link Member} has access to.
2201
+	 *
2202
+	 * @param Member|null $member The {@link Member} to check (or null to check the currently logged in Member)
2203
+	 * @return boolean|null true if $member has access to upload or download to at least one {@link DNEnvironment}.
2204
+	 */
2205
+	public function CanViewArchives(Member $member = null)
2206
+	{
2207
+		if ($member === null) {
2208
+			$member = Member::currentUser();
2209
+		}
2210
+
2211
+		if (Permission::checkMember($member, 'ADMIN')) {
2212
+			return true;
2213
+		}
2214
+
2215
+		$allProjects = $this->DNProjectList();
2216
+		if (!$allProjects) {
2217
+			return false;
2218
+		}
2219
+
2220
+		foreach ($allProjects as $project) {
2221
+			if ($project->Environments()) {
2222
+				foreach ($project->Environments() as $environment) {
2223
+					if (
2224
+						$environment->canRestore($member) ||
2225
+						$environment->canBackup($member) ||
2226
+						$environment->canUploadArchive($member) ||
2227
+						$environment->canDownloadArchive($member)
2228
+					) {
2229
+						// We can return early as we only need to know that we can access one environment
2230
+						return true;
2231
+					}
2232
+				}
2233
+			}
2234
+		}
2235
+	}
2236
+
2237
+	/**
2238
+	 * Returns a list of attempted environment creations.
2239
+	 *
2240
+	 * @return PaginatedList
2241
+	 */
2242
+	public function CreateEnvironmentList()
2243
+	{
2244
+		$project = $this->getCurrentProject();
2245
+		if ($project) {
2246
+			return new PaginatedList($project->CreateEnvironments()->sort("Created DESC"), $this->request);
2247
+		}
2248
+		return new PaginatedList(new ArrayList(), $this->request);
2249
+	}
2250
+
2251
+	/**
2252
+	 * Returns a list of all archive files that can be accessed by the currently logged-in {@link Member}
2253
+	 *
2254
+	 * @return PaginatedList
2255
+	 */
2256
+	public function CompleteDataArchives()
2257
+	{
2258
+		$project = $this->getCurrentProject();
2259
+		$archives = new ArrayList();
2260
+
2261
+		$archiveList = $project->Environments()->relation("DataArchives");
2262
+		if ($archiveList->count() > 0) {
2263
+			foreach ($archiveList as $archive) {
2264
+				if ($archive->canView() && !$archive->isPending()) {
2265
+					$archives->push($archive);
2266
+				}
2267
+			}
2268
+		}
2269
+		return new PaginatedList($archives->sort("Created", "DESC"), $this->request);
2270
+	}
2271
+
2272
+	/**
2273
+	 * @return PaginatedList The list of "pending" data archives which are waiting for a file
2274
+	 * to be delivered offline by post, and manually uploaded into the system.
2275
+	 */
2276
+	public function PendingDataArchives()
2277
+	{
2278
+		$project = $this->getCurrentProject();
2279
+		$archives = new ArrayList();
2280
+		foreach ($project->DNEnvironmentList() as $env) {
2281
+			foreach ($env->DataArchives() as $archive) {
2282
+				if ($archive->canView() && $archive->isPending()) {
2283
+					$archives->push($archive);
2284
+				}
2285
+			}
2286
+		}
2287
+		return new PaginatedList($archives->sort("Created", "DESC"), $this->request);
2288
+	}
2289
+
2290
+	/**
2291
+	 * @return PaginatedList
2292
+	 */
2293
+	public function DataTransferLogs()
2294
+	{
2295
+		$project = $this->getCurrentProject();
2296
+
2297
+		$transfers = DNDataTransfer::get()->filterByCallback(function ($record) use ($project) {
2298
+			return
2299
+				$record->Environment()->Project()->ID == $project->ID && // Ensure only the current Project is shown
2300
+				(
2301
+					$record->Environment()->canRestore() || // Ensure member can perform an action on the transfers env
2302
+					$record->Environment()->canBackup() ||
2303
+					$record->Environment()->canUploadArchive() ||
2304
+					$record->Environment()->canDownloadArchive()
2305
+				);
2306
+		});
2307
+
2308
+		return new PaginatedList($transfers->sort("Created", "DESC"), $this->request);
2309
+	}
2310
+
2311
+	/**
2312
+	 * @return null|PaginatedList
2313
+	 */
2314
+	public function DeployHistory()
2315
+	{
2316
+		if ($env = $this->getCurrentEnvironment()) {
2317
+			$history = $env->DeployHistory();
2318
+			if ($history->count() > 0) {
2319
+				$pagination = new PaginatedList($history, $this->getRequest());
2320
+				$pagination->setPageLength(8);
2321
+				return $pagination;
2322
+			}
2323
+		}
2324
+		return null;
2325
+	}
2326
+
2327
+	/**
2328
+	 * @return SS_HTTPResponse
2329
+	 */
2330
+	protected function project404Response()
2331
+	{
2332
+		return new SS_HTTPResponse(
2333
+			"Project '" . Convert::raw2xml($this->getRequest()->param('Project')) . "' not found.",
2334
+			404
2335
+		);
2336
+	}
2337
+
2338
+	/**
2339
+	 * @return SS_HTTPResponse
2340
+	 */
2341
+	protected function environment404Response()
2342
+	{
2343
+		$envName = Convert::raw2xml($this->getRequest()->param('Environment'));
2344
+		return new SS_HTTPResponse("Environment '" . $envName . "' not found.", 404);
2345
+	}
2346
+
2347
+	/**
2348
+	 * @param string $status
2349
+	 * @param string $content
2350
+	 *
2351
+	 * @return string
2352
+	 */
2353
+	protected function sendResponse($status, $content)
2354
+	{
2355
+		// strip excessive newlines
2356
+		$content = preg_replace('/(?:(?:\r\n|\r|\n)\s*){2}/s', "\n", $content);
2357
+
2358
+		$sendJSON = (strpos($this->getRequest()->getHeader('Accept'), 'application/json') !== false)
2359
+			|| $this->getRequest()->getExtension() == 'json';
2360
+
2361
+		if (!$sendJSON) {
2362
+			$this->response->addHeader("Content-type", "text/plain");
2363
+			return $content;
2364
+		}
2365
+		$this->response->addHeader("Content-type", "application/json");
2366
+		return json_encode(array(
2367
+			'status' => $status,
2368
+			'content' => $content,
2369
+		));
2370
+	}
2371
+
2372
+	/**
2373
+	 * Validate the snapshot mode
2374
+	 *
2375
+	 * @param string $mode
2376
+	 */
2377
+	protected function validateSnapshotMode($mode)
2378
+	{
2379
+		if (!in_array($mode, array('all', 'assets', 'db'))) {
2380
+			throw new LogicException('Invalid mode');
2381
+		}
2382
+	}
2383
+
2384
+	/**
2385
+	 * @param string $sectionName
2386
+	 * @param string $title
2387
+	 *
2388
+	 * @return SS_HTTPResponse
2389
+	 */
2390
+	protected function getCustomisedViewSection($sectionName, $title = '', $data = array())
2391
+	{
2392
+		// Performs canView permission check by limiting visible projects
2393
+		$project = $this->getCurrentProject();
2394
+		if (!$project) {
2395
+			return $this->project404Response();
2396
+		}
2397
+		$data[$sectionName] = 1;
2398
+
2399
+		if ($this !== '') {
2400
+			$data['Title'] = $title;
2401
+		}
2402
+
2403
+		return $this->render($data);
2404
+	}
2405
+
2406
+	/**
2407
+	 * Get items for the ambient menu that should be accessible from all pages.
2408
+	 *
2409
+	 * @return ArrayList
2410
+	 */
2411
+	public function AmbientMenu()
2412
+	{
2413
+		$list = new ArrayList();
2414
+
2415
+		if (Member::currentUserID()) {
2416
+			$list->push(new ArrayData(array(
2417
+				'Classes' => 'logout',
2418
+				'FaIcon' => 'sign-out',
2419
+				'Link' => 'Security/logout',
2420
+				'Title' => 'Log out',
2421
+				'IsCurrent' => false,
2422
+				'IsSection' => false
2423
+			)));
2424
+		}
2425
+
2426
+		$this->extend('updateAmbientMenu', $list);
2427
+		return $list;
2428
+	}
2429
+
2430
+	/**
2431
+	 * Create project action.
2432
+	 *
2433
+	 * @return SS_HTTPResponse
2434
+	 */
2435
+	public function createproject(SS_HTTPRequest $request)
2436
+	{
2437
+		if ($this->canCreateProjects()) {
2438
+			return $this->render(['CurrentTitle' => 'Create Stack']);
2439
+		}
2440
+		return $this->httpError(403);
2441
+	}
2442
+
2443
+	/**
2444
+	 * Checks whether the user can create a project.
2445
+	 *
2446
+	 * @return bool
2447
+	 */
2448
+	public function canCreateProjects($member = null)
2449
+	{
2450
+		if (!$member) {
2451
+			$member = Member::currentUser();
2452
+		}
2453
+		if (!$member) {
2454
+			return false;
2455
+		}
2456
+
2457
+		return singleton('DNProject')->canCreate($member);
2458
+	}
2459
+
2460
+	/**
2461
+	 * @return Form
2462
+	 */
2463
+	public function CreateProjectForm()
2464
+	{
2465
+		$form = Form::create(
2466
+			$this,
2467
+			__FUNCTION__,
2468
+			$this->getCreateProjectFormFields(),
2469
+			$this->getCreateProjectFormActions(),
2470
+			new RequiredFields('Name', 'CVSPath')
2471
+		);
2472
+		$this->extend('updateCreateProjectForm', $form);
2473
+		return $form;
2474
+	}
2475
+
2476
+	/**
2477
+	 * @return FieldList
2478
+	 */
2479
+	protected function getCreateProjectFormFields()
2480
+	{
2481
+		$fields = FieldList::create();
2482
+		$fields->merge([
2483
+			TextField::create('Name', 'Title')->setDescription('Limited to alphanumeric characters, underscores and hyphens.'),
2484
+			TextField::create('CVSPath', 'Git URL')->setDescription('Your repository URL so we can clone your code (eg. [email protected]:silverstripe/silverstripe-installer.git)')
2485
+		]);
2486
+		$this->extend('updateCreateProjectFormFields', $fields);
2487
+		return $fields;
2488
+	}
2489
+
2490
+	/**
2491
+	 * @return FieldList
2492
+	 */
2493
+	protected function getCreateProjectFormActions()
2494
+	{
2495
+		$fields = FieldList::create(
2496
+			FormAction::create('doCreateProject', 'Create Stack')
2497
+		);
2498
+		$this->extend('updateCreateProjectFormActions', $fields);
2499
+		return $fields;
2500
+	}
2501
+
2502
+	/**
2503
+	 * Does the actual project creation.
2504
+	 *
2505
+	 * @param $data array
2506
+	 * @param $form Form
2507
+	 *
2508
+	 * @return SS_HTTPResponse
2509
+	 */
2510
+	public function doCreateProject($data, $form)
2511
+	{
2512
+		$form->loadDataFrom($data);
2513
+		$project = DNProject::create();
2514
+
2515
+		$form->saveInto($project);
2516
+		$this->extend('onBeforeCreateProject', $project, $data, $form);
2517
+		try {
2518
+			if ($project->write() > 0) {
2519
+				$this->extend('onAfterCreateProject', $project, $data, $form);
2520
+
2521
+				// If an extension hasn't redirected us, we'll redirect to the project.
2522
+				if (!$this->redirectedTo()) {
2523
+					return $this->redirect($project->Link());
2524
+				} else {
2525
+					return $this->response;
2526
+				}
2527
+			} else {
2528
+				$form->sessionMessage('Unable to write the stack to the database.', 'bad');
2529
+			}
2530
+		} catch (ValidationException $e) {
2531
+			$form->sessionMessage($e->getMessage(), 'bad');
2532
+		}
2533
+		return $this->redirectBack();
2534
+	}
2535
+
2536
+	/**
2537
+	 * Returns the state of a current project build.
2538
+	 *
2539
+	 * @param SS_HTTPRequest $request
2540
+	 *
2541
+	 * @return SS_HTTPResponse
2542
+	 */
2543
+	public function createprojectprogress(SS_HTTPRequest $request)
2544
+	{
2545
+		$project = $this->getCurrentProject();
2546
+		if (!$project) {
2547
+			return $this->httpError(404);
2548
+		}
2549
+
2550
+		$envCreations = $project->getInitialEnvironmentCreations();
2551
+		$complete = array();
2552
+		$inProgress = array();
2553
+		$failed = array();
2554
+		if ($envCreations->count() > 0) {
2555
+			foreach ($envCreations as $env) {
2556
+				$data = unserialize($env->Data);
2557
+				if (!isset($data['Name'])) {
2558
+					$data['Name'] = 'Unknown';
2559
+				}
2560
+				switch ($env->ResqueStatus()) {
2561
+					case "Queued":
2562
+					case "Running":
2563
+						$inProgress[$env->ID] = Convert::raw2xml($env->ResqueStatus());
2564
+						break;
2565
+					case "Complete":
2566
+						$complete[$env->ID] = Convert::raw2xml($data['Name']);
2567
+						break;
2568
+					case "Failed":
2569
+					case "Invalid":
2570
+					default:
2571
+						$failed[$env->ID] = Convert::raw2xml($data['Name']);
2572
+				}
2573
+			}
2574
+		}
2575
+
2576
+		$data = [
2577
+			'complete' => $project->isProjectReady(),
2578
+			'progress' => [
2579
+				'environments' => [
2580
+					'complete' => $complete,
2581
+					'inProgress' => $inProgress,
2582
+					'failed' => $failed,
2583
+				]
2584
+			]
2585
+		];
2586
+		$this->extend('updateCreateProjectProgressData', $data);
2587
+
2588
+		$response = $this->getResponse();
2589
+		$response->addHeader('Content-Type', 'application/json');
2590
+		$response->setBody(json_encode($data));
2591
+		return $response;
2592
+	}
2593
+
2594
+	public function checkrepoaccess(SS_HTTPRequest $request)
2595
+	{
2596
+		$project = $this->getCurrentProject();
2597
+		if (!$project) {
2598
+			return $this->httpError(404);
2599
+		}
2600
+
2601
+		if ($project->CVSPath) {
2602
+			$fetch = new FetchJob();
2603
+			$fetch->args = array('projectID' => $project->ID);
2604
+			try {
2605
+				$fetch->perform();
2606
+				$canAccessRepo = true;
2607
+			} catch (RuntimeException $e) {
2608
+				$canAccessRepo = false;
2609
+			}
2610
+			$data = ['canAccessRepo' => $canAccessRepo];
2611
+		} else {
2612
+			$data = ['canAccessRepo' => false];
2613
+		}
2614
+
2615
+		$response = $this->getResponse();
2616
+		$response->addHeader("Content-Type", "application/json");
2617
+		$response->setBody(json_encode($data));
2618
+		return $response;
2619
+	}
2620 2620
 }
Please login to merge, or discard this patch.
code/control/DeployForm.php 1 patch
Indentation   +292 added lines, -292 removed lines patch added patch discarded remove patch
@@ -9,49 +9,49 @@  discard block
 block discarded – undo
9 9
 abstract class DeployForm_ValidatorBase extends Validator
10 10
 {
11 11
 
12
-    /**
13
-     * @param string $fieldName
14
-     * @param string $message
15
-     * @param string $messageType
16
-     */
17
-    public function validationError($fieldName, $message, $messageType = '')
18
-    {
19
-        // Just make any error use the form message
20
-        $this->form->sessionMessage($message, $messageType);
21
-        parent::validationError($fieldName, $message, $messageType);
22
-    }
12
+	/**
13
+	 * @param string $fieldName
14
+	 * @param string $message
15
+	 * @param string $messageType
16
+	 */
17
+	public function validationError($fieldName, $message, $messageType = '')
18
+	{
19
+		// Just make any error use the form message
20
+		$this->form->sessionMessage($message, $messageType);
21
+		parent::validationError($fieldName, $message, $messageType);
22
+	}
23 23
 
24
-    /**
25
-     * Validate a commit sha
26
-     *
27
-     * @param string $sha
28
-     * @param string $field
29
-     * @return boolean
30
-     */
31
-    protected function validateCommit($sha, $field)
32
-    {
33
-        // Check selected commit
34
-        if (empty($sha)) {
35
-            $this->validationError(
36
-                $field,
37
-                "No valid release selected",
38
-                "required"
39
-            );
40
-            return false;
41
-        }
24
+	/**
25
+	 * Validate a commit sha
26
+	 *
27
+	 * @param string $sha
28
+	 * @param string $field
29
+	 * @return boolean
30
+	 */
31
+	protected function validateCommit($sha, $field)
32
+	{
33
+		// Check selected commit
34
+		if (empty($sha)) {
35
+			$this->validationError(
36
+				$field,
37
+				"No valid release selected",
38
+				"required"
39
+			);
40
+			return false;
41
+		}
42 42
 
43
-        // Check validity of commit
44
-        if (!preg_match('/^[a-f0-9]{40}$/', $sha)) {
45
-            $this->validationError(
46
-                $field,
47
-                "Invalid release SHA: " . Convert::raw2xml($sha),
48
-                "error"
49
-            );
50
-            return false;
51
-        }
43
+		// Check validity of commit
44
+		if (!preg_match('/^[a-f0-9]{40}$/', $sha)) {
45
+			$this->validationError(
46
+				$field,
47
+				"Invalid release SHA: " . Convert::raw2xml($sha),
48
+				"error"
49
+			);
50
+			return false;
51
+		}
52 52
 
53
-        return true;
54
-    }
53
+		return true;
54
+	}
55 55
 }
56 56
 
57 57
 /**
@@ -63,27 +63,27 @@  discard block
 block discarded – undo
63 63
 class DeployForm_CommitValidator extends DeployForm_ValidatorBase
64 64
 {
65 65
 
66
-    public function php($data)
67
-    {
68
-        // Check release method
69
-        if (empty($data['SelectRelease'])
70
-            || !in_array($data['SelectRelease'], array('Tag', 'Branch', 'Redeploy', 'SHA', 'FilteredCommits'))
71
-        ) {
72
-            $method = empty($data['SelectRelease']) ? '(blank)' : $data['SelectRelease'];
73
-            $this->validationError(
74
-                'SelectRelease',
75
-                "Bad release selection method: $method",
76
-                "error"
77
-            );
78
-            return false;
79
-        }
66
+	public function php($data)
67
+	{
68
+		// Check release method
69
+		if (empty($data['SelectRelease'])
70
+			|| !in_array($data['SelectRelease'], array('Tag', 'Branch', 'Redeploy', 'SHA', 'FilteredCommits'))
71
+		) {
72
+			$method = empty($data['SelectRelease']) ? '(blank)' : $data['SelectRelease'];
73
+			$this->validationError(
74
+				'SelectRelease',
75
+				"Bad release selection method: $method",
76
+				"error"
77
+			);
78
+			return false;
79
+		}
80 80
 
81
-        // Check sha
82
-        return $this->validateCommit(
83
-            $this->form->getSelectedBuild($data),
84
-            'SelectRelease'
85
-        );
86
-    }
81
+		// Check sha
82
+		return $this->validateCommit(
83
+			$this->form->getSelectedBuild($data),
84
+			'SelectRelease'
85
+		);
86
+	}
87 87
 }
88 88
 
89 89
 /**
@@ -95,13 +95,13 @@  discard block
 block discarded – undo
95 95
 class DeployForm_PipelineValidator extends DeployForm_ValidatorBase
96 96
 {
97 97
 
98
-    public function php($data)
99
-    {
100
-        return $this->validateCommit(
101
-            $this->form->getSelectedBuild($data),
102
-            'FilteredCommits'
103
-        );
104
-    }
98
+	public function php($data)
99
+	{
100
+		return $this->validateCommit(
101
+			$this->form->getSelectedBuild($data),
102
+			'FilteredCommits'
103
+		);
104
+	}
105 105
 }
106 106
 
107 107
 /**
@@ -113,240 +113,240 @@  discard block
 block discarded – undo
113 113
 class DeployForm extends Form
114 114
 {
115 115
 
116
-    /**
117
-     * @param DNRoot $controller
118
-     * @param string $name
119
-     * @param DNEnvironment $environment
120
-     * @param DNProject $project
121
-     */
122
-    public function __construct($controller, $name, DNEnvironment $environment, DNProject $project)
123
-    {
124
-        if ($environment->HasPipelineSupport()) {
125
-            list($field, $validator, $actions) = $this->setupPipeline($environment, $project);
126
-        } else {
127
-            list($field, $validator, $actions) = $this->setupSimpleDeploy($project);
128
-        }
129
-        parent::__construct($controller, $name, new FieldList($field), $actions, $validator);
130
-    }
116
+	/**
117
+	 * @param DNRoot $controller
118
+	 * @param string $name
119
+	 * @param DNEnvironment $environment
120
+	 * @param DNProject $project
121
+	 */
122
+	public function __construct($controller, $name, DNEnvironment $environment, DNProject $project)
123
+	{
124
+		if ($environment->HasPipelineSupport()) {
125
+			list($field, $validator, $actions) = $this->setupPipeline($environment, $project);
126
+		} else {
127
+			list($field, $validator, $actions) = $this->setupSimpleDeploy($project);
128
+		}
129
+		parent::__construct($controller, $name, new FieldList($field), $actions, $validator);
130
+	}
131 131
 
132
-    /**
133
-     * @param DNProject $project
134
-     *
135
-     * @return array
136
-     */
137
-    protected function setupSimpleDeploy(DNProject $project)
138
-    {
139
-        // without a pipeline simply allow any commit to be selected
140
-        $field = $this->buildCommitSelector($project);
141
-        $validator = new DeployForm_CommitValidator();
142
-        $actions = new FieldList(
143
-            new FormAction('showDeploySummary', 'Plan deployment', 'Show deployment plan'),
144
-            new FormAction('doDeploy', 'Do deploy', 'Do deploy')
145
-        );
146
-        return array($field, $validator, $actions);
147
-    }
132
+	/**
133
+	 * @param DNProject $project
134
+	 *
135
+	 * @return array
136
+	 */
137
+	protected function setupSimpleDeploy(DNProject $project)
138
+	{
139
+		// without a pipeline simply allow any commit to be selected
140
+		$field = $this->buildCommitSelector($project);
141
+		$validator = new DeployForm_CommitValidator();
142
+		$actions = new FieldList(
143
+			new FormAction('showDeploySummary', 'Plan deployment', 'Show deployment plan'),
144
+			new FormAction('doDeploy', 'Do deploy', 'Do deploy')
145
+		);
146
+		return array($field, $validator, $actions);
147
+	}
148 148
 
149
-    /**
150
-     * @param DNEnvironment $environment
151
-     * @param DNProject $project
152
-     *
153
-     * @return array
154
-     * @throws Exception
155
-     */
156
-    protected function setupPipeline(DNEnvironment $environment, DNProject $project)
157
-    {
158
-        // Determine if commits are filtered
159
-        $canBypass = Permission::check(DNRoot::DEPLOYNAUT_BYPASS_PIPELINE);
160
-        $canDryrun = $environment->DryRunEnabled && Permission::check(DNRoot::DEPLOYNAUT_DRYRUN_PIPELINE);
161
-        $commits = $environment->getDependentFilteredCommits();
162
-        if (empty($commits)) {
163
-            // There are no filtered commits, so show all commits
164
-            $field = $this->buildCommitSelector($project);
165
-            $validator = new DeployForm_CommitValidator();
166
-        } elseif ($canBypass) {
167
-            // Build hybrid selector that allows users to follow pipeline or use any commit
168
-            $field = $this->buildCommitSelector($project, $commits);
169
-            $validator = new DeployForm_CommitValidator();
170
-        } else {
171
-            // Restrict user to only select pipeline filtered commits
172
-            $field = $this->buildPipelineField($commits);
173
-            $validator = new DeployForm_PipelineValidator();
174
-        }
149
+	/**
150
+	 * @param DNEnvironment $environment
151
+	 * @param DNProject $project
152
+	 *
153
+	 * @return array
154
+	 * @throws Exception
155
+	 */
156
+	protected function setupPipeline(DNEnvironment $environment, DNProject $project)
157
+	{
158
+		// Determine if commits are filtered
159
+		$canBypass = Permission::check(DNRoot::DEPLOYNAUT_BYPASS_PIPELINE);
160
+		$canDryrun = $environment->DryRunEnabled && Permission::check(DNRoot::DEPLOYNAUT_DRYRUN_PIPELINE);
161
+		$commits = $environment->getDependentFilteredCommits();
162
+		if (empty($commits)) {
163
+			// There are no filtered commits, so show all commits
164
+			$field = $this->buildCommitSelector($project);
165
+			$validator = new DeployForm_CommitValidator();
166
+		} elseif ($canBypass) {
167
+			// Build hybrid selector that allows users to follow pipeline or use any commit
168
+			$field = $this->buildCommitSelector($project, $commits);
169
+			$validator = new DeployForm_CommitValidator();
170
+		} else {
171
+			// Restrict user to only select pipeline filtered commits
172
+			$field = $this->buildPipelineField($commits);
173
+			$validator = new DeployForm_PipelineValidator();
174
+		}
175 175
 
176
-        // Generate actions allowed for this user
177
-        $actions = new FieldList(
178
-            FormAction::create('startPipeline', "Begin the release process on " . $environment->Name)
179
-                ->addExtraClass('btn btn-primary')
180
-                ->setAttribute('onclick', "return confirm('This will begin a release pipeline. Continue?');")
181
-        );
182
-        if ($canDryrun) {
183
-            $actions->push(
184
-                FormAction::create('doDryRun', "Dry-run release process")
185
-                    ->addExtraClass('btn btn-info')
186
-                    ->setAttribute(
187
-                        'onclick',
188
-                        "return confirm('This will begin a release pipeline, but with the following exclusions:\\n" .
189
-                        " - No messages will be sent\\n" .
190
-                        " - No capistrano actions will be invoked\\n" .
191
-                        " - No deployments or snapshots will be created.');"
192
-                    )
193
-            );
194
-        }
195
-        if ($canBypass) {
196
-            $actions->push(
197
-                FormAction::create('showDeploySummary', "Direct deployment (bypass pipeline)")
198
-                    ->addExtraClass('btn btn-warning')
199
-                    ->setAttribute(
200
-                        'onclick',
201
-                        "return confirm('This will start a direct deployment, bypassing the pipeline " .
202
-                        "process in place.\\n\\nAre you sure this is necessary?');"
203
-                    )
204
-            );
205
-            return array($field, $validator, $actions);
206
-        }
207
-        return array($field, $validator, $actions);
208
-    }
176
+		// Generate actions allowed for this user
177
+		$actions = new FieldList(
178
+			FormAction::create('startPipeline', "Begin the release process on " . $environment->Name)
179
+				->addExtraClass('btn btn-primary')
180
+				->setAttribute('onclick', "return confirm('This will begin a release pipeline. Continue?');")
181
+		);
182
+		if ($canDryrun) {
183
+			$actions->push(
184
+				FormAction::create('doDryRun', "Dry-run release process")
185
+					->addExtraClass('btn btn-info')
186
+					->setAttribute(
187
+						'onclick',
188
+						"return confirm('This will begin a release pipeline, but with the following exclusions:\\n" .
189
+						" - No messages will be sent\\n" .
190
+						" - No capistrano actions will be invoked\\n" .
191
+						" - No deployments or snapshots will be created.');"
192
+					)
193
+			);
194
+		}
195
+		if ($canBypass) {
196
+			$actions->push(
197
+				FormAction::create('showDeploySummary', "Direct deployment (bypass pipeline)")
198
+					->addExtraClass('btn btn-warning')
199
+					->setAttribute(
200
+						'onclick',
201
+						"return confirm('This will start a direct deployment, bypassing the pipeline " .
202
+						"process in place.\\n\\nAre you sure this is necessary?');"
203
+					)
204
+			);
205
+			return array($field, $validator, $actions);
206
+		}
207
+		return array($field, $validator, $actions);
208
+	}
209 209
 
210
-    /**
211
-     * Construct fields to select any commit
212
-     *
213
-     * @param DNProject $project
214
-     * @param DataList|null $pipelineCommits Optional list of pipeline-filtered commits to include
215
-     * @return FormField
216
-     */
217
-    protected function buildCommitSelector($project, $pipelineCommits = null)
218
-    {
219
-        // Branches
220
-        $branches = array();
221
-        foreach ($project->DNBranchList() as $branch) {
222
-            $sha = $branch->SHA();
223
-            $name = $branch->Name();
224
-            $branchValue = sprintf("%s (%s, %s old)",
225
-                $name,
226
-                substr($sha, 0, 8),
227
-                $branch->LastUpdated()->TimeDiff()
228
-            );
229
-            $branches[$sha . '-' . $name] = $branchValue;
230
-        }
210
+	/**
211
+	 * Construct fields to select any commit
212
+	 *
213
+	 * @param DNProject $project
214
+	 * @param DataList|null $pipelineCommits Optional list of pipeline-filtered commits to include
215
+	 * @return FormField
216
+	 */
217
+	protected function buildCommitSelector($project, $pipelineCommits = null)
218
+	{
219
+		// Branches
220
+		$branches = array();
221
+		foreach ($project->DNBranchList() as $branch) {
222
+			$sha = $branch->SHA();
223
+			$name = $branch->Name();
224
+			$branchValue = sprintf("%s (%s, %s old)",
225
+				$name,
226
+				substr($sha, 0, 8),
227
+				$branch->LastUpdated()->TimeDiff()
228
+			);
229
+			$branches[$sha . '-' . $name] = $branchValue;
230
+		}
231 231
 
232
-        // Tags
233
-        $tags = array();
234
-        foreach ($project->DNTagList()->setLimit(null) as $tag) {
235
-            $sha = $tag->SHA();
236
-            $name = $tag->Name();
237
-            $tagValue = sprintf("%s (%s, %s old)",
238
-                $name,
239
-                substr($sha, 0, 8),
240
-                $branch->LastUpdated()->TimeDiff()
241
-            );
242
-            $tags[$sha . '-' . $tag] = $tagValue;
243
-        }
244
-        $tags = array_reverse($tags);
232
+		// Tags
233
+		$tags = array();
234
+		foreach ($project->DNTagList()->setLimit(null) as $tag) {
235
+			$sha = $tag->SHA();
236
+			$name = $tag->Name();
237
+			$tagValue = sprintf("%s (%s, %s old)",
238
+				$name,
239
+				substr($sha, 0, 8),
240
+				$branch->LastUpdated()->TimeDiff()
241
+			);
242
+			$tags[$sha . '-' . $tag] = $tagValue;
243
+		}
244
+		$tags = array_reverse($tags);
245 245
 
246
-        // Past deployments
247
-        $redeploy = array();
248
-        foreach ($project->DNEnvironmentList() as $dnEnvironment) {
249
-            $envName = $dnEnvironment->Name;
250
-            foreach ($dnEnvironment->DeployHistory() as $deploy) {
251
-                $sha = $deploy->SHA;
252
-                if (!isset($redeploy[$envName])) {
253
-                    $redeploy[$envName] = array();
254
-                }
255
-                if (!isset($redeploy[$envName][$sha])) {
256
-                    $pastValue = sprintf("%s (deployed %s)",
257
-                        substr($sha, 0, 8),
258
-                        $deploy->obj('LastEdited')->Ago()
259
-                    );
260
-                    $redeploy[$envName][$sha] = $pastValue;
261
-                }
262
-            }
263
-        }
246
+		// Past deployments
247
+		$redeploy = array();
248
+		foreach ($project->DNEnvironmentList() as $dnEnvironment) {
249
+			$envName = $dnEnvironment->Name;
250
+			foreach ($dnEnvironment->DeployHistory() as $deploy) {
251
+				$sha = $deploy->SHA;
252
+				if (!isset($redeploy[$envName])) {
253
+					$redeploy[$envName] = array();
254
+				}
255
+				if (!isset($redeploy[$envName][$sha])) {
256
+					$pastValue = sprintf("%s (deployed %s)",
257
+						substr($sha, 0, 8),
258
+						$deploy->obj('LastEdited')->Ago()
259
+					);
260
+					$redeploy[$envName][$sha] = $pastValue;
261
+				}
262
+			}
263
+		}
264 264
 
265
-        // Merge fields
266
-        $releaseMethods = array();
267
-        if ($pipelineCommits) {
268
-            $releaseMethods[] = new SelectionGroup_Item(
269
-                'FilteredCommits',
270
-                $this->buildPipelineField($pipelineCommits),
271
-                'Deploy a commit prepared for this pipeline'
272
-            );
273
-        }
274
-        if (!empty($branches)) {
275
-            $releaseMethods[] = new SelectionGroup_Item(
276
-                'Branch',
277
-                new DropdownField('Branch', 'Select a branch', $branches),
278
-                'Deploy the latest version of a branch'
279
-            );
280
-        }
281
-        if ($tags) {
282
-            $releaseMethods[] = new SelectionGroup_Item(
283
-                'Tag',
284
-                new DropdownField('Tag', 'Select a tag', $tags),
285
-                'Deploy a tagged release'
286
-            );
287
-        }
288
-        if ($redeploy) {
289
-            $releaseMethods[] = new SelectionGroup_Item(
290
-                'Redeploy',
291
-                new GroupedDropdownField('Redeploy', 'Redeploy', $redeploy),
292
-                'Redeploy a release that was previously deployed (to any environment)'
293
-            );
294
-        }
265
+		// Merge fields
266
+		$releaseMethods = array();
267
+		if ($pipelineCommits) {
268
+			$releaseMethods[] = new SelectionGroup_Item(
269
+				'FilteredCommits',
270
+				$this->buildPipelineField($pipelineCommits),
271
+				'Deploy a commit prepared for this pipeline'
272
+			);
273
+		}
274
+		if (!empty($branches)) {
275
+			$releaseMethods[] = new SelectionGroup_Item(
276
+				'Branch',
277
+				new DropdownField('Branch', 'Select a branch', $branches),
278
+				'Deploy the latest version of a branch'
279
+			);
280
+		}
281
+		if ($tags) {
282
+			$releaseMethods[] = new SelectionGroup_Item(
283
+				'Tag',
284
+				new DropdownField('Tag', 'Select a tag', $tags),
285
+				'Deploy a tagged release'
286
+			);
287
+		}
288
+		if ($redeploy) {
289
+			$releaseMethods[] = new SelectionGroup_Item(
290
+				'Redeploy',
291
+				new GroupedDropdownField('Redeploy', 'Redeploy', $redeploy),
292
+				'Redeploy a release that was previously deployed (to any environment)'
293
+			);
294
+		}
295 295
 
296
-        $releaseMethods[] = new SelectionGroup_Item(
297
-            'SHA',
298
-            new Textfield('SHA', 'Please specify the full SHA'),
299
-            'Deploy a specific SHA'
300
-        );
296
+		$releaseMethods[] = new SelectionGroup_Item(
297
+			'SHA',
298
+			new Textfield('SHA', 'Please specify the full SHA'),
299
+			'Deploy a specific SHA'
300
+		);
301 301
 
302
-        $field = new TabbedSelectionGroup('SelectRelease', $releaseMethods);
303
-        $field->setValue(reset($releaseMethods)->getValue());
304
-        return $field;
305
-    }
302
+		$field = new TabbedSelectionGroup('SelectRelease', $releaseMethods);
303
+		$field->setValue(reset($releaseMethods)->getValue());
304
+		return $field;
305
+	}
306 306
 
307
-    /**
308
-     * Generate fields necessary to select from a filtered commit list
309
-     *
310
-     * @param DataList $commits List of commits
311
-     * @return FormField
312
-     */
313
-    protected function buildPipelineField($commits)
314
-    {
315
-        // Get filtered commits
316
-        $filteredCommits = array();
317
-        foreach ($commits as $commit) {
318
-            $title = sprintf(
319
-                "%s (%s, %s old)",
320
-                $commit->Message,
321
-                substr($commit->SHA, 0, 8),
322
-                $commit->dbObject('Created')->TimeDiff()
323
-            );
324
-            $filteredCommits[$commit->SHA] = $title;
325
-        }
326
-        if ($filteredCommits) {
327
-            return new DropdownField('FilteredCommits', '', $filteredCommits);
328
-        } else {
329
-            return DropdownField::create('FilteredCommits)', '')
330
-                ->setEmptyString('No deployments available')
331
-                ->performDisabledTransformation();
332
-        }
333
-    }
307
+	/**
308
+	 * Generate fields necessary to select from a filtered commit list
309
+	 *
310
+	 * @param DataList $commits List of commits
311
+	 * @return FormField
312
+	 */
313
+	protected function buildPipelineField($commits)
314
+	{
315
+		// Get filtered commits
316
+		$filteredCommits = array();
317
+		foreach ($commits as $commit) {
318
+			$title = sprintf(
319
+				"%s (%s, %s old)",
320
+				$commit->Message,
321
+				substr($commit->SHA, 0, 8),
322
+				$commit->dbObject('Created')->TimeDiff()
323
+			);
324
+			$filteredCommits[$commit->SHA] = $title;
325
+		}
326
+		if ($filteredCommits) {
327
+			return new DropdownField('FilteredCommits', '', $filteredCommits);
328
+		} else {
329
+			return DropdownField::create('FilteredCommits)', '')
330
+				->setEmptyString('No deployments available')
331
+				->performDisabledTransformation();
332
+		}
333
+	}
334 334
 
335
-    /**
336
-     * Get the build selected from the given data
337
-     *
338
-     * @param array $data
339
-     * @return string SHA of selected build
340
-     */
341
-    public function getSelectedBuild($data)
342
-    {
343
-        if (isset($data['SelectRelease']) && !empty($data[$data['SelectRelease']])) {
344
-            // Filter out the tag/branch name if required
345
-            $array = explode('-', $data[$data['SelectRelease']]);
346
-            return reset($array);
347
-        }
348
-        if (isset($data['FilteredCommits']) && !empty($data['FilteredCommits'])) {
349
-            return $data['FilteredCommits'];
350
-        }
351
-    }
335
+	/**
336
+	 * Get the build selected from the given data
337
+	 *
338
+	 * @param array $data
339
+	 * @return string SHA of selected build
340
+	 */
341
+	public function getSelectedBuild($data)
342
+	{
343
+		if (isset($data['SelectRelease']) && !empty($data[$data['SelectRelease']])) {
344
+			// Filter out the tag/branch name if required
345
+			$array = explode('-', $data[$data['SelectRelease']]);
346
+			return reset($array);
347
+		}
348
+		if (isset($data['FilteredCommits']) && !empty($data['FilteredCommits'])) {
349
+			return $data['FilteredCommits'];
350
+		}
351
+	}
352 352
 }
Please login to merge, or discard this patch.
code/control/DeploynautLogFile.php 1 patch
Indentation   +100 added lines, -100 removed lines patch added patch discarded remove patch
@@ -6,104 +6,104 @@
 block discarded – undo
6 6
 class DeploynautLogFile
7 7
 {
8 8
 
9
-    protected $logFile;
10
-
11
-    protected $basePath;
12
-
13
-    /**
14
-     * @param string $logFile The log filename
15
-     * @param string|null $basePath Base path of where logs reside. Defaults to DEPLOYNAUT_LOG_PATH
16
-     */
17
-    public function __construct($logFile, $basePath = null)
18
-    {
19
-        $this->logFile = $logFile;
20
-        $this->basePath = $basePath ?: DEPLOYNAUT_LOG_PATH;
21
-    }
22
-
23
-    /**
24
-     * Set the log filename
25
-     * @param string $filename
26
-     */
27
-    public function setLogFile($filename)
28
-    {
29
-        $this->logFile = $filename;
30
-    }
31
-
32
-    /**
33
-     * Set the base path of where logs reside
34
-     * @param string $path
35
-     */
36
-    public function setBasePath($path)
37
-    {
38
-        $this->basePath = $path;
39
-    }
40
-
41
-    /**
42
-     * Return the un-sanitised log path.
43
-     * @return string
44
-     */
45
-    public function getRawFilePath()
46
-    {
47
-        return $this->basePath . '/' . $this->logFile;
48
-    }
49
-
50
-    /**
51
-     * Get the sanitised log path.
52
-     * @return string
53
-     */
54
-    public function getSanitisedLogFilePath()
55
-    {
56
-        return $this->basePath . '/' . strtolower(FileNameFilter::create()->filter($this->logFile));
57
-    }
58
-
59
-    /**
60
-     * Return log file path, assuming it exists. Returns NULL if nothing found.
61
-     * @return string|null
62
-     */
63
-    public function getLogFilePath()
64
-    {
65
-        $path = $this->getSanitisedLogFilePath();
66
-
67
-        // for backwards compatibility on old logs
68
-        if (!file_exists($path)) {
69
-            $path = $this->getRawFilePath();
70
-
71
-            if (!file_exists($path)) {
72
-                return null;
73
-            }
74
-        }
75
-
76
-        return $path;
77
-    }
78
-
79
-    /**
80
-     * Write a message line into the log file.
81
-     * @param string $message
82
-     */
83
-    public function write($message)
84
-    {
85
-        // Make sure we write into the old path for existing logs. New logs use the sanitised file path instead.
86
-        $path = file_exists($this->getRawFilePath()) ? $this->getRawFilePath() : $this->getSanitisedLogFilePath();
87
-
88
-        error_log('['.date('Y-m-d H:i:s').'] ' . $message .PHP_EOL, 3, $path);
89
-        @chmod($path, 0666);
90
-    }
91
-
92
-    /**
93
-     * Does the log file exist?
94
-     * @return bool
95
-     */
96
-    public function exists()
97
-    {
98
-        return (bool)$this->getLogFilePath();
99
-    }
100
-
101
-    /**
102
-     * Return the content of the log file.
103
-     * @return string
104
-     */
105
-    public function content()
106
-    {
107
-        return $this->exists() ? file_get_contents($this->getLogFilePath()) : 'Log has not been created yet.';
108
-    }
9
+	protected $logFile;
10
+
11
+	protected $basePath;
12
+
13
+	/**
14
+	 * @param string $logFile The log filename
15
+	 * @param string|null $basePath Base path of where logs reside. Defaults to DEPLOYNAUT_LOG_PATH
16
+	 */
17
+	public function __construct($logFile, $basePath = null)
18
+	{
19
+		$this->logFile = $logFile;
20
+		$this->basePath = $basePath ?: DEPLOYNAUT_LOG_PATH;
21
+	}
22
+
23
+	/**
24
+	 * Set the log filename
25
+	 * @param string $filename
26
+	 */
27
+	public function setLogFile($filename)
28
+	{
29
+		$this->logFile = $filename;
30
+	}
31
+
32
+	/**
33
+	 * Set the base path of where logs reside
34
+	 * @param string $path
35
+	 */
36
+	public function setBasePath($path)
37
+	{
38
+		$this->basePath = $path;
39
+	}
40
+
41
+	/**
42
+	 * Return the un-sanitised log path.
43
+	 * @return string
44
+	 */
45
+	public function getRawFilePath()
46
+	{
47
+		return $this->basePath . '/' . $this->logFile;
48
+	}
49
+
50
+	/**
51
+	 * Get the sanitised log path.
52
+	 * @return string
53
+	 */
54
+	public function getSanitisedLogFilePath()
55
+	{
56
+		return $this->basePath . '/' . strtolower(FileNameFilter::create()->filter($this->logFile));
57
+	}
58
+
59
+	/**
60
+	 * Return log file path, assuming it exists. Returns NULL if nothing found.
61
+	 * @return string|null
62
+	 */
63
+	public function getLogFilePath()
64
+	{
65
+		$path = $this->getSanitisedLogFilePath();
66
+
67
+		// for backwards compatibility on old logs
68
+		if (!file_exists($path)) {
69
+			$path = $this->getRawFilePath();
70
+
71
+			if (!file_exists($path)) {
72
+				return null;
73
+			}
74
+		}
75
+
76
+		return $path;
77
+	}
78
+
79
+	/**
80
+	 * Write a message line into the log file.
81
+	 * @param string $message
82
+	 */
83
+	public function write($message)
84
+	{
85
+		// Make sure we write into the old path for existing logs. New logs use the sanitised file path instead.
86
+		$path = file_exists($this->getRawFilePath()) ? $this->getRawFilePath() : $this->getSanitisedLogFilePath();
87
+
88
+		error_log('['.date('Y-m-d H:i:s').'] ' . $message .PHP_EOL, 3, $path);
89
+		@chmod($path, 0666);
90
+	}
91
+
92
+	/**
93
+	 * Does the log file exist?
94
+	 * @return bool
95
+	 */
96
+	public function exists()
97
+	{
98
+		return (bool)$this->getLogFilePath();
99
+	}
100
+
101
+	/**
102
+	 * Return the content of the log file.
103
+	 * @return string
104
+	 */
105
+	public function content()
106
+	{
107
+		return $this->exists() ? file_get_contents($this->getLogFilePath()) : 'Log has not been created yet.';
108
+	}
109 109
 }
Please login to merge, or discard this patch.