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

DNRoot::abortDeploy()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 30
Code Lines 17

Duplication

Lines 30
Ratio 100 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 30
loc 30
rs 6.7272
cc 7
eloc 17
nc 6
nop 1
1
<?php
2
use \Symfony\Component\Process\Process;
3
4
/**
5
 * God controller for the deploynaut interface
6
 *
7
 * @package deploynaut
8
 * @subpackage control
9
 */
10
class DNRoot extends Controller implements PermissionProvider, TemplateGlobalProvider {
11
12
	/**
13
	 * @const string - action type for actions that perform deployments
14
	 */
15
	const ACTION_DEPLOY = 'deploy';
16
17
	/**
18
	 * @const string - action type for actions that manipulate snapshots
19
	 */
20
	const ACTION_SNAPSHOT = 'snapshot';
21
22
	const ACTION_ENVIRONMENTS = 'createenv';
23
24
	const PROJECT_OVERVIEW = 'overview';
25
26
	/**
27
	 * @var string
28
	 */
29
	private $actionType = self::ACTION_DEPLOY;
30
31
	/**
32
	 * Bypass pipeline permission code
33
	 */
34
	const DEPLOYNAUT_BYPASS_PIPELINE = 'DEPLOYNAUT_BYPASS_PIPELINE';
35
36
	/**
37
	 * Allow dryrun of pipelines
38
	 */
39
	const DEPLOYNAUT_DRYRUN_PIPELINE = 'DEPLOYNAUT_DRYRUN_PIPELINE';
40
41
	/**
42
	 * Allow advanced options on deployments
43
	 */
44
	const DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS = 'DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS';
45
46
	const ALLOW_PROD_DEPLOYMENT = 'ALLOW_PROD_DEPLOYMENT';
47
	const ALLOW_NON_PROD_DEPLOYMENT = 'ALLOW_NON_PROD_DEPLOYMENT';
48
	const ALLOW_PROD_SNAPSHOT = 'ALLOW_PROD_SNAPSHOT';
49
	const ALLOW_NON_PROD_SNAPSHOT = 'ALLOW_NON_PROD_SNAPSHOT';
50
	const ALLOW_CREATE_ENVIRONMENT = 'ALLOW_CREATE_ENVIRONMENT';
51
52
	/**
53
	 * @var array
54
	 */
55
	private static $allowed_actions = array(
56
		'projects',
57
		'nav',
58
		'update',
59
		'project',
60
		'toggleprojectstar',
61
		'branch',
62
		'environment',
63
		'abortpipeline',
64
		'pipeline',
65
		'pipelinelog',
66
		'metrics',
67
		'createenvlog',
68
		'createenv',
69
		'getDeployForm',
70
		'doDeploy',
71
		'deploy',
72
		'deploylog',
73
		'abortDeploy',
74
		'getDataTransferForm',
75
		'transfer',
76
		'transferlog',
77
		'snapshots',
78
		'createsnapshot',
79
		'snapshotslog',
80
		'uploadsnapshot',
81
		'getCreateEnvironmentForm',
82
		'getUploadSnapshotForm',
83
		'getPostSnapshotForm',
84
		'getDataTransferRestoreForm',
85
		'getDeleteForm',
86
		'getMoveForm',
87
		'restoresnapshot',
88
		'deletesnapshot',
89
		'movesnapshot',
90
		'postsnapshotsuccess',
91
		'gitRevisions',
92
		'deploySummary',
93
		'startDeploy'
94
	);
95
96
	/**
97
	 * URL handlers pretending that we have a deep URL structure.
98
	 */
99
	private static $url_handlers = array(
100
		'project/$Project/environment/$Environment/DeployForm' => 'getDeployForm',
101
		'project/$Project/createsnapshot/DataTransferForm' => 'getDataTransferForm',
102
		'project/$Project/DataTransferForm' => 'getDataTransferForm',
103
		'project/$Project/DataTransferRestoreForm' => 'getDataTransferRestoreForm',
104
		'project/$Project/DeleteForm' => 'getDeleteForm',
105
		'project/$Project/MoveForm' => 'getMoveForm',
106
		'project/$Project/UploadSnapshotForm' => 'getUploadSnapshotForm',
107
		'project/$Project/PostSnapshotForm' => 'getPostSnapshotForm',
108
		'project/$Project/environment/$Environment/metrics' => 'metrics',
109
		'project/$Project/environment/$Environment/pipeline/$Identifier//$Action/$ID/$OtherID' => 'pipeline',
110
		'project/$Project/environment/$Environment/deploy_summary' => 'deploySummary',
111
		'project/$Project/environment/$Environment/git_revisions' => 'gitRevisions',
112
		'project/$Project/environment/$Environment/start-deploy' => 'startDeploy',
113
		'project/$Project/environment/$Environment/deploy/$Identifier/log' => 'deploylog',
114
		'project/$Project/environment/$Environment/deploy/$Identifier/abort-deploy' => 'abortDeploy',
115
		'project/$Project/environment/$Environment/deploy/$Identifier' => 'deploy',
116
		'project/$Project/transfer/$Identifier/log' => 'transferlog',
117
		'project/$Project/transfer/$Identifier' => 'transfer',
118
		'project/$Project/environment/$Environment' => 'environment',
119
		'project/$Project/createenv/$Identifier/log' => 'createenvlog',
120
		'project/$Project/createenv/$Identifier' => 'createenv',
121
		'project/$Project/CreateEnvironmentForm' => 'getCreateEnvironmentForm',
122
		'project/$Project/branch' => 'branch',
123
		'project/$Project/build/$Build' => 'build',
124
		'project/$Project/restoresnapshot/$DataArchiveID' => 'restoresnapshot',
125
		'project/$Project/deletesnapshot/$DataArchiveID' => 'deletesnapshot',
126
		'project/$Project/movesnapshot/$DataArchiveID' => 'movesnapshot',
127
		'project/$Project/update' => 'update',
128
		'project/$Project/snapshots' => 'snapshots',
129
		'project/$Project/createsnapshot' => 'createsnapshot',
130
		'project/$Project/uploadsnapshot' => 'uploadsnapshot',
131
		'project/$Project/snapshotslog' => 'snapshotslog',
132
		'project/$Project/postsnapshotsuccess/$DataArchiveID' => 'postsnapshotsuccess',
133
		'project/$Project/star' => 'toggleprojectstar',
134
		'project/$Project' => 'project',
135
		'nav/$Project' => 'nav',
136
		'projects' => 'projects',
137
	);
138
139
	/**
140
	 * @var array
141
	 */
142
	protected static $_project_cache = array();
143
144
	/**
145
	 * @var array
146
	 */
147
	private static $support_links = array();
148
149
	/**
150
	 * @var array
151
	 */
152
	private static $platform_specific_strings = array();
153
154
	/**
155
	 * @var array
156
	 */
157
	private static $action_types = array(
158
		self::ACTION_DEPLOY,
159
		self::ACTION_SNAPSHOT,
160
		self::PROJECT_OVERVIEW
161
	);
162
163
	/**
164
	 * @var DNData
165
	 */
166
	protected $data;
167
168
	/**
169
	 * Include requirements that deploynaut needs, such as javascript.
170
	 */
171
	public static function include_requirements() {
172
173
		// JS should always go to the bottom, otherwise there's the risk that Requirements
174
		// puts them halfway through the page to the nearest <script> tag. We don't want that.
175
		Requirements::set_force_js_to_bottom(true);
176
177
		// todo these should be bundled into the same JS as the others in "static" below.
178
		// We've deliberately not used combined_files as it can mess with some of the JS used
179
		// here and cause sporadic errors.
180
		Requirements::javascript('deploynaut/javascript/jquery.js');
181
		Requirements::javascript('deploynaut/javascript/bootstrap.js');
182
		Requirements::javascript('deploynaut/javascript/q.js');
183
		Requirements::javascript('deploynaut/javascript/tablefilter.js');
184
		Requirements::javascript('deploynaut/javascript/deploynaut.js');
185
		Requirements::javascript('deploynaut/javascript/react-with-addons.js');
186
		Requirements::javascript('deploynaut/javascript/bootstrap.file-input.js');
187
		Requirements::javascript('deploynaut/thirdparty/select2/dist/js/select2.min.js');
188
		Requirements::javascript('deploynaut/thirdparty/bootstrap-switch/dist/js/bootstrap-switch.min.js');
189
		Requirements::javascript('deploynaut/javascript/material.js');
190
191
		// Load the buildable dependencies only if not loaded centrally.
192
		if (!is_dir(BASE_PATH . DIRECTORY_SEPARATOR . 'static')) {
193
			if (\Director::isDev()) {
194
				\Requirements::javascript('deploynaut/static/bundle-debug.js');
195
			} else {
196
				\Requirements::javascript('deploynaut/static/bundle.js');
197
			}
198
		}
199
200
		Requirements::css('deploynaut/static/style.css');
201
	}
202
203
	/**
204
	 * Check for feature flags:
205
	 * - FLAG_SNAPSHOTS_ENABLED: set to true to enable globally
206
	 * - FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS: set to semicolon-separated list of email addresses of allowed users.
207
	 *
208
	 * @return boolean
209
	 */
210
	public static function FlagSnapshotsEnabled() {
211
		if(defined('FLAG_SNAPSHOTS_ENABLED') && FLAG_SNAPSHOTS_ENABLED) {
212
			return true;
213
		}
214
		if(defined('FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS') && FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS) {
215
			$allowedMembers = explode(';', FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS);
216
			$member = Member::currentUser();
217
			if($allowedMembers && $member && in_array($member->Email, $allowedMembers)) {
218
				return true;
219
			}
220
		}
221
		return false;
222
	}
223
224
	/**
225
	 * @return ArrayList
226
	 */
227
	public static function get_support_links() {
228
		$supportLinks = self::config()->support_links;
229
		if($supportLinks) {
230
			return new ArrayList($supportLinks);
231
		}
232
	}
233
234
	/**
235
	 * @return array
236
	 */
237
	public static function get_template_global_variables() {
238
		return array(
239
			'RedisUnavailable' => 'RedisUnavailable',
240
			'RedisWorkersCount' => 'RedisWorkersCount',
241
			'SidebarLinks' => 'SidebarLinks',
242
			"SupportLinks" => 'get_support_links'
243
		);
244
	}
245
246
	/**
247
	 */
248
	public function init() {
249
		parent::init();
250
251
		if(!Member::currentUser() && !Session::get('AutoLoginHash')) {
252
			return Security::permissionFailure();
253
		}
254
255
		// Block framework jquery
256
		Requirements::block(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
257
258
		self::include_requirements();
259
	}
260
261
	/**
262
	 * @return string
263
	 */
264
	public function Link() {
265
		return "naut/";
266
	}
267
268
	/**
269
	 * Actions
270
	 *
271
	 * @param SS_HTTPRequest $request
272
	 * @return \SS_HTTPResponse
273
	 */
274
	public function index(SS_HTTPRequest $request) {
275
		return $this->redirect($this->Link() . 'projects/');
276
	}
277
278
	/**
279
	 * Action
280
	 *
281
	 * @param SS_HTTPRequest $request
282
	 * @return string - HTML
283
	 */
284
	public function projects(SS_HTTPRequest $request) {
285
		// Performs canView permission check by limiting visible projects in DNProjectsList() call.
286
		return $this->customise(array(
287
			'Title' => 'Projects',
288
		))->render();
289
	}
290
291
	/**
292
	 * @param SS_HTTPRequest $request
293
	 * @return HTMLText
294
	 */
295
	public function nav(SS_HTTPRequest $request) {
296
		return $this->renderWith('Nav');
297
	}
298
299
	/**
300
	 * Return a link to the navigation template used for AJAX requests.
301
	 * @return string
302
	 */
303
	public function NavLink() {
304
		$currentProject = $this->getCurrentProject();
305
		$projectName = $currentProject ? $currentProject->Name : null;
306
		return Controller::join_links(Director::absoluteBaseURL(), 'naut', 'nav', $projectName);
307
	}
308
309
	/**
310
	 * Action
311
	 *
312
	 * @param SS_HTTPRequest $request
313
	 * @return SS_HTTPResponse - HTML
314
	 */
315
	public function snapshots(SS_HTTPRequest $request) {
316
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
317
		return $this->getCustomisedViewSection('SnapshotsSection', 'Data Snapshots');
318
	}
319
320
	/**
321
	 * Action
322
	 *
323
	 * @param SS_HTTPRequest $request
324
	 * @return string - HTML
325
	 */
326 View Code Duplication
	public function createsnapshot(SS_HTTPRequest $request) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

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

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

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

Loading history...
353
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
354
355
		// Performs canView permission check by limiting visible projects
356
		$project = $this->getCurrentProject();
357
		if(!$project) {
358
			return $this->project404Response();
359
		}
360
361
		if(!$project->canUploadArchive()) {
362
			return new SS_HTTPResponse("Not allowed to upload", 401);
363
		}
364
365
		return $this->customise(array(
366
			'SnapshotsSection' => 1,
367
			'UploadSnapshotForm' => $this->getUploadSnapshotForm($request),
368
			'PostSnapshotForm' => $this->getPostSnapshotForm($request)
369
		))->render();
370
	}
371
372
	/**
373
	 * Return the upload limit for snapshot uploads
374
	 * @return string
375
	 */
376
	public function UploadLimit() {
377
		return File::format_size(min(
378
			File::ini2bytes(ini_get('upload_max_filesize')),
379
			File::ini2bytes(ini_get('post_max_size'))
380
		));
381
	}
382
383
	/**
384
	 * Construct the upload form.
385
	 *
386
	 * @param SS_HTTPRequest $request
387
	 * @return Form
388
	 */
389
	public function getUploadSnapshotForm(SS_HTTPRequest $request) {
390
		// Performs canView permission check by limiting visible projects
391
		$project = $this->getCurrentProject();
392
		if(!$project) {
393
			return $this->project404Response();
394
		}
395
396
		if(!$project->canUploadArchive()) {
397
			return new SS_HTTPResponse("Not allowed to upload", 401);
398
		}
399
400
		// Framing an environment as a "group of people with download access"
401
		// makes more sense to the user here, while still allowing us to enforce
402
		// environment specific restrictions on downloading the file later on.
403
		$envs = $project->DNEnvironmentList()->filterByCallback(function($item) {
404
			return $item->canUploadArchive();
405
		});
406
		$envsMap = array();
407
		foreach($envs as $env) {
408
			$envsMap[$env->ID] = $env->Name;
409
		}
410
411
		$maxSize = min(File::ini2bytes(ini_get('upload_max_filesize')), File::ini2bytes(ini_get('post_max_size')));
412
		$fileField = DataArchiveFileField::create('ArchiveFile', 'File');
413
		$fileField->getValidator()->setAllowedExtensions(array('sspak'));
414
		$fileField->getValidator()->setAllowedMaxFileSize(array('*' => $maxSize));
415
416
		$form = Form::create(
417
			$this,
418
			'UploadSnapshotForm',
419
			FieldList::create(
420
				$fileField,
421
				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
422
				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
423
					->setEmptyString('Select an environment')
424
			),
425
			FieldList::create(
426
				FormAction::create('doUploadSnapshot', 'Upload File')
427
					->addExtraClass('btn')
428
			),
429
			RequiredFields::create('ArchiveFile')
430
		);
431
432
		$form->disableSecurityToken();
433
		$form->addExtraClass('fields-wide');
434
		// Tweak the action so it plays well with our fake URL structure.
435
		$form->setFormAction($project->Link() . '/UploadSnapshotForm');
436
437
		return $form;
438
	}
439
440
	/**
441
	 * @param array $data
442
	 * @param Form $form
443
	 *
444
	 * @return bool|HTMLText|SS_HTTPResponse
445
	 */
446
	public function doUploadSnapshot($data, Form $form) {
447
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
448
449
		// Performs canView permission check by limiting visible projects
450
		$project = $this->getCurrentProject();
451
		if(!$project) {
452
			return $this->project404Response();
453
		}
454
455
		$validEnvs = $project->DNEnvironmentList()
456
			->filterByCallback(function($item) {
457
				return $item->canUploadArchive();
458
			});
459
460
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
461
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
462
		if(!$environment) {
463
			throw new LogicException('Invalid environment');
464
		}
465
466
		$this->validateSnapshotMode($data['Mode']);
467
468
		$dataArchive = DNDataArchive::create(array(
469
			'AuthorID' => Member::currentUserID(),
470
			'EnvironmentID' => $data['EnvironmentID'],
471
			'IsManualUpload' => true,
472
		));
473
		// needs an ID and transfer to determine upload path
474
		$dataArchive->write();
475
		$dataTransfer = DNDataTransfer::create(array(
476
			'AuthorID' => Member::currentUserID(),
477
			'Mode' => $data['Mode'],
478
			'Origin' => 'ManualUpload',
479
			'EnvironmentID' => $data['EnvironmentID']
480
		));
481
		$dataTransfer->write();
482
		$dataArchive->DataTransfers()->add($dataTransfer);
483
		$form->saveInto($dataArchive);
484
		$dataArchive->write();
485
		$workingDir = TEMP_FOLDER . DIRECTORY_SEPARATOR . 'deploynaut-transfer-' . $dataTransfer->ID;
486
487
		$cleanupFn = function() use($workingDir, $dataTransfer, $dataArchive) {
488
			$process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir)));
489
			$process->run();
490
			$dataTransfer->delete();
491
			$dataArchive->delete();
492
		};
493
494
		// extract the sspak contents so we can inspect them
495
		try {
496
			$dataArchive->extractArchive($workingDir);
497
		} catch(Exception $e) {
498
			$cleanupFn();
499
			$form->sessionMessage(
500
				'There was a problem trying to open your snapshot for processing. Please try uploading again',
501
				'bad'
502
			);
503
			return $this->redirectBack();
504
		}
505
506
		// validate that the sspak contents match the declared contents
507
		$result = $dataArchive->validateArchiveContents();
508
		if(!$result->valid()) {
509
			$cleanupFn();
510
			$form->sessionMessage($result->message(), 'bad');
511
			return $this->redirectBack();
512
		}
513
514
		// fix file permissions of extracted sspak files then re-build the sspak
515
		try {
516
			$dataArchive->fixArchivePermissions($workingDir);
517
			$dataArchive->setArchiveFromFiles($workingDir);
518
		} catch(Exception $e) {
519
			$cleanupFn();
520
			$form->sessionMessage(
521
				'There was a problem processing your snapshot. Please try uploading again',
522
				'bad'
523
			);
524
			return $this->redirectBack();
525
		}
526
527
		// cleanup any extracted sspak contents lying around
528
		$process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir)));
529
		$process->run();
530
531
		return $this->customise(array(
532
			'Project' => $project,
533
			'CurrentProject' => $project,
534
			'SnapshotsSection' => 1,
535
			'DataArchive' => $dataArchive,
536
			'DataTransferRestoreForm' => $this->getDataTransferRestoreForm($this->request, $dataArchive),
537
			'BackURL' => $project->Link('snapshots')
538
		))->renderWith(array('DNRoot_uploadsnapshot', 'DNRoot'));
539
	}
540
541
	/**
542
	 * @param SS_HTTPRequest $request
543
	 * @return Form
544
	 */
545
	public function getPostSnapshotForm(SS_HTTPRequest $request) {
546
		// Performs canView permission check by limiting visible projects
547
		$project = $this->getCurrentProject();
548
		if(!$project) {
549
			return $this->project404Response();
550
		}
551
552
		if(!$project->canUploadArchive()) {
553
			return new SS_HTTPResponse("Not allowed to upload", 401);
554
		}
555
556
		// Framing an environment as a "group of people with download access"
557
		// makes more sense to the user here, while still allowing us to enforce
558
		// environment specific restrictions on downloading the file later on.
559
		$envs = $project->DNEnvironmentList()->filterByCallback(function($item) {
560
			return $item->canUploadArchive();
561
		});
562
		$envsMap = array();
563
		foreach($envs as $env) {
564
			$envsMap[$env->ID] = $env->Name;
565
		}
566
567
		$form = Form::create(
568
			$this,
569
			'PostSnapshotForm',
570
			FieldList::create(
571
				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
572
				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
573
					->setEmptyString('Select an environment')
574
			),
575
			FieldList::create(
576
				FormAction::create('doPostSnapshot', 'Submit request')
577
					->addExtraClass('btn')
578
			),
579
			RequiredFields::create('File')
580
		);
581
582
		$form->disableSecurityToken();
583
		$form->addExtraClass('fields-wide');
584
		// Tweak the action so it plays well with our fake URL structure.
585
		$form->setFormAction($project->Link() . '/PostSnapshotForm');
586
587
		return $form;
588
	}
589
590
	/**
591
	 * @param array $data
592
	 * @param Form $form
593
	 *
594
	 * @return SS_HTTPResponse
595
	 */
596
	public function doPostSnapshot($data, $form) {
597
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
598
599
		$project = $this->getCurrentProject();
600
		if(!$project) {
601
			return $this->project404Response();
602
		}
603
604
		$validEnvs = $project->DNEnvironmentList()->filterByCallback(function($item) {
605
				return $item->canUploadArchive();
606
		});
607
608
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
609
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
610
		if(!$environment) {
611
			throw new LogicException('Invalid environment');
612
		}
613
614
		$dataArchive = DNDataArchive::create(array(
615
			'UploadToken' => DNDataArchive::generate_upload_token(),
616
		));
617
		$form->saveInto($dataArchive);
618
		$dataArchive->write();
619
620
		return $this->redirect(Controller::join_links(
621
			$project->Link(),
622
			'postsnapshotsuccess',
623
			$dataArchive->ID
624
		));
625
	}
626
627
	/**
628
	 * Action
629
	 *
630
	 * @param SS_HTTPRequest $request
631
	 * @return SS_HTTPResponse - HTML
632
	 */
633
	public function snapshotslog(SS_HTTPRequest $request) {
634
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
635
		return $this->getCustomisedViewSection('SnapshotsSection', 'Snapshots log');
636
	}
637
638
	/**
639
	 * @param SS_HTTPRequest $request
640
	 * @return SS_HTTPResponse|string
641
	 * @throws SS_HTTPResponse_Exception
642
	 */
643
	public function postsnapshotsuccess(SS_HTTPRequest $request) {
644
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
645
646
		// Performs canView permission check by limiting visible projects
647
		$project = $this->getCurrentProject();
648
		if(!$project) {
649
			return $this->project404Response();
650
		}
651
652
		if(!$project->canUploadArchive()) {
653
			return new SS_HTTPResponse("Not allowed to upload", 401);
654
		}
655
656
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
657
		if(!$dataArchive) {
658
			return new SS_HTTPResponse("Archive not found.", 404);
659
		}
660
661
		if(!$dataArchive->canRestore()) {
662
			throw new SS_HTTPResponse_Exception('Not allowed to restore archive', 403);
663
		}
664
665
		return $this->render(array(
666
				'Title' => 'How to send us your Data Snapshot by post',
667
				'DataArchive' => $dataArchive,
668
				'Address' => Config::inst()->get('Deploynaut', 'snapshot_post_address'),
669
				'BackURL' => $project->Link(),
670
			));
671
	}
672
673
	/**
674
	 * @param SS_HTTPRequest $request
675
	 * @return \SS_HTTPResponse
676
	 */
677
	public function project(SS_HTTPRequest $request) {
678
		$this->setCurrentActionType(self::PROJECT_OVERVIEW);
679
		return $this->getCustomisedViewSection('ProjectOverview', '', array('IsAdmin' => Permission::check('ADMIN')));
680
	}
681
682
	/**
683
	 * This action will star / unstar a project for the current member
684
	 *
685
	 * @param SS_HTTPRequest $request
686
	 *
687
	 * @return SS_HTTPResponse
688
	 */
689
	public function toggleprojectstar(SS_HTTPRequest $request) {
690
		$project = $this->getCurrentProject();
691
		if(!$project) {
692
			return $this->project404Response();
693
		}
694
695
		$member = Member::currentUser();
696
		if($member === null) {
697
			return $this->project404Response();
698
		}
699
		$favProject = $member->StarredProjects()
700
			->filter('DNProjectID', $project->ID)
701
			->first();
702
703
		if($favProject) {
704
			$member->StarredProjects()->remove($favProject);
705
		} else {
706
			$member->StarredProjects()->add($project);
707
		}
708
		return $this->redirectBack();
709
	}
710
711
	/**
712
	 * @param SS_HTTPRequest $request
713
	 * @return \SS_HTTPResponse
714
	 */
715
	public function branch(SS_HTTPRequest $request) {
716
		$project = $this->getCurrentProject();
717
		if(!$project) {
718
			return $this->project404Response();
719
		}
720
721
		$branchName = $request->getVar('name');
722
		$branch = $project->DNBranchList()->byName($branchName);
723
		if(!$branch) {
724
			return new SS_HTTPResponse("Branch '" . Convert::raw2xml($branchName) . "' not found.", 404);
725
		}
726
727
		return $this->render(array(
728
			'CurrentBranch' => $branch,
729
		));
730
	}
731
732
	/**
733
	 * @param SS_HTTPRequest $request
734
	 * @return \SS_HTTPResponse
735
	 */
736
	public function environment(SS_HTTPRequest $request) {
737
		// Performs canView permission check by limiting visible projects
738
		$project = $this->getCurrentProject();
739
		if(!$project) {
740
			return $this->project404Response();
741
		}
742
743
		// Performs canView permission check by limiting visible projects
744
		$env = $this->getCurrentEnvironment($project);
745
		if(!$env) {
746
			return $this->environment404Response();
747
		}
748
749
		return $this->render(array(
750
			'DNEnvironmentList' => $this->getCurrentProject()->DNEnvironmentList(),
751
			'FlagSnapshotsEnabled' => $this->FlagSnapshotsEnabled(),
752
			'Redeploy' => (bool)$request->getVar('redeploy')
753
		));
754
	}
755
756
757
	/**
758
	 * Initiate a pipeline dry run
759
	 *
760
	 * @param array $data
761
	 * @param DeployForm $form
762
	 *
763
	 * @return SS_HTTPResponse
764
	 */
765
	public function doDryRun($data, DeployForm $form) {
766
		return $this->beginPipeline($data, $form, true);
767
	}
768
769
	/**
770
	 * Initiate a pipeline
771
	 *
772
	 * @param array $data
773
	 * @param DeployForm $form
774
	 * @return \SS_HTTPResponse
775
	 */
776
	public function startPipeline($data, $form) {
777
		return $this->beginPipeline($data, $form);
778
	}
779
780
	/**
781
	 * Start a pipeline
782
	 *
783
	 * @param array $data
784
	 * @param DeployForm $form
785
	 * @param bool $isDryRun
786
	 * @return \SS_HTTPResponse
787
	 */
788
	protected function beginPipeline($data, DeployForm $form, $isDryRun = false) {
789
		$buildName = $form->getSelectedBuild($data);
790
791
		// Performs canView permission check by limiting visible projects
792
		$project = $this->getCurrentProject();
793
		if(!$project) {
794
			return $this->project404Response();
795
		}
796
797
		// Performs canView permission check by limiting visible projects
798
		$environment = $this->getCurrentEnvironment($project);
799
		if(!$environment) {
800
			return $this->environment404Response();
801
		}
802
803
		if(!$environment->DryRunEnabled && $isDryRun) {
804
			return new SS_HTTPResponse("Dry-run for pipelines is not enabled for this environment", 404);
805
		}
806
807
		// Initiate the pipeline
808
		$sha = $project->DNBuildList()->byName($buildName);
809
		$pipeline = Pipeline::create();
810
		$pipeline->DryRun = $isDryRun;
811
		$pipeline->EnvironmentID = $environment->ID;
812
		$pipeline->AuthorID = Member::currentUserID();
813
		$pipeline->SHA = $sha->FullName();
814
		// Record build at time of execution
815
		if($currentBuild = $environment->CurrentBuild()) {
816
			$pipeline->PreviousDeploymentID = $currentBuild->ID;
817
		}
818
		$pipeline->start(); // start() will call write(), so no need to do it here as well.
819
		return $this->redirect($environment->Link());
820
	}
821
822
	/**
823
	 * @param SS_HTTPRequest $request
824
	 *
825
	 * @return SS_HTTPResponse
826
	 * @throws SS_HTTPResponse_Exception
827
	 */
828 View Code Duplication
	public function pipeline(SS_HTTPRequest $request) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
829
		$params = $request->params();
830
		$pipeline = Pipeline::get()->byID($params['Identifier']);
831
832
		if(!$pipeline || !$pipeline->ID || !$pipeline->Environment()) {
833
			throw new SS_HTTPResponse_Exception('Pipeline not found', 404);
834
		}
835
		if(!$pipeline->Environment()->canView()) {
836
			return Security::permissionFailure();
837
		}
838
839
		$environment = $pipeline->Environment();
840
		$project = $pipeline->Environment()->Project();
841
842
		if($environment->Name != $params['Environment']) {
843
			throw new LogicException("Environment in URL doesn't match this pipeline");
844
		}
845
		if($project->Name != $params['Project']) {
846
			throw new LogicException("Project in URL doesn't match this pipeline");
847
		}
848
849
		// Delegate to sub-requesthandler
850
		return PipelineController::create($this, $pipeline);
851
	}
852
853
	/**
854
	 * Shows the creation log.
855
	 *
856
	 * @param SS_HTTPRequest $request
857
	 * @return string
858
	 */
859
	public function createenv(SS_HTTPRequest $request) {
860
		$params = $request->params();
861
		if($params['Identifier']) {
862
			$record = DNCreateEnvironment::get()->byId($params['Identifier']);
863
864
			if(!$record || !$record->ID) {
865
				throw new SS_HTTPResponse_Exception('Create environment not found', 404);
866
			}
867
			if(!$record->canView()) {
868
				return Security::permissionFailure();
869
			}
870
871
			$project = $this->getCurrentProject();
872
			if(!$project) {
873
				return $this->project404Response();
874
			}
875
876
			if($project->Name != $params['Project']) {
877
				throw new LogicException("Project in URL doesn't match this creation");
878
			}
879
880
			return $this->render(array(
881
				'CreateEnvironment' => $record,
882
			));
883
		}
884
		return $this->render(array('CurrentTitle' => 'Create an environment'));
885
	}
886
887
888
	public function createenvlog(SS_HTTPRequest $request) {
889
		$params = $request->params();
890
		$env = DNCreateEnvironment::get()->byId($params['Identifier']);
891
892
		if(!$env || !$env->ID) {
893
			throw new SS_HTTPResponse_Exception('Log not found', 404);
894
		}
895
		if(!$env->canView()) {
896
			return Security::permissionFailure();
897
		}
898
899
		$project = $env->Project();
900
901
		if($project->Name != $params['Project']) {
902
			throw new LogicException("Project in URL doesn't match this deploy");
903
		}
904
905
		$log = $env->log();
906
		if($log->exists()) {
907
			$content = $log->content();
908
		} else {
909
			$content = 'Waiting for action to start';
910
		}
911
912
		return $this->sendResponse($env->ResqueStatus(), $content);
913
	}
914
915
	/**
916
	 * @param SS_HTTPRequest $request
917
	 * @return Form
918
	 */
919
	public function getCreateEnvironmentForm(SS_HTTPRequest $request) {
920
		$this->setCurrentActionType(self::ACTION_ENVIRONMENTS);
921
922
		$project = $this->getCurrentProject();
923
		if(!$project) {
924
			return $this->project404Response();
925
		}
926
927
		$envType = $project->AllowedEnvironmentType;
0 ignored issues
show
Documentation introduced by
The property AllowedEnvironmentType does not exist on object<DNProject>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
982
983
		$job = DNCreateEnvironment::create();
984
985
		$job->Data = serialize($data);
986
		$job->ProjectID = $project->ID;
987
		$job->write();
988
		$job->start();
989
990
		return $this->redirect($project->Link('createenv') . '/' . $job->ID);
991
	}
992
993
	/**
994
	 *
995
	 * @param SS_HTTPRequest $request
996
	 * @return \SS_HTTPResponse
997
	 */
998
	public function metrics(SS_HTTPRequest $request) {
999
		// Performs canView permission check by limiting visible projects
1000
		$project = $this->getCurrentProject();
1001
		if(!$project) {
1002
			return $this->project404Response();
1003
		}
1004
1005
		// Performs canView permission check by limiting visible projects
1006
		$env = $this->getCurrentEnvironment($project);
1007
		if(!$env) {
1008
			return $this->environment404Response();
1009
		}
1010
1011
		return $this->render();
1012
	}
1013
1014
	/**
1015
	 * Get the DNData object.
1016
	 *
1017
	 * @return DNData
1018
	 */
1019
	public function DNData() {
1020
		return DNData::inst();
1021
	}
1022
1023
	/**
1024
	 * Provide a list of all projects.
1025
	 *
1026
	 * @return SS_List
1027
	 */
1028
	public function DNProjectList() {
1029
		$memberId = Member::currentUserID();
1030
		if(!$memberId) {
1031
			return new ArrayList();
1032
		}
1033
1034
		if(Permission::check('ADMIN')) {
1035
			return DNProject::get();
1036
		}
1037
1038
		$projects = Member::get()->filter('ID', $memberId)
1039
			->relation('Groups')
1040
			->relation('Projects');
1041
1042
		$this->extend('updateDNProjectList', $projects);
1043
		return $projects;
1044
	}
1045
1046
	/**
1047
	 * @return ArrayList
1048
	 */
1049
	public function getPlatformSpecificStrings() {
1050
		$strings = $this->config()->platform_specific_strings;
1051
		if ($strings) {
1052
			return new ArrayList($strings);
1053
		}
1054
	}
1055
1056
	/**
1057
	 * Provide a list of all starred projects for the currently logged in member
1058
	 *
1059
	 * @return SS_List
1060
	 */
1061
	public function getStarredProjects() {
1062
		$member = Member::currentUser();
1063
		if($member === null) {
1064
			return new ArrayList();
1065
		}
1066
1067
		$favProjects = $member->StarredProjects();
1068
1069
		$list = new ArrayList();
1070
		foreach($favProjects as $project) {
1071
			if($project->canView($member)) {
1072
				$list->add($project);
1073
			}
1074
		}
1075
		return $list;
1076
	}
1077
1078
	/**
1079
	 * Returns top level navigation of projects.
1080
	 *
1081
	 * @param int $limit
1082
	 *
1083
	 * @return ArrayList
1084
	 */
1085
	public function Navigation($limit = 5) {
1086
		$navigation = new ArrayList();
1087
1088
		$currentProject = $this->getCurrentProject();
1089
		$currentEnvironment = $this->getCurrentEnvironment();
1090
		$actionType = $this->getCurrentActionType();
1091
1092
		$projects = $this->getStarredProjects();
1093
		if($projects->count() < 1) {
1094
			$projects = $this->DNProjectList();
1095
		} else {
1096
			$limit = -1;
1097
		}
1098
1099
		if($projects->count() > 0) {
1100
			$activeProject = false;
1101
1102
			if($limit > 0) {
1103
				$limitedProjects = $projects->limit($limit);
1104
			} else {
1105
				$limitedProjects = $projects;
1106
			}
1107
1108
			foreach($limitedProjects as $project) {
1109
				$isActive = $currentProject && $currentProject->ID == $project->ID;
1110
				if($isActive) {
1111
					$activeProject = true;
1112
				}
1113
1114
				$isCurrentEnvironment = false;
1115
				if($project && $currentEnvironment) {
1116
					$isCurrentEnvironment = (bool) $project->DNEnvironmentList()->find('ID', $currentEnvironment->ID);
1117
				}
1118
1119
				$navigation->push(array(
1120
					'Project' => $project,
1121
					'IsCurrentEnvironment' => $isCurrentEnvironment,
1122
					'IsActive' => $currentProject && $currentProject->ID == $project->ID,
1123
					'IsOverview' => $actionType == self::PROJECT_OVERVIEW && $currentProject->ID == $project->ID
1124
				));
1125
			}
1126
1127
			// Ensure the current project is in the list
1128
			if(!$activeProject && $currentProject) {
1129
				$navigation->unshift(array(
1130
					'Project' => $currentProject,
1131
					'IsActive' => true,
1132
					'IsCurrentEnvironment' => $currentEnvironment,
1133
					'IsOverview' => $actionType == self::PROJECT_OVERVIEW
1134
				));
1135
				if($limit > 0 && $navigation->count() > $limit) {
1136
					$navigation->pop();
1137
				}
1138
			}
1139
		}
1140
1141
		return $navigation;
1142
	}
1143
1144
	/**
1145
	 * Construct the deployment form
1146
	 *
1147
	 * @return Form
1148
	 */
1149
	public function getDeployForm($request = null) {
1150
1151
		// Performs canView permission check by limiting visible projects
1152
		$project = $this->getCurrentProject();
1153
		if(!$project) {
1154
			return $this->project404Response();
1155
		}
1156
1157
		// Performs canView permission check by limiting visible projects
1158
		$environment = $this->getCurrentEnvironment($project);
1159
		if(!$environment) {
1160
			return $this->environment404Response();
1161
		}
1162
1163
		if(!$environment->canDeploy()) {
1164
			return new SS_HTTPResponse("Not allowed to deploy", 401);
1165
		}
1166
1167
		// Generate the form
1168
		$form = new DeployForm($this, 'DeployForm', $environment, $project);
1169
1170
		// If this is an ajax request we don't want to submit the form - we just want to retrieve the markup.
1171
		if(
1172
			$request &&
1173
			!$request->requestVar('action_showDeploySummary') &&
1174
			$this->getRequest()->isAjax() &&
1175
			$this->getRequest()->isGET()
1176
		) {
1177
			// We can just use the URL we're accessing
1178
			$form->setFormAction($this->getRequest()->getURL());
1179
1180
			$body = json_encode(array('Content' => $form->forAjaxTemplate()->forTemplate()));
1181
			$this->getResponse()->addHeader('Content-Type', 'application/json');
1182
			$this->getResponse()->setBody($body);
1183
			return $body;
1184
		}
1185
1186
		$form->setFormAction($this->getRequest()->getURL() . '/DeployForm');
1187
		return $form;
1188
	}
1189
1190
	/**
1191
	 * @param SS_HTTPRequest $request
1192
	 *
1193
	 * @return SS_HTTPResponse|string
1194
	 */
1195
	public function gitRevisions(SS_HTTPRequest $request) {
1196
1197
		// Performs canView permission check by limiting visible projects
1198
		$project = $this->getCurrentProject();
1199
		if(!$project) {
1200
			return $this->project404Response();
1201
		}
1202
1203
		// Performs canView permission check by limiting visible projects
1204
		$env = $this->getCurrentEnvironment($project);
1205
		if(!$env) {
1206
			return $this->environment404Response();
1207
		}
1208
1209
		// For now only permit advanced options on one environment type, because we hacked the "full-deploy"
1210
		// checkbox in. Other environments such as the fast or capistrano one wouldn't know what to do with it.
1211
		if(get_class($env) === 'RainforestEnvironment') {
1212
			$advanced = Permission::check('DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS') ? 'true' : 'false';
1213
		} else {
1214
			$advanced = 'false';
1215
		}
1216
1217
		$tabs = array();
1218
		$id = 0;
1219
		$data = array(
1220
			'id' => ++$id,
1221
			'name' => 'Deploy the latest version of a branch',
1222
			'field_type' => 'dropdown',
1223
			'field_label' => 'Choose a branch',
1224
			'field_id' => 'branch',
1225
			'field_data' => array(),
1226
			'advanced_opts' => $advanced
1227
		);
1228
		foreach($project->DNBranchList() as $branch) {
1229
			$sha = $branch->SHA();
1230
			$name = $branch->Name();
1231
			$branchValue = sprintf("%s (%s, %s old)",
1232
				$name,
1233
				substr($sha, 0, 8),
1234
				$branch->LastUpdated()->TimeDiff()
1235
			);
1236
			$data['field_data'][] = array(
1237
				'id' => $sha,
1238
				'text' => $branchValue,
1239
				'branch_name' => $name // the raw branch name, not including the time etc
1240
			);
1241
		}
1242
		$tabs[] = $data;
1243
1244
		$data = array(
1245
			'id' => ++$id,
1246
			'name' => 'Deploy a tagged release',
1247
			'field_type' => 'dropdown',
1248
			'field_label' => 'Choose a tag',
1249
			'field_id' => 'tag',
1250
			'field_data' => array(),
1251
			'advanced_opts' => $advanced
1252
		);
1253
1254
		foreach($project->DNTagList()->setLimit(null) as $tag) {
1255
			$name = $tag->Name();
1256
			$data['field_data'][] = array(
1257
				'id' => $tag->SHA(),
1258
				'text' => sprintf("%s", $name)
1259
			);
1260
		}
1261
1262
		// show newest tags first.
1263
		$data['field_data'] = array_reverse($data['field_data']);
1264
1265
		$tabs[] = $data;
1266
1267
		// Past deployments
1268
		$data = array(
1269
			'id' => ++$id,
1270
			'name' => 'Redeploy a release that was previously deployed (to any environment)',
1271
			'field_type' => 'dropdown',
1272
			'field_label' => 'Choose a previously deployed release',
1273
			'field_id' => 'release',
1274
			'field_data' => array(),
1275
			'advanced_opts' => $advanced
1276
		);
1277
		// We are aiming at the format:
1278
		// [{text: 'optgroup text', children: [{id: '<sha>', text: '<inner text>'}]}]
0 ignored issues
show
Unused Code Comprehensibility introduced by
55% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
1279
		$redeploy = array();
1280
		foreach($project->DNEnvironmentList() as $dnEnvironment) {
1281
			$envName = $dnEnvironment->Name;
1282
			$perEnvDeploys = array();
1283
1284
			foreach($dnEnvironment->DeployHistory() as $deploy) {
1285
				$sha = $deploy->SHA;
1286
1287
				// Check if exists to make sure the newest deployment date is used.
1288
				if(!isset($perEnvDeploys[$sha])) {
1289
					$pastValue = sprintf("%s (deployed %s)",
1290
						substr($sha, 0, 8),
1291
						$deploy->obj('LastEdited')->Ago()
1292
					);
1293
					$perEnvDeploys[$sha] = array(
1294
						'id' => $sha,
1295
						'text' => $pastValue
1296
					);
1297
				}
1298
			}
1299
1300
			if(!empty($perEnvDeploys)) {
1301
				$redeploy[$envName] = array_values($perEnvDeploys);
1302
			}
1303
		}
1304
		// Convert the array to the frontend format (i.e. keyed to regular array)
1305
		foreach($redeploy as $env => $descr) {
1306
			$data['field_data'][] = array('text'=>$env, 'children'=>$descr);
1307
		}
1308
		$tabs[] = $data;
1309
1310
		$data = array(
1311
			'id' => ++$id,
1312
			'name' => 'Deploy a specific SHA',
1313
			'field_type' => 'textfield',
1314
			'field_label' => 'Choose a SHA',
1315
			'field_id' => 'SHA',
1316
			'field_data' => array(),
1317
			'advanced_opts' => $advanced
1318
		);
1319
		$tabs[] = $data;
1320
1321
		// get the last time git fetch was run
1322
		$lastFetched = 'never';
1323
		$fetch = DNGitFetch::get()
1324
			->filter('ProjectID', $project->ID)
1325
			->sort('LastEdited', 'DESC')
1326
			->first();
1327
		if($fetch) {
1328
			$lastFetched = $fetch->dbObject('LastEdited')->Ago();
1329
		}
1330
1331
		$data = array(
1332
			'Tabs' => $tabs,
1333
			'last_fetched' => $lastFetched
1334
		);
1335
1336
		$this->applyRedeploy($request, $data);
1337
1338
		return json_encode($data, JSON_PRETTY_PRINT);
1339
	}
1340
1341
	protected function applyRedeploy(SS_HTTPRequest $request, &$data) {
1342
		if (!$request->getVar('redeploy')) return;
1343
1344
		$project = $this->getCurrentProject();
1345
		if(!$project) {
1346
			return $this->project404Response();
1347
		}
1348
1349
		// Performs canView permission check by limiting visible projects
1350
		$env = $this->getCurrentEnvironment($project);
1351
		if(!$env) {
1352
			return $this->environment404Response();
1353
		}
1354
1355
		$current = $env->CurrentBuild();
1356
		if ($current && $current->exists()) {
1357
			$data['preselect_tab'] = 3;
1358
			$data['preselect_sha'] = $current->SHA;
1359
		} else {
1360
			$master = $project->DNBranchList()->byName('master');
1361
			if ($master) {
1362
				$data['preselect_tab'] = 1;
1363
				$data['preselect_sha'] = $master->SHA();
0 ignored issues
show
Bug introduced by
The method SHA cannot be called on $master (of type string).

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

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

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

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

Loading history...
1569
		$params = $request->params();
1570
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1571
1572
		if(!$deployment || !$deployment->ID) {
1573
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1574
		}
1575
		if(!$deployment->canView()) {
1576
			return Security::permissionFailure();
1577
		}
1578
1579
		// For now restrict to ADMINs only.
1580
		if(!Permission::check('ADMIN')) {
1581
			return Security::permissionFailure();
1582
		}
1583
1584
		$environment = $deployment->Environment();
1585
		$project = $environment->Project();
1586
1587
		if($environment->Name != $params['Environment']) {
1588
			throw new LogicException("Environment in URL doesn't match this deploy");
1589
		}
1590
		if($project->Name != $params['Project']) {
1591
			throw new LogicException("Project in URL doesn't match this deploy");
1592
		}
1593
1594
		$deployment->abort();
1595
1596
		return $this->sendResponse($deployment->ResqueStatus(), []);
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

Loading history...
2252
		} else {
2253
			$dataList = new ArrayList();
2254
		}
2255
2256
		$this->extend('updateCreateEnvironmentList', $dataList);
2257
		return new PaginatedList($dataList->sort('Created DESC'), $this->request);
0 ignored issues
show
Documentation introduced by
$this->request is of type object<SS_HTTPRequest>, but the function expects a array.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2258
	}
2259
2260
	/**
2261
	 * Returns a list of all archive files that can be accessed by the currently logged-in {@link Member}
2262
	 *
2263
	 * @return PaginatedList
2264
	 */
2265
	public function CompleteDataArchives() {
2266
		$project = $this->getCurrentProject();
2267
		$archives = new ArrayList();
2268
2269
		$archiveList = $project->Environments()->relation("DataArchives");
2270
		if($archiveList->count() > 0) {
2271
			foreach($archiveList as $archive) {
2272
				if($archive->canView() && !$archive->isPending()) {
2273
					$archives->push($archive);
2274
				}
2275
			}
2276
		}
2277
		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...
2278
	}
2279
2280
	/**
2281
	 * @return PaginatedList The list of "pending" data archives which are waiting for a file
2282
	 * to be delivered offline by post, and manually uploaded into the system.
2283
	 */
2284
	public function PendingDataArchives() {
2285
		$project = $this->getCurrentProject();
2286
		$archives = new ArrayList();
2287
		foreach($project->DNEnvironmentList() as $env) {
2288
			foreach($env->DataArchives() as $archive) {
2289
				if($archive->canView() && $archive->isPending()) {
2290
					$archives->push($archive);
2291
				}
2292
			}
2293
		}
2294
		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...
2295
	}
2296
2297
	/**
2298
	 * @return PaginatedList
2299
	 */
2300
	public function DataTransferLogs() {
2301
		$project = $this->getCurrentProject();
2302
2303
		$transfers = DNDataTransfer::get()->filterByCallback(function($record) use($project) {
2304
			return
2305
				$record->Environment()->Project()->ID == $project->ID && // Ensure only the current Project is shown
2306
				(
2307
					$record->Environment()->canRestore() || // Ensure member can perform an action on the transfers env
2308
					$record->Environment()->canBackup() ||
2309
					$record->Environment()->canUploadArchive() ||
2310
					$record->Environment()->canDownloadArchive()
2311
				);
2312
		});
2313
2314
		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...
2315
	}
2316
2317
	/**
2318
	 * @return null|PaginatedList
2319
	 */
2320
	public function DeployHistory() {
2321
		if($env = $this->getCurrentEnvironment()) {
2322
			$history = $env->DeployHistory();
2323
			if($history->count() > 0) {
2324
				$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...
2325
				$pagination->setPageLength(8);
2326
				return $pagination;
2327
			}
2328
		}
2329
		return null;
2330
	}
2331
2332
	/**
2333
	 * @return SS_HTTPResponse
2334
	 */
2335
	protected function project404Response() {
2336
		return new SS_HTTPResponse(
2337
			"Project '" . Convert::raw2xml($this->getRequest()->param('Project')) . "' not found.",
2338
			404
2339
		);
2340
	}
2341
2342
	/**
2343
	 * @return SS_HTTPResponse
2344
	 */
2345
	protected function environment404Response() {
2346
		$envName = Convert::raw2xml($this->getRequest()->param('Environment'));
2347
		return new SS_HTTPResponse("Environment '" . $envName . "' not found.", 404);
2348
	}
2349
2350
	/**
2351
	 * @param string $status
2352
	 * @param string $content
2353
	 *
2354
	 * @return string
2355
	 */
2356
	public function sendResponse($status, $content) {
2357
		// strip excessive newlines
2358
		$content = preg_replace('/(?:(?:\r\n|\r|\n)\s*){2}/s', "\n", $content);
2359
2360
		$sendJSON = (strpos($this->getRequest()->getHeader('Accept'), 'application/json') !== false)
2361
			|| $this->getRequest()->getExtension() == 'json';
2362
2363
		if(!$sendJSON) {
2364
			$this->response->addHeader("Content-type", "text/plain");
2365
			return $content;
2366
		}
2367
		$this->response->addHeader("Content-type", "application/json");
2368
		return json_encode(array(
2369
			'status' => $status,
2370
			'content' => $content,
2371
		));
2372
	}
2373
2374
	/**
2375
	 * Validate the snapshot mode
2376
	 *
2377
	 * @param string $mode
2378
	 */
2379
	protected function validateSnapshotMode($mode) {
2380
		if(!in_array($mode, array('all', 'assets', 'db'))) {
2381
			throw new LogicException('Invalid mode');
2382
		}
2383
	}
2384
2385
	/**
2386
	 * @param string $sectionName
2387
	 * @param string $title
2388
	 *
2389
	 * @return SS_HTTPResponse
2390
	 */
2391
	protected function getCustomisedViewSection($sectionName, $title = '', $data = array()) {
2392
		// Performs canView permission check by limiting visible projects
2393
		$project = $this->getCurrentProject();
2394
		if(!$project) {
2395
			return $this->project404Response();
2396
		}
2397
		$data[$sectionName] = 1;
2398
2399
		if($this !== '') {
2400
			$data['Title'] = $title;
2401
		}
2402
2403
		return $this->render($data);
2404
	}
2405
2406
	/**
2407
	 * Get items for the ambient menu that should be accessible from all pages.
2408
	 *
2409
	 * @return ArrayList
2410
	 */
2411
	public function AmbientMenu() {
2412
		$list = new ArrayList();
2413
2414
		if (Member::currentUserID()) {
2415
			$list->push(new ArrayData(array(
2416
				'Classes' => 'logout',
2417
				'FaIcon' => 'sign-out',
2418
				'Link' => 'Security/logout',
2419
				'Title' => 'Log out',
2420
				'IsCurrent' => false,
2421
				'IsSection' => false
2422
			)));
2423
		}
2424
2425
		$this->extend('updateAmbientMenu', $list);
2426
		return $list;
2427
	}
2428
2429
	/**
2430
	 * Checks whether the user can create a project.
2431
	 *
2432
	 * @return bool
2433
	 */
2434
	public function canCreateProjects($member = null) {
2435
		if(!$member) $member = Member::currentUser();
2436
		if(!$member) return false;
2437
2438
		return singleton('DNProject')->canCreate($member);
2439
	}
2440
2441
}
2442
2443