Completed
Pull Request — master (#575)
by Mateusz
03:31
created

DNRoot::validateSnapshotMode()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 3
nc 2
nop 1
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 {
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(
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
		'abortDeploy',
74
		'getDataTransferForm',
75
		'transfer',
76
		'transferlog',
77
		'snapshots',
78
		'createsnapshot',
79
		'snapshotslog',
80
		'uploadsnapshot',
81
		'getCreateEnvironmentForm',
82
		'getUploadSnapshotForm',
83
		'getPostSnapshotForm',
84
		'getDataTransferRestoreForm',
85
		'getDeleteForm',
86
		'getMoveForm',
87
		'restoresnapshot',
88
		'deletesnapshot',
89
		'movesnapshot',
90
		'postsnapshotsuccess',
91
		'gitRevisions',
92
		'deploySummary',
93
		'startDeploy'
94
	);
95
96
	/**
97
	 * URL handlers pretending that we have a deep URL structure.
98
	 */
99
	private static $url_handlers = array(
100
		'project/$Project/environment/$Environment/DeployForm' => 'getDeployForm',
101
		'project/$Project/createsnapshot/DataTransferForm' => 'getDataTransferForm',
102
		'project/$Project/DataTransferForm' => 'getDataTransferForm',
103
		'project/$Project/DataTransferRestoreForm' => 'getDataTransferRestoreForm',
104
		'project/$Project/DeleteForm' => 'getDeleteForm',
105
		'project/$Project/MoveForm' => 'getMoveForm',
106
		'project/$Project/UploadSnapshotForm' => 'getUploadSnapshotForm',
107
		'project/$Project/PostSnapshotForm' => 'getPostSnapshotForm',
108
		'project/$Project/environment/$Environment/metrics' => 'metrics',
109
		'project/$Project/environment/$Environment/pipeline/$Identifier//$Action/$ID/$OtherID' => 'pipeline',
110
		'project/$Project/environment/$Environment/deploy_summary' => 'deploySummary',
111
		'project/$Project/environment/$Environment/git_revisions' => 'gitRevisions',
112
		'project/$Project/environment/$Environment/start-deploy' => 'startDeploy',
113
		'project/$Project/environment/$Environment/deploy/$Identifier/log' => 'deploylog',
114
		'project/$Project/environment/$Environment/deploy/$Identifier/abort-deploy' => 'abortDeploy',
115
		'project/$Project/environment/$Environment/deploy/$Identifier' => 'deploy',
116
		'project/$Project/transfer/$Identifier/log' => 'transferlog',
117
		'project/$Project/transfer/$Identifier' => 'transfer',
118
		'project/$Project/environment/$Environment' => 'environment',
119
		'project/$Project/createenv/$Identifier/log' => 'createenvlog',
120
		'project/$Project/createenv/$Identifier' => 'createenv',
121
		'project/$Project/CreateEnvironmentForm' => 'getCreateEnvironmentForm',
122
		'project/$Project/branch' => 'branch',
123
		'project/$Project/build/$Build' => 'build',
124
		'project/$Project/restoresnapshot/$DataArchiveID' => 'restoresnapshot',
125
		'project/$Project/deletesnapshot/$DataArchiveID' => 'deletesnapshot',
126
		'project/$Project/movesnapshot/$DataArchiveID' => 'movesnapshot',
127
		'project/$Project/update' => 'update',
128
		'project/$Project/snapshots' => 'snapshots',
129
		'project/$Project/createsnapshot' => 'createsnapshot',
130
		'project/$Project/uploadsnapshot' => 'uploadsnapshot',
131
		'project/$Project/snapshotslog' => 'snapshotslog',
132
		'project/$Project/postsnapshotsuccess/$DataArchiveID' => 'postsnapshotsuccess',
133
		'project/$Project/star' => 'toggleprojectstar',
134
		'project/$Project' => 'project',
135
		'nav/$Project' => 'nav',
136
		'projects' => 'projects',
137
	);
138
139
	/**
140
	 * @var array
141
	 */
142
	protected static $_project_cache = array();
143
144
	/**
145
	 * @var array
146
	 */
147
	private static $support_links = array();
148
149
	/**
150
	 * @var array
151
	 */
152
	private static $platform_specific_strings = array();
153
154
	/**
155
	 * @var array
156
	 */
157
	private static $action_types = array(
158
		self::ACTION_DEPLOY,
159
		self::ACTION_SNAPSHOT,
160
		self::PROJECT_OVERVIEW
161
	);
162
163
	/**
164
	 * @var DNData
165
	 */
166
	protected $data;
167
168
	/**
169
	 * Include requirements that deploynaut needs, such as javascript.
170
	 */
171
	public static function include_requirements() {
172
173
		// JS should always go to the bottom, otherwise there's the risk that Requirements
174
		// puts them halfway through the page to the nearest <script> tag. We don't want that.
175
		Requirements::set_force_js_to_bottom(true);
176
177
		// todo these should be bundled into the same JS as the others in "static" below.
178
		// We've deliberately not used combined_files as it can mess with some of the JS used
179
		// here and cause sporadic errors.
180
		Requirements::javascript('deploynaut/javascript/jquery.js');
181
		Requirements::javascript('deploynaut/javascript/bootstrap.js');
182
		Requirements::javascript('deploynaut/javascript/q.js');
183
		Requirements::javascript('deploynaut/javascript/tablefilter.js');
184
		Requirements::javascript('deploynaut/javascript/deploynaut.js');
185
		Requirements::javascript('deploynaut/javascript/react-with-addons.js');
186
		Requirements::javascript('deploynaut/javascript/bootstrap.file-input.js');
187
		Requirements::javascript('deploynaut/thirdparty/select2/dist/js/select2.min.js');
188
		Requirements::javascript('deploynaut/thirdparty/bootstrap-switch/dist/js/bootstrap-switch.min.js');
189
		Requirements::javascript('deploynaut/javascript/material.js');
190
191
		// Load the buildable dependencies only if not loaded centrally.
192
		if (!is_dir(BASE_PATH . DIRECTORY_SEPARATOR . 'static')) {
193
			if (\Director::isDev()) {
194
				\Requirements::javascript('deploynaut/static/bundle-debug.js');
195
			} else {
196
				\Requirements::javascript('deploynaut/static/bundle.js');
197
			}
198
		}
199
200
		Requirements::css('deploynaut/static/style.css');
201
	}
202
203
	/**
204
	 * Check for feature flags:
205
	 * - FLAG_SNAPSHOTS_ENABLED: set to true to enable globally
206
	 * - FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS: set to semicolon-separated list of email addresses of allowed users.
207
	 *
208
	 * @return boolean
209
	 */
210
	public static function FlagSnapshotsEnabled() {
211
		if(defined('FLAG_SNAPSHOTS_ENABLED') && FLAG_SNAPSHOTS_ENABLED) {
212
			return true;
213
		}
214
		if(defined('FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS') && FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS) {
215
			$allowedMembers = explode(';', FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS);
216
			$member = Member::currentUser();
217
			if($allowedMembers && $member && in_array($member->Email, $allowedMembers)) {
218
				return true;
219
			}
220
		}
221
		return false;
222
	}
223
224
	/**
225
	 * @return ArrayList
226
	 */
227
	public static function get_support_links() {
228
		$supportLinks = self::config()->support_links;
229
		if($supportLinks) {
230
			return new ArrayList($supportLinks);
231
		}
232
	}
233
234
	/**
235
	 * @return array
236
	 */
237
	public static function get_template_global_variables() {
238
		return array(
239
			'RedisUnavailable' => 'RedisUnavailable',
240
			'RedisWorkersCount' => 'RedisWorkersCount',
241
			'SidebarLinks' => 'SidebarLinks',
242
			"SupportLinks" => 'get_support_links'
243
		);
244
	}
245
246
	/**
247
	 */
248
	public function init() {
249
		parent::init();
250
251
		if(!Member::currentUser() && !Session::get('AutoLoginHash')) {
252
			return Security::permissionFailure();
253
		}
254
255
		// Block framework jquery
256
		Requirements::block(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
257
258
		self::include_requirements();
259
	}
260
261
	/**
262
	 * @return string
263
	 */
264
	public function Link() {
265
		return "naut/";
266
	}
267
268
	/**
269
	 * Actions
270
	 *
271
	 * @param SS_HTTPRequest $request
272
	 * @return \SS_HTTPResponse
273
	 */
274
	public function index(SS_HTTPRequest $request) {
275
		return $this->redirect($this->Link() . 'projects/');
276
	}
277
278
	/**
279
	 * Action
280
	 *
281
	 * @param SS_HTTPRequest $request
282
	 * @return string - HTML
283
	 */
284
	public function projects(SS_HTTPRequest $request) {
285
		// Performs canView permission check by limiting visible projects in DNProjectsList() call.
286
		return $this->customise(array(
287
			'Title' => 'Projects',
288
		))->render();
289
	}
290
291
	/**
292
	 * @param SS_HTTPRequest $request
293
	 * @return HTMLText
294
	 */
295
	public function nav(SS_HTTPRequest $request) {
296
		return $this->renderWith('Nav');
297
	}
298
299
	/**
300
	 * Return a link to the navigation template used for AJAX requests.
301
	 * @return string
302
	 */
303
	public function NavLink() {
304
		$currentProject = $this->getCurrentProject();
305
		$projectName = $currentProject ? $currentProject->Name : null;
306
		return Controller::join_links(Director::absoluteBaseURL(), 'naut', 'nav', $projectName);
307
	}
308
309
	/**
310
	 * Action
311
	 *
312
	 * @param SS_HTTPRequest $request
313
	 * @return SS_HTTPResponse - HTML
314
	 */
315
	public function snapshots(SS_HTTPRequest $request) {
316
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
317
		return $this->getCustomisedViewSection('SnapshotsSection', 'Data Snapshots');
318
	}
319
320
	/**
321
	 * Action
322
	 *
323
	 * @param SS_HTTPRequest $request
324
	 * @return string - HTML
325
	 */
326 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...
327
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
328
329
		// Performs canView permission check by limiting visible projects
330
		$project = $this->getCurrentProject();
331
		if(!$project) {
332
			return $this->project404Response();
333
		}
334
335
		if(!$project->canBackup()) {
336
			return new SS_HTTPResponse("Not allowed to create snapshots on any environments", 401);
337
		}
338
339
		return $this->customise(array(
340
			'Title' => 'Create Data Snapshot',
341
			'SnapshotsSection' => 1,
342
			'DataTransferForm' => $this->getDataTransferForm($request)
343
		))->render();
344
	}
345
346
	/**
347
	 * Action
348
	 *
349
	 * @param SS_HTTPRequest $request
350
	 * @return string - HTML
351
	 */
352 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...
353
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
354
355
		// Performs canView permission check by limiting visible projects
356
		$project = $this->getCurrentProject();
357
		if(!$project) {
358
			return $this->project404Response();
359
		}
360
361
		if(!$project->canUploadArchive()) {
362
			return new SS_HTTPResponse("Not allowed to upload", 401);
363
		}
364
365
		return $this->customise(array(
366
			'SnapshotsSection' => 1,
367
			'UploadSnapshotForm' => $this->getUploadSnapshotForm($request),
368
			'PostSnapshotForm' => $this->getPostSnapshotForm($request)
369
		))->render();
370
	}
371
372
	/**
373
	 * Return the upload limit for snapshot uploads
374
	 * @return string
375
	 */
376
	public function UploadLimit() {
377
		return File::format_size(min(
378
			File::ini2bytes(ini_get('upload_max_filesize')),
379
			File::ini2bytes(ini_get('post_max_size'))
380
		));
381
	}
382
383
	/**
384
	 * Construct the upload form.
385
	 *
386
	 * @param SS_HTTPRequest $request
387
	 * @return Form
388
	 */
389
	public function getUploadSnapshotForm(SS_HTTPRequest $request) {
390
		// Performs canView permission check by limiting visible projects
391
		$project = $this->getCurrentProject();
392
		if(!$project) {
393
			return $this->project404Response();
394
		}
395
396
		if(!$project->canUploadArchive()) {
397
			return new SS_HTTPResponse("Not allowed to upload", 401);
398
		}
399
400
		// Framing an environment as a "group of people with download access"
401
		// makes more sense to the user here, while still allowing us to enforce
402
		// environment specific restrictions on downloading the file later on.
403
		$envs = $project->DNEnvironmentList()->filterByCallback(function($item) {
404
			return $item->canUploadArchive();
405
		});
406
		$envsMap = array();
407
		foreach($envs as $env) {
408
			$envsMap[$env->ID] = $env->Name;
409
		}
410
411
		$maxSize = min(File::ini2bytes(ini_get('upload_max_filesize')), File::ini2bytes(ini_get('post_max_size')));
412
		$fileField = DataArchiveFileField::create('ArchiveFile', 'File');
413
		$fileField->getValidator()->setAllowedExtensions(array('sspak'));
414
		$fileField->getValidator()->setAllowedMaxFileSize(array('*' => $maxSize));
415
416
		$form = Form::create(
417
			$this,
418
			'UploadSnapshotForm',
419
			FieldList::create(
420
				$fileField,
421
				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
422
				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
423
					->setEmptyString('Select an environment')
424
			),
425
			FieldList::create(
426
				FormAction::create('doUploadSnapshot', 'Upload File')
427
					->addExtraClass('btn')
428
			),
429
			RequiredFields::create('ArchiveFile')
430
		);
431
432
		$form->disableSecurityToken();
433
		$form->addExtraClass('fields-wide');
434
		// Tweak the action so it plays well with our fake URL structure.
435
		$form->setFormAction($project->Link() . '/UploadSnapshotForm');
436
437
		return $form;
438
	}
439
440
	/**
441
	 * @param array $data
442
	 * @param Form $form
443
	 *
444
	 * @return bool|HTMLText|SS_HTTPResponse
445
	 */
446
	public function doUploadSnapshot($data, Form $form) {
447
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
448
449
		// Performs canView permission check by limiting visible projects
450
		$project = $this->getCurrentProject();
451
		if(!$project) {
452
			return $this->project404Response();
453
		}
454
455
		$validEnvs = $project->DNEnvironmentList()
456
			->filterByCallback(function($item) {
457
				return $item->canUploadArchive();
458
			});
459
460
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
461
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
462
		if(!$environment) {
463
			throw new LogicException('Invalid environment');
464
		}
465
466
		$this->validateSnapshotMode($data['Mode']);
467
468
		$dataArchive = DNDataArchive::create(array(
469
			'AuthorID' => Member::currentUserID(),
470
			'EnvironmentID' => $data['EnvironmentID'],
471
			'IsManualUpload' => true,
472
		));
473
		// needs an ID and transfer to determine upload path
474
		$dataArchive->write();
475
		$dataTransfer = DNDataTransfer::create(array(
476
			'AuthorID' => Member::currentUserID(),
477
			'Mode' => $data['Mode'],
478
			'Origin' => 'ManualUpload',
479
			'EnvironmentID' => $data['EnvironmentID']
480
		));
481
		$dataTransfer->write();
482
		$dataArchive->DataTransfers()->add($dataTransfer);
483
		$form->saveInto($dataArchive);
484
		$dataArchive->write();
485
		$workingDir = TEMP_FOLDER . DIRECTORY_SEPARATOR . 'deploynaut-transfer-' . $dataTransfer->ID;
486
487
		$cleanupFn = function() use($workingDir, $dataTransfer, $dataArchive) {
488
			$process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir)));
489
			$process->run();
490
			$dataTransfer->delete();
491
			$dataArchive->delete();
492
		};
493
494
		// extract the sspak contents so we can inspect them
495
		try {
496
			$dataArchive->extractArchive($workingDir);
497
		} catch(Exception $e) {
498
			$cleanupFn();
499
			$form->sessionMessage(
500
				'There was a problem trying to open your snapshot for processing. Please try uploading again',
501
				'bad'
502
			);
503
			return $this->redirectBack();
504
		}
505
506
		// validate that the sspak contents match the declared contents
507
		$result = $dataArchive->validateArchiveContents();
508
		if(!$result->valid()) {
509
			$cleanupFn();
510
			$form->sessionMessage($result->message(), 'bad');
511
			return $this->redirectBack();
512
		}
513
514
		// fix file permissions of extracted sspak files then re-build the sspak
515
		try {
516
			$dataArchive->fixArchivePermissions($workingDir);
517
			$dataArchive->setArchiveFromFiles($workingDir);
518
		} catch(Exception $e) {
519
			$cleanupFn();
520
			$form->sessionMessage(
521
				'There was a problem processing your snapshot. Please try uploading again',
522
				'bad'
523
			);
524
			return $this->redirectBack();
525
		}
526
527
		// cleanup any extracted sspak contents lying around
528
		$process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir)));
529
		$process->run();
530
531
		return $this->customise(array(
532
			'Project' => $project,
533
			'CurrentProject' => $project,
534
			'SnapshotsSection' => 1,
535
			'DataArchive' => $dataArchive,
536
			'DataTransferRestoreForm' => $this->getDataTransferRestoreForm($this->request, $dataArchive),
537
			'BackURL' => $project->Link('snapshots')
538
		))->renderWith(array('DNRoot_uploadsnapshot', 'DNRoot'));
539
	}
540
541
	/**
542
	 * @param SS_HTTPRequest $request
543
	 * @return Form
544
	 */
545
	public function getPostSnapshotForm(SS_HTTPRequest $request) {
546
		// Performs canView permission check by limiting visible projects
547
		$project = $this->getCurrentProject();
548
		if(!$project) {
549
			return $this->project404Response();
550
		}
551
552
		if(!$project->canUploadArchive()) {
553
			return new SS_HTTPResponse("Not allowed to upload", 401);
554
		}
555
556
		// Framing an environment as a "group of people with download access"
557
		// makes more sense to the user here, while still allowing us to enforce
558
		// environment specific restrictions on downloading the file later on.
559
		$envs = $project->DNEnvironmentList()->filterByCallback(function($item) {
560
			return $item->canUploadArchive();
561
		});
562
		$envsMap = array();
563
		foreach($envs as $env) {
564
			$envsMap[$env->ID] = $env->Name;
565
		}
566
567
		$form = Form::create(
568
			$this,
569
			'PostSnapshotForm',
570
			FieldList::create(
571
				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
572
				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
573
					->setEmptyString('Select an environment')
574
			),
575
			FieldList::create(
576
				FormAction::create('doPostSnapshot', 'Submit request')
577
					->addExtraClass('btn')
578
			),
579
			RequiredFields::create('File')
580
		);
581
582
		$form->disableSecurityToken();
583
		$form->addExtraClass('fields-wide');
584
		// Tweak the action so it plays well with our fake URL structure.
585
		$form->setFormAction($project->Link() . '/PostSnapshotForm');
586
587
		return $form;
588
	}
589
590
	/**
591
	 * @param array $data
592
	 * @param Form $form
593
	 *
594
	 * @return SS_HTTPResponse
595
	 */
596
	public function doPostSnapshot($data, $form) {
597
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
598
599
		$project = $this->getCurrentProject();
600
		if(!$project) {
601
			return $this->project404Response();
602
		}
603
604
		$validEnvs = $project->DNEnvironmentList()->filterByCallback(function($item) {
605
				return $item->canUploadArchive();
606
		});
607
608
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
609
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
610
		if(!$environment) {
611
			throw new LogicException('Invalid environment');
612
		}
613
614
		$dataArchive = DNDataArchive::create(array(
615
			'UploadToken' => DNDataArchive::generate_upload_token(),
616
		));
617
		$form->saveInto($dataArchive);
618
		$dataArchive->write();
619
620
		return $this->redirect(Controller::join_links(
621
			$project->Link(),
622
			'postsnapshotsuccess',
623
			$dataArchive->ID
624
		));
625
	}
626
627
	/**
628
	 * Action
629
	 *
630
	 * @param SS_HTTPRequest $request
631
	 * @return SS_HTTPResponse - HTML
632
	 */
633
	public function snapshotslog(SS_HTTPRequest $request) {
634
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
635
		return $this->getCustomisedViewSection('SnapshotsSection', 'Snapshots log');
636
	}
637
638
	/**
639
	 * @param SS_HTTPRequest $request
640
	 * @return SS_HTTPResponse|string
641
	 * @throws SS_HTTPResponse_Exception
642
	 */
643
	public function postsnapshotsuccess(SS_HTTPRequest $request) {
644
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
645
646
		// Performs canView permission check by limiting visible projects
647
		$project = $this->getCurrentProject();
648
		if(!$project) {
649
			return $this->project404Response();
650
		}
651
652
		if(!$project->canUploadArchive()) {
653
			return new SS_HTTPResponse("Not allowed to upload", 401);
654
		}
655
656
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
657
		if(!$dataArchive) {
658
			return new SS_HTTPResponse("Archive not found.", 404);
659
		}
660
661
		if(!$dataArchive->canRestore()) {
662
			throw new SS_HTTPResponse_Exception('Not allowed to restore archive', 403);
663
		}
664
665
		return $this->render(array(
666
				'Title' => 'How to send us your Data Snapshot by post',
667
				'DataArchive' => $dataArchive,
668
				'Address' => Config::inst()->get('Deploynaut', 'snapshot_post_address'),
669
				'BackURL' => $project->Link(),
670
			));
671
	}
672
673
	/**
674
	 * @param SS_HTTPRequest $request
675
	 * @return \SS_HTTPResponse
676
	 */
677
	public function project(SS_HTTPRequest $request) {
678
		$this->setCurrentActionType(self::PROJECT_OVERVIEW);
679
		return $this->getCustomisedViewSection('ProjectOverview', '', array('IsAdmin' => Permission::check('ADMIN')));
680
	}
681
682
	/**
683
	 * This action will star / unstar a project for the current member
684
	 *
685
	 * @param SS_HTTPRequest $request
686
	 *
687
	 * @return SS_HTTPResponse
688
	 */
689
	public function toggleprojectstar(SS_HTTPRequest $request) {
690
		$project = $this->getCurrentProject();
691
		if(!$project) {
692
			return $this->project404Response();
693
		}
694
695
		$member = Member::currentUser();
696
		if($member === null) {
697
			return $this->project404Response();
698
		}
699
		$favProject = $member->StarredProjects()
700
			->filter('DNProjectID', $project->ID)
701
			->first();
702
703
		if($favProject) {
704
			$member->StarredProjects()->remove($favProject);
705
		} else {
706
			$member->StarredProjects()->add($project);
707
		}
708
		return $this->redirectBack();
709
	}
710
711
	/**
712
	 * @param SS_HTTPRequest $request
713
	 * @return \SS_HTTPResponse
714
	 */
715
	public function branch(SS_HTTPRequest $request) {
716
		$project = $this->getCurrentProject();
717
		if(!$project) {
718
			return $this->project404Response();
719
		}
720
721
		$branchName = $request->getVar('name');
722
		$branch = $project->DNBranchList()->byName($branchName);
723
		if(!$branch) {
724
			return new SS_HTTPResponse("Branch '" . Convert::raw2xml($branchName) . "' not found.", 404);
725
		}
726
727
		return $this->render(array(
728
			'CurrentBranch' => $branch,
729
		));
730
	}
731
732
	/**
733
	 * @param SS_HTTPRequest $request
734
	 * @return \SS_HTTPResponse
735
	 */
736
	public function environment(SS_HTTPRequest $request) {
737
		// Performs canView permission check by limiting visible projects
738
		$project = $this->getCurrentProject();
739
		if(!$project) {
740
			return $this->project404Response();
741
		}
742
743
		// Performs canView permission check by limiting visible projects
744
		$env = $this->getCurrentEnvironment($project);
745
		if(!$env) {
746
			return $this->environment404Response();
747
		}
748
749
		return $this->render(array(
750
			'DNEnvironmentList' => $this->getCurrentProject()->DNEnvironmentList(),
751
			'FlagSnapshotsEnabled' => $this->FlagSnapshotsEnabled(),
752
			'Redeploy' => (bool)$request->getVar('redeploy')
753
		));
754
	}
755
756
757
	/**
758
	 * Initiate a pipeline dry run
759
	 *
760
	 * @param array $data
761
	 * @param DeployForm $form
762
	 *
763
	 * @return SS_HTTPResponse
764
	 */
765
	public function doDryRun($data, DeployForm $form) {
766
		return $this->beginPipeline($data, $form, true);
767
	}
768
769
	/**
770
	 * Initiate a pipeline
771
	 *
772
	 * @param array $data
773
	 * @param DeployForm $form
774
	 * @return \SS_HTTPResponse
775
	 */
776
	public function startPipeline($data, $form) {
777
		return $this->beginPipeline($data, $form);
778
	}
779
780
	/**
781
	 * Start a pipeline
782
	 *
783
	 * @param array $data
784
	 * @param DeployForm $form
785
	 * @param bool $isDryRun
786
	 * @return \SS_HTTPResponse
787
	 */
788
	protected function beginPipeline($data, DeployForm $form, $isDryRun = false) {
789
		$buildName = $form->getSelectedBuild($data);
790
791
		// Performs canView permission check by limiting visible projects
792
		$project = $this->getCurrentProject();
793
		if(!$project) {
794
			return $this->project404Response();
795
		}
796
797
		// Performs canView permission check by limiting visible projects
798
		$environment = $this->getCurrentEnvironment($project);
799
		if(!$environment) {
800
			return $this->environment404Response();
801
		}
802
803
		if(!$environment->DryRunEnabled && $isDryRun) {
804
			return new SS_HTTPResponse("Dry-run for pipelines is not enabled for this environment", 404);
805
		}
806
807
		// Initiate the pipeline
808
		$sha = $project->DNBuildList()->byName($buildName);
809
		$pipeline = Pipeline::create();
810
		$pipeline->DryRun = $isDryRun;
811
		$pipeline->EnvironmentID = $environment->ID;
812
		$pipeline->AuthorID = Member::currentUserID();
813
		$pipeline->SHA = $sha->FullName();
814
		// Record build at time of execution
815
		if($currentBuild = $environment->CurrentBuild()) {
816
			$pipeline->PreviousDeploymentID = $currentBuild->ID;
817
		}
818
		$pipeline->start(); // start() will call write(), so no need to do it here as well.
819
		return $this->redirect($environment->Link());
820
	}
821
822
	/**
823
	 * @param SS_HTTPRequest $request
824
	 *
825
	 * @return SS_HTTPResponse
826
	 * @throws SS_HTTPResponse_Exception
827
	 */
828 View Code Duplication
	public function pipeline(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...
829
		$params = $request->params();
830
		$pipeline = Pipeline::get()->byID($params['Identifier']);
831
832
		if(!$pipeline || !$pipeline->ID || !$pipeline->Environment()) {
833
			throw new SS_HTTPResponse_Exception('Pipeline not found', 404);
834
		}
835
		if(!$pipeline->Environment()->canView()) {
836
			return Security::permissionFailure();
837
		}
838
839
		$environment = $pipeline->Environment();
840
		$project = $pipeline->Environment()->Project();
841
842
		if($environment->Name != $params['Environment']) {
843
			throw new LogicException("Environment in URL doesn't match this pipeline");
844
		}
845
		if($project->Name != $params['Project']) {
846
			throw new LogicException("Project in URL doesn't match this pipeline");
847
		}
848
849
		// Delegate to sub-requesthandler
850
		return PipelineController::create($this, $pipeline);
851
	}
852
853
	/**
854
	 * Shows the creation log.
855
	 *
856
	 * @param SS_HTTPRequest $request
857
	 * @return string
858
	 */
859
	public function createenv(SS_HTTPRequest $request) {
860
		$params = $request->params();
861
		if($params['Identifier']) {
862
			$record = DNCreateEnvironment::get()->byId($params['Identifier']);
863
864
			if(!$record || !$record->ID) {
865
				throw new SS_HTTPResponse_Exception('Create environment not found', 404);
866
			}
867
			if(!$record->canView()) {
868
				return Security::permissionFailure();
869
			}
870
871
			$project = $this->getCurrentProject();
872
			if(!$project) {
873
				return $this->project404Response();
874
			}
875
876
			if($project->Name != $params['Project']) {
877
				throw new LogicException("Project in URL doesn't match this creation");
878
			}
879
880
			return $this->render(array(
881
				'CreateEnvironment' => $record,
882
			));
883
		}
884
		return $this->render(array('CurrentTitle' => 'Create an environment'));
885
	}
886
887
888
	public function createenvlog(SS_HTTPRequest $request) {
889
		$params = $request->params();
890
		$env = DNCreateEnvironment::get()->byId($params['Identifier']);
891
892
		if(!$env || !$env->ID) {
893
			throw new SS_HTTPResponse_Exception('Log not found', 404);
894
		}
895
		if(!$env->canView()) {
896
			return Security::permissionFailure();
897
		}
898
899
		$project = $env->Project();
900
901
		if($project->Name != $params['Project']) {
902
			throw new LogicException("Project in URL doesn't match this deploy");
903
		}
904
905
		$log = $env->log();
906
		if($log->exists()) {
907
			$content = $log->content();
908
		} else {
909
			$content = 'Waiting for action to start';
910
		}
911
912
		return $this->sendResponse($env->ResqueStatus(), $content);
913
	}
914
915
	/**
916
	 * @param SS_HTTPRequest $request
917
	 * @return Form
918
	 */
919
	public function getCreateEnvironmentForm(SS_HTTPRequest $request) {
920
		$this->setCurrentActionType(self::ACTION_ENVIRONMENTS);
921
922
		$project = $this->getCurrentProject();
923
		if(!$project) {
924
			return $this->project404Response();
925
		}
926
927
		$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...
928
		if(!$envType || !class_exists($envType)) {
929
			return null;
930
		}
931
932
		$backend = Injector::inst()->get($envType);
933
		if(!($backend instanceof EnvironmentCreateBackend)) {
934
			// Only allow this for supported backends.
935
			return null;
936
		}
937
938
		$fields = $backend->getCreateEnvironmentFields($project);
939
		if(!$fields) return null;
940
941
		if(!$project->canCreateEnvironments()) {
942
			return new SS_HTTPResponse('Not allowed to create environments for this project', 401);
943
		}
944
945
		$form = Form::create(
946
			$this,
947
			'CreateEnvironmentForm',
948
			$fields,
949
			FieldList::create(
950
				FormAction::create('doCreateEnvironment', 'Create')
951
					->addExtraClass('btn')
952
			),
953
			$backend->getCreateEnvironmentValidator()
954
		);
955
956
		// Tweak the action so it plays well with our fake URL structure.
957
		$form->setFormAction($project->Link() . '/CreateEnvironmentForm');
958
959
		return $form;
960
	}
961
962
	/**
963
	 * @param array $data
964
	 * @param Form $form
965
	 *
966
	 * @return bool|HTMLText|SS_HTTPResponse
967
	 */
968
	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...
969
		$this->setCurrentActionType(self::ACTION_ENVIRONMENTS);
970
971
		$project = $this->getCurrentProject();
972
		if(!$project) {
973
			return $this->project404Response();
974
		}
975
976
		if(!$project->canCreateEnvironments()) {
977
			return new SS_HTTPResponse('Not allowed to create environments for this project', 401);
978
		}
979
980
		// Set the environment type so we know what we're creating.
981
		$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...
982
983
		$job = DNCreateEnvironment::create();
984
985
		$job->Data = serialize($data);
986
		$job->ProjectID = $project->ID;
987
		$job->write();
988
		$job->start();
989
990
		return $this->redirect($project->Link('createenv') . '/' . $job->ID);
991
	}
992
993
	/**
994
	 *
995
	 * @param SS_HTTPRequest $request
996
	 * @return \SS_HTTPResponse
997
	 */
998
	public function metrics(SS_HTTPRequest $request) {
999
		// Performs canView permission check by limiting visible projects
1000
		$project = $this->getCurrentProject();
1001
		if(!$project) {
1002
			return $this->project404Response();
1003
		}
1004
1005
		// Performs canView permission check by limiting visible projects
1006
		$env = $this->getCurrentEnvironment($project);
1007
		if(!$env) {
1008
			return $this->environment404Response();
1009
		}
1010
1011
		return $this->render();
1012
	}
1013
1014
	/**
1015
	 * Get the DNData object.
1016
	 *
1017
	 * @return DNData
1018
	 */
1019
	public function DNData() {
1020
		return DNData::inst();
1021
	}
1022
1023
	/**
1024
	 * Provide a list of all projects.
1025
	 *
1026
	 * @return SS_List
1027
	 */
1028
	public function DNProjectList() {
1029
		$memberId = Member::currentUserID();
1030
		if(!$memberId) {
1031
			return new ArrayList();
1032
		}
1033
1034
		if(Permission::check('ADMIN')) {
1035
			return DNProject::get();
1036
		}
1037
1038
		$projects = Member::get()->filter('ID', $memberId)
1039
			->relation('Groups')
1040
			->relation('Projects');
1041
1042
		$this->extend('updateDNProjectList', $projects);
1043
		return $projects;
1044
	}
1045
1046
	/**
1047
	 * @return ArrayList
1048
	 */
1049
	public function getPlatformSpecificStrings() {
1050
		$strings = $this->config()->platform_specific_strings;
1051
		if ($strings) {
1052
			return new ArrayList($strings);
1053
		}
1054
	}
1055
1056
	/**
1057
	 * Provide a list of all starred projects for the currently logged in member
1058
	 *
1059
	 * @return SS_List
1060
	 */
1061
	public function getStarredProjects() {
1062
		$member = Member::currentUser();
1063
		if($member === null) {
1064
			return new ArrayList();
1065
		}
1066
1067
		$favProjects = $member->StarredProjects();
1068
1069
		$list = new ArrayList();
1070
		foreach($favProjects as $project) {
1071
			if($project->canView($member)) {
1072
				$list->add($project);
1073
			}
1074
		}
1075
		return $list;
1076
	}
1077
1078
	/**
1079
	 * Returns top level navigation of projects.
1080
	 *
1081
	 * @param int $limit
1082
	 *
1083
	 * @return ArrayList
1084
	 */
1085
	public function Navigation($limit = 5) {
1086
		$navigation = new ArrayList();
1087
1088
		$currentProject = $this->getCurrentProject();
1089
		$currentEnvironment = $this->getCurrentEnvironment();
1090
		$actionType = $this->getCurrentActionType();
1091
1092
		$projects = $this->getStarredProjects();
1093
		if($projects->count() < 1) {
1094
			$projects = $this->DNProjectList();
1095
		} else {
1096
			$limit = -1;
1097
		}
1098
1099
		if($projects->count() > 0) {
1100
			$activeProject = false;
1101
1102
			if($limit > 0) {
1103
				$limitedProjects = $projects->limit($limit);
1104
			} else {
1105
				$limitedProjects = $projects;
1106
			}
1107
1108
			foreach($limitedProjects as $project) {
1109
				$isActive = $currentProject && $currentProject->ID == $project->ID;
1110
				if($isActive) {
1111
					$activeProject = true;
1112
				}
1113
1114
				$isCurrentEnvironment = false;
1115
				if($project && $currentEnvironment) {
1116
					$isCurrentEnvironment = (bool) $project->DNEnvironmentList()->find('ID', $currentEnvironment->ID);
1117
				}
1118
1119
				$navigation->push(array(
1120
					'Project' => $project,
1121
					'IsCurrentEnvironment' => $isCurrentEnvironment,
1122
					'IsActive' => $currentProject && $currentProject->ID == $project->ID,
1123
					'IsOverview' => $actionType == self::PROJECT_OVERVIEW && $currentProject->ID == $project->ID
1124
				));
1125
			}
1126
1127
			// Ensure the current project is in the list
1128
			if(!$activeProject && $currentProject) {
1129
				$navigation->unshift(array(
1130
					'Project' => $currentProject,
1131
					'IsActive' => true,
1132
					'IsCurrentEnvironment' => $currentEnvironment,
1133
					'IsOverview' => $actionType == self::PROJECT_OVERVIEW
1134
				));
1135
				if($limit > 0 && $navigation->count() > $limit) {
1136
					$navigation->pop();
1137
				}
1138
			}
1139
		}
1140
1141
		return $navigation;
1142
	}
1143
1144
	/**
1145
	 * Construct the deployment form
1146
	 *
1147
	 * @return Form
1148
	 */
1149
	public function getDeployForm($request = null) {
1150
1151
		// Performs canView permission check by limiting visible projects
1152
		$project = $this->getCurrentProject();
1153
		if(!$project) {
1154
			return $this->project404Response();
1155
		}
1156
1157
		// Performs canView permission check by limiting visible projects
1158
		$environment = $this->getCurrentEnvironment($project);
1159
		if(!$environment) {
1160
			return $this->environment404Response();
1161
		}
1162
1163
		if(!$environment->canDeploy()) {
1164
			return new SS_HTTPResponse("Not allowed to deploy", 401);
1165
		}
1166
1167
		// Generate the form
1168
		$form = new DeployForm($this, 'DeployForm', $environment, $project);
1169
1170
		// If this is an ajax request we don't want to submit the form - we just want to retrieve the markup.
1171
		if(
1172
			$request &&
1173
			!$request->requestVar('action_showDeploySummary') &&
1174
			$this->getRequest()->isAjax() &&
1175
			$this->getRequest()->isGET()
1176
		) {
1177
			// We can just use the URL we're accessing
1178
			$form->setFormAction($this->getRequest()->getURL());
1179
1180
			$body = json_encode(array('Content' => $form->forAjaxTemplate()->forTemplate()));
1181
			$this->getResponse()->addHeader('Content-Type', 'application/json');
1182
			$this->getResponse()->setBody($body);
1183
			return $body;
1184
		}
1185
1186
		$form->setFormAction($this->getRequest()->getURL() . '/DeployForm');
1187
		return $form;
1188
	}
1189
1190
	/**
1191
	 * @param SS_HTTPRequest $request
1192
	 *
1193
	 * @return SS_HTTPResponse|string
1194
	 */
1195
	public function gitRevisions(SS_HTTPRequest $request) {
1196
1197
		// Performs canView permission check by limiting visible projects
1198
		$project = $this->getCurrentProject();
1199
		if(!$project) {
1200
			return $this->project404Response();
1201
		}
1202
1203
		// Performs canView permission check by limiting visible projects
1204
		$env = $this->getCurrentEnvironment($project);
1205
		if(!$env) {
1206
			return $this->environment404Response();
1207
		}
1208
1209
		// For now only permit advanced options on one environment type, because we hacked the "full-deploy"
1210
		// checkbox in. Other environments such as the fast or capistrano one wouldn't know what to do with it.
1211
		if(get_class($env) === 'RainforestEnvironment') {
1212
			$advanced = Permission::check('DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS') ? 'true' : 'false';
1213
		} else {
1214
			$advanced = 'false';
1215
		}
1216
1217
		$tabs = array();
1218
		$id = 0;
1219
		$data = array(
1220
			'id' => ++$id,
1221
			'name' => 'Deploy the latest version of a branch',
1222
			'field_type' => 'dropdown',
1223
			'field_label' => 'Choose a branch',
1224
			'field_id' => 'branch',
1225
			'field_data' => array(),
1226
			'advanced_opts' => $advanced
1227
		);
1228
		foreach($project->DNBranchList() as $branch) {
1229
			$sha = $branch->SHA();
1230
			$name = $branch->Name();
1231
			$branchValue = sprintf("%s (%s, %s old)",
1232
				$name,
1233
				substr($sha, 0, 8),
1234
				$branch->LastUpdated()->TimeDiff()
1235
			);
1236
			$data['field_data'][] = array(
1237
				'id' => $sha,
1238
				'text' => $branchValue,
1239
				'branch_name' => $name // the raw branch name, not including the time etc
1240
			);
1241
		}
1242
		$tabs[] = $data;
1243
1244
		$data = array(
1245
			'id' => ++$id,
1246
			'name' => 'Deploy a tagged release',
1247
			'field_type' => 'dropdown',
1248
			'field_label' => 'Choose a tag',
1249
			'field_id' => 'tag',
1250
			'field_data' => array(),
1251
			'advanced_opts' => $advanced
1252
		);
1253
1254
		foreach($project->DNTagList()->setLimit(null) as $tag) {
1255
			$name = $tag->Name();
1256
			$data['field_data'][] = array(
1257
				'id' => $tag->SHA(),
1258
				'text' => sprintf("%s", $name)
1259
			);
1260
		}
1261
1262
		// show newest tags first.
1263
		$data['field_data'] = array_reverse($data['field_data']);
1264
1265
		$tabs[] = $data;
1266
1267
		// Past deployments
1268
		$data = array(
1269
			'id' => ++$id,
1270
			'name' => 'Redeploy a release that was previously deployed (to any environment)',
1271
			'field_type' => 'dropdown',
1272
			'field_label' => 'Choose a previously deployed release',
1273
			'field_id' => 'release',
1274
			'field_data' => array(),
1275
			'advanced_opts' => $advanced
1276
		);
1277
		// We are aiming at the format:
1278
		// [{text: 'optgroup text', children: [{id: '<sha>', text: '<inner text>'}]}]
0 ignored issues
show
Unused Code Comprehensibility introduced by
55% 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...
1279
		$redeploy = array();
1280
		foreach($project->DNEnvironmentList() as $dnEnvironment) {
1281
			$envName = $dnEnvironment->Name;
1282
			$perEnvDeploys = array();
1283
1284
			foreach($dnEnvironment->DeployHistory() as $deploy) {
1285
				$sha = $deploy->SHA;
1286
1287
				// Check if exists to make sure the newest deployment date is used.
1288
				if(!isset($perEnvDeploys[$sha])) {
1289
					$pastValue = sprintf("%s (deployed %s)",
1290
						substr($sha, 0, 8),
1291
						$deploy->obj('LastEdited')->Ago()
1292
					);
1293
					$perEnvDeploys[$sha] = array(
1294
						'id' => $sha,
1295
						'text' => $pastValue
1296
					);
1297
				}
1298
			}
1299
1300
			if(!empty($perEnvDeploys)) {
1301
				$redeploy[$envName] = array_values($perEnvDeploys);
1302
			}
1303
		}
1304
		// Convert the array to the frontend format (i.e. keyed to regular array)
1305
		foreach($redeploy as $env => $descr) {
1306
			$data['field_data'][] = array('text'=>$env, 'children'=>$descr);
1307
		}
1308
		$tabs[] = $data;
1309
1310
		$data = array(
1311
			'id' => ++$id,
1312
			'name' => 'Deploy a specific SHA',
1313
			'field_type' => 'textfield',
1314
			'field_label' => 'Choose a SHA',
1315
			'field_id' => 'SHA',
1316
			'field_data' => array(),
1317
			'advanced_opts' => $advanced
1318
		);
1319
		$tabs[] = $data;
1320
1321
		// get the last time git fetch was run
1322
		$lastFetched = 'never';
1323
		$fetch = DNGitFetch::get()
1324
			->filter('ProjectID', $project->ID)
1325
			->sort('LastEdited', 'DESC')
1326
			->first();
1327
		if($fetch) {
1328
			$lastFetched = $fetch->dbObject('LastEdited')->Ago();
1329
		}
1330
1331
		$data = array(
1332
			'Tabs' => $tabs,
1333
			'last_fetched' => $lastFetched
1334
		);
1335
1336
		$this->applyRedeploy($request, $data);
1337
1338
		return json_encode($data, JSON_PRETTY_PRINT);
1339
	}
1340
1341
	protected function applyRedeploy(SS_HTTPRequest $request, &$data) {
1342
		if (!$request->getVar('redeploy')) return;
1343
1344
		$project = $this->getCurrentProject();
1345
		if(!$project) {
1346
			return $this->project404Response();
1347
		}
1348
1349
		// Performs canView permission check by limiting visible projects
1350
		$env = $this->getCurrentEnvironment($project);
1351
		if(!$env) {
1352
			return $this->environment404Response();
1353
		}
1354
1355
		$current = $env->CurrentBuild();
1356
		if ($current && $current->exists()) {
1357
			$data['preselect_tab'] = 3;
1358
			$data['preselect_sha'] = $current->SHA;
1359
		} else {
1360
			$master = $project->DNBranchList()->byName('master');
1361
			if ($master) {
1362
				$data['preselect_tab'] = 1;
1363
				$data['preselect_sha'] = $master->SHA();
0 ignored issues
show
Bug introduced by
The method SHA cannot be called on $master (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
1364
			}
1365
		}
1366
	}
1367
1368
	/**
1369
	 * Check and regenerate a global CSRF token
1370
	 *
1371
	 * @param SS_HTTPRequest $request
1372
	 * @param bool $resetToken
1373
	 *
1374
	 * @return bool
1375
	 */
1376
	protected function checkCsrfToken(SS_HTTPRequest $request, $resetToken = true) {
1377
		$token = SecurityToken::inst();
1378
1379
		// Ensure the submitted token has a value
1380
		$submittedToken = $request->postVar('SecurityID');
1381
		if(!$submittedToken) {
1382
			return false;
1383
		}
1384
1385
		// Do the actual check.
1386
		$check = $token->check($submittedToken);
1387
1388
		// Reset the token after we've checked the existing token
1389
		if($resetToken) {
1390
			$token->reset();
1391
		}
1392
1393
		// Return whether the token was correct or not
1394
		return $check;
1395
	}
1396
1397
	/**
1398
	 * @param SS_HTTPRequest $request
1399
	 *
1400
	 * @return string
1401
	 */
1402
	public function deploySummary(SS_HTTPRequest $request) {
1403
1404
		// Performs canView permission check by limiting visible projects
1405
		$project = $this->getCurrentProject();
1406
		if(!$project) {
1407
			return $this->project404Response();
1408
		}
1409
1410
		// Performs canView permission check by limiting visible projects
1411
		$environment = $this->getCurrentEnvironment($project);
1412
		if(!$environment) {
1413
			return $this->environment404Response();
1414
		}
1415
1416
		// Plan the deployment.
1417
		$strategy = $environment->getDeployStrategy($request);
1418
		$data = $strategy->toArray();
1419
1420
		// Add in a URL for comparing from->to code changes. Ensure that we have
1421
		// two proper 40 character SHAs, otherwise we can't show the compare link.
1422
		$interface = $project->getRepositoryInterface();
1423
		if(
1424
			!empty($interface) && !empty($interface->URL)
1425
			&& !empty($data['changes']['Code version']['from'])
1426
			&& strlen($data['changes']['Code version']['from']) == '40'
1427
			&& !empty($data['changes']['Code version']['to'])
1428
			&& strlen($data['changes']['Code version']['to']) == '40'
1429
		) {
1430
			$compareurl = sprintf(
1431
				'%s/compare/%s...%s',
1432
				$interface->URL,
1433
				$data['changes']['Code version']['from'],
1434
				$data['changes']['Code version']['to']
1435
			);
1436
			$data['changes']['Code version']['compareUrl'] = $compareurl;
1437
		}
1438
1439
		// Append json to response
1440
		$token = SecurityToken::inst();
1441
		$data['SecurityID'] = $token->getValue();
1442
1443
		$this->extend('updateDeploySummary', $data);
1444
1445
		return json_encode($data);
1446
	}
1447
1448
	/**
1449
	 * Deployment form submission handler.
1450
	 *
1451
	 * Initiate a DNDeployment record and redirect to it for status polling
1452
	 *
1453
	 * @param SS_HTTPRequest $request
1454
	 *
1455
	 * @return SS_HTTPResponse
1456
	 * @throws ValidationException
1457
	 * @throws null
1458
	 */
1459
	public function startDeploy(SS_HTTPRequest $request) {
1460
1461
		// Ensure the CSRF Token is correct
1462
		if(!$this->checkCsrfToken($request)) {
1463
			// CSRF token didn't match
1464
			return $this->httpError(400, 'Bad Request');
1465
		}
1466
1467
		// Performs canView permission check by limiting visible projects
1468
		$project = $this->getCurrentProject();
1469
		if(!$project) {
1470
			return $this->project404Response();
1471
		}
1472
1473
		// Performs canView permission check by limiting visible projects
1474
		$environment = $this->getCurrentEnvironment($project);
1475
		if(!$environment) {
1476
			return $this->environment404Response();
1477
		}
1478
1479
		// Initiate the deployment
1480
		// The extension point should pass in: Project, Environment, SelectRelease, buildName
1481
		$this->extend('doDeploy', $project, $environment, $buildName, $data);
1482
1483
		// Start the deployment based on the approved strategy.
1484
		$strategy = new DeploymentStrategy($environment);
1485
		$strategy->fromArray($request->requestVar('strategy'));
1486
		$deployment = $strategy->createDeployment();
1487
		$deployment->start();
1488
1489
		return json_encode(array(
1490
			'url' => Director::absoluteBaseURL() . $deployment->Link()
1491
		), JSON_PRETTY_PRINT);
1492
	}
1493
1494
	/**
1495
	 * Action - Do the actual deploy
1496
	 *
1497
	 * @param SS_HTTPRequest $request
1498
	 *
1499
	 * @return SS_HTTPResponse|string
1500
	 * @throws SS_HTTPResponse_Exception
1501
	 */
1502 View Code Duplication
	public function deploy(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...
1503
		$params = $request->params();
1504
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1505
1506
		if(!$deployment || !$deployment->ID) {
1507
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1508
		}
1509
		if(!$deployment->canView()) {
1510
			return Security::permissionFailure();
1511
		}
1512
1513
		$environment = $deployment->Environment();
1514
		$project = $environment->Project();
1515
1516
		if($environment->Name != $params['Environment']) {
1517
			throw new LogicException("Environment in URL doesn't match this deploy");
1518
		}
1519
		if($project->Name != $params['Project']) {
1520
			throw new LogicException("Project in URL doesn't match this deploy");
1521
		}
1522
1523
		return $this->render(array(
1524
			'Deployment' => $deployment,
1525
		));
1526
	}
1527
1528
1529
	/**
1530
	 * Action - Get the latest deploy log
1531
	 *
1532
	 * @param SS_HTTPRequest $request
1533
	 *
1534
	 * @return string
1535
	 * @throws SS_HTTPResponse_Exception
1536
	 */
1537
	public function deploylog(SS_HTTPRequest $request) {
1538
		$params = $request->params();
1539
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1540
1541
		if(!$deployment || !$deployment->ID) {
1542
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1543
		}
1544
		if(!$deployment->canView()) {
1545
			return Security::permissionFailure();
1546
		}
1547
1548
		$environment = $deployment->Environment();
1549
		$project = $environment->Project();
1550
1551
		if($environment->Name != $params['Environment']) {
1552
			throw new LogicException("Environment in URL doesn't match this deploy");
1553
		}
1554
		if($project->Name != $params['Project']) {
1555
			throw new LogicException("Project in URL doesn't match this deploy");
1556
		}
1557
1558
		$log = $deployment->log();
1559
		if($log->exists()) {
1560
			$content = $log->content();
1561
		} else {
1562
			$content = 'Waiting for action to start';
1563
		}
1564
1565
		return $this->sendResponse($deployment->ResqueStatus(), $content);
1566
	}
1567
1568 View Code Duplication
	public function abortDeploy(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...
1569
		$params = $request->params();
1570
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1571
1572
		if(!$deployment || !$deployment->ID) {
1573
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1574
		}
1575
		if(!$deployment->canView()) {
1576
			return Security::permissionFailure();
1577
		}
1578
1579
		$environment = $deployment->Environment();
1580
		$project = $environment->Project();
1581
1582
		if($environment->Name != $params['Environment']) {
1583
			throw new LogicException("Environment in URL doesn't match this deploy");
1584
		}
1585
		if($project->Name != $params['Project']) {
1586
			throw new LogicException("Project in URL doesn't match this deploy");
1587
		}
1588
1589
		$deployment->abort();
1590
1591
		return $this->sendResponse($deployment->ResqueStatus(), []);
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a string.

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...
1592
	}
1593
1594
	/**
1595
	 * @param SS_HTTPRequest|null $request
1596
	 *
1597
	 * @return Form
1598
	 */
1599
	public function getDataTransferForm(SS_HTTPRequest $request = null) {
1600
		// Performs canView permission check by limiting visible projects
1601
		$envs = $this->getCurrentProject()->DNEnvironmentList()->filterByCallback(function($item) {
1602
			return $item->canBackup();
1603
		});
1604
1605
		if(!$envs) {
1606
			return $this->environment404Response();
1607
		}
1608
1609
		$form = Form::create(
1610
			$this,
1611
			'DataTransferForm',
1612
			FieldList::create(
1613
				HiddenField::create('Direction', null, 'get'),
1614
				DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1615
					->setEmptyString('Select an environment'),
1616
				DropdownField::create('Mode', 'Transfer', DNDataArchive::get_mode_map())
1617
			),
1618
			FieldList::create(
1619
				FormAction::create('doDataTransfer', 'Create')
1620
					->addExtraClass('btn')
1621
			)
1622
		);
1623
		$form->setFormAction($this->getRequest()->getURL() . '/DataTransferForm');
1624
1625
		return $form;
1626
	}
1627
1628
	/**
1629
	 * @param array $data
1630
	 * @param Form $form
1631
	 *
1632
	 * @return SS_HTTPResponse
1633
	 * @throws SS_HTTPResponse_Exception
1634
	 */
1635
	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...
1636
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1637
1638
		// Performs canView permission check by limiting visible projects
1639
		$project = $this->getCurrentProject();
1640
		if(!$project) {
1641
			return $this->project404Response();
1642
		}
1643
1644
		$dataArchive = null;
1645
1646
		// Validate direction.
1647
		if($data['Direction'] == 'get') {
1648
			$validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1649
				->filterByCallback(function($item) {
1650
					return $item->canBackup();
1651
				});
1652
		} else if($data['Direction'] == 'push') {
1653
			$validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1654
				->filterByCallback(function($item) {
1655
					return $item->canRestore();
1656
				});
1657
		} else {
1658
			throw new LogicException('Invalid direction');
1659
		}
1660
1661
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
1662
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
1663
		if(!$environment) {
1664
			throw new LogicException('Invalid environment');
1665
		}
1666
1667
		$this->validateSnapshotMode($data['Mode']);
1668
1669
1670
		// Only 'push' direction is allowed an association with an existing archive.
1671
		if(
1672
			$data['Direction'] == 'push'
1673
			&& isset($data['DataArchiveID'])
1674
			&& is_numeric($data['DataArchiveID'])
1675
		) {
1676
			$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1677
			if(!$dataArchive) {
1678
				throw new LogicException('Invalid data archive');
1679
			}
1680
1681
			if(!$dataArchive->canDownload()) {
1682
				throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1683
			}
1684
		}
1685
1686
		$transfer = DNDataTransfer::create();
1687
		$transfer->EnvironmentID = $environment->ID;
1688
		$transfer->Direction = $data['Direction'];
1689
		$transfer->Mode = $data['Mode'];
1690
		$transfer->DataArchiveID = $dataArchive ? $dataArchive->ID : null;
1691
		if($data['Direction'] == 'push') {
1692
			$transfer->setBackupBeforePush(!empty($data['BackupBeforePush']));
1693
		}
1694
		$transfer->write();
1695
		$transfer->start();
1696
1697
		return $this->redirect($transfer->Link());
1698
	}
1699
1700
	/**
1701
	 * View into the log for a {@link DNDataTransfer}.
1702
	 *
1703
	 * @param SS_HTTPRequest $request
1704
	 *
1705
	 * @return SS_HTTPResponse|string
1706
	 * @throws SS_HTTPResponse_Exception
1707
	 */
1708
	public function transfer(SS_HTTPRequest $request) {
1709
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1710
1711
		$params = $request->params();
1712
		$transfer = DNDataTransfer::get()->byId($params['Identifier']);
1713
1714
		if(!$transfer || !$transfer->ID) {
1715
			throw new SS_HTTPResponse_Exception('Transfer not found', 404);
1716
		}
1717
		if(!$transfer->canView()) {
1718
			return Security::permissionFailure();
1719
		}
1720
1721
		$environment = $transfer->Environment();
1722
		$project = $environment->Project();
1723
1724
		if($project->Name != $params['Project']) {
1725
			throw new LogicException("Project in URL doesn't match this deploy");
1726
		}
1727
1728
		return $this->render(array(
1729
			'CurrentTransfer' => $transfer,
1730
			'SnapshotsSection' => 1,
1731
		));
1732
	}
1733
1734
	/**
1735
	 * Action - Get the latest deploy log
1736
	 *
1737
	 * @param SS_HTTPRequest $request
1738
	 *
1739
	 * @return string
1740
	 * @throws SS_HTTPResponse_Exception
1741
	 */
1742
	public function transferlog(SS_HTTPRequest $request) {
1743
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1744
1745
		$params = $request->params();
1746
		$transfer = DNDataTransfer::get()->byId($params['Identifier']);
1747
1748
		if(!$transfer || !$transfer->ID) {
1749
			throw new SS_HTTPResponse_Exception('Transfer not found', 404);
1750
		}
1751
		if(!$transfer->canView()) {
1752
			return Security::permissionFailure();
1753
		}
1754
1755
		$environment = $transfer->Environment();
1756
		$project = $environment->Project();
1757
1758
		if($project->Name != $params['Project']) {
1759
			throw new LogicException("Project in URL doesn't match this deploy");
1760
		}
1761
1762
		$log = $transfer->log();
1763
		if($log->exists()) {
1764
			$content = $log->content();
1765
		} else {
1766
			$content = 'Waiting for action to start';
1767
		}
1768
1769
		return $this->sendResponse($transfer->ResqueStatus(), $content);
1770
	}
1771
1772
	/**
1773
	 * Note: Submits to the same action as {@link getDataTransferForm()},
1774
	 * but with a Direction=push and an archive reference.
1775
	 *
1776
	 * @param SS_HTTPRequest $request
1777
	 * @param DNDataArchive|null $dataArchive Only set when method is called manually in {@link restore()},
1778
	 *                            otherwise the state is inferred from the request data.
1779
	 * @return Form
1780
	 */
1781
	public function getDataTransferRestoreForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) {
1782
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1783
1784
		// Performs canView permission check by limiting visible projects
1785
		$project = $this->getCurrentProject();
1786
		$envs = $project->DNEnvironmentList()->filterByCallback(function($item) {
1787
			return $item->canRestore();
1788
		});
1789
1790
		if(!$envs) {
1791
			return $this->environment404Response();
1792
		}
1793
1794
		$modesMap = array();
1795
		if(in_array($dataArchive->Mode, array('all'))) {
1796
			$modesMap['all'] = 'Database and Assets';
1797
		};
1798
		if(in_array($dataArchive->Mode, array('all', 'db'))) {
1799
			$modesMap['db'] = 'Database only';
1800
		};
1801
		if(in_array($dataArchive->Mode, array('all', 'assets'))) {
1802
			$modesMap['assets'] = 'Assets only';
1803
		};
1804
1805
		$alertMessage = '<div class="alert alert-warning"><strong>Warning:</strong> '
1806
			. 'This restore will overwrite the data on the chosen environment below</div>';
1807
1808
		$form = Form::create(
1809
			$this,
1810
			'DataTransferRestoreForm',
1811
			FieldList::create(
1812
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1813
				HiddenField::create('Direction', null, 'push'),
1814
				LiteralField::create('Warning', $alertMessage),
1815
				DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1816
					->setEmptyString('Select an environment'),
1817
				DropdownField::create('Mode', 'Transfer', $modesMap),
1818
				CheckboxField::create('BackupBeforePush', 'Backup existing data', '1')
1819
			),
1820
			FieldList::create(
1821
				FormAction::create('doDataTransfer', 'Restore Data')
1822
					->addExtraClass('btn')
1823
			)
1824
		);
1825
		$form->setFormAction($project->Link() . '/DataTransferRestoreForm');
1826
1827
		return $form;
1828
	}
1829
1830
	/**
1831
	 * View a form to restore a specific {@link DataArchive}.
1832
	 * Permission checks are handled in {@link DataArchives()}.
1833
	 * Submissions are handled through {@link doDataTransfer()}, same as backup operations.
1834
	 *
1835
	 * @param SS_HTTPRequest $request
1836
	 *
1837
	 * @return HTMLText
1838
	 * @throws SS_HTTPResponse_Exception
1839
	 */
1840 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...
1841
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1842
1843
		/** @var DNDataArchive $dataArchive */
1844
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1845
1846
		if(!$dataArchive) {
1847
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1848
		}
1849
1850
		// We check for canDownload because that implies access to the data.
1851
		// canRestore is later checked on the actual restore action per environment.
1852
		if(!$dataArchive->canDownload()) {
1853
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1854
		}
1855
1856
		$form = $this->getDataTransferRestoreForm($this->request, $dataArchive);
1857
1858
		// View currently only available via ajax
1859
		return $form->forTemplate();
1860
	}
1861
1862
	/**
1863
	 * View a form to delete a specific {@link DataArchive}.
1864
	 * Permission checks are handled in {@link DataArchives()}.
1865
	 * Submissions are handled through {@link doDelete()}.
1866
	 *
1867
	 * @param SS_HTTPRequest $request
1868
	 *
1869
	 * @return HTMLText
1870
	 * @throws SS_HTTPResponse_Exception
1871
	 */
1872 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...
1873
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1874
1875
		/** @var DNDataArchive $dataArchive */
1876
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1877
1878
		if(!$dataArchive) {
1879
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1880
		}
1881
1882
		if(!$dataArchive->canDelete()) {
1883
			throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403);
1884
		}
1885
1886
		$form = $this->getDeleteForm($this->request, $dataArchive);
1887
1888
		// View currently only available via ajax
1889
		return $form->forTemplate();
1890
	}
1891
1892
	/**
1893
	 * @param SS_HTTPRequest $request
1894
	 * @param DNDataArchive|null $dataArchive Only set when method is called manually, otherwise the state is inferred
1895
	 *        from the request data.
1896
	 * @return Form
1897
	 */
1898
	public function getDeleteForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) {
1899
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1900
1901
		// Performs canView permission check by limiting visible projects
1902
		$project = $this->getCurrentProject();
1903
		if(!$project) {
1904
			return $this->project404Response();
1905
		}
1906
1907
		$snapshotDeleteWarning = '<div class="alert alert-warning">'
1908
			. 'Are you sure you want to permanently delete this snapshot from this archive area?'
1909
			. '</div>';
1910
1911
		$form = Form::create(
1912
			$this,
1913
			'DeleteForm',
1914
			FieldList::create(
1915
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1916
				LiteralField::create('Warning', $snapshotDeleteWarning)
1917
			),
1918
			FieldList::create(
1919
				FormAction::create('doDelete', 'Delete')
1920
					->addExtraClass('btn')
1921
			)
1922
		);
1923
		$form->setFormAction($project->Link() . '/DeleteForm');
1924
1925
		return $form;
1926
	}
1927
1928
	/**
1929
	 * @param array $data
1930
	 * @param Form $form
1931
	 *
1932
	 * @return bool|SS_HTTPResponse
1933
	 * @throws SS_HTTPResponse_Exception
1934
	 */
1935
	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...
1936
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1937
1938
		// Performs canView permission check by limiting visible projects
1939
		$project = $this->getCurrentProject();
1940
		if(!$project) {
1941
			return $this->project404Response();
1942
		}
1943
1944
		$dataArchive = null;
1945
1946
		if(
1947
			isset($data['DataArchiveID'])
1948
			&& is_numeric($data['DataArchiveID'])
1949
		) {
1950
			$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1951
		}
1952
1953
		if(!$dataArchive) {
1954
			throw new LogicException('Invalid data archive');
1955
		}
1956
1957
		if(!$dataArchive->canDelete()) {
1958
			throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403);
1959
		}
1960
1961
		$dataArchive->delete();
1962
1963
		return $this->redirectBack();
1964
	}
1965
1966
	/**
1967
	 * View a form to move a specific {@link DataArchive}.
1968
	 *
1969
	 * @param SS_HTTPRequest $request
1970
	 *
1971
	 * @return HTMLText
1972
	 * @throws SS_HTTPResponse_Exception
1973
	 */
1974 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...
1975
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1976
1977
		/** @var DNDataArchive $dataArchive */
1978
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1979
1980
		if(!$dataArchive) {
1981
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1982
		}
1983
1984
		// We check for canDownload because that implies access to the data.
1985
		if(!$dataArchive->canDownload()) {
1986
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1987
		}
1988
1989
		$form = $this->getMoveForm($this->request, $dataArchive);
1990
1991
		// View currently only available via ajax
1992
		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...
1993
	}
1994
1995
	/**
1996
	 * Build snapshot move form.
1997
	 *
1998
	 * @param SS_HTTPRequest $request
1999
	 * @param DNDataArchive|null $dataArchive
2000
	 *
2001
	 * @return Form|SS_HTTPResponse
2002
	 */
2003
	public function getMoveForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) {
2004
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
2005
2006
		$envs = $dataArchive->validTargetEnvironments();
2007
		if(!$envs) {
2008
			return $this->environment404Response();
2009
		}
2010
2011
		$warningMessage = '<div class="alert alert-warning"><strong>Warning:</strong> This will make the snapshot '
2012
			. 'available to people with access to the target environment.<br>By pressing "Change ownership" you '
2013
			. 'confirm that you have considered data confidentiality regulations.</div>';
2014
2015
		$form = Form::create(
2016
			$this,
2017
			'MoveForm',
2018
			FieldList::create(
2019
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
2020
				LiteralField::create('Warning', $warningMessage),
2021
				DropdownField::create('EnvironmentID', 'Environment', $envs->map())
2022
					->setEmptyString('Select an environment')
2023
			),
2024
			FieldList::create(
2025
				FormAction::create('doMove', 'Change ownership')
2026
					->addExtraClass('btn')
2027
			)
2028
		);
2029
		$form->setFormAction($this->getCurrentProject()->Link() . '/MoveForm');
2030
2031
		return $form;
2032
	}
2033
2034
	/**
2035
	 * @param array $data
2036
	 * @param Form $form
2037
	 *
2038
	 * @return bool|SS_HTTPResponse
2039
	 * @throws SS_HTTPResponse_Exception
2040
	 * @throws ValidationException
2041
	 * @throws null
2042
	 */
2043
	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...
2044
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
2045
2046
		// Performs canView permission check by limiting visible projects
2047
		$project = $this->getCurrentProject();
2048
		if(!$project) {
2049
			return $this->project404Response();
2050
		}
2051
2052
		/** @var DNDataArchive $dataArchive */
2053
		$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
2054
		if(!$dataArchive) {
2055
			throw new LogicException('Invalid data archive');
2056
		}
2057
2058
		// We check for canDownload because that implies access to the data.
2059
		if(!$dataArchive->canDownload()) {
2060
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
2061
		}
2062
2063
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
2064
		$validEnvs = $dataArchive->validTargetEnvironments();
2065
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
2066
		if(!$environment) {
2067
			throw new LogicException('Invalid environment');
2068
		}
2069
2070
		$dataArchive->EnvironmentID = $environment->ID;
2071
		$dataArchive->write();
2072
2073
		return $this->redirectBack();
2074
	}
2075
2076
	/**
2077
	 * Returns an error message if redis is unavailable
2078
	 *
2079
	 * @return string
2080
	 */
2081
	public static function RedisUnavailable() {
2082
		try {
2083
			Resque::queues();
2084
		} catch(Exception $e) {
2085
			return $e->getMessage();
2086
		}
2087
		return '';
2088
	}
2089
2090
	/**
2091
	 * Returns the number of connected Redis workers
2092
	 *
2093
	 * @return int
2094
	 */
2095
	public static function RedisWorkersCount() {
2096
		return count(Resque_Worker::all());
2097
	}
2098
2099
	/**
2100
	 * @return array
2101
	 */
2102
	public function providePermissions() {
2103
		return array(
2104
			self::DEPLOYNAUT_BYPASS_PIPELINE => array(
2105
				'name' => "Bypass Pipeline",
2106
				'category' => "Deploynaut",
2107
				'help' => "Enables users to directly initiate deployments, bypassing any pipeline",
2108
			),
2109
			self::DEPLOYNAUT_DRYRUN_PIPELINE => array(
2110
				'name' => 'Dry-run Pipeline',
2111
				'category' => 'Deploynaut',
2112
				'help' => 'Enable dry-run execution of pipelines for testing'
2113
			),
2114
			self::DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS => array(
2115
				'name' => "Access to advanced deploy options",
2116
				'category' => "Deploynaut",
2117
			),
2118
2119
			// Permissions that are intended to be added to the roles.
2120
			self::ALLOW_PROD_DEPLOYMENT => array(
2121
				'name' => "Ability to deploy to production environments",
2122
				'category' => "Deploynaut",
2123
			),
2124
			self::ALLOW_NON_PROD_DEPLOYMENT => array(
2125
				'name' => "Ability to deploy to non-production environments",
2126
				'category' => "Deploynaut",
2127
			),
2128
			self::ALLOW_PROD_SNAPSHOT => array(
2129
				'name' => "Ability to make production snapshots",
2130
				'category' => "Deploynaut",
2131
			),
2132
			self::ALLOW_NON_PROD_SNAPSHOT => array(
2133
				'name' => "Ability to make non-production snapshots",
2134
				'category' => "Deploynaut",
2135
			),
2136
			self::ALLOW_CREATE_ENVIRONMENT => array(
2137
				'name' => "Ability to create environments",
2138
				'category' => "Deploynaut",
2139
			),
2140
		);
2141
	}
2142
2143
	/**
2144
	 * @return DNProject|null
2145
	 */
2146
	public function getCurrentProject() {
2147
		$projectName = trim($this->getRequest()->param('Project'));
2148
		if(!$projectName) {
2149
			return null;
2150
		}
2151
		if(empty(self::$_project_cache[$projectName])) {
2152
			self::$_project_cache[$projectName] = $this->DNProjectList()->filter('Name', $projectName)->First();
2153
		}
2154
		return self::$_project_cache[$projectName];
2155
	}
2156
2157
	/**
2158
	 * @param DNProject|null $project
2159
	 * @return DNEnvironment|null
2160
	 */
2161
	public function getCurrentEnvironment(DNProject $project = null) {
2162
		if($this->getRequest()->param('Environment') === null) {
2163
			return null;
2164
		}
2165
		if($project === null) {
2166
			$project = $this->getCurrentProject();
2167
		}
2168
		// project can still be null
2169
		if($project === null) {
2170
			return null;
2171
		}
2172
		return $project->DNEnvironmentList()->filter('Name', $this->getRequest()->param('Environment'))->First();
2173
	}
2174
2175
	/**
2176
	 * This will return a const that indicates the class of action currently being performed
2177
	 *
2178
	 * Until DNRoot is de-godded, it does a bunch of different actions all in the same class.
2179
	 * So we just have each action handler calll setCurrentActionType to define what sort of
2180
	 * action it is.
2181
	 *
2182
	 * @return string - one of the consts from self::$action_types
2183
	 */
2184
	public function getCurrentActionType() {
2185
		return $this->actionType;
2186
	}
2187
2188
	/**
2189
	 * Sets the current action type
2190
	 *
2191
	 * @param string $actionType string - one of the consts from self::$action_types
2192
	 */
2193
	public function setCurrentActionType($actionType) {
2194
		$this->actionType = $actionType;
2195
	}
2196
2197
	/**
2198
	 * Helper method to allow templates to know whether they should show the 'Archive List' include or not.
2199
	 * The actual permissions are set on a per-environment level, so we need to find out if this $member can upload to
2200
	 * or download from *any* {@link DNEnvironment} that (s)he has access to.
2201
	 *
2202
	 * TODO To be replaced with a method that just returns the list of archives this {@link Member} has access to.
2203
	 *
2204
	 * @param Member|null $member The {@link Member} to check (or null to check the currently logged in Member)
2205
	 * @return boolean|null true if $member has access to upload or download to at least one {@link DNEnvironment}.
2206
	 */
2207
	public function CanViewArchives(Member $member = null) {
2208
		if($member === null) {
2209
			$member = Member::currentUser();
2210
		}
2211
2212
		if(Permission::checkMember($member, 'ADMIN')) {
2213
			return true;
2214
		}
2215
2216
		$allProjects = $this->DNProjectList();
2217
		if(!$allProjects) {
2218
			return false;
2219
		}
2220
2221
		foreach($allProjects as $project) {
2222
			if($project->Environments()) {
2223
				foreach($project->Environments() as $environment) {
2224
					if(
2225
						$environment->canRestore($member) ||
2226
						$environment->canBackup($member) ||
2227
						$environment->canUploadArchive($member) ||
2228
						$environment->canDownloadArchive($member)
2229
					) {
2230
						// We can return early as we only need to know that we can access one environment
2231
						return true;
2232
					}
2233
				}
2234
			}
2235
		}
2236
	}
2237
2238
	/**
2239
	 * Returns a list of attempted environment creations.
2240
	 *
2241
	 * @return PaginatedList
2242
	 */
2243
	public function CreateEnvironmentList() {
2244
		$project = $this->getCurrentProject();
2245
		if($project) {
2246
			$dataList = $project->CreateEnvironments();
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...
2247
		} else {
2248
			$dataList = new ArrayList();
2249
		}
2250
2251
		$this->extend('updateCreateEnvironmentList', $dataList);
2252
		return new PaginatedList($dataList->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...
2253
	}
2254
2255
	/**
2256
	 * Returns a list of all archive files that can be accessed by the currently logged-in {@link Member}
2257
	 *
2258
	 * @return PaginatedList
2259
	 */
2260
	public function CompleteDataArchives() {
2261
		$project = $this->getCurrentProject();
2262
		$archives = new ArrayList();
2263
2264
		$archiveList = $project->Environments()->relation("DataArchives");
2265
		if($archiveList->count() > 0) {
2266
			foreach($archiveList as $archive) {
2267
				if($archive->canView() && !$archive->isPending()) {
2268
					$archives->push($archive);
2269
				}
2270
			}
2271
		}
2272
		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...
2273
	}
2274
2275
	/**
2276
	 * @return PaginatedList The list of "pending" data archives which are waiting for a file
2277
	 * to be delivered offline by post, and manually uploaded into the system.
2278
	 */
2279
	public function PendingDataArchives() {
2280
		$project = $this->getCurrentProject();
2281
		$archives = new ArrayList();
2282
		foreach($project->DNEnvironmentList() as $env) {
2283
			foreach($env->DataArchives() as $archive) {
2284
				if($archive->canView() && $archive->isPending()) {
2285
					$archives->push($archive);
2286
				}
2287
			}
2288
		}
2289
		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...
2290
	}
2291
2292
	/**
2293
	 * @return PaginatedList
2294
	 */
2295
	public function DataTransferLogs() {
2296
		$project = $this->getCurrentProject();
2297
2298
		$transfers = DNDataTransfer::get()->filterByCallback(function($record) use($project) {
2299
			return
2300
				$record->Environment()->Project()->ID == $project->ID && // Ensure only the current Project is shown
2301
				(
2302
					$record->Environment()->canRestore() || // Ensure member can perform an action on the transfers env
2303
					$record->Environment()->canBackup() ||
2304
					$record->Environment()->canUploadArchive() ||
2305
					$record->Environment()->canDownloadArchive()
2306
				);
2307
		});
2308
2309
		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...
2310
	}
2311
2312
	/**
2313
	 * @return null|PaginatedList
2314
	 */
2315
	public function DeployHistory() {
2316
		if($env = $this->getCurrentEnvironment()) {
2317
			$history = $env->DeployHistory();
2318
			if($history->count() > 0) {
2319
				$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...
2320
				$pagination->setPageLength(8);
2321
				return $pagination;
2322
			}
2323
		}
2324
		return null;
2325
	}
2326
2327
	/**
2328
	 * @return SS_HTTPResponse
2329
	 */
2330
	protected function project404Response() {
2331
		return new SS_HTTPResponse(
2332
			"Project '" . Convert::raw2xml($this->getRequest()->param('Project')) . "' not found.",
2333
			404
2334
		);
2335
	}
2336
2337
	/**
2338
	 * @return SS_HTTPResponse
2339
	 */
2340
	protected function environment404Response() {
2341
		$envName = Convert::raw2xml($this->getRequest()->param('Environment'));
2342
		return new SS_HTTPResponse("Environment '" . $envName . "' not found.", 404);
2343
	}
2344
2345
	/**
2346
	 * @param string $status
2347
	 * @param string $content
2348
	 *
2349
	 * @return string
2350
	 */
2351
	public function sendResponse($status, $content) {
2352
		// strip excessive newlines
2353
		$content = preg_replace('/(?:(?:\r\n|\r|\n)\s*){2}/s', "\n", $content);
2354
2355
		$sendJSON = (strpos($this->getRequest()->getHeader('Accept'), 'application/json') !== false)
2356
			|| $this->getRequest()->getExtension() == 'json';
2357
2358
		if(!$sendJSON) {
2359
			$this->response->addHeader("Content-type", "text/plain");
2360
			return $content;
2361
		}
2362
		$this->response->addHeader("Content-type", "application/json");
2363
		return json_encode(array(
2364
			'status' => $status,
2365
			'content' => $content,
2366
		));
2367
	}
2368
2369
	/**
2370
	 * Validate the snapshot mode
2371
	 *
2372
	 * @param string $mode
2373
	 */
2374
	protected function validateSnapshotMode($mode) {
2375
		if(!in_array($mode, array('all', 'assets', 'db'))) {
2376
			throw new LogicException('Invalid mode');
2377
		}
2378
	}
2379
2380
	/**
2381
	 * @param string $sectionName
2382
	 * @param string $title
2383
	 *
2384
	 * @return SS_HTTPResponse
2385
	 */
2386
	protected function getCustomisedViewSection($sectionName, $title = '', $data = array()) {
2387
		// Performs canView permission check by limiting visible projects
2388
		$project = $this->getCurrentProject();
2389
		if(!$project) {
2390
			return $this->project404Response();
2391
		}
2392
		$data[$sectionName] = 1;
2393
2394
		if($this !== '') {
2395
			$data['Title'] = $title;
2396
		}
2397
2398
		return $this->render($data);
2399
	}
2400
2401
	/**
2402
	 * Get items for the ambient menu that should be accessible from all pages.
2403
	 *
2404
	 * @return ArrayList
2405
	 */
2406
	public function AmbientMenu() {
2407
		$list = new ArrayList();
2408
2409
		if (Member::currentUserID()) {
2410
			$list->push(new ArrayData(array(
2411
				'Classes' => 'logout',
2412
				'FaIcon' => 'sign-out',
2413
				'Link' => 'Security/logout',
2414
				'Title' => 'Log out',
2415
				'IsCurrent' => false,
2416
				'IsSection' => false
2417
			)));
2418
		}
2419
2420
		$this->extend('updateAmbientMenu', $list);
2421
		return $list;
2422
	}
2423
2424
	/**
2425
	 * Checks whether the user can create a project.
2426
	 *
2427
	 * @return bool
2428
	 */
2429
	public function canCreateProjects($member = null) {
2430
		if(!$member) $member = Member::currentUser();
2431
		if(!$member) return false;
2432
2433
		return singleton('DNProject')->canCreate($member);
2434
	}
2435
2436
}
2437
2438