Completed
Pull Request — master (#421)
by Michael
1349:23 queued 1346:09
created

DNRoot::getDeployForm()   C

Complexity

Conditions 8
Paths 5

Size

Total Lines 40
Code Lines 22

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 40
rs 5.3846
cc 8
eloc 22
nc 5
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
	/**
25
	 * @var string
26
	 */
27
	private $actionType = self::ACTION_DEPLOY;
28
29
	/**
30
	 * Bypass pipeline permission code
31
	 */
32
	const DEPLOYNAUT_BYPASS_PIPELINE = 'DEPLOYNAUT_BYPASS_PIPELINE';
33
34
	/**
35
	 * Allow dryrun of pipelines
36
	 */
37
	const DEPLOYNAUT_DRYRUN_PIPELINE = 'DEPLOYNAUT_DRYRUN_PIPELINE';
38
39
	/**
40
	 * Allow advanced options on deployments
41
	 */
42
	const DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS = 'DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS';
43
44
	const ALLOW_PROD_DEPLOYMENT = 'ALLOW_PROD_DEPLOYMENT';
45
	const ALLOW_NON_PROD_DEPLOYMENT = 'ALLOW_NON_PROD_DEPLOYMENT';
46
	const ALLOW_PROD_SNAPSHOT = 'ALLOW_PROD_SNAPSHOT';
47
	const ALLOW_NON_PROD_SNAPSHOT = 'ALLOW_NON_PROD_SNAPSHOT';
48
	const ALLOW_CREATE_ENVIRONMENT = 'ALLOW_CREATE_ENVIRONMENT';
49
50
	/**
51
	 * @var array
52
	 */
53
	private static $allowed_actions = array(
54
		'projects',
55
		'nav',
56
		'update',
57
		'project',
58
		'toggleprojectstar',
59
		'branch',
60
		'environment',
61
		'abortpipeline',
62
		'pipeline',
63
		'pipelinelog',
64
		'metrics',
65
		'createenvlog',
66
		'createenv',
67
		'getDeployForm',
68
		'doDeploy',
69
		'deploy',
70
		'deploylog',
71
		'getDataTransferForm',
72
		'transfer',
73
		'transferlog',
74
		'snapshots',
75
		'createsnapshot',
76
		'snapshotslog',
77
		'uploadsnapshot',
78
		'getCreateEnvironmentForm',
79
		'getUploadSnapshotForm',
80
		'getPostSnapshotForm',
81
		'getDataTransferRestoreForm',
82
		'getDeleteForm',
83
		'getMoveForm',
84
		'restoresnapshot',
85
		'deletesnapshot',
86
		'movesnapshot',
87
		'postsnapshotsuccess',
88
		'gitRevisions',
89
		'deploySummary',
90
		'startDeploy',
91
		'createproject',
92
		'CreateProjectForm',
93
	);
94
95
	/**
96
	 * URL handlers pretending that we have a deep URL structure.
97
	 */
98
	private static $url_handlers = array(
99
		'project/$Project/environment/$Environment/DeployForm' => 'getDeployForm',
100
		'project/$Project/createsnapshot/DataTransferForm' => 'getDataTransferForm',
101
		'project/$Project/DataTransferForm' => 'getDataTransferForm',
102
		'project/$Project/DataTransferRestoreForm' => 'getDataTransferRestoreForm',
103
		'project/$Project/DeleteForm' => 'getDeleteForm',
104
		'project/$Project/MoveForm' => 'getMoveForm',
105
		'project/$Project/UploadSnapshotForm' => 'getUploadSnapshotForm',
106
		'project/$Project/PostSnapshotForm' => 'getPostSnapshotForm',
107
		'project/$Project/environment/$Environment/metrics' => 'metrics',
108
		'project/$Project/environment/$Environment/pipeline/$Identifier//$Action/$ID/$OtherID' => 'pipeline',
109
		'project/$Project/environment/$Environment/deploy_summary' => 'deploySummary',
110
		'project/$Project/environment/$Environment/git_revisions' => 'gitRevisions',
111
		'project/$Project/environment/$Environment/start-deploy' => 'startDeploy',
112
		'project/$Project/environment/$Environment/deploy/$Identifier/log' => 'deploylog',
113
		'project/$Project/environment/$Environment/deploy/$Identifier' => 'deploy',
114
		'project/$Project/transfer/$Identifier/log' => 'transferlog',
115
		'project/$Project/transfer/$Identifier' => 'transfer',
116
		'project/$Project/environment/$Environment' => 'environment',
117
		'project/$Project/createenv/$Identifier/log' => 'createenvlog',
118
		'project/$Project/createenv/$Identifier' => 'createenv',
119
		'project/$Project/CreateEnvironmentForm' => 'getCreateEnvironmentForm',
120
		'project/$Project/branch' => 'branch',
121
		'project/$Project/build/$Build' => 'build',
122
		'project/$Project/restoresnapshot/$DataArchiveID' => 'restoresnapshot',
123
		'project/$Project/deletesnapshot/$DataArchiveID' => 'deletesnapshot',
124
		'project/$Project/movesnapshot/$DataArchiveID' => 'movesnapshot',
125
		'project/$Project/update' => 'update',
126
		'project/$Project/snapshots' => 'snapshots',
127
		'project/$Project/createsnapshot' => 'createsnapshot',
128
		'project/$Project/uploadsnapshot' => 'uploadsnapshot',
129
		'project/$Project/snapshotslog' => 'snapshotslog',
130
		'project/$Project/postsnapshotsuccess/$DataArchiveID' => 'postsnapshotsuccess',
131
		'project/$Project/star' => 'toggleprojectstar',
132
		'project/$Project' => 'project',
133
		'projects' => 'projects',
134
	);
135
136
	/**
137
	 * @var array
138
	 */
139
	protected static $_project_cache = array();
140
141
	/**
142
	 * @var array
143
	 */
144
	private static $support_links = array();
145
146
	/**
147
	 * @var array
148
	 */
149
	private static $platform_specific_strings = array();
150
151
	/**
152
	 * @var array
153
	 */
154
	private static $action_types = array(
155
		self::ACTION_DEPLOY,
156
		self::ACTION_SNAPSHOT
157
	);
158
159
	/**
160
	 * @var DNData
161
	 */
162
	protected $data;
163
164
	/**
165
	 * Include requirements that deploynaut needs, such as javascript.
166
	 */
167
	public static function include_requirements() {
168
169
		// JS should always go to the bottom, otherwise there's the risk that Requirements
170
		// puts them halfway through the page to the nearest <script> tag. We don't want that.
171
		Requirements::set_force_js_to_bottom(true);
172
173
		Requirements::combine_files(
174
			'deploynaut.js',
175
			array(
176
				'deploynaut/javascript/jquery.js',
177
				'deploynaut/javascript/bootstrap.js',
178
				'deploynaut/javascript/q.js',
179
				'deploynaut/javascript/tablefilter.js',
180
				'deploynaut/javascript/deploynaut.js',
181
				'deploynaut/javascript/react-with-addons.js',
182
				'deploynaut/javascript/bootstrap.file-input.js',
183
				'deploynaut/thirdparty/select2/dist/js/select2.min.js',
184
				'deploynaut/javascript/material.js',
185
			)
186
		);
187
188
		if (\Director::isDev()) {
189
			\Requirements::javascript('deploynaut/static/bundle-debug.js');
190
		} else {
191
			\Requirements::javascript('deploynaut/static/bundle.js');
192
		}
193
194
		Requirements::css('deploynaut/static/style.css');
195
	}
196
197
	/**
198
	 * Check for feature flags:
199
	 * - FLAG_SNAPSHOTS_ENABLED: set to true to enable globally
200
	 * - FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS: set to semicolon-separated list of email addresses of allowed users.
201
	 *
202
	 * @return boolean
203
	 */
204
	public static function FlagSnapshotsEnabled() {
205
		if(defined('FLAG_SNAPSHOTS_ENABLED') && FLAG_SNAPSHOTS_ENABLED) {
206
			return true;
207
		}
208
		if(defined('FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS') && FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS) {
209
			$allowedMembers = explode(';', FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS);
210
			$member = Member::currentUser();
211
			if($allowedMembers && $member && in_array($member->Email, $allowedMembers)) {
212
				return true;
213
			}
214
		}
215
		return false;
216
	}
217
218
	/**
219
	 * @return ArrayList
220
	 */
221
	public static function get_support_links() {
222
		$supportLinks = self::config()->support_links;
223
		if($supportLinks) {
224
			return new ArrayList($supportLinks);
225
		}
226
	}
227
228
	/**
229
	 * @return array
230
	 */
231
	public static function get_template_global_variables() {
232
		return array(
233
			'RedisUnavailable' => 'RedisUnavailable',
234
			'RedisWorkersCount' => 'RedisWorkersCount',
235
			'SidebarLinks' => 'SidebarLinks',
236
			"SupportLinks" => 'get_support_links'
237
		);
238
	}
239
240
	/**
241
	 */
242
	public function init() {
243
		parent::init();
244
245
		if(!Member::currentUser() && !Session::get('AutoLoginHash')) {
246
			return Security::permissionFailure();
247
		}
248
249
		// Block framework jquery
250
		Requirements::block(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
251
252
		self::include_requirements();
253
	}
254
255
	/**
256
	 * @return string
257
	 */
258
	public function Link() {
259
		return "naut/";
260
	}
261
262
	/**
263
	 * Actions
264
	 *
265
	 * @param SS_HTTPRequest $request
266
	 * @return \SS_HTTPResponse
267
	 */
268
	public function index(SS_HTTPRequest $request) {
269
		return $this->redirect($this->Link() . 'projects/');
270
	}
271
272
	/**
273
	 * Action
274
	 *
275
	 * @param SS_HTTPRequest $request
276
	 * @return string - HTML
277
	 */
278
	public function projects(SS_HTTPRequest $request) {
279
		// Performs canView permission check by limiting visible projects in DNProjectsList() call.
280
		return $this->customise(array(
281
			'Title' => 'Projects',
282
		))->render();
283
	}
284
285
	/**
286
	 * @param SS_HTTPRequest $request
287
	 * @return HTMLText
288
	 */
289
	public function nav(SS_HTTPRequest $request) {
290
		return $this->renderWith('Nav');
291
	}
292
293
	/**
294
	 * Return a link to the navigation template used for AJAX requests.
295
	 * @return string
296
	 */
297
	public function NavLink() {
298
		return Controller::join_links(Director::absoluteBaseURL(), 'naut', 'nav');
299
	}
300
301
	/**
302
	 * Action
303
	 *
304
	 * @param SS_HTTPRequest $request
305
	 * @return SS_HTTPResponse - HTML
306
	 */
307
	public function snapshots(SS_HTTPRequest $request) {
308
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
309
		return $this->getCustomisedViewSection('SnapshotsSection', 'Data Snapshots');
310
	}
311
312
	/**
313
	 * Action
314
	 *
315
	 * @param SS_HTTPRequest $request
316
	 * @return string - HTML
317
	 */
318 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...
319
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
320
321
		// Performs canView permission check by limiting visible projects
322
		$project = $this->getCurrentProject();
323
		if(!$project) {
324
			return $this->project404Response();
325
		}
326
327
		if(!$project->canBackup()) {
328
			return new SS_HTTPResponse("Not allowed to create snapshots on any environments", 401);
329
		}
330
331
		return $this->customise(array(
332
			'Title' => 'Create Data Snapshot',
333
			'SnapshotsSection' => 1,
334
			'DataTransferForm' => $this->getDataTransferForm($request)
335
		))->render();
336
	}
337
338
	/**
339
	 * Action
340
	 *
341
	 * @param SS_HTTPRequest $request
342
	 * @return string - HTML
343
	 */
344 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...
345
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
346
347
		// Performs canView permission check by limiting visible projects
348
		$project = $this->getCurrentProject();
349
		if(!$project) {
350
			return $this->project404Response();
351
		}
352
353
		if(!$project->canUploadArchive()) {
354
			return new SS_HTTPResponse("Not allowed to upload", 401);
355
		}
356
357
		return $this->customise(array(
358
			'SnapshotsSection' => 1,
359
			'UploadSnapshotForm' => $this->getUploadSnapshotForm($request),
360
			'PostSnapshotForm' => $this->getPostSnapshotForm($request)
361
		))->render();
362
	}
363
364
	/**
365
	 * Return the upload limit for snapshot uploads
366
	 * @return string
367
	 */
368
	public function UploadLimit() {
369
		return File::format_size(min(
370
			File::ini2bytes(ini_get('upload_max_filesize')),
371
			File::ini2bytes(ini_get('post_max_size'))
372
		));
373
	}
374
375
	/**
376
	 * Construct the upload form.
377
	 *
378
	 * @param SS_HTTPRequest $request
379
	 * @return Form
380
	 */
381
	public function getUploadSnapshotForm(SS_HTTPRequest $request) {
382
		// Performs canView permission check by limiting visible projects
383
		$project = $this->getCurrentProject();
384
		if(!$project) {
385
			return $this->project404Response();
386
		}
387
388
		if(!$project->canUploadArchive()) {
389
			return new SS_HTTPResponse("Not allowed to upload", 401);
390
		}
391
392
		// Framing an environment as a "group of people with download access"
393
		// makes more sense to the user here, while still allowing us to enforce
394
		// environment specific restrictions on downloading the file later on.
395
		$envs = $project->DNEnvironmentList()->filterByCallback(function($item) {
396
			return $item->canUploadArchive();
397
		});
398
		$envsMap = array();
399
		foreach($envs as $env) {
400
			$envsMap[$env->ID] = $env->Name;
401
		}
402
403
		$maxSize = min(File::ini2bytes(ini_get('upload_max_filesize')), File::ini2bytes(ini_get('post_max_size')));
404
		$fileField = DataArchiveFileField::create('ArchiveFile', 'File');
405
		$fileField->getValidator()->setAllowedExtensions(array('sspak'));
406
		$fileField->getValidator()->setAllowedMaxFileSize(array('*' => $maxSize));
407
408
		$form = Form::create(
409
			$this,
410
			'UploadSnapshotForm',
411
			FieldList::create(
412
				$fileField,
413
				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
414
				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
415
					->setEmptyString('Select an environment')
416
			),
417
			FieldList::create(
418
				FormAction::create('doUploadSnapshot', 'Upload File')
419
					->addExtraClass('btn')
420
			),
421
			RequiredFields::create('ArchiveFile')
422
		);
423
424
		$form->disableSecurityToken();
425
		$form->addExtraClass('fields-wide');
426
		// Tweak the action so it plays well with our fake URL structure.
427
		$form->setFormAction($project->Link() . '/UploadSnapshotForm');
428
429
		return $form;
430
	}
431
432
	/**
433
	 * @param array $data
434
	 * @param Form $form
435
	 *
436
	 * @return bool|HTMLText|SS_HTTPResponse
437
	 */
438
	public function doUploadSnapshot($data, Form $form) {
439
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
440
441
		// Performs canView permission check by limiting visible projects
442
		$project = $this->getCurrentProject();
443
		if(!$project) {
444
			return $this->project404Response();
445
		}
446
447
		$validEnvs = $project->DNEnvironmentList()
448
			->filterByCallback(function($item) {
449
				return $item->canUploadArchive();
450
			});
451
452
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
453
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
454
		if(!$environment) {
455
			throw new LogicException('Invalid environment');
456
		}
457
458
		$this->validateSnapshotMode($data['Mode']);
459
460
		$dataArchive = DNDataArchive::create(array(
461
			'AuthorID' => Member::currentUserID(),
462
			'EnvironmentID' => $data['EnvironmentID'],
463
			'IsManualUpload' => true,
464
		));
465
		// needs an ID and transfer to determine upload path
466
		$dataArchive->write();
467
		$dataTransfer = DNDataTransfer::create(array(
468
			'AuthorID' => Member::currentUserID(),
469
			'Mode' => $data['Mode'],
470
			'Origin' => 'ManualUpload',
471
			'EnvironmentID' => $data['EnvironmentID']
472
		));
473
		$dataTransfer->write();
474
		$dataArchive->DataTransfers()->add($dataTransfer);
475
		$form->saveInto($dataArchive);
476
		$dataArchive->write();
477
		$workingDir = TEMP_FOLDER . DIRECTORY_SEPARATOR . 'deploynaut-transfer-' . $dataTransfer->ID;
478
479
		$cleanupFn = function() use($workingDir, $dataTransfer, $dataArchive) {
480
			$process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir)));
481
			$process->run();
482
			$dataTransfer->delete();
483
			$dataArchive->delete();
484
		};
485
486
		// extract the sspak contents so we can inspect them
487
		try {
488
			$dataArchive->extractArchive($workingDir);
489
		} catch(Exception $e) {
490
			$cleanupFn();
491
			$form->sessionMessage(
492
				'There was a problem trying to open your snapshot for processing. Please try uploading again',
493
				'bad'
494
			);
495
			return $this->redirectBack();
496
		}
497
498
		// validate that the sspak contents match the declared contents
499
		$result = $dataArchive->validateArchiveContents();
500
		if(!$result->valid()) {
501
			$cleanupFn();
502
			$form->sessionMessage($result->message(), 'bad');
503
			return $this->redirectBack();
504
		}
505
506
		// fix file permissions of extracted sspak files then re-build the sspak
507
		try {
508
			$dataArchive->fixArchivePermissions($workingDir);
509
			$dataArchive->setArchiveFromFiles($workingDir);
510
		} catch(Exception $e) {
511
			$cleanupFn();
512
			$form->sessionMessage(
513
				'There was a problem processing your snapshot. Please try uploading again',
514
				'bad'
515
			);
516
			return $this->redirectBack();
517
		}
518
519
		// cleanup any extracted sspak contents lying around
520
		$process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir)));
521
		$process->run();
522
523
		return $this->customise(array(
524
			'Project' => $project,
525
			'CurrentProject' => $project,
526
			'SnapshotsSection' => 1,
527
			'DataArchive' => $dataArchive,
528
			'DataTransferRestoreForm' => $this->getDataTransferRestoreForm($this->request, $dataArchive),
529
			'BackURL' => $project->Link('snapshots')
530
		))->renderWith(array('DNRoot_uploadsnapshot', 'DNRoot'));
531
	}
532
533
	/**
534
	 * @param SS_HTTPRequest $request
535
	 * @return Form
536
	 */
537
	public function getPostSnapshotForm(SS_HTTPRequest $request) {
538
		// Performs canView permission check by limiting visible projects
539
		$project = $this->getCurrentProject();
540
		if(!$project) {
541
			return $this->project404Response();
542
		}
543
544
		if(!$project->canUploadArchive()) {
545
			return new SS_HTTPResponse("Not allowed to upload", 401);
546
		}
547
548
		// Framing an environment as a "group of people with download access"
549
		// makes more sense to the user here, while still allowing us to enforce
550
		// environment specific restrictions on downloading the file later on.
551
		$envs = $project->DNEnvironmentList()->filterByCallback(function($item) {
552
			return $item->canUploadArchive();
553
		});
554
		$envsMap = array();
555
		foreach($envs as $env) {
556
			$envsMap[$env->ID] = $env->Name;
557
		}
558
559
		$form = Form::create(
560
			$this,
561
			'PostSnapshotForm',
562
			FieldList::create(
563
				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
564
				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
565
					->setEmptyString('Select an environment')
566
			),
567
			FieldList::create(
568
				FormAction::create('doPostSnapshot', 'Submit request')
569
					->addExtraClass('btn')
570
			),
571
			RequiredFields::create('File')
572
		);
573
574
		$form->disableSecurityToken();
575
		$form->addExtraClass('fields-wide');
576
		// Tweak the action so it plays well with our fake URL structure.
577
		$form->setFormAction($project->Link() . '/PostSnapshotForm');
578
579
		return $form;
580
	}
581
582
	/**
583
	 * @param array $data
584
	 * @param Form $form
585
	 *
586
	 * @return SS_HTTPResponse
587
	 */
588
	public function doPostSnapshot($data, $form) {
589
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
590
591
		$project = $this->getCurrentProject();
592
		if(!$project) {
593
			return $this->project404Response();
594
		}
595
596
		$validEnvs = $project->DNEnvironmentList()->filterByCallback(function($item) {
597
				return $item->canUploadArchive();
598
		});
599
600
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
601
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
602
		if(!$environment) {
603
			throw new LogicException('Invalid environment');
604
		}
605
606
		$dataArchive = DNDataArchive::create(array(
607
			'UploadToken' => DNDataArchive::generate_upload_token(),
608
		));
609
		$form->saveInto($dataArchive);
610
		$dataArchive->write();
611
612
		return $this->redirect(Controller::join_links(
613
			$project->Link(),
614
			'postsnapshotsuccess',
615
			$dataArchive->ID
616
		));
617
	}
618
619
	/**
620
	 * Action
621
	 *
622
	 * @param SS_HTTPRequest $request
623
	 * @return SS_HTTPResponse - HTML
624
	 */
625
	public function snapshotslog(SS_HTTPRequest $request) {
626
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
627
		return $this->getCustomisedViewSection('SnapshotsSection', 'Data Snapshots Log');
628
	}
629
630
	/**
631
	 * @param SS_HTTPRequest $request
632
	 * @return SS_HTTPResponse|string
633
	 * @throws SS_HTTPResponse_Exception
634
	 */
635
	public function postsnapshotsuccess(SS_HTTPRequest $request) {
636
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
637
638
		// Performs canView permission check by limiting visible projects
639
		$project = $this->getCurrentProject();
640
		if(!$project) {
641
			return $this->project404Response();
642
		}
643
644
		if(!$project->canUploadArchive()) {
645
			return new SS_HTTPResponse("Not allowed to upload", 401);
646
		}
647
648
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
649
		if(!$dataArchive) {
650
			return new SS_HTTPResponse("Archive not found.", 404);
651
		}
652
653
		if(!$dataArchive->canRestore()) {
654
			throw new SS_HTTPResponse_Exception('Not allowed to restore archive', 403);
655
		}
656
657
		return $this->render(array(
658
				'Title' => 'How to send us your Data Snapshot by post',
659
				'DataArchive' => $dataArchive,
660
				'Address' => Config::inst()->get('Deploynaut', 'snapshot_post_address'),
661
				'BackURL' => $project->Link(),
662
			));
663
	}
664
665
	/**
666
	 * @param SS_HTTPRequest $request
667
	 * @return \SS_HTTPResponse
668
	 */
669
	public function project(SS_HTTPRequest $request) {
670
		return $this->getCustomisedViewSection('ProjectOverview');
671
	}
672
673
	/**
674
	 * This action will star / unstar a project for the current member
675
	 *
676
	 * @param SS_HTTPRequest $request
677
	 *
678
	 * @return SS_HTTPResponse
679
	 */
680
	public function toggleprojectstar(SS_HTTPRequest $request) {
681
		$project = $this->getCurrentProject();
682
		if(!$project) {
683
			return $this->project404Response();
684
		}
685
686
		$member = Member::currentUser();
687
		if($member === null) {
688
			return $this->project404Response();
689
		}
690
		$favProject = $member->StarredProjects()
691
			->filter('DNProjectID', $project->ID)
692
			->first();
693
694
		if($favProject) {
695
			$member->StarredProjects()->remove($favProject);
696
		} else {
697
			$member->StarredProjects()->add($project);
698
		}
699
		return $this->redirectBack();
700
	}
701
702
	/**
703
	 * @param SS_HTTPRequest $request
704
	 * @return \SS_HTTPResponse
705
	 */
706
	public function branch(SS_HTTPRequest $request) {
707
		$project = $this->getCurrentProject();
708
		if(!$project) {
709
			return $this->project404Response();
710
		}
711
712
		$branchName = $request->getVar('name');
713
		$branch = $project->DNBranchList()->byName($branchName);
714
		if(!$branch) {
715
			return new SS_HTTPResponse("Branch '" . Convert::raw2xml($branchName) . "' not found.", 404);
716
		}
717
718
		return $this->render(array(
719
			'CurrentBranch' => $branch,
720
		));
721
	}
722
723
	/**
724
	 * @param SS_HTTPRequest $request
725
	 * @return \SS_HTTPResponse
726
	 */
727
	public function environment(SS_HTTPRequest $request) {
728
		// Performs canView permission check by limiting visible projects
729
		$project = $this->getCurrentProject();
730
		if(!$project) {
731
			return $this->project404Response();
732
		}
733
734
		// Performs canView permission check by limiting visible projects
735
		$env = $this->getCurrentEnvironment($project);
736
		if(!$env) {
737
			return $this->environment404Response();
738
		}
739
740
		return $this->render(array(
741
			'DNEnvironmentList' => $this->getCurrentProject()->DNEnvironmentList(),
742
			'FlagSnapshotsEnabled' => $this->FlagSnapshotsEnabled(),
743
		));
744
	}
745
746
747
	/**
748
	 * Initiate a pipeline dry run
749
	 *
750
	 * @param array $data
751
	 * @param DeployForm $form
752
	 *
753
	 * @return SS_HTTPResponse
754
	 */
755
	public function doDryRun($data, DeployForm $form) {
756
		return $this->beginPipeline($data, $form, true);
757
	}
758
759
	/**
760
	 * Initiate a pipeline
761
	 *
762
	 * @param array $data
763
	 * @param DeployForm $form
764
	 * @return \SS_HTTPResponse
765
	 */
766
	public function startPipeline($data, $form) {
767
		return $this->beginPipeline($data, $form);
768
	}
769
770
	/**
771
	 * Start a pipeline
772
	 *
773
	 * @param array $data
774
	 * @param DeployForm $form
775
	 * @param bool $isDryRun
776
	 * @return \SS_HTTPResponse
777
	 */
778
	protected function beginPipeline($data, DeployForm $form, $isDryRun = false) {
779
		$buildName = $form->getSelectedBuild($data);
780
781
		// Performs canView permission check by limiting visible projects
782
		$project = $this->getCurrentProject();
783
		if(!$project) {
784
			return $this->project404Response();
785
		}
786
787
		// Performs canView permission check by limiting visible projects
788
		$environment = $this->getCurrentEnvironment($project);
789
		if(!$environment) {
790
			return $this->environment404Response();
791
		}
792
793
		if(!$environment->DryRunEnabled && $isDryRun) {
794
			return new SS_HTTPResponse("Dry-run for pipelines is not enabled for this environment", 404);
795
		}
796
797
		// Initiate the pipeline
798
		$sha = $project->DNBuildList()->byName($buildName);
799
		$pipeline = Pipeline::create();
800
		$pipeline->DryRun = $isDryRun;
801
		$pipeline->EnvironmentID = $environment->ID;
802
		$pipeline->AuthorID = Member::currentUserID();
803
		$pipeline->SHA = $sha->FullName();
804
		// Record build at time of execution
805
		if($currentBuild = $environment->CurrentBuild()) {
806
			$pipeline->PreviousDeploymentID = $currentBuild->ID;
807
		}
808
		$pipeline->start(); // start() will call write(), so no need to do it here as well.
809
		return $this->redirect($environment->Link());
810
	}
811
812
	/**
813
	 * @param SS_HTTPRequest $request
814
	 *
815
	 * @return SS_HTTPResponse
816
	 * @throws SS_HTTPResponse_Exception
817
	 */
818
	public function pipeline(SS_HTTPRequest $request) {
819
		$params = $request->params();
820
		$pipeline = Pipeline::get()->byID($params['Identifier']);
821
822
		if(!$pipeline || !$pipeline->ID || !$pipeline->Environment()) {
823
			throw new SS_HTTPResponse_Exception('Pipeline not found', 404);
824
		}
825
		if(!$pipeline->Environment()->canView()) {
826
			return Security::permissionFailure();
827
		}
828
829
		$environment = $pipeline->Environment();
830
		$project = $pipeline->Environment()->Project();
831
832
		if($environment->Name != $params['Environment']) {
833
			throw new LogicException("Environment in URL doesn't match this pipeline");
834
		}
835
		if($project->Name != $params['Project']) {
836
			throw new LogicException("Project in URL doesn't match this pipeline");
837
		}
838
839
		// Delegate to sub-requesthandler
840
		return PipelineController::create($this, $pipeline);
841
	}
842
843
	/**
844
	 * Shows the creation log.
845
	 *
846
	 * @param SS_HTTPRequest $request
847
	 * @return string
848
	 */
849
	public function createenv(SS_HTTPRequest $request) {
850
		$params = $request->params();
851
		if($params['Identifier']) {
852
			$record = DNCreateEnvironment::get()->byId($params['Identifier']);
853
854
			if(!$record || !$record->ID) {
855
				throw new SS_HTTPResponse_Exception('Create environment not found', 404);
856
			}
857
			if(!$record->canView()) {
858
				return Security::permissionFailure();
859
			}
860
861
			$project = $this->getCurrentProject();
862
			if(!$project) {
863
				return $this->project404Response();
864
			}
865
866
			if($project->Name != $params['Project']) {
867
				throw new LogicException("Project in URL doesn't match this creation");
868
			}
869
870
			return $this->render(array(
871
				'CreateEnvironment' => $record,
872
			));
873
		}
874
		return $this->render();
875
	}
876
877
878 View Code Duplication
	public function createenvlog(SS_HTTPRequest $request) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
879
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
880
881
		$params = $request->params();
882
		$env = DNCreateEnvironment::get()->byId($params['Identifier']);
883
884
		if(!$env || !$env->ID) {
885
			throw new SS_HTTPResponse_Exception('Log not found', 404);
886
		}
887
		if(!$env->canView()) {
888
			return Security::permissionFailure();
889
		}
890
891
		$project = $env->Project();
892
893
		if($project->Name != $params['Project']) {
894
			throw new LogicException("Project in URL doesn't match this deploy");
895
		}
896
897
		$log = $env->log();
898
		if($log->exists()) {
899
			$content = $log->content();
900
		} else {
901
			$content = 'Waiting for action to start';
902
		}
903
904
		return $this->sendResponse($env->ResqueStatus(), $content);
905
	}
906
907
	/**
908
	 * @param SS_HTTPRequest $request
909
	 * @return Form
910
	 */
911
	public function getCreateEnvironmentForm(SS_HTTPRequest $request) {
912
		$this->setCurrentActionType(self::ACTION_ENVIRONMENTS);
913
914
		$project = $this->getCurrentProject();
915
		if(!$project) {
916
			return $this->project404Response();
917
		}
918
919
		$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...
920
		if(!$envType || !class_exists($envType)) {
921
			return null;
922
		}
923
924
		$backend = Injector::inst()->get($envType);
925
		if(!($backend instanceof EnvironmentCreateBackend)) {
926
			// Only allow this for supported backends.
927
			return null;
928
		}
929
930
		$fields = $backend->getCreateEnvironmentFields($project);
931
		if(!$fields) return null;
932
933
		if(!$project->canCreateEnvironments()) {
934
			return new SS_HTTPResponse('Not allowed to create environments for this project', 401);
935
		}
936
937
		$form = Form::create(
938
			$this,
939
			'CreateEnvironmentForm',
940
			$fields,
941
			FieldList::create(
942
				FormAction::create('doCreateEnvironment', 'Create')
943
					->addExtraClass('btn')
944
			),
945
			$backend->getCreateEnvironmentValidator()
946
		);
947
948
		// Tweak the action so it plays well with our fake URL structure.
949
		$form->setFormAction($project->Link() . '/CreateEnvironmentForm');
950
951
		return $form;
952
	}
953
954
	/**
955
	 * @param array $data
956
	 * @param Form $form
957
	 *
958
	 * @return bool|HTMLText|SS_HTTPResponse
959
	 */
960
	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...
961
		$this->setCurrentActionType(self::ACTION_ENVIRONMENTS);
962
963
		$project = $this->getCurrentProject();
964
		if(!$project) {
965
			return $this->project404Response();
966
		}
967
968
		if(!$project->canCreateEnvironments()) {
969
			return new SS_HTTPResponse('Not allowed to create environments for this project', 401);
970
		}
971
972
		// Set the environment type so we know what we're creating.
973
		$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...
974
975
		$job = DNCreateEnvironment::create();
976
977
		$job->Data = serialize($data);
0 ignored issues
show
Documentation introduced by
The property Data does not exist on object<DNCreateEnvironment>. 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...
978
		$job->ProjectID = $project->ID;
0 ignored issues
show
Documentation introduced by
The property ProjectID does not exist on object<DNCreateEnvironment>. 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...
979
		$job->write();
980
		$job->start();
981
982
		return $this->redirect($project->Link('createenv') . '/' . $job->ID);
983
	}
984
985
	/**
986
	 *
987
	 * @param SS_HTTPRequest $request
988
	 * @return \SS_HTTPResponse
989
	 */
990
	public function metrics(SS_HTTPRequest $request) {
991
		// Performs canView permission check by limiting visible projects
992
		$project = $this->getCurrentProject();
993
		if(!$project) {
994
			return $this->project404Response();
995
		}
996
997
		// Performs canView permission check by limiting visible projects
998
		$env = $this->getCurrentEnvironment($project);
999
		if(!$env) {
1000
			return $this->environment404Response();
1001
		}
1002
1003
		return $this->render();
1004
	}
1005
1006
	/**
1007
	 * Get the DNData object.
1008
	 *
1009
	 * @return DNData
1010
	 */
1011
	public function DNData() {
1012
		return DNData::inst();
1013
	}
1014
1015
	/**
1016
	 * Provide a list of all projects.
1017
	 *
1018
	 * @return SS_List
1019
	 */
1020
	public function DNProjectList() {
1021
		$memberId = Member::currentUserID();
1022
		if(!$memberId) {
1023
			return new ArrayList();
1024
		}
1025
1026
		if(Permission::check('ADMIN')) {
1027
			return DNProject::get();
1028
		}
1029
1030
		return Member::get()->filter('ID', $memberId)
1031
			->relation('Groups')
1032
			->relation('Projects');
1033
	}
1034
1035
	/**
1036
	 * @return ArrayList
1037
	 */
1038
	public function getPlatformSpecificStrings() {
1039
		$strings = $this->config()->platform_specific_strings;
1040
		if ($strings) {
1041
			return new ArrayList($strings);
1042
		}
1043
	}
1044
1045
	/**
1046
	 * Provide a list of all starred projects for the currently logged in member
1047
	 *
1048
	 * @return SS_List
1049
	 */
1050
	public function getStarredProjects() {
1051
		$member = Member::currentUser();
1052
		if($member === null) {
1053
			return new ArrayList();
1054
		}
1055
1056
		$favProjects = $member->StarredProjects();
1057
1058
		$list = new ArrayList();
1059
		foreach($favProjects as $project) {
1060
			if($project->canView($member)) {
1061
				$list->add($project);
1062
			}
1063
		}
1064
		return $list;
1065
	}
1066
1067
	/**
1068
	 * Returns top level navigation of projects.
1069
	 *
1070
	 * @param int $limit
1071
	 *
1072
	 * @return ArrayList
1073
	 */
1074
	public function Navigation($limit = 5) {
1075
		$navigation = new ArrayList();
1076
1077
		$currentProject = $this->getCurrentProject();
1078
1079
		$projects = $this->getStarredProjects();
1080
		if($projects->count() < 1) {
1081
			$projects = $this->DNProjectList();
1082
		} else {
1083
			$limit = -1;
1084
		}
1085
1086
		if($projects->count() > 0) {
1087
			$activeProject = false;
1088
1089
			if($limit > 0) {
1090
				$limitedProjects = $projects->limit($limit);
1091
			} else {
1092
				$limitedProjects = $projects;
1093
			}
1094
1095
			foreach($limitedProjects as $project) {
1096
				$isActive = $currentProject && $currentProject->ID == $project->ID;
1097
				if($isActive) {
1098
					$activeProject = true;
1099
				}
1100
1101
				$navigation->push(array(
1102
					'Project' => $project,
1103
					'IsActive' => $currentProject && $currentProject->ID == $project->ID,
1104
				));
1105
			}
1106
1107
			// Ensure the current project is in the list
1108
			if(!$activeProject && $currentProject) {
1109
				$navigation->unshift(array(
1110
					'Project' => $currentProject,
1111
					'IsActive' => true,
1112
				));
1113
				if($limit > 0 && $navigation->count() > $limit) {
1114
					$navigation->pop();
1115
				}
1116
			}
1117
		}
1118
1119
		return $navigation;
1120
	}
1121
1122
	/**
1123
	 * Construct the deployment form
1124
	 *
1125
	 * @return Form
1126
	 */
1127
	public function getDeployForm($request = null) {
1128
1129
		// Performs canView permission check by limiting visible projects
1130
		$project = $this->getCurrentProject();
1131
		if(!$project) {
1132
			return $this->project404Response();
1133
		}
1134
1135
		// Performs canView permission check by limiting visible projects
1136
		$environment = $this->getCurrentEnvironment($project);
1137
		if(!$environment) {
1138
			return $this->environment404Response();
1139
		}
1140
1141
		if(!$environment->canDeploy()) {
1142
			return new SS_HTTPResponse("Not allowed to deploy", 401);
1143
		}
1144
1145
		// Generate the form
1146
		$form = new DeployForm($this, 'DeployForm', $environment, $project);
1147
1148
		// If this is an ajax request we don't want to submit the form - we just want to retrieve the markup.
1149
		if(
1150
			$request &&
1151
			!$request->requestVar('action_showDeploySummary') &&
1152
			$this->getRequest()->isAjax() &&
1153
			$this->getRequest()->isGET()
1154
		) {
1155
			// We can just use the URL we're accessing
1156
			$form->setFormAction($this->getRequest()->getURL());
1157
1158
			$body = json_encode(array('Content' => $form->forAjaxTemplate()->forTemplate()));
1159
			$this->getResponse()->addHeader('Content-Type', 'application/json');
1160
			$this->getResponse()->setBody($body);
1161
			return $body;
1162
		}
1163
1164
		$form->setFormAction($this->getRequest()->getURL() . '/DeployForm');
1165
		return $form;
1166
	}
1167
1168
	/**
1169
	 * @param SS_HTTPRequest $request
1170
	 *
1171
	 * @return SS_HTTPResponse|string
1172
	 */
1173
	public function gitRevisions(SS_HTTPRequest $request) {
1174
1175
		// Performs canView permission check by limiting visible projects
1176
		$project = $this->getCurrentProject();
1177
		if(!$project) {
1178
			return $this->project404Response();
1179
		}
1180
1181
		// Performs canView permission check by limiting visible projects
1182
		$env = $this->getCurrentEnvironment($project);
1183
		if(!$env) {
1184
			return $this->environment404Response();
1185
		}
1186
1187
		// For now only permit advanced options on one environment type, because we hacked the "full-deploy"
1188
		// checkbox in. Other environments such as the fast or capistrano one wouldn't know what to do with it.
1189
		if(get_class($env) === 'RainforestEnvironment') {
1190
			$advanced = Permission::check('DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS') ? 'true' : 'false';
1191
		} else {
1192
			$advanced = 'false';
1193
		}
1194
1195
		$tabs = array();
1196
		$id = 0;
1197
		$data = array(
1198
			'id' => ++$id,
1199
			'name' => 'Deploy the latest version of a branch',
1200
			'field_type' => 'dropdown',
1201
			'field_label' => 'Choose a branch',
1202
			'field_id' => 'branch',
1203
			'field_data' => array(),
1204
			'advanced_opts' => $advanced
1205
		);
1206
		foreach($project->DNBranchList() as $branch) {
1207
			$sha = $branch->SHA();
1208
			$name = $branch->Name();
1209
			$branchValue = sprintf("%s (%s, %s old)",
1210
				$name,
1211
				substr($sha, 0, 8),
1212
				$branch->LastUpdated()->TimeDiff()
1213
			);
1214
			$data['field_data'][] = array(
1215
				'id' => $sha,
1216
				'text' => $branchValue
1217
			);
1218
		}
1219
		$tabs[] = $data;
1220
1221
		$data = array(
1222
			'id' => ++$id,
1223
			'name' => 'Deploy a tagged release',
1224
			'field_type' => 'dropdown',
1225
			'field_label' => 'Choose a tag',
1226
			'field_id' => 'tag',
1227
			'field_data' => array(),
1228
			'advanced_opts' => $advanced
1229
		);
1230
1231
		foreach($project->DNTagList()->setLimit(null) as $tag) {
1232
			$name = $tag->Name();
1233
			$data['field_data'][] = array(
1234
				'id' => $tag->SHA(),
1235
				'text' => sprintf("%s", $name)
1236
			);
1237
		}
1238
1239
		// show newest tags first.
1240
		$data['field_data'] = array_reverse($data['field_data']);
1241
1242
		$tabs[] = $data;
1243
1244
		// Past deployments
1245
		$data = array(
1246
			'id' => ++$id,
1247
			'name' => 'Redeploy a release that was previously deployed (to any environment)',
1248
			'field_type' => 'dropdown',
1249
			'field_label' => 'Choose a previously deployed release',
1250
			'field_id' => 'release',
1251
			'field_data' => array(),
1252
			'advanced_opts' => $advanced
1253
		);
1254
		// We are aiming at the format:
1255
		// [{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...
1256
		$redeploy = array();
1257
		foreach($project->DNEnvironmentList() as $dnEnvironment) {
1258
			$envName = $dnEnvironment->Name;
1259
			$perEnvDeploys = array();
1260
1261
			foreach($dnEnvironment->DeployHistory() as $deploy) {
1262
				$sha = $deploy->SHA;
1263
1264
				// Check if exists to make sure the newest deployment date is used.
1265
				if(!isset($perEnvDeploys[$sha])) {
1266
					$pastValue = sprintf("%s (deployed %s)",
1267
						substr($sha, 0, 8),
1268
						$deploy->obj('LastEdited')->Ago()
1269
					);
1270
					$perEnvDeploys[$sha] = array(
1271
						'id' => $sha,
1272
						'text' => $pastValue
1273
					);
1274
				}
1275
			}
1276
1277
			if(!empty($perEnvDeploys)) {
1278
				$redeploy[$envName] = array_values($perEnvDeploys);
1279
			}
1280
		}
1281
		// Convert the array to the frontend format (i.e. keyed to regular array)
1282
		foreach($redeploy as $env => $descr) {
1283
			$data['field_data'][] = array('text'=>$env, 'children'=>$descr);
1284
		}
1285
		$tabs[] = $data;
1286
1287
		$data = array(
1288
			'id' => ++$id,
1289
			'name' => 'Deploy a specific SHA',
1290
			'field_type' => 'textfield',
1291
			'field_label' => 'Choose a SHA',
1292
			'field_id' => 'SHA',
1293
			'field_data' => array(),
1294
			'advanced_opts' => $advanced
1295
		);
1296
		$tabs[] = $data;
1297
1298
		// get the last time git fetch was run
1299
		$lastFetched = 'never';
1300
		$fetch = DNGitFetch::get()
1301
			->filter('ProjectID', $project->ID)
1302
			->sort('LastEdited', 'DESC')
1303
			->first();
1304
		if($fetch) {
1305
			$lastFetched = $fetch->dbObject('LastEdited')->Ago();
1306
		}
1307
1308
		$data = array(
1309
			'Tabs' => $tabs,
1310
			'last_fetched' => $lastFetched
1311
		);
1312
1313
		return json_encode($data, JSON_PRETTY_PRINT);
1314
	}
1315
1316
	/**
1317
	 * Check and regenerate a global CSRF token
1318
	 *
1319
	 * @param SS_HTTPRequest $request
1320
	 * @param bool $resetToken
1321
	 *
1322
	 * @return bool
1323
	 */
1324
	protected function checkCsrfToken(SS_HTTPRequest $request, $resetToken = true) {
1325
		$token = SecurityToken::inst();
1326
1327
		// Ensure the submitted token has a value
1328
		$submittedToken = $request->postVar('SecurityID');
1329
		if(!$submittedToken) {
1330
			return false;
1331
		}
1332
1333
		// Do the actual check.
1334
		$check = $token->check($submittedToken);
1335
1336
		// Reset the token after we've checked the existing token
1337
		if($resetToken) {
1338
			$token->reset();
1339
		}
1340
1341
		// Return whether the token was correct or not
1342
		return $check;
1343
	}
1344
1345
	/**
1346
	 * @param SS_HTTPRequest $request
1347
	 *
1348
	 * @return string
1349
	 */
1350
	public function deploySummary(SS_HTTPRequest $request) {
1351
1352
		// Performs canView permission check by limiting visible projects
1353
		$project = $this->getCurrentProject();
1354
		if(!$project) {
1355
			return $this->project404Response();
1356
		}
1357
1358
		// Performs canView permission check by limiting visible projects
1359
		$environment = $this->getCurrentEnvironment($project);
1360
		if(!$environment) {
1361
			return $this->environment404Response();
1362
		}
1363
1364
		// Plan the deployment.
1365
		$strategy = $environment->Backend()->planDeploy(
1366
			$environment,
1367
			$request->requestVars()
0 ignored issues
show
Bug introduced by
It seems like $request->requestVars() targeting SS_HTTPRequest::requestVars() can also be of type null; however, DeploymentBackend::planDeploy() does only seem to accept array, maybe add an additional type check?

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

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

An additional type check may prevent trouble.

Loading history...
1368
		);
1369
		$data = $strategy->toArray();
1370
1371
		// Add in a URL for comparing from->to code changes. Ensure that we have
1372
		// two proper 40 character SHAs, otherwise we can't show the compare link.
1373
		if(
1374
			!empty($data['changes']['Code version']['from'])
1375
			&& strlen($data['changes']['Code version']['from']) == '40'
1376
			&& !empty($data['changes']['Code version']['to'])
1377
			&& strlen($data['changes']['Code version']['to']) == '40'
1378
		) {
1379
			$interface = $project->getRepositoryInterface();
1380
			$compareurl = sprintf(
1381
				'%s/compare/%s...%s',
1382
				$interface->URL,
1383
				$data['changes']['Code version']['from'],
1384
				$data['changes']['Code version']['to']
1385
			);
1386
			$data['changes']['Code version']['compareUrl'] = $compareurl;
1387
		}
1388
1389
		// Append json to response
1390
		$token = SecurityToken::inst();
1391
		$data['SecurityID'] = $token->getValue();
1392
1393
		return json_encode($data);
1394
	}
1395
1396
	/**
1397
	 * Deployment form submission handler.
1398
	 *
1399
	 * Initiate a DNDeployment record and redirect to it for status polling
1400
	 *
1401
	 * @param SS_HTTPRequest $request
1402
	 *
1403
	 * @return SS_HTTPResponse
1404
	 * @throws ValidationException
1405
	 * @throws null
1406
	 */
1407
	public function startDeploy(SS_HTTPRequest $request) {
1408
1409
		// Ensure the CSRF Token is correct
1410
		if(!$this->checkCsrfToken($request)) {
1411
			// CSRF token didn't match
1412
			return $this->httpError(400, 'Bad Request');
1413
		}
1414
1415
		// Performs canView permission check by limiting visible projects
1416
		$project = $this->getCurrentProject();
1417
		if(!$project) {
1418
			return $this->project404Response();
1419
		}
1420
1421
		// Performs canView permission check by limiting visible projects
1422
		$environment = $this->getCurrentEnvironment($project);
1423
		if(!$environment) {
1424
			return $this->environment404Response();
1425
		}
1426
1427
		// Initiate the deployment
1428
		// The extension point should pass in: Project, Environment, SelectRelease, buildName
1429
		$this->extend('doDeploy', $project, $environment, $buildName, $data);
1430
1431
		// Start the deployment based on the approved strategy.
1432
		$strategy = new DeploymentStrategy($environment);
1433
		$strategy->fromArray($request->requestVar('strategy'));
1434
		$deployment = $strategy->createDeployment();
1435
		$deployment->start();
1436
1437
		return json_encode(array(
1438
			'url' => Director::absoluteBaseURL() . $deployment->Link()
1439
		), JSON_PRETTY_PRINT);
1440
	}
1441
1442
	/**
1443
	 * Action - Do the actual deploy
1444
	 *
1445
	 * @param SS_HTTPRequest $request
1446
	 *
1447
	 * @return SS_HTTPResponse|string
1448
	 * @throws SS_HTTPResponse_Exception
1449
	 */
1450
	public function deploy(SS_HTTPRequest $request) {
1451
		$params = $request->params();
1452
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1453
1454
		if(!$deployment || !$deployment->ID) {
1455
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1456
		}
1457
		if(!$deployment->canView()) {
1458
			return Security::permissionFailure();
1459
		}
1460
1461
		$environment = $deployment->Environment();
1462
		$project = $environment->Project();
1463
1464
		if($environment->Name != $params['Environment']) {
1465
			throw new LogicException("Environment in URL doesn't match this deploy");
1466
		}
1467
		if($project->Name != $params['Project']) {
1468
			throw new LogicException("Project in URL doesn't match this deploy");
1469
		}
1470
1471
		return $this->render(array(
1472
			'Deployment' => $deployment,
1473
		));
1474
	}
1475
1476
1477
	/**
1478
	 * Action - Get the latest deploy log
1479
	 *
1480
	 * @param SS_HTTPRequest $request
1481
	 *
1482
	 * @return string
1483
	 * @throws SS_HTTPResponse_Exception
1484
	 */
1485
	public function deploylog(SS_HTTPRequest $request) {
1486
		$params = $request->params();
1487
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1488
1489
		if(!$deployment || !$deployment->ID) {
1490
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1491
		}
1492
		if(!$deployment->canView()) {
1493
			return Security::permissionFailure();
1494
		}
1495
1496
		$environment = $deployment->Environment();
1497
		$project = $environment->Project();
1498
1499
		if($environment->Name != $params['Environment']) {
1500
			throw new LogicException("Environment in URL doesn't match this deploy");
1501
		}
1502
		if($project->Name != $params['Project']) {
1503
			throw new LogicException("Project in URL doesn't match this deploy");
1504
		}
1505
1506
		$log = $deployment->log();
1507
		if($log->exists()) {
1508
			$content = $log->content();
1509
		} else {
1510
			$content = 'Waiting for action to start';
1511
		}
1512
1513
		return $this->sendResponse($deployment->ResqueStatus(), $content);
1514
	}
1515
1516
	/**
1517
	 * @param SS_HTTPRequest|null $request
1518
	 *
1519
	 * @return Form
1520
	 */
1521
	public function getDataTransferForm(SS_HTTPRequest $request = null) {
1522
		// Performs canView permission check by limiting visible projects
1523
		$envs = $this->getCurrentProject()->DNEnvironmentList()->filterByCallback(function($item) {
1524
			return $item->canBackup();
1525
		});
1526
1527
		if(!$envs) {
1528
			return $this->environment404Response();
1529
		}
1530
1531
		$form = Form::create(
1532
			$this,
1533
			'DataTransferForm',
1534
			FieldList::create(
1535
				HiddenField::create('Direction', null, 'get'),
1536
				DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1537
					->setEmptyString('Select an environment'),
1538
				DropdownField::create('Mode', 'Transfer', DNDataArchive::get_mode_map())
1539
			),
1540
			FieldList::create(
1541
				FormAction::create('doDataTransfer', 'Create')
1542
					->addExtraClass('btn')
1543
			)
1544
		);
1545
		$form->setFormAction($this->getRequest()->getURL() . '/DataTransferForm');
1546
1547
		return $form;
1548
	}
1549
1550
	/**
1551
	 * @param array $data
1552
	 * @param Form $form
1553
	 *
1554
	 * @return SS_HTTPResponse
1555
	 * @throws SS_HTTPResponse_Exception
1556
	 */
1557
	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...
1558
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1559
1560
		// Performs canView permission check by limiting visible projects
1561
		$project = $this->getCurrentProject();
1562
		if(!$project) {
1563
			return $this->project404Response();
1564
		}
1565
1566
		$dataArchive = null;
1567
1568
		// Validate direction.
1569
		if($data['Direction'] == 'get') {
1570
			$validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1571
				->filterByCallback(function($item) {
1572
					return $item->canBackup();
1573
				});
1574
		} else if($data['Direction'] == 'push') {
1575
			$validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1576
				->filterByCallback(function($item) {
1577
					return $item->canRestore();
1578
				});
1579
		} else {
1580
			throw new LogicException('Invalid direction');
1581
		}
1582
1583
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
1584
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
1585
		if(!$environment) {
1586
			throw new LogicException('Invalid environment');
1587
		}
1588
1589
		$this->validateSnapshotMode($data['Mode']);
1590
1591
1592
		// Only 'push' direction is allowed an association with an existing archive.
1593
		if(
1594
			$data['Direction'] == 'push'
1595
			&& isset($data['DataArchiveID'])
1596
			&& is_numeric($data['DataArchiveID'])
1597
		) {
1598
			$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1599
			if(!$dataArchive) {
1600
				throw new LogicException('Invalid data archive');
1601
			}
1602
1603
			if(!$dataArchive->canDownload()) {
1604
				throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1605
			}
1606
		}
1607
1608
		$transfer = DNDataTransfer::create();
1609
		$transfer->EnvironmentID = $environment->ID;
1610
		$transfer->Direction = $data['Direction'];
1611
		$transfer->Mode = $data['Mode'];
1612
		$transfer->DataArchiveID = $dataArchive ? $dataArchive->ID : null;
1613
		if($data['Direction'] == 'push') {
1614
			$transfer->setBackupBeforePush(!empty($data['BackupBeforePush']));
1615
		}
1616
		$transfer->write();
1617
		$transfer->start();
1618
1619
		return $this->redirect($transfer->Link());
1620
	}
1621
1622
	/**
1623
	 * View into the log for a {@link DNDataTransfer}.
1624
	 *
1625
	 * @param SS_HTTPRequest $request
1626
	 *
1627
	 * @return SS_HTTPResponse|string
1628
	 * @throws SS_HTTPResponse_Exception
1629
	 */
1630
	public function transfer(SS_HTTPRequest $request) {
1631
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1632
1633
		$params = $request->params();
1634
		$transfer = DNDataTransfer::get()->byId($params['Identifier']);
1635
1636
		if(!$transfer || !$transfer->ID) {
1637
			throw new SS_HTTPResponse_Exception('Transfer not found', 404);
1638
		}
1639
		if(!$transfer->canView()) {
1640
			return Security::permissionFailure();
1641
		}
1642
1643
		$environment = $transfer->Environment();
1644
		$project = $environment->Project();
1645
1646
		if($project->Name != $params['Project']) {
1647
			throw new LogicException("Project in URL doesn't match this deploy");
1648
		}
1649
1650
		return $this->render(array(
1651
			'CurrentTransfer' => $transfer,
1652
			'SnapshotsSection' => 1,
1653
		));
1654
	}
1655
1656
	/**
1657
	 * Action - Get the latest deploy log
1658
	 *
1659
	 * @param SS_HTTPRequest $request
1660
	 *
1661
	 * @return string
1662
	 * @throws SS_HTTPResponse_Exception
1663
	 */
1664 View Code Duplication
	public function transferlog(SS_HTTPRequest $request) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1665
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1666
1667
		$params = $request->params();
1668
		$transfer = DNDataTransfer::get()->byId($params['Identifier']);
1669
1670
		if(!$transfer || !$transfer->ID) {
1671
			throw new SS_HTTPResponse_Exception('Transfer not found', 404);
1672
		}
1673
		if(!$transfer->canView()) {
1674
			return Security::permissionFailure();
1675
		}
1676
1677
		$environment = $transfer->Environment();
1678
		$project = $environment->Project();
1679
1680
		if($project->Name != $params['Project']) {
1681
			throw new LogicException("Project in URL doesn't match this deploy");
1682
		}
1683
1684
		$log = $transfer->log();
1685
		if($log->exists()) {
1686
			$content = $log->content();
1687
		} else {
1688
			$content = 'Waiting for action to start';
1689
		}
1690
1691
		return $this->sendResponse($transfer->ResqueStatus(), $content);
1692
	}
1693
1694
	/**
1695
	 * Note: Submits to the same action as {@link getDataTransferForm()},
1696
	 * but with a Direction=push and an archive reference.
1697
	 *
1698
	 * @param SS_HTTPRequest $request
1699
	 * @param DNDataArchive|null $dataArchive Only set when method is called manually in {@link restore()},
1700
	 *                            otherwise the state is inferred from the request data.
1701
	 * @return Form
1702
	 */
1703
	public function getDataTransferRestoreForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) {
1704
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1705
1706
		// Performs canView permission check by limiting visible projects
1707
		$project = $this->getCurrentProject();
1708
		$envs = $project->DNEnvironmentList()->filterByCallback(function($item) {
1709
			return $item->canRestore();
1710
		});
1711
1712
		if(!$envs) {
1713
			return $this->environment404Response();
1714
		}
1715
1716
		$modesMap = array();
1717
		if(in_array($dataArchive->Mode, array('all'))) {
1718
			$modesMap['all'] = 'Database and Assets';
1719
		};
1720
		if(in_array($dataArchive->Mode, array('all', 'db'))) {
1721
			$modesMap['db'] = 'Database only';
1722
		};
1723
		if(in_array($dataArchive->Mode, array('all', 'assets'))) {
1724
			$modesMap['assets'] = 'Assets only';
1725
		};
1726
1727
		$alertMessage = '<div class="alert alert-warning"><strong>Warning:</strong> '
1728
			. 'This restore will overwrite the data on the chosen environment below</div>';
1729
1730
		$form = Form::create(
1731
			$this,
1732
			'DataTransferRestoreForm',
1733
			FieldList::create(
1734
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1735
				HiddenField::create('Direction', null, 'push'),
1736
				LiteralField::create('Warning', $alertMessage),
1737
				DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1738
					->setEmptyString('Select an environment'),
1739
				DropdownField::create('Mode', 'Transfer', $modesMap),
1740
				CheckboxField::create('BackupBeforePush', 'Backup existing data', '1')
1741
			),
1742
			FieldList::create(
1743
				FormAction::create('doDataTransfer', 'Restore Data')
1744
					->addExtraClass('btn')
1745
			)
1746
		);
1747
		$form->setFormAction($project->Link() . '/DataTransferRestoreForm');
1748
1749
		return $form;
1750
	}
1751
1752
	/**
1753
	 * View a form to restore a specific {@link DataArchive}.
1754
	 * Permission checks are handled in {@link DataArchives()}.
1755
	 * Submissions are handled through {@link doDataTransfer()}, same as backup operations.
1756
	 *
1757
	 * @param SS_HTTPRequest $request
1758
	 *
1759
	 * @return HTMLText
1760
	 * @throws SS_HTTPResponse_Exception
1761
	 */
1762 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...
1763
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1764
1765
		/** @var DNDataArchive $dataArchive */
1766
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1767
1768
		if(!$dataArchive) {
1769
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1770
		}
1771
1772
		// We check for canDownload because that implies access to the data.
1773
		// canRestore is later checked on the actual restore action per environment.
1774
		if(!$dataArchive->canDownload()) {
1775
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1776
		}
1777
1778
		$form = $this->getDataTransferRestoreForm($this->request, $dataArchive);
1779
1780
		// View currently only available via ajax
1781
		return $form->forTemplate();
1782
	}
1783
1784
	/**
1785
	 * View a form to delete a specific {@link DataArchive}.
1786
	 * Permission checks are handled in {@link DataArchives()}.
1787
	 * Submissions are handled through {@link doDelete()}.
1788
	 *
1789
	 * @param SS_HTTPRequest $request
1790
	 *
1791
	 * @return HTMLText
1792
	 * @throws SS_HTTPResponse_Exception
1793
	 */
1794 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...
1795
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1796
1797
		/** @var DNDataArchive $dataArchive */
1798
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1799
1800
		if(!$dataArchive) {
1801
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1802
		}
1803
1804
		if(!$dataArchive->canDelete()) {
1805
			throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403);
1806
		}
1807
1808
		$form = $this->getDeleteForm($this->request, $dataArchive);
1809
1810
		// View currently only available via ajax
1811
		return $form->forTemplate();
1812
	}
1813
1814
	/**
1815
	 * @param SS_HTTPRequest $request
1816
	 * @param DNDataArchive|null $dataArchive Only set when method is called manually, otherwise the state is inferred
1817
	 *        from the request data.
1818
	 * @return Form
1819
	 */
1820
	public function getDeleteForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) {
1821
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1822
1823
		// Performs canView permission check by limiting visible projects
1824
		$project = $this->getCurrentProject();
1825
		if(!$project) {
1826
			return $this->project404Response();
1827
		}
1828
1829
		$snapshotDeleteWarning = '<div class="alert alert-warning">'
1830
			. 'Are you sure you want to permanently delete this snapshot from this archive area?'
1831
			. '</div>';
1832
1833
		$form = Form::create(
1834
			$this,
1835
			'DeleteForm',
1836
			FieldList::create(
1837
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1838
				LiteralField::create('Warning', $snapshotDeleteWarning)
1839
			),
1840
			FieldList::create(
1841
				FormAction::create('doDelete', 'Delete')
1842
					->addExtraClass('btn')
1843
			)
1844
		);
1845
		$form->setFormAction($project->Link() . '/DeleteForm');
1846
1847
		return $form;
1848
	}
1849
1850
	/**
1851
	 * @param array $data
1852
	 * @param Form $form
1853
	 *
1854
	 * @return bool|SS_HTTPResponse
1855
	 * @throws SS_HTTPResponse_Exception
1856
	 */
1857
	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...
1858
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1859
1860
		// Performs canView permission check by limiting visible projects
1861
		$project = $this->getCurrentProject();
1862
		if(!$project) {
1863
			return $this->project404Response();
1864
		}
1865
1866
		$dataArchive = null;
1867
1868
		if(
1869
			isset($data['DataArchiveID'])
1870
			&& is_numeric($data['DataArchiveID'])
1871
		) {
1872
			$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1873
		}
1874
1875
		if(!$dataArchive) {
1876
			throw new LogicException('Invalid data archive');
1877
		}
1878
1879
		if(!$dataArchive->canDelete()) {
1880
			throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403);
1881
		}
1882
1883
		$dataArchive->delete();
1884
1885
		return $this->redirectBack();
1886
	}
1887
1888
	/**
1889
	 * View a form to move a specific {@link DataArchive}.
1890
	 *
1891
	 * @param SS_HTTPRequest $request
1892
	 *
1893
	 * @return HTMLText
1894
	 * @throws SS_HTTPResponse_Exception
1895
	 */
1896 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...
1897
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1898
1899
		/** @var DNDataArchive $dataArchive */
1900
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1901
1902
		if(!$dataArchive) {
1903
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1904
		}
1905
1906
		// We check for canDownload because that implies access to the data.
1907
		if(!$dataArchive->canDownload()) {
1908
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1909
		}
1910
1911
		$form = $this->getMoveForm($this->request, $dataArchive);
1912
1913
		// View currently only available via ajax
1914
		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...
1915
	}
1916
1917
	/**
1918
	 * Build snapshot move form.
1919
	 *
1920
	 * @param SS_HTTPRequest $request
1921
	 * @param DNDataArchive|null $dataArchive
1922
	 *
1923
	 * @return Form|SS_HTTPResponse
1924
	 */
1925
	public function getMoveForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) {
1926
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1927
1928
		$envs = $dataArchive->validTargetEnvironments();
1929
		if(!$envs) {
1930
			return $this->environment404Response();
1931
		}
1932
1933
		$warningMessage = '<div class="alert alert-warning"><strong>Warning:</strong> This will make the snapshot '
1934
			. 'available to people with access to the target environment.<br>By pressing "Change ownership" you '
1935
			. 'confirm that you have considered data confidentiality regulations.</div>';
1936
1937
		$form = Form::create(
1938
			$this,
1939
			'MoveForm',
1940
			FieldList::create(
1941
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1942
				LiteralField::create('Warning', $warningMessage),
1943
				DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1944
					->setEmptyString('Select an environment')
1945
			),
1946
			FieldList::create(
1947
				FormAction::create('doMove', 'Change ownership')
1948
					->addExtraClass('btn')
1949
			)
1950
		);
1951
		$form->setFormAction($this->getCurrentProject()->Link() . '/MoveForm');
1952
1953
		return $form;
1954
	}
1955
1956
	/**
1957
	 * @param array $data
1958
	 * @param Form $form
1959
	 *
1960
	 * @return bool|SS_HTTPResponse
1961
	 * @throws SS_HTTPResponse_Exception
1962
	 * @throws ValidationException
1963
	 * @throws null
1964
	 */
1965
	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...
1966
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1967
1968
		// Performs canView permission check by limiting visible projects
1969
		$project = $this->getCurrentProject();
1970
		if(!$project) {
1971
			return $this->project404Response();
1972
		}
1973
1974
		/** @var DNDataArchive $dataArchive */
1975
		$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1976
		if(!$dataArchive) {
1977
			throw new LogicException('Invalid data archive');
1978
		}
1979
1980
		// We check for canDownload because that implies access to the data.
1981
		if(!$dataArchive->canDownload()) {
1982
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1983
		}
1984
1985
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
1986
		$validEnvs = $dataArchive->validTargetEnvironments();
1987
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
1988
		if(!$environment) {
1989
			throw new LogicException('Invalid environment');
1990
		}
1991
1992
		$dataArchive->EnvironmentID = $environment->ID;
1993
		$dataArchive->write();
1994
1995
		return $this->redirectBack();
1996
	}
1997
1998
	/**
1999
	 * Returns an error message if redis is unavailable
2000
	 *
2001
	 * @return string
2002
	 */
2003
	public static function RedisUnavailable() {
2004
		try {
2005
			Resque::queues();
2006
		} catch(Exception $e) {
2007
			return $e->getMessage();
2008
		}
2009
		return '';
2010
	}
2011
2012
	/**
2013
	 * Returns the number of connected Redis workers
2014
	 *
2015
	 * @return int
2016
	 */
2017
	public static function RedisWorkersCount() {
2018
		return count(Resque_Worker::all());
2019
	}
2020
2021
	/**
2022
	 * @return array
2023
	 */
2024
	public function providePermissions() {
2025
		return array(
2026
			self::DEPLOYNAUT_BYPASS_PIPELINE => array(
2027
				'name' => "Bypass Pipeline",
2028
				'category' => "Deploynaut",
2029
				'help' => "Enables users to directly initiate deployments, bypassing any pipeline",
2030
			),
2031
			self::DEPLOYNAUT_DRYRUN_PIPELINE => array(
2032
				'name' => 'Dry-run Pipeline',
2033
				'category' => 'Deploynaut',
2034
				'help' => 'Enable dry-run execution of pipelines for testing'
2035
			),
2036
			self::DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS => array(
2037
				'name' => "Access to advanced deploy options",
2038
				'category' => "Deploynaut",
2039
			),
2040
2041
			// Permissions that are intended to be added to the roles.
2042
			self::ALLOW_PROD_DEPLOYMENT => array(
2043
				'name' => "Ability to deploy to production environments",
2044
				'category' => "Deploynaut",
2045
			),
2046
			self::ALLOW_NON_PROD_DEPLOYMENT => array(
2047
				'name' => "Ability to deploy to non-production environments",
2048
				'category' => "Deploynaut",
2049
			),
2050
			self::ALLOW_PROD_SNAPSHOT => array(
2051
				'name' => "Ability to make production snapshots",
2052
				'category' => "Deploynaut",
2053
			),
2054
			self::ALLOW_NON_PROD_SNAPSHOT => array(
2055
				'name' => "Ability to make non-production snapshots",
2056
				'category' => "Deploynaut",
2057
			),
2058
			self::ALLOW_CREATE_ENVIRONMENT => array(
2059
				'name' => "Ability to create environments",
2060
				'category' => "Deploynaut",
2061
			),
2062
		);
2063
	}
2064
2065
	/**
2066
	 * @return DNProject|null
2067
	 */
2068
	public function getCurrentProject() {
2069
		$projectName = trim($this->getRequest()->param('Project'));
2070
		if(!$projectName) {
2071
			return null;
2072
		}
2073
		if(empty(self::$_project_cache[$projectName])) {
2074
			self::$_project_cache[$projectName] = $this->DNProjectList()->filter('Name', $projectName)->First();
2075
		}
2076
		return self::$_project_cache[$projectName];
2077
	}
2078
2079
	/**
2080
	 * @param DNProject|null $project
2081
	 * @return DNEnvironment|null
2082
	 */
2083
	public function getCurrentEnvironment(DNProject $project = null) {
2084
		if($this->getRequest()->param('Environment') === null) {
2085
			return null;
2086
		}
2087
		if($project === null) {
2088
			$project = $this->getCurrentProject();
2089
		}
2090
		// project can still be null
2091
		if($project === null) {
2092
			return null;
2093
		}
2094
		return $project->DNEnvironmentList()->filter('Name', $this->getRequest()->param('Environment'))->First();
2095
	}
2096
2097
	/**
2098
	 * This will return a const that indicates the class of action currently being performed
2099
	 *
2100
	 * Until DNRoot is de-godded, it does a bunch of different actions all in the same class.
2101
	 * So we just have each action handler calll setCurrentActionType to define what sort of
2102
	 * action it is.
2103
	 *
2104
	 * @return string - one of the consts from self::$action_types
2105
	 */
2106
	public function getCurrentActionType() {
2107
		return $this->actionType;
2108
	}
2109
2110
	/**
2111
	 * Sets the current action type
2112
	 *
2113
	 * @param string $actionType string - one of the consts from self::$action_types
2114
	 */
2115
	public function setCurrentActionType($actionType) {
2116
		$this->actionType = $actionType;
2117
	}
2118
2119
	/**
2120
	 * Helper method to allow templates to know whether they should show the 'Archive List' include or not.
2121
	 * The actual permissions are set on a per-environment level, so we need to find out if this $member can upload to
2122
	 * or download from *any* {@link DNEnvironment} that (s)he has access to.
2123
	 *
2124
	 * TODO To be replaced with a method that just returns the list of archives this {@link Member} has access to.
2125
	 *
2126
	 * @param Member|null $member The {@link Member} to check (or null to check the currently logged in Member)
2127
	 * @return boolean|null true if $member has access to upload or download to at least one {@link DNEnvironment}.
2128
	 */
2129
	public function CanViewArchives(Member $member = null) {
2130
		if($member === null) {
2131
			$member = Member::currentUser();
2132
		}
2133
2134
		if(Permission::checkMember($member, 'ADMIN')) {
2135
			return true;
2136
		}
2137
2138
		$allProjects = $this->DNProjectList();
2139
		if(!$allProjects) {
2140
			return false;
2141
		}
2142
2143
		foreach($allProjects as $project) {
2144
			if($project->Environments()) {
2145
				foreach($project->Environments() as $environment) {
2146
					if(
2147
						$environment->canRestore($member) ||
2148
						$environment->canBackup($member) ||
2149
						$environment->canUploadArchive($member) ||
2150
						$environment->canDownloadArchive($member)
2151
					) {
2152
						// We can return early as we only need to know that we can access one environment
2153
						return true;
2154
					}
2155
				}
2156
			}
2157
		}
2158
	}
2159
2160
	/**
2161
	 * Returns a list of attempted environment creations.
2162
	 *
2163
	 * @return PaginatedList
2164
	 */
2165
	public function CreateEnvironmentList() {
2166
		$project = $this->getCurrentProject();
2167
		if($project) {
2168
			return new PaginatedList($project->CreateEnvironments()->sort("Created DESC"), $this->request);
0 ignored issues
show
Bug introduced by
The method CreateEnvironments() does not exist on DNProject. Did you maybe mean canCreateEnvironments()?

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

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

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2171
	}
2172
2173
	/**
2174
	 * Returns a list of all archive files that can be accessed by the currently logged-in {@link Member}
2175
	 *
2176
	 * @return PaginatedList
2177
	 */
2178
	public function CompleteDataArchives() {
2179
		$project = $this->getCurrentProject();
2180
		$archives = new ArrayList();
2181
2182
		$archiveList = $project->Environments()->relation("DataArchives");
2183
		if($archiveList->count() > 0) {
2184
			foreach($archiveList as $archive) {
2185
				if($archive->canView() && !$archive->isPending()) {
2186
					$archives->push($archive);
2187
				}
2188
			}
2189
		}
2190
		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...
2191
	}
2192
2193
	/**
2194
	 * @return PaginatedList The list of "pending" data archives which are waiting for a file
2195
	 * to be delivered offline by post, and manually uploaded into the system.
2196
	 */
2197
	public function PendingDataArchives() {
2198
		$project = $this->getCurrentProject();
2199
		$archives = new ArrayList();
2200
		foreach($project->DNEnvironmentList() as $env) {
2201
			foreach($env->DataArchives() as $archive) {
2202
				if($archive->canView() && $archive->isPending()) {
2203
					$archives->push($archive);
2204
				}
2205
			}
2206
		}
2207
		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...
2208
	}
2209
2210
	/**
2211
	 * @return PaginatedList
2212
	 */
2213
	public function DataTransferLogs() {
2214
		$project = $this->getCurrentProject();
2215
2216
		$transfers = DNDataTransfer::get()->filterByCallback(function($record) use($project) {
2217
			return
2218
				$record->Environment()->Project()->ID == $project->ID && // Ensure only the current Project is shown
2219
				(
2220
					$record->Environment()->canRestore() || // Ensure member can perform an action on the transfers env
2221
					$record->Environment()->canBackup() ||
2222
					$record->Environment()->canUploadArchive() ||
2223
					$record->Environment()->canDownloadArchive()
2224
				);
2225
		});
2226
2227
		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...
2228
	}
2229
2230
	/**
2231
	 * @return null|PaginatedList
2232
	 */
2233
	public function DeployHistory() {
2234
		if($env = $this->getCurrentEnvironment()) {
2235
			$history = $env->DeployHistory();
2236
			if($history->count() > 0) {
2237
				$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...
2238
				$pagination->setPageLength(8);
2239
				return $pagination;
2240
			}
2241
		}
2242
		return null;
2243
	}
2244
2245
	/**
2246
	 * @return SS_HTTPResponse
2247
	 */
2248
	protected function project404Response() {
2249
		return new SS_HTTPResponse(
2250
			"Project '" . Convert::raw2xml($this->getRequest()->param('Project')) . "' not found.",
2251
			404
2252
		);
2253
	}
2254
2255
	/**
2256
	 * @return SS_HTTPResponse
2257
	 */
2258
	protected function environment404Response() {
2259
		$envName = Convert::raw2xml($this->getRequest()->param('Environment'));
2260
		return new SS_HTTPResponse("Environment '" . $envName . "' not found.", 404);
2261
	}
2262
2263
	/**
2264
	 * @param string $status
2265
	 * @param string $content
2266
	 *
2267
	 * @return string
2268
	 */
2269
	protected function sendResponse($status, $content) {
2270
		// strip excessive newlines
2271
		$content = preg_replace('/(?:(?:\r\n|\r|\n)\s*){2}/s', "\n", $content);
2272
2273
		$sendJSON = (strpos($this->getRequest()->getHeader('Accept'), 'application/json') !== false)
2274
			|| $this->getRequest()->getExtension() == 'json';
2275
2276
		if(!$sendJSON) {
2277
			$this->response->addHeader("Content-type", "text/plain");
2278
			return $content;
2279
		}
2280
		$this->response->addHeader("Content-type", "application/json");
2281
		return json_encode(array(
2282
			'status' => $status,
2283
			'content' => $content,
2284
		));
2285
	}
2286
2287
	/**
2288
	 * Validate the snapshot mode
2289
	 *
2290
	 * @param string $mode
2291
	 */
2292
	protected function validateSnapshotMode($mode) {
2293
		if(!in_array($mode, array('all', 'assets', 'db'))) {
2294
			throw new LogicException('Invalid mode');
2295
		}
2296
	}
2297
2298
	/**
2299
	 * @param string $sectionName
2300
	 * @param string $title
2301
	 *
2302
	 * @return SS_HTTPResponse
2303
	 */
2304
	protected function getCustomisedViewSection($sectionName, $title = '') {
2305
		// Performs canView permission check by limiting visible projects
2306
		$project = $this->getCurrentProject();
2307
		if(!$project) {
2308
			return $this->project404Response();
2309
		}
2310
		$data = array(
2311
			$sectionName => 1,
2312
		);
2313
2314
		if($this !== '') {
2315
			$data['Title'] = $title;
2316
		}
2317
2318
		return $this->customise($data)->render();
2319
	}
2320
2321
	/**
2322
	 * Get items for the ambient menu that should be accessible from all pages.
2323
	 *
2324
	 * @return ArrayList
2325
	 */
2326
	public function AmbientMenu() {
2327
		$list = new ArrayList();
2328
2329
		if (Member::currentUserID()) {
2330
			$list->push(new ArrayData(array(
2331
				'Classes' => 'logout',
2332
				'FaIcon' => 'sign-out',
2333
				'Link' => 'Security/logout',
2334
				'Title' => 'Log out',
2335
				'IsCurrent' => false,
2336
				'IsSection' => false
2337
			)));
2338
		}
2339
2340
		$this->extend('updateAmbientMenu', $list);
2341
		return $list;
2342
	}
2343
2344
	/**
2345
	 * Create project action.
2346
	 *
2347
	 * @return SS_HTTPResponse
2348
	 */
2349
	public function createproject(SS_HTTPRequest $request) {
2350
		if($this->canCreateProjects()) {
2351
			return $this->render(['CurrentTitle' => 'Create Stack']);
2352
		}
2353
		return $this->httpError(403);
2354
	}
2355
2356
	/**
2357
	 * Checks whether the user can create a project.
2358
	 *
2359
	 * @return bool
2360
	 */
2361
	protected function canCreateProjects($member = null) {
2362
		if(!$member) $member = Member::currentUser();
2363
		if(!$member) return false;
2364
2365
		return Permission::checkMember($member, 'ADMIN');
2366
	}
2367
		
2368
	/**
2369
	 * @return Form
2370
	 */
2371
	public function CreateProjectForm() {
2372
		$form = Form::create(
2373
			$this, 
2374
			__FUNCTION__, 
2375
			$this->getCreateProjectFormFields(), 
2376
			$this->getCreateProjectFormActions(),
2377
			new RequiredFields('Name', 'CVSPath')
2378
		);
2379
		$this->extend('updateCreateProjectForm', $form);
2380
		return $form;
2381
	}
2382
2383
	/**
2384
	 * @return FieldList
2385
	 */
2386
	protected function getCreateProjectFormFields() {
2387
		$fields = FieldList::create();
2388
		$fields->merge([
2389
			TextField::create('Name', 'Title'),
2390
			TextField::create('CVSPath', 'Git URL'),
2391
			TextField::create('Client', 'Client'),
2392
		]);
2393
		$this->extend('updateCreateProjectFormFields', $fields);
2394
		return $fields;
2395
	}
2396
2397
	/**
2398
	 * @return FieldList
2399
	 */
2400
	protected function getCreateProjectFormActions() {
2401
		$fields = FieldList::create(
2402
			FormAction::create('doCreateProject', 'Create Project')
2403
		);
2404
		$this->extend('updateCreateProjectFormActions', $fields);
2405
		return $fields;
2406
	}
2407
2408
	/**
2409
	 * Does the actual project creation.
2410
	 *
2411
	 * @param $data array
2412
	 * @param $form Form
2413
	 *
2414
	 * @return SS_HTTPResponse
2415
	 */
2416
	public function doCreateProject($data, $form) {
2417
		$form->loadDataFrom($data);
2418
		$project = DNProject::create();
2419
2420
		$form->saveInto($project);
2421
		$this->extend('onBeforeCreateProject', $project, $data, $form);
2422
		try {
2423
			if($project->write() > 0) {
2424
				$this->extend('onAfterCreateProject', $project, $data, $form);
2425
2426
				// If an extension hasn't redirected us, we'll redirect to the project.
2427
				if(!$this->redirectedTo()) {
2428
					return $this->redirect($project->Link());
2429
				} else {
2430
					return $this->response;
2431
				}
2432
			} else {
2433
				$form->sessionMessage('Unable to write the stack to the database.', 'bad');
2434
			}
2435
		} catch (ValidationException $e) {
2436
			$form->sessionMessage($e->getMessage(), 'bad');
2437
		}
2438
		return $this->redirectBack();
2439
	}
2440
2441
}
2442