Completed
Pull Request — master (#513)
by Helpful
04:05
created

DNRoot::getCreateProjectFormFields()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 9
rs 9.6666
cc 1
eloc 7
nc 1
nop 0
1
<?php
2
use \Symfony\Component\Process\Process;
3
4
/**
5
 * God controller for the deploynaut interface
6
 *
7
 * @package deploynaut
8
 * @subpackage control
9
 */
10
class DNRoot extends Controller implements PermissionProvider, TemplateGlobalProvider {
1 ignored issue
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
11
12
	/**
13
	 * @const string - action type for actions that perform deployments
14
	 */
15
	const ACTION_DEPLOY = 'deploy';
16
17
	/**
18
	 * @const string - action type for actions that manipulate snapshots
19
	 */
20
	const ACTION_SNAPSHOT = 'snapshot';
21
22
	const ACTION_ENVIRONMENTS = 'createenv';
23
24
	const PROJECT_OVERVIEW = 'overview';
25
26
	/**
27
	 * @var string
28
	 */
29
	private $actionType = self::ACTION_DEPLOY;
30
31
	/**
32
	 * Bypass pipeline permission code
33
	 */
34
	const DEPLOYNAUT_BYPASS_PIPELINE = 'DEPLOYNAUT_BYPASS_PIPELINE';
35
36
	/**
37
	 * Allow dryrun of pipelines
38
	 */
39
	const DEPLOYNAUT_DRYRUN_PIPELINE = 'DEPLOYNAUT_DRYRUN_PIPELINE';
40
41
	/**
42
	 * Allow advanced options on deployments
43
	 */
44
	const DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS = 'DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS';
45
46
	const ALLOW_PROD_DEPLOYMENT = 'ALLOW_PROD_DEPLOYMENT';
47
	const ALLOW_NON_PROD_DEPLOYMENT = 'ALLOW_NON_PROD_DEPLOYMENT';
48
	const ALLOW_PROD_SNAPSHOT = 'ALLOW_PROD_SNAPSHOT';
49
	const ALLOW_NON_PROD_SNAPSHOT = 'ALLOW_NON_PROD_SNAPSHOT';
50
	const ALLOW_CREATE_ENVIRONMENT = 'ALLOW_CREATE_ENVIRONMENT';
51
52
	/**
53
	 * @var array
54
	 */
55
	private static $allowed_actions = array(
1 ignored issue
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
56
		'projects',
57
		'nav',
58
		'update',
59
		'project',
60
		'toggleprojectstar',
61
		'branch',
62
		'environment',
63
		'abortpipeline',
64
		'pipeline',
65
		'pipelinelog',
66
		'metrics',
67
		'createenvlog',
68
		'createenv',
69
		'getDeployForm',
70
		'doDeploy',
71
		'deploy',
72
		'deploylog',
73
		'getDataTransferForm',
74
		'transfer',
75
		'transferlog',
76
		'snapshots',
77
		'createsnapshot',
78
		'snapshotslog',
79
		'uploadsnapshot',
80
		'getCreateEnvironmentForm',
81
		'getUploadSnapshotForm',
82
		'getPostSnapshotForm',
83
		'getDataTransferRestoreForm',
84
		'getDeleteForm',
85
		'getMoveForm',
86
		'restoresnapshot',
87
		'deletesnapshot',
88
		'movesnapshot',
89
		'postsnapshotsuccess',
90
		'gitRevisions',
91
		'deploySummary',
92
		'startDeploy'
93
	);
94
95
	/**
96
	 * URL handlers pretending that we have a deep URL structure.
97
	 */
98
	private static $url_handlers = array(
1 ignored issue
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
99
		'project/$Project/environment/$Environment/DeployForm' => 'getDeployForm',
100
		'project/$Project/createsnapshot/DataTransferForm' => 'getDataTransferForm',
101
		'project/$Project/DataTransferForm' => 'getDataTransferForm',
102
		'project/$Project/DataTransferRestoreForm' => 'getDataTransferRestoreForm',
103
		'project/$Project/DeleteForm' => 'getDeleteForm',
104
		'project/$Project/MoveForm' => 'getMoveForm',
105
		'project/$Project/UploadSnapshotForm' => 'getUploadSnapshotForm',
106
		'project/$Project/PostSnapshotForm' => 'getPostSnapshotForm',
107
		'project/$Project/environment/$Environment/metrics' => 'metrics',
108
		'project/$Project/environment/$Environment/pipeline/$Identifier//$Action/$ID/$OtherID' => 'pipeline',
109
		'project/$Project/environment/$Environment/deploy_summary' => 'deploySummary',
110
		'project/$Project/environment/$Environment/git_revisions' => 'gitRevisions',
111
		'project/$Project/environment/$Environment/start-deploy' => 'startDeploy',
112
		'project/$Project/environment/$Environment/deploy/$Identifier/log' => 'deploylog',
113
		'project/$Project/environment/$Environment/deploy/$Identifier' => 'deploy',
114
		'project/$Project/transfer/$Identifier/log' => 'transferlog',
115
		'project/$Project/transfer/$Identifier' => 'transfer',
116
		'project/$Project/environment/$Environment' => 'environment',
117
		'project/$Project/createenv/$Identifier/log' => 'createenvlog',
118
		'project/$Project/createenv/$Identifier' => 'createenv',
119
		'project/$Project/CreateEnvironmentForm' => 'getCreateEnvironmentForm',
120
		'project/$Project/branch' => 'branch',
121
		'project/$Project/build/$Build' => 'build',
122
		'project/$Project/restoresnapshot/$DataArchiveID' => 'restoresnapshot',
123
		'project/$Project/deletesnapshot/$DataArchiveID' => 'deletesnapshot',
124
		'project/$Project/movesnapshot/$DataArchiveID' => 'movesnapshot',
125
		'project/$Project/update' => 'update',
126
		'project/$Project/snapshots' => 'snapshots',
127
		'project/$Project/createsnapshot' => 'createsnapshot',
128
		'project/$Project/uploadsnapshot' => 'uploadsnapshot',
129
		'project/$Project/snapshotslog' => 'snapshotslog',
130
		'project/$Project/postsnapshotsuccess/$DataArchiveID' => 'postsnapshotsuccess',
131
		'project/$Project/star' => 'toggleprojectstar',
132
		'project/$Project' => 'project',
133
		'nav/$Project' => 'nav',
134
		'projects' => 'projects',
135
	);
136
137
	/**
138
	 * @var array
139
	 */
140
	protected static $_project_cache = array();
141
142
	/**
143
	 * @var array
144
	 */
145
	private static $support_links = array();
0 ignored issues
show
Unused Code introduced by
The property $support_links is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
146
147
	/**
148
	 * @var array
149
	 */
150
	private static $platform_specific_strings = array();
0 ignored issues
show
Unused Code introduced by
The property $platform_specific_strings is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
151
152
	/**
153
	 * @var array
154
	 */
155
	private static $action_types = array(
0 ignored issues
show
Unused Code introduced by
The property $action_types is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
156
		self::ACTION_DEPLOY,
157
		self::ACTION_SNAPSHOT,
158
		self::PROJECT_OVERVIEW
159
	);
160
161
	/**
162
	 * @var DNData
163
	 */
164
	protected $data;
165
166
	/**
167
	 * Include requirements that deploynaut needs, such as javascript.
168
	 */
169
	public static function include_requirements() {
170
171
		// JS should always go to the bottom, otherwise there's the risk that Requirements
172
		// puts them halfway through the page to the nearest <script> tag. We don't want that.
173
		Requirements::set_force_js_to_bottom(true);
174
175
		// todo these should be bundled into the same JS as the others in "static" below.
176
		// We've deliberately not used combined_files as it can mess with some of the JS used
177
		// here and cause sporadic errors.
178
		Requirements::javascript('deploynaut/javascript/jquery.js');
179
		Requirements::javascript('deploynaut/javascript/bootstrap.js');
180
		Requirements::javascript('deploynaut/javascript/q.js');
181
		Requirements::javascript('deploynaut/javascript/tablefilter.js');
182
		Requirements::javascript('deploynaut/javascript/deploynaut.js');
183
		Requirements::javascript('deploynaut/javascript/react-with-addons.js');
184
		Requirements::javascript('deploynaut/javascript/bootstrap.file-input.js');
185
		Requirements::javascript('deploynaut/thirdparty/select2/dist/js/select2.min.js');
186
		Requirements::javascript('deploynaut/javascript/material.js');
187
188
		// Load the buildable dependencies only if not loaded centrally.
189
		if (!is_dir(BASE_PATH . DIRECTORY_SEPARATOR . 'static')) {
190
			if (\Director::isDev()) {
191
				\Requirements::javascript('deploynaut/static/bundle-debug.js');
192
			} else {
193
				\Requirements::javascript('deploynaut/static/bundle.js');
194
			}
195
		}
196
197
		Requirements::css('deploynaut/static/style.css');
198
	}
199
200
	/**
201
	 * Check for feature flags:
202
	 * - FLAG_SNAPSHOTS_ENABLED: set to true to enable globally
203
	 * - FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS: set to semicolon-separated list of email addresses of allowed users.
204
	 *
205
	 * @return boolean
206
	 */
207
	public static function FlagSnapshotsEnabled() {
208
		if(defined('FLAG_SNAPSHOTS_ENABLED') && FLAG_SNAPSHOTS_ENABLED) {
209
			return true;
210
		}
211
		if(defined('FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS') && FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS) {
212
			$allowedMembers = explode(';', FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS);
213
			$member = Member::currentUser();
214
			if($allowedMembers && $member && in_array($member->Email, $allowedMembers)) {
215
				return true;
216
			}
217
		}
218
		return false;
219
	}
220
221
	/**
222
	 * @return ArrayList
0 ignored issues
show
Documentation introduced by
Should the return type not be ArrayList|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
223
	 */
224
	public static function get_support_links() {
225
		$supportLinks = self::config()->support_links;
226
		if($supportLinks) {
227
			return new ArrayList($supportLinks);
228
		}
229
	}
230
231
	/**
232
	 * @return array
233
	 */
234
	public static function get_template_global_variables() {
235
		return array(
236
			'RedisUnavailable' => 'RedisUnavailable',
237
			'RedisWorkersCount' => 'RedisWorkersCount',
238
			'SidebarLinks' => 'SidebarLinks',
239
			"SupportLinks" => 'get_support_links'
240
		);
241
	}
242
243
	/**
244
	 */
245
	public function init() {
246
		parent::init();
247
248
		if(!Member::currentUser() && !Session::get('AutoLoginHash')) {
249
			return Security::permissionFailure();
250
		}
251
252
		// Block framework jquery
253
		Requirements::block(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
254
255
		self::include_requirements();
256
	}
257
258
	/**
259
	 * @return string
260
	 */
261
	public function Link() {
262
		return "naut/";
263
	}
264
265
	/**
266
	 * Actions
267
	 *
268
	 * @param SS_HTTPRequest $request
269
	 * @return \SS_HTTPResponse
0 ignored issues
show
Documentation introduced by
Should the return type not be SS_HTTPResponse|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
270
	 */
271
	public function index(SS_HTTPRequest $request) {
272
		return $this->redirect($this->Link() . 'projects/');
273
	}
274
275
	/**
276
	 * Action
277
	 *
278
	 * @param SS_HTTPRequest $request
279
	 * @return string - HTML
280
	 */
281
	public function projects(SS_HTTPRequest $request) {
282
		// Performs canView permission check by limiting visible projects in DNProjectsList() call.
283
		return $this->customise(array(
284
			'Title' => 'Projects',
285
		))->render();
286
	}
287
288
	/**
289
	 * @param SS_HTTPRequest $request
290
	 * @return HTMLText
291
	 */
292
	public function nav(SS_HTTPRequest $request) {
293
		return $this->renderWith('Nav');
294
	}
295
296
	/**
297
	 * Return a link to the navigation template used for AJAX requests.
298
	 * @return string
299
	 */
300
	public function NavLink() {
301
		$currentProject = $this->getCurrentProject();
302
		$projectName = $currentProject ? $currentProject->Name : null;
303
		return Controller::join_links(Director::absoluteBaseURL(), 'naut', 'nav', $projectName);
304
	}
305
306
	/**
307
	 * Action
308
	 *
309
	 * @param SS_HTTPRequest $request
310
	 * @return SS_HTTPResponse - HTML
311
	 */
312
	public function snapshots(SS_HTTPRequest $request) {
313
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
314
		return $this->getCustomisedViewSection('SnapshotsSection', 'Data Snapshots');
315
	}
316
317
	/**
318
	 * Action
319
	 *
320
	 * @param SS_HTTPRequest $request
321
	 * @return string - HTML
322
	 */
323 View Code Duplication
	public function createsnapshot(SS_HTTPRequest $request) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
324
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
325
326
		// Performs canView permission check by limiting visible projects
327
		$project = $this->getCurrentProject();
328
		if(!$project) {
329
			return $this->project404Response();
330
		}
331
332
		if(!$project->canBackup()) {
333
			return new SS_HTTPResponse("Not allowed to create snapshots on any environments", 401);
334
		}
335
336
		return $this->customise(array(
337
			'Title' => 'Create Data Snapshot',
338
			'SnapshotsSection' => 1,
339
			'DataTransferForm' => $this->getDataTransferForm($request)
340
		))->render();
341
	}
342
343
	/**
344
	 * Action
345
	 *
346
	 * @param SS_HTTPRequest $request
347
	 * @return string - HTML
348
	 */
349 View Code Duplication
	public function uploadsnapshot(SS_HTTPRequest $request) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
350
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
351
352
		// Performs canView permission check by limiting visible projects
353
		$project = $this->getCurrentProject();
354
		if(!$project) {
355
			return $this->project404Response();
356
		}
357
358
		if(!$project->canUploadArchive()) {
359
			return new SS_HTTPResponse("Not allowed to upload", 401);
360
		}
361
362
		return $this->customise(array(
363
			'SnapshotsSection' => 1,
364
			'UploadSnapshotForm' => $this->getUploadSnapshotForm($request),
365
			'PostSnapshotForm' => $this->getPostSnapshotForm($request)
366
		))->render();
367
	}
368
369
	/**
370
	 * Return the upload limit for snapshot uploads
371
	 * @return string
372
	 */
373
	public function UploadLimit() {
374
		return File::format_size(min(
375
			File::ini2bytes(ini_get('upload_max_filesize')),
376
			File::ini2bytes(ini_get('post_max_size'))
377
		));
378
	}
379
380
	/**
381
	 * Construct the upload form.
382
	 *
383
	 * @param SS_HTTPRequest $request
384
	 * @return Form
385
	 */
386
	public function getUploadSnapshotForm(SS_HTTPRequest $request) {
387
		// Performs canView permission check by limiting visible projects
388
		$project = $this->getCurrentProject();
389
		if(!$project) {
390
			return $this->project404Response();
391
		}
392
393
		if(!$project->canUploadArchive()) {
394
			return new SS_HTTPResponse("Not allowed to upload", 401);
395
		}
396
397
		// Framing an environment as a "group of people with download access"
398
		// makes more sense to the user here, while still allowing us to enforce
399
		// environment specific restrictions on downloading the file later on.
400
		$envs = $project->DNEnvironmentList()->filterByCallback(function($item) {
401
			return $item->canUploadArchive();
402
		});
403
		$envsMap = array();
404
		foreach($envs as $env) {
405
			$envsMap[$env->ID] = $env->Name;
406
		}
407
408
		$maxSize = min(File::ini2bytes(ini_get('upload_max_filesize')), File::ini2bytes(ini_get('post_max_size')));
409
		$fileField = DataArchiveFileField::create('ArchiveFile', 'File');
410
		$fileField->getValidator()->setAllowedExtensions(array('sspak'));
411
		$fileField->getValidator()->setAllowedMaxFileSize(array('*' => $maxSize));
412
413
		$form = Form::create(
414
			$this,
415
			'UploadSnapshotForm',
416
			FieldList::create(
417
				$fileField,
418
				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
419
				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
420
					->setEmptyString('Select an environment')
421
			),
422
			FieldList::create(
423
				FormAction::create('doUploadSnapshot', 'Upload File')
424
					->addExtraClass('btn')
425
			),
426
			RequiredFields::create('ArchiveFile')
427
		);
428
429
		$form->disableSecurityToken();
430
		$form->addExtraClass('fields-wide');
431
		// Tweak the action so it plays well with our fake URL structure.
432
		$form->setFormAction($project->Link() . '/UploadSnapshotForm');
433
434
		return $form;
435
	}
436
437
	/**
438
	 * @param array $data
439
	 * @param Form $form
440
	 *
441
	 * @return bool|HTMLText|SS_HTTPResponse
0 ignored issues
show
Documentation introduced by
Should the return type not be SS_HTTPResponse|null|false|HTMLText?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
442
	 */
443
	public function doUploadSnapshot($data, Form $form) {
444
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
445
446
		// Performs canView permission check by limiting visible projects
447
		$project = $this->getCurrentProject();
448
		if(!$project) {
449
			return $this->project404Response();
450
		}
451
452
		$validEnvs = $project->DNEnvironmentList()
453
			->filterByCallback(function($item) {
454
				return $item->canUploadArchive();
455
			});
456
457
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
458
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
459
		if(!$environment) {
460
			throw new LogicException('Invalid environment');
461
		}
462
463
		$this->validateSnapshotMode($data['Mode']);
464
465
		$dataArchive = DNDataArchive::create(array(
466
			'AuthorID' => Member::currentUserID(),
467
			'EnvironmentID' => $data['EnvironmentID'],
468
			'IsManualUpload' => true,
469
		));
470
		// needs an ID and transfer to determine upload path
471
		$dataArchive->write();
472
		$dataTransfer = DNDataTransfer::create(array(
473
			'AuthorID' => Member::currentUserID(),
474
			'Mode' => $data['Mode'],
475
			'Origin' => 'ManualUpload',
476
			'EnvironmentID' => $data['EnvironmentID']
477
		));
478
		$dataTransfer->write();
479
		$dataArchive->DataTransfers()->add($dataTransfer);
480
		$form->saveInto($dataArchive);
481
		$dataArchive->write();
482
		$workingDir = TEMP_FOLDER . DIRECTORY_SEPARATOR . 'deploynaut-transfer-' . $dataTransfer->ID;
483
484
		$cleanupFn = function() use($workingDir, $dataTransfer, $dataArchive) {
485
			$process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir)));
486
			$process->run();
487
			$dataTransfer->delete();
488
			$dataArchive->delete();
489
		};
490
491
		// extract the sspak contents so we can inspect them
492
		try {
493
			$dataArchive->extractArchive($workingDir);
494
		} catch(Exception $e) {
495
			$cleanupFn();
496
			$form->sessionMessage(
497
				'There was a problem trying to open your snapshot for processing. Please try uploading again',
498
				'bad'
499
			);
500
			return $this->redirectBack();
501
		}
502
503
		// validate that the sspak contents match the declared contents
504
		$result = $dataArchive->validateArchiveContents();
505
		if(!$result->valid()) {
506
			$cleanupFn();
507
			$form->sessionMessage($result->message(), 'bad');
508
			return $this->redirectBack();
509
		}
510
511
		// fix file permissions of extracted sspak files then re-build the sspak
512
		try {
513
			$dataArchive->fixArchivePermissions($workingDir);
514
			$dataArchive->setArchiveFromFiles($workingDir);
515
		} catch(Exception $e) {
516
			$cleanupFn();
517
			$form->sessionMessage(
518
				'There was a problem processing your snapshot. Please try uploading again',
519
				'bad'
520
			);
521
			return $this->redirectBack();
522
		}
523
524
		// cleanup any extracted sspak contents lying around
525
		$process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir)));
526
		$process->run();
527
528
		return $this->customise(array(
529
			'Project' => $project,
530
			'CurrentProject' => $project,
531
			'SnapshotsSection' => 1,
532
			'DataArchive' => $dataArchive,
533
			'DataTransferRestoreForm' => $this->getDataTransferRestoreForm($this->request, $dataArchive),
534
			'BackURL' => $project->Link('snapshots')
535
		))->renderWith(array('DNRoot_uploadsnapshot', 'DNRoot'));
536
	}
537
538
	/**
539
	 * @param SS_HTTPRequest $request
540
	 * @return Form
541
	 */
542
	public function getPostSnapshotForm(SS_HTTPRequest $request) {
543
		// Performs canView permission check by limiting visible projects
544
		$project = $this->getCurrentProject();
545
		if(!$project) {
546
			return $this->project404Response();
547
		}
548
549
		if(!$project->canUploadArchive()) {
550
			return new SS_HTTPResponse("Not allowed to upload", 401);
551
		}
552
553
		// Framing an environment as a "group of people with download access"
554
		// makes more sense to the user here, while still allowing us to enforce
555
		// environment specific restrictions on downloading the file later on.
556
		$envs = $project->DNEnvironmentList()->filterByCallback(function($item) {
557
			return $item->canUploadArchive();
558
		});
559
		$envsMap = array();
560
		foreach($envs as $env) {
561
			$envsMap[$env->ID] = $env->Name;
562
		}
563
564
		$form = Form::create(
565
			$this,
566
			'PostSnapshotForm',
567
			FieldList::create(
568
				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
569
				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
570
					->setEmptyString('Select an environment')
571
			),
572
			FieldList::create(
573
				FormAction::create('doPostSnapshot', 'Submit request')
574
					->addExtraClass('btn')
575
			),
576
			RequiredFields::create('File')
577
		);
578
579
		$form->disableSecurityToken();
580
		$form->addExtraClass('fields-wide');
581
		// Tweak the action so it plays well with our fake URL structure.
582
		$form->setFormAction($project->Link() . '/PostSnapshotForm');
583
584
		return $form;
585
	}
586
587
	/**
588
	 * @param array $data
589
	 * @param Form $form
590
	 *
591
	 * @return SS_HTTPResponse
0 ignored issues
show
Documentation introduced by
Should the return type not be SS_HTTPResponse|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
592
	 */
593
	public function doPostSnapshot($data, $form) {
594
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
595
596
		$project = $this->getCurrentProject();
597
		if(!$project) {
598
			return $this->project404Response();
599
		}
600
601
		$validEnvs = $project->DNEnvironmentList()->filterByCallback(function($item) {
602
				return $item->canUploadArchive();
603
		});
604
605
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
606
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
607
		if(!$environment) {
608
			throw new LogicException('Invalid environment');
609
		}
610
611
		$dataArchive = DNDataArchive::create(array(
612
			'UploadToken' => DNDataArchive::generate_upload_token(),
613
		));
614
		$form->saveInto($dataArchive);
615
		$dataArchive->write();
616
617
		return $this->redirect(Controller::join_links(
618
			$project->Link(),
619
			'postsnapshotsuccess',
620
			$dataArchive->ID
621
		));
622
	}
623
624
	/**
625
	 * Action
626
	 *
627
	 * @param SS_HTTPRequest $request
628
	 * @return SS_HTTPResponse - HTML
629
	 */
630
	public function snapshotslog(SS_HTTPRequest $request) {
631
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
632
		return $this->getCustomisedViewSection('SnapshotsSection', 'Snapshots log');
633
	}
634
635
	/**
636
	 * @param SS_HTTPRequest $request
637
	 * @return SS_HTTPResponse|string
0 ignored issues
show
Documentation introduced by
Should the return type not be SS_HTTPResponse|HTMLText?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
638
	 * @throws SS_HTTPResponse_Exception
639
	 */
640
	public function postsnapshotsuccess(SS_HTTPRequest $request) {
641
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
642
643
		// Performs canView permission check by limiting visible projects
644
		$project = $this->getCurrentProject();
645
		if(!$project) {
646
			return $this->project404Response();
647
		}
648
649
		if(!$project->canUploadArchive()) {
650
			return new SS_HTTPResponse("Not allowed to upload", 401);
651
		}
652
653
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
654
		if(!$dataArchive) {
655
			return new SS_HTTPResponse("Archive not found.", 404);
656
		}
657
658
		if(!$dataArchive->canRestore()) {
659
			throw new SS_HTTPResponse_Exception('Not allowed to restore archive', 403);
660
		}
661
662
		return $this->render(array(
663
				'Title' => 'How to send us your Data Snapshot by post',
664
				'DataArchive' => $dataArchive,
665
				'Address' => Config::inst()->get('Deploynaut', 'snapshot_post_address'),
666
				'BackURL' => $project->Link(),
667
			));
668
	}
669
670
	/**
671
	 * @param SS_HTTPRequest $request
672
	 * @return \SS_HTTPResponse
673
	 */
674
	public function project(SS_HTTPRequest $request) {
675
		$this->setCurrentActionType(self::PROJECT_OVERVIEW);
676
		return $this->getCustomisedViewSection('ProjectOverview', '', array('IsAdmin' => Permission::check('ADMIN')));
677
	}
678
679
	/**
680
	 * This action will star / unstar a project for the current member
681
	 *
682
	 * @param SS_HTTPRequest $request
683
	 *
684
	 * @return SS_HTTPResponse
0 ignored issues
show
Documentation introduced by
Should the return type not be SS_HTTPResponse|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
685
	 */
686
	public function toggleprojectstar(SS_HTTPRequest $request) {
687
		$project = $this->getCurrentProject();
688
		if(!$project) {
689
			return $this->project404Response();
690
		}
691
692
		$member = Member::currentUser();
693
		if($member === null) {
694
			return $this->project404Response();
695
		}
696
		$favProject = $member->StarredProjects()
697
			->filter('DNProjectID', $project->ID)
698
			->first();
699
700
		if($favProject) {
701
			$member->StarredProjects()->remove($favProject);
702
		} else {
703
			$member->StarredProjects()->add($project);
704
		}
705
		return $this->redirectBack();
706
	}
707
708
	/**
709
	 * @param SS_HTTPRequest $request
710
	 * @return \SS_HTTPResponse
711
	 */
712
	public function branch(SS_HTTPRequest $request) {
713
		$project = $this->getCurrentProject();
714
		if(!$project) {
715
			return $this->project404Response();
716
		}
717
718
		$branchName = $request->getVar('name');
719
		$branch = $project->DNBranchList()->byName($branchName);
720
		if(!$branch) {
721
			return new SS_HTTPResponse("Branch '" . Convert::raw2xml($branchName) . "' not found.", 404);
722
		}
723
724
		return $this->render(array(
725
			'CurrentBranch' => $branch,
726
		));
727
	}
728
729
	/**
730
	 * @param SS_HTTPRequest $request
731
	 * @return \SS_HTTPResponse
732
	 */
733
	public function environment(SS_HTTPRequest $request) {
734
		// Performs canView permission check by limiting visible projects
735
		$project = $this->getCurrentProject();
736
		if(!$project) {
737
			return $this->project404Response();
738
		}
739
740
		// Performs canView permission check by limiting visible projects
741
		$env = $this->getCurrentEnvironment($project);
742
		if(!$env) {
743
			return $this->environment404Response();
744
		}
745
746
		return $this->render(array(
747
			'DNEnvironmentList' => $this->getCurrentProject()->DNEnvironmentList(),
748
			'FlagSnapshotsEnabled' => $this->FlagSnapshotsEnabled(),
749
		));
750
	}
751
752
753
	/**
754
	 * Initiate a pipeline dry run
755
	 *
756
	 * @param array $data
757
	 * @param DeployForm $form
758
	 *
759
	 * @return SS_HTTPResponse
0 ignored issues
show
Documentation introduced by
Should the return type not be SS_HTTPResponse|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
760
	 */
761
	public function doDryRun($data, DeployForm $form) {
762
		return $this->beginPipeline($data, $form, true);
763
	}
764
765
	/**
766
	 * Initiate a pipeline
767
	 *
768
	 * @param array $data
769
	 * @param DeployForm $form
770
	 * @return \SS_HTTPResponse
0 ignored issues
show
Documentation introduced by
Should the return type not be SS_HTTPResponse|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
771
	 */
772
	public function startPipeline($data, $form) {
773
		return $this->beginPipeline($data, $form);
774
	}
775
776
	/**
777
	 * Start a pipeline
778
	 *
779
	 * @param array $data
780
	 * @param DeployForm $form
781
	 * @param bool $isDryRun
782
	 * @return \SS_HTTPResponse
0 ignored issues
show
Documentation introduced by
Should the return type not be SS_HTTPResponse|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
783
	 */
784
	protected function beginPipeline($data, DeployForm $form, $isDryRun = false) {
785
		$buildName = $form->getSelectedBuild($data);
786
787
		// Performs canView permission check by limiting visible projects
788
		$project = $this->getCurrentProject();
789
		if(!$project) {
790
			return $this->project404Response();
791
		}
792
793
		// Performs canView permission check by limiting visible projects
794
		$environment = $this->getCurrentEnvironment($project);
795
		if(!$environment) {
796
			return $this->environment404Response();
797
		}
798
799
		if(!$environment->DryRunEnabled && $isDryRun) {
800
			return new SS_HTTPResponse("Dry-run for pipelines is not enabled for this environment", 404);
801
		}
802
803
		// Initiate the pipeline
804
		$sha = $project->DNBuildList()->byName($buildName);
805
		$pipeline = Pipeline::create();
806
		$pipeline->DryRun = $isDryRun;
807
		$pipeline->EnvironmentID = $environment->ID;
808
		$pipeline->AuthorID = Member::currentUserID();
809
		$pipeline->SHA = $sha->FullName();
810
		// Record build at time of execution
811
		if($currentBuild = $environment->CurrentBuild()) {
812
			$pipeline->PreviousDeploymentID = $currentBuild->ID;
813
		}
814
		$pipeline->start(); // start() will call write(), so no need to do it here as well.
815
		return $this->redirect($environment->Link());
816
	}
817
818
	/**
819
	 * @param SS_HTTPRequest $request
820
	 *
821
	 * @return SS_HTTPResponse
0 ignored issues
show
Documentation introduced by
Should the return type not be SS_HTTPResponse|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
822
	 * @throws SS_HTTPResponse_Exception
823
	 */
824
	public function pipeline(SS_HTTPRequest $request) {
825
		$params = $request->params();
826
		$pipeline = Pipeline::get()->byID($params['Identifier']);
827
828
		if(!$pipeline || !$pipeline->ID || !$pipeline->Environment()) {
829
			throw new SS_HTTPResponse_Exception('Pipeline not found', 404);
830
		}
831
		if(!$pipeline->Environment()->canView()) {
832
			return Security::permissionFailure();
833
		}
834
835
		$environment = $pipeline->Environment();
836
		$project = $pipeline->Environment()->Project();
837
838
		if($environment->Name != $params['Environment']) {
839
			throw new LogicException("Environment in URL doesn't match this pipeline");
840
		}
841
		if($project->Name != $params['Project']) {
842
			throw new LogicException("Project in URL doesn't match this pipeline");
843
		}
844
845
		// Delegate to sub-requesthandler
846
		return PipelineController::create($this, $pipeline);
847
	}
848
849
	/**
850
	 * Shows the creation log.
851
	 *
852
	 * @param SS_HTTPRequest $request
853
	 * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be SS_HTTPResponse|null|HTMLText?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
854
	 */
855
	public function createenv(SS_HTTPRequest $request) {
856
		$params = $request->params();
857
		if($params['Identifier']) {
858
			$record = DNCreateEnvironment::get()->byId($params['Identifier']);
859
860
			if(!$record || !$record->ID) {
861
				throw new SS_HTTPResponse_Exception('Create environment not found', 404);
862
			}
863
			if(!$record->canView()) {
864
				return Security::permissionFailure();
865
			}
866
867
			$project = $this->getCurrentProject();
868
			if(!$project) {
869
				return $this->project404Response();
870
			}
871
872
			if($project->Name != $params['Project']) {
873
				throw new LogicException("Project in URL doesn't match this creation");
874
			}
875
876
			return $this->render(array(
877
				'CreateEnvironment' => $record,
878
			));
879
		}
880
		return $this->render(array('CurrentTitle' => 'Create an environment'));
881
	}
882
883
884 View Code Duplication
	public function createenvlog(SS_HTTPRequest $request) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
885
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
886
887
		$params = $request->params();
888
		$env = DNCreateEnvironment::get()->byId($params['Identifier']);
889
890
		if(!$env || !$env->ID) {
891
			throw new SS_HTTPResponse_Exception('Log not found', 404);
892
		}
893
		if(!$env->canView()) {
894
			return Security::permissionFailure();
895
		}
896
897
		$project = $env->Project();
898
899
		if($project->Name != $params['Project']) {
900
			throw new LogicException("Project in URL doesn't match this deploy");
901
		}
902
903
		$log = $env->log();
904
		if($log->exists()) {
905
			$content = $log->content();
906
		} else {
907
			$content = 'Waiting for action to start';
908
		}
909
910
		return $this->sendResponse($env->ResqueStatus(), $content);
911
	}
912
913
	/**
914
	 * @param SS_HTTPRequest $request
915
	 * @return Form
0 ignored issues
show
Documentation introduced by
Should the return type not be Form|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
916
	 */
917
	public function getCreateEnvironmentForm(SS_HTTPRequest $request) {
918
		$this->setCurrentActionType(self::ACTION_ENVIRONMENTS);
919
920
		$project = $this->getCurrentProject();
921
		if(!$project) {
922
			return $this->project404Response();
923
		}
924
925
		$envType = $project->AllowedEnvironmentType;
0 ignored issues
show
Documentation introduced by
The property AllowedEnvironmentType does not exist on object<DNProject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
926
		if(!$envType || !class_exists($envType)) {
927
			return null;
928
		}
929
930
		$backend = Injector::inst()->get($envType);
931
		if(!($backend instanceof EnvironmentCreateBackend)) {
932
			// Only allow this for supported backends.
933
			return null;
934
		}
935
936
		$fields = $backend->getCreateEnvironmentFields($project);
937
		if(!$fields) return null;
938
939
		if(!$project->canCreateEnvironments()) {
940
			return new SS_HTTPResponse('Not allowed to create environments for this project', 401);
941
		}
942
943
		$form = Form::create(
944
			$this,
945
			'CreateEnvironmentForm',
946
			$fields,
947
			FieldList::create(
948
				FormAction::create('doCreateEnvironment', 'Create')
949
					->addExtraClass('btn')
950
			),
951
			$backend->getCreateEnvironmentValidator()
952
		);
953
954
		// Tweak the action so it plays well with our fake URL structure.
955
		$form->setFormAction($project->Link() . '/CreateEnvironmentForm');
956
957
		return $form;
958
	}
959
960
	/**
961
	 * @param array $data
962
	 * @param Form $form
963
	 *
964
	 * @return bool|HTMLText|SS_HTTPResponse
0 ignored issues
show
Documentation introduced by
Should the return type not be SS_HTTPResponse|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
965
	 */
966
	public function doCreateEnvironment($data, Form $form) {
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
967
		$this->setCurrentActionType(self::ACTION_ENVIRONMENTS);
968
969
		$project = $this->getCurrentProject();
970
		if(!$project) {
971
			return $this->project404Response();
972
		}
973
974
		if(!$project->canCreateEnvironments()) {
975
			return new SS_HTTPResponse('Not allowed to create environments for this project', 401);
976
		}
977
978
		// Set the environment type so we know what we're creating.
979
		$data['EnvironmentType'] = $project->AllowedEnvironmentType;
0 ignored issues
show
Documentation introduced by
The property AllowedEnvironmentType does not exist on object<DNProject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
980
981
		$job = DNCreateEnvironment::create();
982
983
		$job->Data = serialize($data);
984
		$job->ProjectID = $project->ID;
985
		$job->write();
986
		$job->start();
987
988
		return $this->redirect($project->Link('createenv') . '/' . $job->ID);
989
	}
990
991
	/**
992
	 *
993
	 * @param SS_HTTPRequest $request
994
	 * @return \SS_HTTPResponse
995
	 */
996
	public function metrics(SS_HTTPRequest $request) {
997
		// Performs canView permission check by limiting visible projects
998
		$project = $this->getCurrentProject();
999
		if(!$project) {
1000
			return $this->project404Response();
1001
		}
1002
1003
		// Performs canView permission check by limiting visible projects
1004
		$env = $this->getCurrentEnvironment($project);
1005
		if(!$env) {
1006
			return $this->environment404Response();
1007
		}
1008
1009
		return $this->render();
1010
	}
1011
1012
	/**
1013
	 * Get the DNData object.
1014
	 *
1015
	 * @return DNData
1016
	 */
1017
	public function DNData() {
1018
		return DNData::inst();
1019
	}
1020
1021
	/**
1022
	 * Provide a list of all projects.
1023
	 *
1024
	 * @return SS_List
1025
	 */
1026
	public function DNProjectList() {
1027
		$memberId = Member::currentUserID();
1028
		if(!$memberId) {
1029
			return new ArrayList();
1030
		}
1031
1032
		if(Permission::check('ADMIN')) {
1033
			return DNProject::get();
1034
		}
1035
1036
		$projects = Member::get()->filter('ID', $memberId)
1037
			->relation('Groups')
1038
			->relation('Projects');
1039
1040
		$this->extend('updateDNProjectList', $projects);
1041
		return $projects;
1042
	}
1043
1044
	/**
1045
	 * @return ArrayList
0 ignored issues
show
Documentation introduced by
Should the return type not be ArrayList|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1046
	 */
1047
	public function getPlatformSpecificStrings() {
1048
		$strings = $this->config()->platform_specific_strings;
1049
		if ($strings) {
1050
			return new ArrayList($strings);
1051
		}
1052
	}
1053
1054
	/**
1055
	 * Provide a list of all starred projects for the currently logged in member
1056
	 *
1057
	 * @return SS_List
1058
	 */
1059
	public function getStarredProjects() {
1060
		$member = Member::currentUser();
1061
		if($member === null) {
1062
			return new ArrayList();
1063
		}
1064
1065
		$favProjects = $member->StarredProjects();
1066
1067
		$list = new ArrayList();
1068
		foreach($favProjects as $project) {
1069
			if($project->canView($member)) {
1070
				$list->add($project);
1071
			}
1072
		}
1073
		return $list;
1074
	}
1075
1076
	/**
1077
	 * Returns top level navigation of projects.
1078
	 *
1079
	 * @param int $limit
1080
	 *
1081
	 * @return ArrayList
1082
	 */
1083
	public function Navigation($limit = 5) {
1084
		$navigation = new ArrayList();
1085
1086
		$currentProject = $this->getCurrentProject();
1087
		$currentEnvironment = $this->getCurrentEnvironment();
1088
		$actionType = $this->getCurrentActionType();
1089
1090
		$projects = $this->getStarredProjects();
1091
		if($projects->count() < 1) {
1092
			$projects = $this->DNProjectList();
1093
		} else {
1094
			$limit = -1;
1095
		}
1096
1097
		if($projects->count() > 0) {
1098
			$activeProject = false;
1099
1100
			if($limit > 0) {
1101
				$limitedProjects = $projects->limit($limit);
1102
			} else {
1103
				$limitedProjects = $projects;
1104
			}
1105
1106
			foreach($limitedProjects as $project) {
1107
				$isActive = $currentProject && $currentProject->ID == $project->ID;
1108
				if($isActive) {
1109
					$activeProject = true;
1110
				}
1111
1112
				$isCurrentEnvironment = false;
1113
				if($project && $currentEnvironment) {
1114
					$isCurrentEnvironment = (bool) $project->DNEnvironmentList()->find('ID', $currentEnvironment->ID);
1115
				}
1116
1117
				$navigation->push(array(
1118
					'Project' => $project,
1119
					'IsCurrentEnvironment' => $isCurrentEnvironment,
1120
					'IsActive' => $currentProject && $currentProject->ID == $project->ID,
1121
					'IsOverview' => $actionType == self::PROJECT_OVERVIEW && $currentProject->ID == $project->ID
1122
				));
1123
			}
1124
1125
			// Ensure the current project is in the list
1126
			if(!$activeProject && $currentProject) {
1127
				$navigation->unshift(array(
1128
					'Project' => $currentProject,
1129
					'IsActive' => true,
1130
					'IsCurrentEnvironment' => $currentEnvironment,
1131
					'IsOverview' => $actionType == self::PROJECT_OVERVIEW
1132
				));
1133
				if($limit > 0 && $navigation->count() > $limit) {
1134
					$navigation->pop();
1135
				}
1136
			}
1137
		}
1138
1139
		return $navigation;
1140
	}
1141
1142
	/**
1143
	 * Construct the deployment form
1144
	 *
1145
	 * @return Form
0 ignored issues
show
Documentation introduced by
Should the return type not be SS_HTTPResponse|string|DeployForm?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1146
	 */
1147
	public function getDeployForm($request = null) {
1148
1149
		// Performs canView permission check by limiting visible projects
1150
		$project = $this->getCurrentProject();
1151
		if(!$project) {
1152
			return $this->project404Response();
1153
		}
1154
1155
		// Performs canView permission check by limiting visible projects
1156
		$environment = $this->getCurrentEnvironment($project);
1157
		if(!$environment) {
1158
			return $this->environment404Response();
1159
		}
1160
1161
		if(!$environment->canDeploy()) {
1162
			return new SS_HTTPResponse("Not allowed to deploy", 401);
1163
		}
1164
1165
		// Generate the form
1166
		$form = new DeployForm($this, 'DeployForm', $environment, $project);
1167
1168
		// If this is an ajax request we don't want to submit the form - we just want to retrieve the markup.
1169
		if(
1170
			$request &&
1171
			!$request->requestVar('action_showDeploySummary') &&
1172
			$this->getRequest()->isAjax() &&
1173
			$this->getRequest()->isGET()
1174
		) {
1175
			// We can just use the URL we're accessing
1176
			$form->setFormAction($this->getRequest()->getURL());
1177
1178
			$body = json_encode(array('Content' => $form->forAjaxTemplate()->forTemplate()));
1179
			$this->getResponse()->addHeader('Content-Type', 'application/json');
1180
			$this->getResponse()->setBody($body);
1181
			return $body;
1182
		}
1183
1184
		$form->setFormAction($this->getRequest()->getURL() . '/DeployForm');
1185
		return $form;
1186
	}
1187
1188
	/**
1189
	 * @param SS_HTTPRequest $request
1190
	 *
1191
	 * @return SS_HTTPResponse|string
1192
	 */
1193
	public function gitRevisions(SS_HTTPRequest $request) {
1194
1195
		// Performs canView permission check by limiting visible projects
1196
		$project = $this->getCurrentProject();
1197
		if(!$project) {
1198
			return $this->project404Response();
1199
		}
1200
1201
		// Performs canView permission check by limiting visible projects
1202
		$env = $this->getCurrentEnvironment($project);
1203
		if(!$env) {
1204
			return $this->environment404Response();
1205
		}
1206
1207
		// For now only permit advanced options on one environment type, because we hacked the "full-deploy"
1208
		// checkbox in. Other environments such as the fast or capistrano one wouldn't know what to do with it.
1209
		if(get_class($env) === 'RainforestEnvironment') {
1210
			$advanced = Permission::check('DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS') ? 'true' : 'false';
1211
		} else {
1212
			$advanced = 'false';
1213
		}
1214
1215
		$tabs = array();
1216
		$id = 0;
1217
		$data = array(
1218
			'id' => ++$id,
1219
			'name' => 'Deploy the latest version of a branch',
1220
			'field_type' => 'dropdown',
1221
			'field_label' => 'Choose a branch',
1222
			'field_id' => 'branch',
1223
			'field_data' => array(),
1224
			'advanced_opts' => $advanced
1225
		);
1226
		foreach($project->DNBranchList() as $branch) {
1227
			$sha = $branch->SHA();
1228
			$name = $branch->Name();
1229
			$branchValue = sprintf("%s (%s, %s old)",
1230
				$name,
1231
				substr($sha, 0, 8),
1232
				$branch->LastUpdated()->TimeDiff()
1233
			);
1234
			$data['field_data'][] = array(
1235
				'id' => $sha,
1236
				'text' => $branchValue
1237
			);
1238
		}
1239
		$tabs[] = $data;
1240
1241
		$data = array(
1242
			'id' => ++$id,
1243
			'name' => 'Deploy a tagged release',
1244
			'field_type' => 'dropdown',
1245
			'field_label' => 'Choose a tag',
1246
			'field_id' => 'tag',
1247
			'field_data' => array(),
1248
			'advanced_opts' => $advanced
1249
		);
1250
1251
		foreach($project->DNTagList()->setLimit(null) as $tag) {
1252
			$name = $tag->Name();
1253
			$data['field_data'][] = array(
1254
				'id' => $tag->SHA(),
1255
				'text' => sprintf("%s", $name)
1256
			);
1257
		}
1258
1259
		// show newest tags first.
1260
		$data['field_data'] = array_reverse($data['field_data']);
1261
1262
		$tabs[] = $data;
1263
1264
		// Past deployments
1265
		$data = array(
1266
			'id' => ++$id,
1267
			'name' => 'Redeploy a release that was previously deployed (to any environment)',
1268
			'field_type' => 'dropdown',
1269
			'field_label' => 'Choose a previously deployed release',
1270
			'field_id' => 'release',
1271
			'field_data' => array(),
1272
			'advanced_opts' => $advanced
1273
		);
1274
		// We are aiming at the format:
1275
		// [{text: 'optgroup text', children: [{id: '<sha>', text: '<inner text>'}]}]
0 ignored issues
show
Unused Code Comprehensibility introduced by
71% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1276
		$redeploy = array();
1277
		foreach($project->DNEnvironmentList() as $dnEnvironment) {
1278
			$envName = $dnEnvironment->Name;
1279
			$perEnvDeploys = array();
1280
1281
			foreach($dnEnvironment->DeployHistory() as $deploy) {
1282
				$sha = $deploy->SHA;
1283
1284
				// Check if exists to make sure the newest deployment date is used.
1285
				if(!isset($perEnvDeploys[$sha])) {
1286
					$pastValue = sprintf("%s (deployed %s)",
1287
						substr($sha, 0, 8),
1288
						$deploy->obj('LastEdited')->Ago()
1289
					);
1290
					$perEnvDeploys[$sha] = array(
1291
						'id' => $sha,
1292
						'text' => $pastValue
1293
					);
1294
				}
1295
			}
1296
1297
			if(!empty($perEnvDeploys)) {
1298
				$redeploy[$envName] = array_values($perEnvDeploys);
1299
			}
1300
		}
1301
		// Convert the array to the frontend format (i.e. keyed to regular array)
1302
		foreach($redeploy as $env => $descr) {
1303
			$data['field_data'][] = array('text'=>$env, 'children'=>$descr);
1304
		}
1305
		$tabs[] = $data;
1306
1307
		$data = array(
1308
			'id' => ++$id,
1309
			'name' => 'Deploy a specific SHA',
1310
			'field_type' => 'textfield',
1311
			'field_label' => 'Choose a SHA',
1312
			'field_id' => 'SHA',
1313
			'field_data' => array(),
1314
			'advanced_opts' => $advanced
1315
		);
1316
		$tabs[] = $data;
1317
1318
		// get the last time git fetch was run
1319
		$lastFetched = 'never';
1320
		$fetch = DNGitFetch::get()
1321
			->filter('ProjectID', $project->ID)
1322
			->sort('LastEdited', 'DESC')
1323
			->first();
1324
		if($fetch) {
1325
			$lastFetched = $fetch->dbObject('LastEdited')->Ago();
1326
		}
1327
1328
		$data = array(
1329
			'Tabs' => $tabs,
1330
			'last_fetched' => $lastFetched
1331
		);
1332
1333
		return json_encode($data, JSON_PRETTY_PRINT);
1334
	}
1335
1336
	/**
1337
	 * Check and regenerate a global CSRF token
1338
	 *
1339
	 * @param SS_HTTPRequest $request
1340
	 * @param bool $resetToken
1341
	 *
1342
	 * @return bool
1343
	 */
1344
	protected function checkCsrfToken(SS_HTTPRequest $request, $resetToken = true) {
1345
		$token = SecurityToken::inst();
1346
1347
		// Ensure the submitted token has a value
1348
		$submittedToken = $request->postVar('SecurityID');
1349
		if(!$submittedToken) {
1350
			return false;
1351
		}
1352
1353
		// Do the actual check.
1354
		$check = $token->check($submittedToken);
1355
1356
		// Reset the token after we've checked the existing token
1357
		if($resetToken) {
1358
			$token->reset();
1359
		}
1360
1361
		// Return whether the token was correct or not
1362
		return $check;
1363
	}
1364
1365
	/**
1366
	 * @param SS_HTTPRequest $request
1367
	 *
1368
	 * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be SS_HTTPResponse|string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1369
	 */
1370
	public function deploySummary(SS_HTTPRequest $request) {
1371
1372
		// Performs canView permission check by limiting visible projects
1373
		$project = $this->getCurrentProject();
1374
		if(!$project) {
1375
			return $this->project404Response();
1376
		}
1377
1378
		// Performs canView permission check by limiting visible projects
1379
		$environment = $this->getCurrentEnvironment($project);
1380
		if(!$environment) {
1381
			return $this->environment404Response();
1382
		}
1383
1384
		// Plan the deployment.
1385
		$strategy = $environment->Backend()->planDeploy(
1386
			$environment,
1387
			$request->requestVars()
0 ignored issues
show
Bug introduced by
It seems like $request->requestVars() targeting SS_HTTPRequest::requestVars() can also be of type null; however, DeploymentBackend::planDeploy() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1388
		);
1389
		$data = $strategy->toArray();
1390
1391
		// Add in a URL for comparing from->to code changes. Ensure that we have
1392
		// two proper 40 character SHAs, otherwise we can't show the compare link.
1393
		$interface = $project->getRepositoryInterface();
1394
		if(
1395
			!empty($interface) && !empty($interface->URL)
1396
			&& !empty($data['changes']['Code version']['from'])
1397
			&& strlen($data['changes']['Code version']['from']) == '40'
1398
			&& !empty($data['changes']['Code version']['to'])
1399
			&& strlen($data['changes']['Code version']['to']) == '40'
1400
		) {
1401
			$compareurl = sprintf(
1402
				'%s/compare/%s...%s',
1403
				$interface->URL,
1404
				$data['changes']['Code version']['from'],
1405
				$data['changes']['Code version']['to']
1406
			);
1407
			$data['changes']['Code version']['compareUrl'] = $compareurl;
1408
		}
1409
1410
		// Append json to response
1411
		$token = SecurityToken::inst();
1412
		$data['SecurityID'] = $token->getValue();
1413
1414
		return json_encode($data);
1415
	}
1416
1417
	/**
1418
	 * Deployment form submission handler.
1419
	 *
1420
	 * Initiate a DNDeployment record and redirect to it for status polling
1421
	 *
1422
	 * @param SS_HTTPRequest $request
1423
	 *
1424
	 * @return SS_HTTPResponse
1425
	 * @throws ValidationException
1426
	 * @throws null
1427
	 */
1428
	public function startDeploy(SS_HTTPRequest $request) {
1429
1430
		// Ensure the CSRF Token is correct
1431
		if(!$this->checkCsrfToken($request)) {
1432
			// CSRF token didn't match
1433
			return $this->httpError(400, 'Bad Request');
1434
		}
1435
1436
		// Performs canView permission check by limiting visible projects
1437
		$project = $this->getCurrentProject();
1438
		if(!$project) {
1439
			return $this->project404Response();
1440
		}
1441
1442
		// Performs canView permission check by limiting visible projects
1443
		$environment = $this->getCurrentEnvironment($project);
1444
		if(!$environment) {
1445
			return $this->environment404Response();
1446
		}
1447
1448
		// Initiate the deployment
1449
		// The extension point should pass in: Project, Environment, SelectRelease, buildName
1450
		$this->extend('doDeploy', $project, $environment, $buildName, $data);
1451
1452
		// Start the deployment based on the approved strategy.
1453
		$strategy = new DeploymentStrategy($environment);
1454
		$strategy->fromArray($request->requestVar('strategy'));
1455
		$deployment = $strategy->createDeployment();
1456
		$deployment->start();
1457
1458
		return json_encode(array(
1459
			'url' => Director::absoluteBaseURL() . $deployment->Link()
1460
		), JSON_PRETTY_PRINT);
1461
	}
1462
1463
	/**
1464
	 * Action - Do the actual deploy
1465
	 *
1466
	 * @param SS_HTTPRequest $request
1467
	 *
1468
	 * @return SS_HTTPResponse|string
0 ignored issues
show
Documentation introduced by
Should the return type not be SS_HTTPResponse|null|HTMLText?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1469
	 * @throws SS_HTTPResponse_Exception
1470
	 */
1471
	public function deploy(SS_HTTPRequest $request) {
1472
		$params = $request->params();
1473
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1474
1475
		if(!$deployment || !$deployment->ID) {
1476
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1477
		}
1478
		if(!$deployment->canView()) {
1479
			return Security::permissionFailure();
1480
		}
1481
1482
		$environment = $deployment->Environment();
1483
		$project = $environment->Project();
1484
1485
		if($environment->Name != $params['Environment']) {
1486
			throw new LogicException("Environment in URL doesn't match this deploy");
1487
		}
1488
		if($project->Name != $params['Project']) {
1489
			throw new LogicException("Project in URL doesn't match this deploy");
1490
		}
1491
1492
		return $this->render(array(
1493
			'Deployment' => $deployment,
1494
		));
1495
	}
1496
1497
1498
	/**
1499
	 * Action - Get the latest deploy log
1500
	 *
1501
	 * @param SS_HTTPRequest $request
1502
	 *
1503
	 * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be SS_HTTPResponse|null|string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1504
	 * @throws SS_HTTPResponse_Exception
1505
	 */
1506
	public function deploylog(SS_HTTPRequest $request) {
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
		$log = $deployment->log();
1528
		if($log->exists()) {
1529
			$content = $log->content();
1530
		} else {
1531
			$content = 'Waiting for action to start';
1532
		}
1533
1534
		return $this->sendResponse($deployment->ResqueStatus(), $content);
1535
	}
1536
1537
	/**
1538
	 * @param SS_HTTPRequest|null $request
1539
	 *
1540
	 * @return Form
1541
	 */
1542
	public function getDataTransferForm(SS_HTTPRequest $request = null) {
1543
		// Performs canView permission check by limiting visible projects
1544
		$envs = $this->getCurrentProject()->DNEnvironmentList()->filterByCallback(function($item) {
1545
			return $item->canBackup();
1546
		});
1547
1548
		if(!$envs) {
1549
			return $this->environment404Response();
1550
		}
1551
1552
		$form = Form::create(
1553
			$this,
1554
			'DataTransferForm',
1555
			FieldList::create(
1556
				HiddenField::create('Direction', null, 'get'),
1557
				DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1558
					->setEmptyString('Select an environment'),
1559
				DropdownField::create('Mode', 'Transfer', DNDataArchive::get_mode_map())
1560
			),
1561
			FieldList::create(
1562
				FormAction::create('doDataTransfer', 'Create')
1563
					->addExtraClass('btn')
1564
			)
1565
		);
1566
		$form->setFormAction($this->getRequest()->getURL() . '/DataTransferForm');
1567
1568
		return $form;
1569
	}
1570
1571
	/**
1572
	 * @param array $data
1573
	 * @param Form $form
1574
	 *
1575
	 * @return SS_HTTPResponse
0 ignored issues
show
Documentation introduced by
Should the return type not be SS_HTTPResponse|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1576
	 * @throws SS_HTTPResponse_Exception
1577
	 */
1578
	public function doDataTransfer($data, Form $form) {
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1579
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1580
1581
		// Performs canView permission check by limiting visible projects
1582
		$project = $this->getCurrentProject();
1583
		if(!$project) {
1584
			return $this->project404Response();
1585
		}
1586
1587
		$dataArchive = null;
1588
1589
		// Validate direction.
1590
		if($data['Direction'] == 'get') {
1591
			$validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1592
				->filterByCallback(function($item) {
1593
					return $item->canBackup();
1594
				});
1595
		} else if($data['Direction'] == 'push') {
1596
			$validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1597
				->filterByCallback(function($item) {
1598
					return $item->canRestore();
1599
				});
1600
		} else {
1601
			throw new LogicException('Invalid direction');
1602
		}
1603
1604
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
1605
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
1606
		if(!$environment) {
1607
			throw new LogicException('Invalid environment');
1608
		}
1609
1610
		$this->validateSnapshotMode($data['Mode']);
1611
1612
1613
		// Only 'push' direction is allowed an association with an existing archive.
1614
		if(
1615
			$data['Direction'] == 'push'
1616
			&& isset($data['DataArchiveID'])
1617
			&& is_numeric($data['DataArchiveID'])
1618
		) {
1619
			$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1620
			if(!$dataArchive) {
1621
				throw new LogicException('Invalid data archive');
1622
			}
1623
1624
			if(!$dataArchive->canDownload()) {
1625
				throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1626
			}
1627
		}
1628
1629
		$transfer = DNDataTransfer::create();
1630
		$transfer->EnvironmentID = $environment->ID;
1631
		$transfer->Direction = $data['Direction'];
1632
		$transfer->Mode = $data['Mode'];
1633
		$transfer->DataArchiveID = $dataArchive ? $dataArchive->ID : null;
1634
		if($data['Direction'] == 'push') {
1635
			$transfer->setBackupBeforePush(!empty($data['BackupBeforePush']));
1636
		}
1637
		$transfer->write();
1638
		$transfer->start();
1639
1640
		return $this->redirect($transfer->Link());
1641
	}
1642
1643
	/**
1644
	 * View into the log for a {@link DNDataTransfer}.
1645
	 *
1646
	 * @param SS_HTTPRequest $request
1647
	 *
1648
	 * @return SS_HTTPResponse|string
0 ignored issues
show
Documentation introduced by
Should the return type not be SS_HTTPResponse|null|HTMLText?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1649
	 * @throws SS_HTTPResponse_Exception
1650
	 */
1651
	public function transfer(SS_HTTPRequest $request) {
1652
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1653
1654
		$params = $request->params();
1655
		$transfer = DNDataTransfer::get()->byId($params['Identifier']);
1656
1657
		if(!$transfer || !$transfer->ID) {
1658
			throw new SS_HTTPResponse_Exception('Transfer not found', 404);
1659
		}
1660
		if(!$transfer->canView()) {
1661
			return Security::permissionFailure();
1662
		}
1663
1664
		$environment = $transfer->Environment();
1665
		$project = $environment->Project();
1666
1667
		if($project->Name != $params['Project']) {
1668
			throw new LogicException("Project in URL doesn't match this deploy");
1669
		}
1670
1671
		return $this->render(array(
1672
			'CurrentTransfer' => $transfer,
1673
			'SnapshotsSection' => 1,
1674
		));
1675
	}
1676
1677
	/**
1678
	 * Action - Get the latest deploy log
1679
	 *
1680
	 * @param SS_HTTPRequest $request
1681
	 *
1682
	 * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be SS_HTTPResponse|null|string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1683
	 * @throws SS_HTTPResponse_Exception
1684
	 */
1685 View Code Duplication
	public function transferlog(SS_HTTPRequest $request) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1686
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1687
1688
		$params = $request->params();
1689
		$transfer = DNDataTransfer::get()->byId($params['Identifier']);
1690
1691
		if(!$transfer || !$transfer->ID) {
1692
			throw new SS_HTTPResponse_Exception('Transfer not found', 404);
1693
		}
1694
		if(!$transfer->canView()) {
1695
			return Security::permissionFailure();
1696
		}
1697
1698
		$environment = $transfer->Environment();
1699
		$project = $environment->Project();
1700
1701
		if($project->Name != $params['Project']) {
1702
			throw new LogicException("Project in URL doesn't match this deploy");
1703
		}
1704
1705
		$log = $transfer->log();
1706
		if($log->exists()) {
1707
			$content = $log->content();
1708
		} else {
1709
			$content = 'Waiting for action to start';
1710
		}
1711
1712
		return $this->sendResponse($transfer->ResqueStatus(), $content);
1713
	}
1714
1715
	/**
1716
	 * Note: Submits to the same action as {@link getDataTransferForm()},
1717
	 * but with a Direction=push and an archive reference.
1718
	 *
1719
	 * @param SS_HTTPRequest $request
1720
	 * @param DNDataArchive|null $dataArchive Only set when method is called manually in {@link restore()},
1721
	 *                            otherwise the state is inferred from the request data.
1722
	 * @return Form
1723
	 */
1724
	public function getDataTransferRestoreForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) {
1725
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1726
1727
		// Performs canView permission check by limiting visible projects
1728
		$project = $this->getCurrentProject();
1729
		$envs = $project->DNEnvironmentList()->filterByCallback(function($item) {
1730
			return $item->canRestore();
1731
		});
1732
1733
		if(!$envs) {
1734
			return $this->environment404Response();
1735
		}
1736
1737
		$modesMap = array();
1738
		if(in_array($dataArchive->Mode, array('all'))) {
1739
			$modesMap['all'] = 'Database and Assets';
1740
		};
1741
		if(in_array($dataArchive->Mode, array('all', 'db'))) {
1742
			$modesMap['db'] = 'Database only';
1743
		};
1744
		if(in_array($dataArchive->Mode, array('all', 'assets'))) {
1745
			$modesMap['assets'] = 'Assets only';
1746
		};
1747
1748
		$alertMessage = '<div class="alert alert-warning"><strong>Warning:</strong> '
1749
			. 'This restore will overwrite the data on the chosen environment below</div>';
1750
1751
		$form = Form::create(
1752
			$this,
1753
			'DataTransferRestoreForm',
1754
			FieldList::create(
1755
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1756
				HiddenField::create('Direction', null, 'push'),
1757
				LiteralField::create('Warning', $alertMessage),
1758
				DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1759
					->setEmptyString('Select an environment'),
1760
				DropdownField::create('Mode', 'Transfer', $modesMap),
1761
				CheckboxField::create('BackupBeforePush', 'Backup existing data', '1')
1762
			),
1763
			FieldList::create(
1764
				FormAction::create('doDataTransfer', 'Restore Data')
1765
					->addExtraClass('btn')
1766
			)
1767
		);
1768
		$form->setFormAction($project->Link() . '/DataTransferRestoreForm');
1769
1770
		return $form;
1771
	}
1772
1773
	/**
1774
	 * View a form to restore a specific {@link DataArchive}.
1775
	 * Permission checks are handled in {@link DataArchives()}.
1776
	 * Submissions are handled through {@link doDataTransfer()}, same as backup operations.
1777
	 *
1778
	 * @param SS_HTTPRequest $request
1779
	 *
1780
	 * @return HTMLText
1781
	 * @throws SS_HTTPResponse_Exception
1782
	 */
1783 View Code Duplication
	public function restoresnapshot(SS_HTTPRequest $request) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1784
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1785
1786
		/** @var DNDataArchive $dataArchive */
1787
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1788
1789
		if(!$dataArchive) {
1790
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1791
		}
1792
1793
		// We check for canDownload because that implies access to the data.
1794
		// canRestore is later checked on the actual restore action per environment.
1795
		if(!$dataArchive->canDownload()) {
1796
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1797
		}
1798
1799
		$form = $this->getDataTransferRestoreForm($this->request, $dataArchive);
1800
1801
		// View currently only available via ajax
1802
		return $form->forTemplate();
1803
	}
1804
1805
	/**
1806
	 * View a form to delete a specific {@link DataArchive}.
1807
	 * Permission checks are handled in {@link DataArchives()}.
1808
	 * Submissions are handled through {@link doDelete()}.
1809
	 *
1810
	 * @param SS_HTTPRequest $request
1811
	 *
1812
	 * @return HTMLText
1813
	 * @throws SS_HTTPResponse_Exception
1814
	 */
1815 View Code Duplication
	public function deletesnapshot(SS_HTTPRequest $request) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1816
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1817
1818
		/** @var DNDataArchive $dataArchive */
1819
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1820
1821
		if(!$dataArchive) {
1822
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1823
		}
1824
1825
		if(!$dataArchive->canDelete()) {
1826
			throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403);
1827
		}
1828
1829
		$form = $this->getDeleteForm($this->request, $dataArchive);
1830
1831
		// View currently only available via ajax
1832
		return $form->forTemplate();
1833
	}
1834
1835
	/**
1836
	 * @param SS_HTTPRequest $request
1837
	 * @param DNDataArchive|null $dataArchive Only set when method is called manually, otherwise the state is inferred
1838
	 *        from the request data.
1839
	 * @return Form
1840
	 */
1841
	public function getDeleteForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) {
1842
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1843
1844
		// Performs canView permission check by limiting visible projects
1845
		$project = $this->getCurrentProject();
1846
		if(!$project) {
1847
			return $this->project404Response();
1848
		}
1849
1850
		$snapshotDeleteWarning = '<div class="alert alert-warning">'
1851
			. 'Are you sure you want to permanently delete this snapshot from this archive area?'
1852
			. '</div>';
1853
1854
		$form = Form::create(
1855
			$this,
1856
			'DeleteForm',
1857
			FieldList::create(
1858
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1859
				LiteralField::create('Warning', $snapshotDeleteWarning)
1860
			),
1861
			FieldList::create(
1862
				FormAction::create('doDelete', 'Delete')
1863
					->addExtraClass('btn')
1864
			)
1865
		);
1866
		$form->setFormAction($project->Link() . '/DeleteForm');
1867
1868
		return $form;
1869
	}
1870
1871
	/**
1872
	 * @param array $data
1873
	 * @param Form $form
1874
	 *
1875
	 * @return bool|SS_HTTPResponse
0 ignored issues
show
Documentation introduced by
Should the return type not be SS_HTTPResponse|null|false?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1876
	 * @throws SS_HTTPResponse_Exception
1877
	 */
1878
	public function doDelete($data, Form $form) {
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1879
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1880
1881
		// Performs canView permission check by limiting visible projects
1882
		$project = $this->getCurrentProject();
1883
		if(!$project) {
1884
			return $this->project404Response();
1885
		}
1886
1887
		$dataArchive = null;
1888
1889
		if(
1890
			isset($data['DataArchiveID'])
1891
			&& is_numeric($data['DataArchiveID'])
1892
		) {
1893
			$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1894
		}
1895
1896
		if(!$dataArchive) {
1897
			throw new LogicException('Invalid data archive');
1898
		}
1899
1900
		if(!$dataArchive->canDelete()) {
1901
			throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403);
1902
		}
1903
1904
		$dataArchive->delete();
1905
1906
		return $this->redirectBack();
1907
	}
1908
1909
	/**
1910
	 * View a form to move a specific {@link DataArchive}.
1911
	 *
1912
	 * @param SS_HTTPRequest $request
1913
	 *
1914
	 * @return HTMLText
1915
	 * @throws SS_HTTPResponse_Exception
1916
	 */
1917 View Code Duplication
	public function movesnapshot(SS_HTTPRequest $request) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1918
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1919
1920
		/** @var DNDataArchive $dataArchive */
1921
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1922
1923
		if(!$dataArchive) {
1924
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1925
		}
1926
1927
		// We check for canDownload because that implies access to the data.
1928
		if(!$dataArchive->canDownload()) {
1929
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1930
		}
1931
1932
		$form = $this->getMoveForm($this->request, $dataArchive);
1933
1934
		// View currently only available via ajax
1935
		return $form->forTemplate();
0 ignored issues
show
Bug introduced by
The method forTemplate does only exist in Form, but not in SS_HTTPResponse.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1936
	}
1937
1938
	/**
1939
	 * Build snapshot move form.
1940
	 *
1941
	 * @param SS_HTTPRequest $request
1942
	 * @param DNDataArchive|null $dataArchive
1943
	 *
1944
	 * @return Form|SS_HTTPResponse
1945
	 */
1946
	public function getMoveForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) {
1947
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1948
1949
		$envs = $dataArchive->validTargetEnvironments();
1950
		if(!$envs) {
1951
			return $this->environment404Response();
1952
		}
1953
1954
		$warningMessage = '<div class="alert alert-warning"><strong>Warning:</strong> This will make the snapshot '
1955
			. 'available to people with access to the target environment.<br>By pressing "Change ownership" you '
1956
			. 'confirm that you have considered data confidentiality regulations.</div>';
1957
1958
		$form = Form::create(
1959
			$this,
1960
			'MoveForm',
1961
			FieldList::create(
1962
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1963
				LiteralField::create('Warning', $warningMessage),
1964
				DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1965
					->setEmptyString('Select an environment')
1966
			),
1967
			FieldList::create(
1968
				FormAction::create('doMove', 'Change ownership')
1969
					->addExtraClass('btn')
1970
			)
1971
		);
1972
		$form->setFormAction($this->getCurrentProject()->Link() . '/MoveForm');
1973
1974
		return $form;
1975
	}
1976
1977
	/**
1978
	 * @param array $data
1979
	 * @param Form $form
1980
	 *
1981
	 * @return bool|SS_HTTPResponse
0 ignored issues
show
Documentation introduced by
Should the return type not be SS_HTTPResponse|null|false?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1982
	 * @throws SS_HTTPResponse_Exception
1983
	 * @throws ValidationException
1984
	 * @throws null
1985
	 */
1986
	public function doMove($data, Form $form) {
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1987
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1988
1989
		// Performs canView permission check by limiting visible projects
1990
		$project = $this->getCurrentProject();
1991
		if(!$project) {
1992
			return $this->project404Response();
1993
		}
1994
1995
		/** @var DNDataArchive $dataArchive */
1996
		$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1997
		if(!$dataArchive) {
1998
			throw new LogicException('Invalid data archive');
1999
		}
2000
2001
		// We check for canDownload because that implies access to the data.
2002
		if(!$dataArchive->canDownload()) {
2003
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
2004
		}
2005
2006
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
2007
		$validEnvs = $dataArchive->validTargetEnvironments();
2008
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
2009
		if(!$environment) {
2010
			throw new LogicException('Invalid environment');
2011
		}
2012
2013
		$dataArchive->EnvironmentID = $environment->ID;
2014
		$dataArchive->write();
2015
2016
		return $this->redirectBack();
2017
	}
2018
2019
	/**
2020
	 * Returns an error message if redis is unavailable
2021
	 *
2022
	 * @return string
2023
	 */
2024
	public static function RedisUnavailable() {
2025
		try {
2026
			Resque::queues();
2027
		} catch(Exception $e) {
2028
			return $e->getMessage();
2029
		}
2030
		return '';
2031
	}
2032
2033
	/**
2034
	 * Returns the number of connected Redis workers
2035
	 *
2036
	 * @return int
2037
	 */
2038
	public static function RedisWorkersCount() {
2039
		return count(Resque_Worker::all());
2040
	}
2041
2042
	/**
2043
	 * @return array
2044
	 */
2045
	public function providePermissions() {
2046
		return array(
2047
			self::DEPLOYNAUT_BYPASS_PIPELINE => array(
2048
				'name' => "Bypass Pipeline",
2049
				'category' => "Deploynaut",
2050
				'help' => "Enables users to directly initiate deployments, bypassing any pipeline",
2051
			),
2052
			self::DEPLOYNAUT_DRYRUN_PIPELINE => array(
2053
				'name' => 'Dry-run Pipeline',
2054
				'category' => 'Deploynaut',
2055
				'help' => 'Enable dry-run execution of pipelines for testing'
2056
			),
2057
			self::DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS => array(
2058
				'name' => "Access to advanced deploy options",
2059
				'category' => "Deploynaut",
2060
			),
2061
2062
			// Permissions that are intended to be added to the roles.
2063
			self::ALLOW_PROD_DEPLOYMENT => array(
2064
				'name' => "Ability to deploy to production environments",
2065
				'category' => "Deploynaut",
2066
			),
2067
			self::ALLOW_NON_PROD_DEPLOYMENT => array(
2068
				'name' => "Ability to deploy to non-production environments",
2069
				'category' => "Deploynaut",
2070
			),
2071
			self::ALLOW_PROD_SNAPSHOT => array(
2072
				'name' => "Ability to make production snapshots",
2073
				'category' => "Deploynaut",
2074
			),
2075
			self::ALLOW_NON_PROD_SNAPSHOT => array(
2076
				'name' => "Ability to make non-production snapshots",
2077
				'category' => "Deploynaut",
2078
			),
2079
			self::ALLOW_CREATE_ENVIRONMENT => array(
2080
				'name' => "Ability to create environments",
2081
				'category' => "Deploynaut",
2082
			),
2083
		);
2084
	}
2085
2086
	/**
2087
	 * @return DNProject|null
2088
	 */
2089
	public function getCurrentProject() {
2090
		$projectName = trim($this->getRequest()->param('Project'));
2091
		if(!$projectName) {
2092
			return null;
2093
		}
2094
		if(empty(self::$_project_cache[$projectName])) {
2095
			self::$_project_cache[$projectName] = $this->DNProjectList()->filter('Name', $projectName)->First();
2096
		}
2097
		return self::$_project_cache[$projectName];
2098
	}
2099
2100
	/**
2101
	 * @param DNProject|null $project
2102
	 * @return DNEnvironment|null
2103
	 */
2104
	public function getCurrentEnvironment(DNProject $project = null) {
2105
		if($this->getRequest()->param('Environment') === null) {
2106
			return null;
2107
		}
2108
		if($project === null) {
2109
			$project = $this->getCurrentProject();
2110
		}
2111
		// project can still be null
2112
		if($project === null) {
2113
			return null;
2114
		}
2115
		return $project->DNEnvironmentList()->filter('Name', $this->getRequest()->param('Environment'))->First();
2116
	}
2117
2118
	/**
2119
	 * This will return a const that indicates the class of action currently being performed
2120
	 *
2121
	 * Until DNRoot is de-godded, it does a bunch of different actions all in the same class.
2122
	 * So we just have each action handler calll setCurrentActionType to define what sort of
2123
	 * action it is.
2124
	 *
2125
	 * @return string - one of the consts from self::$action_types
2126
	 */
2127
	public function getCurrentActionType() {
2128
		return $this->actionType;
2129
	}
2130
2131
	/**
2132
	 * Sets the current action type
2133
	 *
2134
	 * @param string $actionType string - one of the consts from self::$action_types
2135
	 */
2136
	public function setCurrentActionType($actionType) {
2137
		$this->actionType = $actionType;
2138
	}
2139
2140
	/**
2141
	 * Helper method to allow templates to know whether they should show the 'Archive List' include or not.
2142
	 * The actual permissions are set on a per-environment level, so we need to find out if this $member can upload to
2143
	 * or download from *any* {@link DNEnvironment} that (s)he has access to.
2144
	 *
2145
	 * TODO To be replaced with a method that just returns the list of archives this {@link Member} has access to.
2146
	 *
2147
	 * @param Member|null $member The {@link Member} to check (or null to check the currently logged in Member)
2148
	 * @return boolean|null true if $member has access to upload or download to at least one {@link DNEnvironment}.
2149
	 */
2150
	public function CanViewArchives(Member $member = null) {
2151
		if($member === null) {
2152
			$member = Member::currentUser();
2153
		}
2154
2155
		if(Permission::checkMember($member, 'ADMIN')) {
2156
			return true;
2157
		}
2158
2159
		$allProjects = $this->DNProjectList();
2160
		if(!$allProjects) {
2161
			return false;
2162
		}
2163
2164
		foreach($allProjects as $project) {
2165
			if($project->Environments()) {
2166
				foreach($project->Environments() as $environment) {
2167
					if(
2168
						$environment->canRestore($member) ||
2169
						$environment->canBackup($member) ||
2170
						$environment->canUploadArchive($member) ||
2171
						$environment->canDownloadArchive($member)
2172
					) {
2173
						// We can return early as we only need to know that we can access one environment
2174
						return true;
2175
					}
2176
				}
2177
			}
2178
		}
2179
	}
2180
2181
	/**
2182
	 * Returns a list of attempted environment creations.
2183
	 *
2184
	 * @return PaginatedList
2185
	 */
2186
	public function CreateEnvironmentList() {
2187
		$project = $this->getCurrentProject();
2188
		if($project) {
2189
			return new PaginatedList($project->CreateEnvironments()->sort("Created DESC"), $this->request);
0 ignored issues
show
Bug introduced by
The method CreateEnvironments() does not exist on DNProject. Did you maybe mean canCreateEnvironments()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
Documentation introduced by
$this->request is of type object<SS_HTTPRequest>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2190
		}
2191
		return new PaginatedList(new ArrayList(), $this->request);
0 ignored issues
show
Documentation introduced by
$this->request is of type object<SS_HTTPRequest>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2192
	}
2193
2194
	/**
2195
	 * Returns a list of all archive files that can be accessed by the currently logged-in {@link Member}
2196
	 *
2197
	 * @return PaginatedList
2198
	 */
2199
	public function CompleteDataArchives() {
2200
		$project = $this->getCurrentProject();
2201
		$archives = new ArrayList();
2202
2203
		$archiveList = $project->Environments()->relation("DataArchives");
2204
		if($archiveList->count() > 0) {
2205
			foreach($archiveList as $archive) {
2206
				if($archive->canView() && !$archive->isPending()) {
2207
					$archives->push($archive);
2208
				}
2209
			}
2210
		}
2211
		return new PaginatedList($archives->sort("Created", "DESC"), $this->request);
0 ignored issues
show
Documentation introduced by
$this->request is of type object<SS_HTTPRequest>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2212
	}
2213
2214
	/**
2215
	 * @return PaginatedList The list of "pending" data archives which are waiting for a file
2216
	 * to be delivered offline by post, and manually uploaded into the system.
2217
	 */
2218
	public function PendingDataArchives() {
2219
		$project = $this->getCurrentProject();
2220
		$archives = new ArrayList();
2221
		foreach($project->DNEnvironmentList() as $env) {
2222
			foreach($env->DataArchives() as $archive) {
2223
				if($archive->canView() && $archive->isPending()) {
2224
					$archives->push($archive);
2225
				}
2226
			}
2227
		}
2228
		return new PaginatedList($archives->sort("Created", "DESC"), $this->request);
0 ignored issues
show
Documentation introduced by
$this->request is of type object<SS_HTTPRequest>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2229
	}
2230
2231
	/**
2232
	 * @return PaginatedList
2233
	 */
2234
	public function DataTransferLogs() {
2235
		$project = $this->getCurrentProject();
2236
2237
		$transfers = DNDataTransfer::get()->filterByCallback(function($record) use($project) {
2238
			return
2239
				$record->Environment()->Project()->ID == $project->ID && // Ensure only the current Project is shown
2240
				(
2241
					$record->Environment()->canRestore() || // Ensure member can perform an action on the transfers env
2242
					$record->Environment()->canBackup() ||
2243
					$record->Environment()->canUploadArchive() ||
2244
					$record->Environment()->canDownloadArchive()
2245
				);
2246
		});
2247
2248
		return new PaginatedList($transfers->sort("Created", "DESC"), $this->request);
0 ignored issues
show
Documentation introduced by
$this->request is of type object<SS_HTTPRequest>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2249
	}
2250
2251
	/**
2252
	 * @return null|PaginatedList
2253
	 */
2254
	public function DeployHistory() {
2255
		if($env = $this->getCurrentEnvironment()) {
2256
			$history = $env->DeployHistory();
2257
			if($history->count() > 0) {
2258
				$pagination = new PaginatedList($history, $this->getRequest());
0 ignored issues
show
Documentation introduced by
$this->getRequest() is of type object<SS_HTTPRequest>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2259
				$pagination->setPageLength(8);
2260
				return $pagination;
2261
			}
2262
		}
2263
		return null;
2264
	}
2265
2266
	/**
2267
	 * @return SS_HTTPResponse
2268
	 */
2269
	protected function project404Response() {
2270
		return new SS_HTTPResponse(
2271
			"Project '" . Convert::raw2xml($this->getRequest()->param('Project')) . "' not found.",
2272
			404
2273
		);
2274
	}
2275
2276
	/**
2277
	 * @return SS_HTTPResponse
2278
	 */
2279
	protected function environment404Response() {
2280
		$envName = Convert::raw2xml($this->getRequest()->param('Environment'));
2281
		return new SS_HTTPResponse("Environment '" . $envName . "' not found.", 404);
2282
	}
2283
2284
	/**
2285
	 * @param string $status
2286
	 * @param string $content
2287
	 *
2288
	 * @return string
2289
	 */
2290
	protected function sendResponse($status, $content) {
2291
		// strip excessive newlines
2292
		$content = preg_replace('/(?:(?:\r\n|\r|\n)\s*){2}/s', "\n", $content);
2293
2294
		$sendJSON = (strpos($this->getRequest()->getHeader('Accept'), 'application/json') !== false)
2295
			|| $this->getRequest()->getExtension() == 'json';
2296
2297
		if(!$sendJSON) {
2298
			$this->response->addHeader("Content-type", "text/plain");
2299
			return $content;
2300
		}
2301
		$this->response->addHeader("Content-type", "application/json");
2302
		return json_encode(array(
2303
			'status' => $status,
2304
			'content' => $content,
2305
		));
2306
	}
2307
2308
	/**
2309
	 * Validate the snapshot mode
2310
	 *
2311
	 * @param string $mode
2312
	 */
2313
	protected function validateSnapshotMode($mode) {
2314
		if(!in_array($mode, array('all', 'assets', 'db'))) {
2315
			throw new LogicException('Invalid mode');
2316
		}
2317
	}
2318
2319
	/**
2320
	 * @param string $sectionName
2321
	 * @param string $title
2322
	 *
2323
	 * @return SS_HTTPResponse
2324
	 */
2325
	protected function getCustomisedViewSection($sectionName, $title = '', $data = array()) {
2326
		// Performs canView permission check by limiting visible projects
2327
		$project = $this->getCurrentProject();
2328
		if(!$project) {
2329
			return $this->project404Response();
2330
		}
2331
		$data[$sectionName] = 1;
2332
2333
		if($this !== '') {
2334
			$data['Title'] = $title;
2335
		}
2336
2337
		return $this->render($data);
2338
	}
2339
2340
	/**
2341
	 * Get items for the ambient menu that should be accessible from all pages.
2342
	 *
2343
	 * @return ArrayList
2344
	 */
2345
	public function AmbientMenu() {
2346
		$list = new ArrayList();
2347
2348
		if (Member::currentUserID()) {
2349
			$list->push(new ArrayData(array(
2350
				'Classes' => 'logout',
2351
				'FaIcon' => 'sign-out',
2352
				'Link' => 'Security/logout',
2353
				'Title' => 'Log out',
2354
				'IsCurrent' => false,
2355
				'IsSection' => false
2356
			)));
2357
		}
2358
2359
		$this->extend('updateAmbientMenu', $list);
2360
		return $list;
2361
	}
2362
2363
	/**
2364
	 * Checks whether the user can create a project.
2365
	 *
2366
	 * @return bool
2367
	 */
2368
	public function canCreateProjects($member = null) {
2369
		if(!$member) $member = Member::currentUser();
2370
		if(!$member) return false;
2371
2372
		return singleton('DNProject')->canCreate($member);
2373
	}
2374
2375
}
2376
2377