Completed
Pull Request — master (#741)
by Sean
07:08 queued 02:56
created

DNRoot::environment()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 23
rs 8.7972
c 0
b 0
f 0
cc 4
eloc 12
nc 4
nop 1
1
<?php
2
3
/**
4
 * God controller for the deploynaut interface
5
 *
6
 * @package deploynaut
7
 * @subpackage control
8
 */
9
class DNRoot extends Controller implements PermissionProvider, TemplateGlobalProvider {
0 ignored issues
show
Coding Style introduced by
The property $_project_cache is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
Coding Style introduced by
The property $allowed_actions is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
Coding Style introduced by
The property $url_handlers is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
Coding Style introduced by
The property $support_links is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
Coding Style introduced by
The property $platform_specific_strings is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
Coding Style introduced by
The property $action_types is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

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

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

Loading history...
140
141
	/**
142
	 * @var array
143
	 */
144
	private static $platform_specific_strings = [];
0 ignored issues
show
Unused Code introduced by
The property $platform_specific_strings is not used and could be removed.

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

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

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

Loading history...
150
		self::ACTION_DEPLOY,
151
		self::ACTION_SNAPSHOT,
152
		self::PROJECT_OVERVIEW
153
	];
154
155
	/**
156
	 * Include requirements that deploynaut needs, such as javascript.
157
	 */
158
	public static function include_requirements() {
159
160
		// JS should always go to the bottom, otherwise there's the risk that Requirements
161
		// puts them halfway through the page to the nearest <script> tag. We don't want that.
162
		Requirements::set_force_js_to_bottom(true);
163
164
		// todo these should be bundled into the same JS as the others in "static" below.
165
		// We've deliberately not used combined_files as it can mess with some of the JS used
166
		// here and cause sporadic errors.
167
		Requirements::javascript('deploynaut/javascript/jquery.js');
168
		Requirements::javascript('deploynaut/javascript/bootstrap.js');
169
		Requirements::javascript('deploynaut/javascript/q.js');
170
		Requirements::javascript('deploynaut/javascript/tablefilter.js');
171
		Requirements::javascript('deploynaut/javascript/deploynaut.js');
172
173
		Requirements::javascript('deploynaut/javascript/bootstrap.file-input.js');
174
		Requirements::javascript('deploynaut/thirdparty/select2/dist/js/select2.min.js');
175
		Requirements::javascript('deploynaut/javascript/selectize.js');
176
		Requirements::javascript('deploynaut/thirdparty/bootstrap-switch/dist/js/bootstrap-switch.min.js');
177
		Requirements::javascript('deploynaut/javascript/material.js');
178
179
		// Load the buildable dependencies only if not loaded centrally.
180
		if (!is_dir(BASE_PATH . DIRECTORY_SEPARATOR . 'static')) {
181
			if (\Director::isDev()) {
182
				\Requirements::javascript('deploynaut/static/bundle-debug.js');
183
			} else {
184
				\Requirements::javascript('deploynaut/static/bundle.js');
185
			}
186
		}
187
188
		Requirements::css('deploynaut/static/style.css');
189
	}
190
191
	/**
192
	 * @return ArrayList
193
	 */
194
	public static function get_support_links() {
195
		$supportLinks = self::config()->support_links;
196
		if ($supportLinks) {
197
			return new ArrayList($supportLinks);
198
		}
199
	}
200
201
	/**
202
	 * @return array
203
	 */
204
	public static function get_template_global_variables() {
205
		return [
206
			'RedisUnavailable' => 'RedisUnavailable',
207
			'RedisWorkersCount' => 'RedisWorkersCount',
208
			'SidebarLinks' => 'SidebarLinks',
209
			"SupportLinks" => 'get_support_links'
210
		];
211
	}
212
213
	/**
214
	 */
215
	public function init() {
216
		parent::init();
217
218
		if (!Member::currentUser() && !Session::get('AutoLoginHash')) {
219
			return Security::permissionFailure();
220
		}
221
222
		// Block framework jquery
223
		Requirements::block(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
224
225
		self::include_requirements();
226
	}
227
228
	/**
229
	 * @return string
230
	 */
231
	public function Link() {
232
		return "naut/";
233
	}
234
235
	/**
236
	 * Actions
237
	 *
238
	 * @param \SS_HTTPRequest $request
239
	 * @return \SS_HTTPResponse
240
	 */
241
	public function index(\SS_HTTPRequest $request) {
242
		return $this->redirect($this->Link() . 'projects/');
243
	}
244
245
	/**
246
	 * Action
247
	 *
248
	 * @param \SS_HTTPRequest $request
249
	 * @return string - HTML
250
	 */
251
	public function projects(\SS_HTTPRequest $request) {
252
		// Performs canView permission check by limiting visible projects in DNProjectsList() call.
253
		return $this->customise([
254
			'Title' => 'Projects',
255
		])->render();
256
	}
257
258
	/**
259
	 * @param \SS_HTTPRequest $request
260
	 * @return HTMLText
261
	 */
262
	public function nav(\SS_HTTPRequest $request) {
263
		return $this->renderWith('Nav');
264
	}
265
266
	/**
267
	 * Return a link to the navigation template used for AJAX requests.
268
	 * @return string
269
	 */
270
	public function NavLink() {
271
		$currentProject = $this->getCurrentProject();
272
		$projectName = $currentProject ? $currentProject->Name : null;
273
		return Controller::join_links(Director::absoluteBaseURL(), 'naut', 'nav', $projectName);
274
	}
275
276
	/**
277
	 * Action
278
	 *
279
	 * @param \SS_HTTPRequest $request
280
	 * @return SS_HTTPResponse - HTML
281
	 */
282
	public function snapshots(\SS_HTTPRequest $request) {
283
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
284
		return $this->getCustomisedViewSection('SnapshotsSection', 'Data Snapshots');
285
	}
286
287
	/**
288
	 * Action
289
	 *
290
	 * @param \SS_HTTPRequest $request
291
	 * @return string - HTML
292
	 */
293 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...
294
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
295
296
		// Performs canView permission check by limiting visible projects
297
		$project = $this->getCurrentProject();
298
		if (!$project) {
299
			return $this->project404Response();
300
		}
301
302
		if (!$project->canBackup()) {
303
			return new SS_HTTPResponse("Not allowed to create snapshots on any environments", 401);
304
		}
305
306
		return $this->customise([
307
			'Title' => 'Create Data Snapshot',
308
			'SnapshotsSection' => 1,
309
			'DataTransferForm' => $this->getDataTransferForm($request)
310
		])->render();
311
	}
312
313
	/**
314
	 * Action
315
	 *
316
	 * @param \SS_HTTPRequest $request
317
	 * @return string - HTML
318
	 */
319 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...
320
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
321
322
		// Performs canView permission check by limiting visible projects
323
		$project = $this->getCurrentProject();
324
		if (!$project) {
325
			return $this->project404Response();
326
		}
327
328
		if (!$project->canUploadArchive()) {
329
			return new SS_HTTPResponse("Not allowed to upload", 401);
330
		}
331
332
		return $this->customise([
333
			'SnapshotsSection' => 1,
334
			'UploadSnapshotForm' => $this->getUploadSnapshotForm($request),
335
			'PostSnapshotForm' => $this->getPostSnapshotForm($request)
336
		])->render();
337
	}
338
339
	/**
340
	 * Return the upload limit for snapshot uploads
341
	 * @return string
342
	 */
343
	public function UploadLimit() {
344
		return File::format_size(min(
345
			File::ini2bytes(ini_get('upload_max_filesize')),
346
			File::ini2bytes(ini_get('post_max_size'))
347
		));
348
	}
349
350
	/**
351
	 * Construct the upload form.
352
	 *
353
	 * @param \SS_HTTPRequest $request
354
	 * @return Form
355
	 */
356
	public function getUploadSnapshotForm(\SS_HTTPRequest $request) {
357
		// Performs canView permission check by limiting visible projects
358
		$project = $this->getCurrentProject();
359
		if (!$project) {
360
			return $this->project404Response();
361
		}
362
363
		if (!$project->canUploadArchive()) {
364
			return new SS_HTTPResponse("Not allowed to upload", 401);
365
		}
366
367
		// Framing an environment as a "group of people with download access"
368
		// makes more sense to the user here, while still allowing us to enforce
369
		// environment specific restrictions on downloading the file later on.
370
		$envs = $project->DNEnvironmentList()->filterByCallback(function ($item) {
371
			return $item->canUploadArchive();
372
		});
373
		$envsMap = [];
374
		foreach ($envs as $env) {
375
			$envsMap[$env->ID] = $env->Name;
376
		}
377
378
		$maxSize = min(File::ini2bytes(ini_get('upload_max_filesize')), File::ini2bytes(ini_get('post_max_size')));
379
		$fileField = DataArchiveFileField::create('ArchiveFile', 'File');
380
		$fileField->getValidator()->setAllowedExtensions(['sspak']);
381
		$fileField->getValidator()->setAllowedMaxFileSize(['*' => $maxSize]);
382
383
		$form = Form::create(
384
			$this,
385
			'UploadSnapshotForm',
386
			FieldList::create(
387
				$fileField,
388
				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
389
				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
390
					->setEmptyString('Select an environment')
391
			),
392
			FieldList::create(
393
				FormAction::create('doUploadSnapshot', 'Upload File')
394
					->addExtraClass('btn')
395
			),
396
			RequiredFields::create('ArchiveFile')
397
		);
398
399
		$form->disableSecurityToken();
400
		$form->addExtraClass('fields-wide');
401
		// Tweak the action so it plays well with our fake URL structure.
402
		$form->setFormAction($project->Link() . '/UploadSnapshotForm');
403
404
		return $form;
405
	}
406
407
	/**
408
	 * @param array $data
409
	 * @param Form $form
410
	 *
411
	 * @return bool|HTMLText|SS_HTTPResponse
412
	 */
413
	public function doUploadSnapshot($data, \Form $form) {
414
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
415
416
		// Performs canView permission check by limiting visible projects
417
		$project = $this->getCurrentProject();
418
		if (!$project) {
419
			return $this->project404Response();
420
		}
421
422
		$validEnvs = $project->DNEnvironmentList()
423
			->filterByCallback(function ($item) {
424
				return $item->canUploadArchive();
425
			});
426
427
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
428
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
429
		if (!$environment) {
430
			throw new LogicException('Invalid environment');
431
		}
432
433
		$this->validateSnapshotMode($data['Mode']);
434
435
		$dataArchive = DNDataArchive::create([
436
			'AuthorID' => Member::currentUserID(),
437
			'EnvironmentID' => $data['EnvironmentID'],
438
			'IsManualUpload' => true,
439
		]);
440
		// needs an ID and transfer to determine upload path
441
		$dataArchive->write();
442
		$dataTransfer = DNDataTransfer::create([
443
			'AuthorID' => Member::currentUserID(),
444
			'Mode' => $data['Mode'],
445
			'Origin' => 'ManualUpload',
446
			'EnvironmentID' => $data['EnvironmentID']
447
		]);
448
		$dataTransfer->write();
449
		$dataArchive->DataTransfers()->add($dataTransfer);
450
		$form->saveInto($dataArchive);
451
		$dataArchive->write();
452
		$workingDir = TEMP_FOLDER . DIRECTORY_SEPARATOR . 'deploynaut-transfer-' . $dataTransfer->ID;
453
454 View Code Duplication
		$cleanupFn = function () use ($workingDir, $dataTransfer, $dataArchive) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
455
			$process = new AbortableProcess(sprintf('rm -rf %s', escapeshellarg($workingDir)));
456
			$process->setTimeout(120);
457
			$process->run();
458
			$dataTransfer->delete();
459
			$dataArchive->delete();
460
		};
461
462
		// extract the sspak contents so we can inspect them
463
		try {
464
			$dataArchive->extractArchive($workingDir);
465
		} catch (Exception $e) {
466
			$cleanupFn();
467
			$form->sessionMessage(
468
				'There was a problem trying to open your snapshot for processing. Please try uploading again',
469
				'bad'
470
			);
471
			return $this->redirectBack();
472
		}
473
474
		// validate that the sspak contents match the declared contents
475
		$result = $dataArchive->validateArchiveContents();
476
		if (!$result->valid()) {
477
			$cleanupFn();
478
			$form->sessionMessage($result->message(), 'bad');
479
			return $this->redirectBack();
480
		}
481
482
		// fix file permissions of extracted sspak files then re-build the sspak
483
		try {
484
			$dataArchive->fixArchivePermissions($workingDir);
485
			$dataArchive->setArchiveFromFiles($workingDir);
486
		} catch (Exception $e) {
487
			$cleanupFn();
488
			$form->sessionMessage(
489
				'There was a problem processing your snapshot. Please try uploading again',
490
				'bad'
491
			);
492
			return $this->redirectBack();
493
		}
494
495
		// cleanup any extracted sspak contents lying around
496
		$process = new AbortableProcess(sprintf('rm -rf %s', escapeshellarg($workingDir)));
497
		$process->setTimeout(120);
498
		$process->run();
499
500
		return $this->customise([
501
			'Project' => $project,
502
			'CurrentProject' => $project,
503
			'SnapshotsSection' => 1,
504
			'DataArchive' => $dataArchive,
505
			'DataTransferRestoreForm' => $this->getDataTransferRestoreForm($this->request, $dataArchive),
506
			'BackURL' => $project->Link('snapshots')
507
		])->renderWith(['DNRoot_uploadsnapshot', 'DNRoot']);
508
	}
509
510
	/**
511
	 * @param \SS_HTTPRequest $request
512
	 * @return Form
513
	 */
514
	public function getPostSnapshotForm(\SS_HTTPRequest $request) {
515
		// Performs canView permission check by limiting visible projects
516
		$project = $this->getCurrentProject();
517
		if (!$project) {
518
			return $this->project404Response();
519
		}
520
521
		if (!$project->canUploadArchive()) {
522
			return new SS_HTTPResponse("Not allowed to upload", 401);
523
		}
524
525
		// Framing an environment as a "group of people with download access"
526
		// makes more sense to the user here, while still allowing us to enforce
527
		// environment specific restrictions on downloading the file later on.
528
		$envs = $project->DNEnvironmentList()->filterByCallback(function ($item) {
529
			return $item->canUploadArchive();
530
		});
531
		$envsMap = [];
532
		foreach ($envs as $env) {
533
			$envsMap[$env->ID] = $env->Name;
534
		}
535
536
		$form = Form::create(
537
			$this,
538
			'PostSnapshotForm',
539
			FieldList::create(
540
				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
541
				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
542
					->setEmptyString('Select an environment')
543
			),
544
			FieldList::create(
545
				FormAction::create('doPostSnapshot', 'Submit request')
546
					->addExtraClass('btn')
547
			),
548
			RequiredFields::create('File')
549
		);
550
551
		$form->disableSecurityToken();
552
		$form->addExtraClass('fields-wide');
553
		// Tweak the action so it plays well with our fake URL structure.
554
		$form->setFormAction($project->Link() . '/PostSnapshotForm');
555
556
		return $form;
557
	}
558
559
	/**
560
	 * @param array $data
561
	 * @param Form $form
562
	 *
563
	 * @return SS_HTTPResponse
564
	 */
565
	public function doPostSnapshot($data, $form) {
566
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
567
568
		$project = $this->getCurrentProject();
569
		if (!$project) {
570
			return $this->project404Response();
571
		}
572
573
		$validEnvs = $project->DNEnvironmentList()->filterByCallback(function ($item) {
574
			return $item->canUploadArchive();
575
		});
576
577
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
578
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
579
		if (!$environment) {
580
			throw new LogicException('Invalid environment');
581
		}
582
583
		$dataArchive = DNDataArchive::create([
584
			'UploadToken' => DNDataArchive::generate_upload_token(),
585
		]);
586
		$form->saveInto($dataArchive);
587
		$dataArchive->write();
588
589
		return $this->redirect(Controller::join_links(
590
			$project->Link(),
591
			'postsnapshotsuccess',
592
			$dataArchive->ID
593
		));
594
	}
595
596
	/**
597
	 * Action
598
	 *
599
	 * @param \SS_HTTPRequest $request
600
	 * @return SS_HTTPResponse - HTML
601
	 */
602
	public function snapshotslog(\SS_HTTPRequest $request) {
603
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
604
		return $this->getCustomisedViewSection('SnapshotsSection', 'Snapshots log');
605
	}
606
607
	/**
608
	 * @param \SS_HTTPRequest $request
609
	 * @return SS_HTTPResponse|string
610
	 * @throws SS_HTTPResponse_Exception
611
	 */
612
	public function postsnapshotsuccess(\SS_HTTPRequest $request) {
613
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
614
615
		// Performs canView permission check by limiting visible projects
616
		$project = $this->getCurrentProject();
617
		if (!$project) {
618
			return $this->project404Response();
619
		}
620
621
		if (!$project->canUploadArchive()) {
622
			return new SS_HTTPResponse("Not allowed to upload", 401);
623
		}
624
625
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
626
		if (!$dataArchive) {
627
			return new SS_HTTPResponse("Archive not found.", 404);
628
		}
629
630
		if (!$dataArchive->canRestore()) {
631
			throw new SS_HTTPResponse_Exception('Not allowed to restore archive', 403);
632
		}
633
634
		return $this->render([
635
			'Title' => 'How to send us your Data Snapshot by post',
636
			'DataArchive' => $dataArchive,
637
			'Address' => Config::inst()->get('Deploynaut', 'snapshot_post_address'),
638
			'BackURL' => $project->Link(),
639
		]);
640
	}
641
642
	/**
643
	 * @param \SS_HTTPRequest $request
644
	 * @return \SS_HTTPResponse
645
	 */
646
	public function project(\SS_HTTPRequest $request) {
647
		$this->setCurrentActionType(self::PROJECT_OVERVIEW);
648
		return $this->getCustomisedViewSection('ProjectOverview', '', ['IsAdmin' => Permission::check('ADMIN')]);
649
	}
650
651
	/**
652
	 * This action will star / unstar a project for the current member
653
	 *
654
	 * @param \SS_HTTPRequest $request
655
	 *
656
	 * @return SS_HTTPResponse
657
	 */
658
	public function toggleprojectstar(\SS_HTTPRequest $request) {
659
		$project = $this->getCurrentProject();
660
		if (!$project) {
661
			return $this->project404Response();
662
		}
663
664
		$member = Member::currentUser();
665
		if ($member === null) {
666
			return $this->project404Response();
667
		}
668
		$favProject = $member->StarredProjects()
669
			->filter('DNProjectID', $project->ID)
670
			->first();
671
672
		if ($favProject) {
673
			$member->StarredProjects()->remove($favProject);
674
		} else {
675
			$member->StarredProjects()->add($project);
676
		}
677
		return $this->redirectBack();
678
	}
679
680
	/**
681
	 * @param \SS_HTTPRequest $request
682
	 * @return \SS_HTTPResponse
683
	 */
684
	public function branch(\SS_HTTPRequest $request) {
685
		$project = $this->getCurrentProject();
686
		if (!$project) {
687
			return $this->project404Response();
688
		}
689
690
		$branchName = $request->getVar('name');
691
		$branch = $project->DNBranchList()->byName($branchName);
692
		if (!$branch) {
693
			return new SS_HTTPResponse("Branch '" . Convert::raw2xml($branchName) . "' not found.", 404);
694
		}
695
696
		return $this->render([
697
			'CurrentBranch' => $branch,
698
		]);
699
	}
700
701
	/**
702
	 * @param \SS_HTTPRequest $request
703
	 * @return \SS_HTTPResponse
704
	 */
705
	public function environment(\SS_HTTPRequest $request) {
706
		// Performs canView permission check by limiting visible projects
707
		$project = $this->getCurrentProject();
708
		if (!$project) {
709
			return $this->project404Response();
710
		}
711
712
		// Performs canView permission check by limiting visible projects
713
		$env = $this->getCurrentEnvironment($project);
714
		if (!$env) {
715
			return $this->environment404Response();
716
		}
717
718
		// we redirect to the EnvironmentOverview dispatcher if using the new deploy form.
719
		if (\DNDeployment::flag_new_deploy_enabled()) {
720
			return $this->redirect($env->Link(\EnvironmentOverview::ACTION_OVERVIEW));
721
		}
722
723
		return $this->render([
724
			'DNEnvironmentList' => $this->getCurrentProject()->DNEnvironmentList(),
725
			'Redeploy' => (bool) $request->getVar('redeploy')
726
		]);
727
	}
728
729
	/**
730
	 * Shows the creation log.
731
	 *
732
	 * @param \SS_HTTPRequest $request
733
	 * @return string
734
	 */
735
	public function createenv(\SS_HTTPRequest $request) {
736
		$params = $request->params();
737
		if ($params['Identifier']) {
738
			$record = DNCreateEnvironment::get()->byId($params['Identifier']);
739
740
			if (!$record || !$record->ID) {
741
				throw new SS_HTTPResponse_Exception('Create environment not found', 404);
742
			}
743
			if (!$record->canView()) {
744
				return Security::permissionFailure();
745
			}
746
747
			$project = $this->getCurrentProject();
748
			if (!$project) {
749
				return $this->project404Response();
750
			}
751
752
			if ($project->Name != $params['Project']) {
753
				throw new LogicException("Project in URL doesn't match this creation");
754
			}
755
756
			return $this->render([
757
				'CreateEnvironment' => $record,
758
			]);
759
		}
760
		return $this->render(['CurrentTitle' => 'Create an environment']);
761
	}
762
763
	public function createenvlog(\SS_HTTPRequest $request) {
764
		$params = $request->params();
765
		$env = DNCreateEnvironment::get()->byId($params['Identifier']);
766
767
		if (!$env || !$env->ID) {
768
			throw new SS_HTTPResponse_Exception('Log not found', 404);
769
		}
770
		if (!$env->canView()) {
771
			return Security::permissionFailure();
772
		}
773
774
		$project = $env->Project();
775
776
		if ($project->Name != $params['Project']) {
777
			throw new LogicException("Project in URL doesn't match this deploy");
778
		}
779
780
		$log = $env->log();
781
		if ($log->exists()) {
782
			$content = $log->content();
783
		} else {
784
			$content = 'Waiting for action to start';
785
		}
786
787
		return $this->sendResponse($env->ResqueStatus(), $content);
788
	}
789
790
	/**
791
	 * @param \SS_HTTPRequest $request
792
	 * @return Form
793
	 */
794
	public function getCreateEnvironmentForm(\SS_HTTPRequest $request) {
795
		$this->setCurrentActionType(self::ACTION_ENVIRONMENTS);
796
797
		$project = $this->getCurrentProject();
798
		if (!$project) {
799
			return $this->project404Response();
800
		}
801
802
		$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...
803
		if (!$envType || !class_exists($envType)) {
804
			return null;
805
		}
806
807
		$backend = Injector::inst()->get($envType);
808
		if (!($backend instanceof EnvironmentCreateBackend)) {
809
			// Only allow this for supported backends.
810
			return null;
811
		}
812
813
		$fields = $backend->getCreateEnvironmentFields($project);
814
		if (!$fields) {
815
			return null;
816
		}
817
818
		if (!$project->canCreateEnvironments()) {
819
			return new SS_HTTPResponse('Not allowed to create environments for this project', 401);
820
		}
821
822
		$form = Form::create(
823
			$this,
824
			'CreateEnvironmentForm',
825
			$fields,
826
			FieldList::create(
827
				FormAction::create('doCreateEnvironment', 'Create')
828
					->addExtraClass('btn')
829
			),
830
			$backend->getCreateEnvironmentValidator()
831
		);
832
833
		// Tweak the action so it plays well with our fake URL structure.
834
		$form->setFormAction($project->Link() . '/CreateEnvironmentForm');
835
836
		return $form;
837
	}
838
839
	/**
840
	 * @param array $data
841
	 * @param Form $form
842
	 *
843
	 * @return bool|HTMLText|SS_HTTPResponse
844
	 */
845
	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...
846
		$this->setCurrentActionType(self::ACTION_ENVIRONMENTS);
847
848
		$project = $this->getCurrentProject();
849
		if (!$project) {
850
			return $this->project404Response();
851
		}
852
853
		if (!$project->canCreateEnvironments()) {
854
			return new SS_HTTPResponse('Not allowed to create environments for this project', 401);
855
		}
856
857
		// Set the environment type so we know what we're creating.
858
		$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...
859
860
		$job = DNCreateEnvironment::create();
861
862
		$job->Data = serialize($data);
863
		$job->ProjectID = $project->ID;
864
		$job->write();
865
		$job->start();
866
867
		return $this->redirect($project->Link('createenv') . '/' . $job->ID);
868
	}
869
870
	/**
871
	 * Get the DNData object.
872
	 *
873
	 * @return DNData
874
	 */
875
	public function DNData() {
876
		return DNData::inst();
877
	}
878
879
	/**
880
	 * Provide a list of all projects.
881
	 *
882
	 * @return SS_List
883
	 */
884
	public function DNProjectList() {
885
		$memberId = Member::currentUserID();
886
		if (!$memberId) {
887
			return new ArrayList();
888
		}
889
890
		if (Permission::check('ADMIN')) {
891
			return DNProject::get();
892
		}
893
894
		$projects = Member::get()->filter('ID', $memberId)
895
			->relation('Groups')
896
			->relation('Projects');
897
898
		$this->extend('updateDNProjectList', $projects);
899
		return $projects;
900
	}
901
902
	/**
903
	 * @return ArrayList
904
	 */
905
	public function getPlatformSpecificStrings() {
906
		$strings = $this->config()->platform_specific_strings;
907
		if ($strings) {
908
			return new ArrayList($strings);
909
		}
910
	}
911
912
	/**
913
	 * Provide a list of all starred projects for the currently logged in member
914
	 *
915
	 * @return SS_List
916
	 */
917
	public function getStarredProjects() {
918
		$member = Member::currentUser();
919
		if ($member === null) {
920
			return new ArrayList();
921
		}
922
923
		$favProjects = $member->StarredProjects();
924
925
		$list = new ArrayList();
926
		foreach ($favProjects as $project) {
927
			if ($project->canView($member)) {
928
				$list->add($project);
929
			}
930
		}
931
		return $list;
932
	}
933
934
	/**
935
	 * Returns top level navigation of projects.
936
	 *
937
	 * @param int $limit
938
	 *
939
	 * @return ArrayList
940
	 */
941
	public function Navigation($limit = 5) {
942
		$navigation = new ArrayList();
943
944
		$currentProject = $this->getCurrentProject();
945
		$currentEnvironment = $this->getCurrentEnvironment();
946
		$actionType = $this->getCurrentActionType();
947
948
		$projects = $this->getStarredProjects();
949
		if ($projects->count() < 1) {
950
			$projects = $this->DNProjectList();
951
		} else {
952
			$limit = -1;
953
		}
954
955
		if ($projects->count() > 0) {
956
			$activeProject = false;
957
958
			if ($limit > 0) {
959
				$limitedProjects = $projects->limit($limit);
960
			} else {
961
				$limitedProjects = $projects;
962
			}
963
964
			foreach ($limitedProjects as $project) {
965
				$isActive = $currentProject && $currentProject->ID == $project->ID;
966
				if ($isActive) {
967
					$activeProject = true;
968
				}
969
970
				$isCurrentEnvironment = false;
971
				if ($project && $currentEnvironment) {
972
					$isCurrentEnvironment = (bool) $project->DNEnvironmentList()->find('ID', $currentEnvironment->ID);
973
				}
974
975
				$navigation->push([
976
					'Project' => $project,
977
					'IsCurrentEnvironment' => $isCurrentEnvironment,
978
					'IsActive' => $currentProject && $currentProject->ID == $project->ID,
979
					'IsOverview' => $actionType == self::PROJECT_OVERVIEW && $currentProject->ID == $project->ID
980
				]);
981
			}
982
983
			// Ensure the current project is in the list
984
			if (!$activeProject && $currentProject) {
985
				$navigation->unshift([
986
					'Project' => $currentProject,
987
					'IsActive' => true,
988
					'IsCurrentEnvironment' => $currentEnvironment,
989
					'IsOverview' => $actionType == self::PROJECT_OVERVIEW
990
				]);
991
				if ($limit > 0 && $navigation->count() > $limit) {
992
					$navigation->pop();
993
				}
994
			}
995
		}
996
997
		return $navigation;
998
	}
999
1000
	/**
1001
	 * Construct the deployment form
1002
	 *
1003
	 * @deprecated 2.0.0 - moved to DeployDispatcher
1004
	 *
1005
	 * @return Form
1006
	 */
1007
	public function getDeployForm($request = null) {
1008
1009
		// Performs canView permission check by limiting visible projects
1010
		$project = $this->getCurrentProject();
1011
		if (!$project) {
1012
			return $this->project404Response();
1013
		}
1014
1015
		// Performs canView permission check by limiting visible projects
1016
		$environment = $this->getCurrentEnvironment($project);
1017
		if (!$environment) {
1018
			return $this->environment404Response();
1019
		}
1020
1021
		if (!$environment->canDeploy()) {
1022
			return new SS_HTTPResponse("Not allowed to deploy", 401);
1023
		}
1024
1025
		// Generate the form
1026
		$form = new DeployForm($this, 'DeployForm', $environment, $project);
0 ignored issues
show
Deprecated Code introduced by
The class DeployForm has been deprecated with message: 2.0.0 - moved to Dispatchers and frontend Form for generating deployments from a specified commit

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
1027
1028
		// If this is an ajax request we don't want to submit the form - we just want to retrieve the markup.
1029
		if (
1030
			$request &&
1031
			!$request->requestVar('action_showDeploySummary') &&
1032
			$this->getRequest()->isAjax() &&
1033
			$this->getRequest()->isGET()
1034
		) {
1035
			// We can just use the URL we're accessing
1036
			$form->setFormAction($this->getRequest()->getURL());
1037
1038
			$body = json_encode(['Content' => $form->forAjaxTemplate()->forTemplate()]);
1039
			$this->getResponse()->addHeader('Content-Type', 'application/json');
1040
			$this->getResponse()->setBody($body);
1041
			return $body;
1042
		}
1043
1044
		$form->setFormAction($this->getRequest()->getURL() . '/DeployForm');
1045
		return $form;
1046
	}
1047
1048
	/**
1049
	 * @deprecated 2.0.0 - moved to GitDispatcher
1050
	 *
1051
	 * @param \SS_HTTPRequest $request
1052
	 *
1053
	 * @return SS_HTTPResponse|string
1054
	 */
1055
	public function gitRevisions(\SS_HTTPRequest $request) {
1056
1057
		// Performs canView permission check by limiting visible projects
1058
		$project = $this->getCurrentProject();
1059
		if (!$project) {
1060
			return $this->project404Response();
1061
		}
1062
1063
		// Performs canView permission check by limiting visible projects
1064
		$env = $this->getCurrentEnvironment($project);
1065
		if (!$env) {
1066
			return $this->environment404Response();
1067
		}
1068
1069
		$options = [];
1070
		foreach ($env->getSupportedOptions() as $option) {
1071
			$options[] = [
1072
				'name' => $option->getName(),
1073
				'title' => $option->getTitle(),
1074
				'defaultValue' => $option->getDefaultValue()
1075
			];
1076
		}
1077
1078
		$tabs = [];
1079
		$id = 0;
1080
		$data = [
1081
			'id' => ++$id,
1082
			'name' => 'Deploy the latest version of a branch',
1083
			'field_type' => 'dropdown',
1084
			'field_label' => 'Choose a branch',
1085
			'field_id' => 'branch',
1086
			'field_data' => [],
1087
			'options' => $options
1088
		];
1089
		foreach ($project->DNBranchList() as $branch) {
1090
			$sha = $branch->SHA();
1091
			$name = $branch->Name();
1092
			$branchValue = sprintf("%s (%s, %s old)",
1093
				$name,
1094
				substr($sha, 0, 8),
1095
				$branch->LastUpdated()->TimeDiff()
1096
			);
1097
			$data['field_data'][] = [
1098
				'id' => $sha,
1099
				'text' => $branchValue,
1100
				'branch_name' => $name // the raw branch name, not including the time etc
1101
			];
1102
		}
1103
		$tabs[] = $data;
1104
1105
		$data = [
1106
			'id' => ++$id,
1107
			'name' => 'Deploy a tagged release',
1108
			'field_type' => 'dropdown',
1109
			'field_label' => 'Choose a tag',
1110
			'field_id' => 'tag',
1111
			'field_data' => [],
1112
			'options' => $options
1113
		];
1114
1115
		foreach ($project->DNTagList()->setLimit(null) as $tag) {
1116
			$name = $tag->Name();
1117
			$data['field_data'][] = [
1118
				'id' => $tag->SHA(),
1119
				'text' => sprintf("%s", $name)
1120
			];
1121
		}
1122
1123
		// show newest tags first.
1124
		$data['field_data'] = array_reverse($data['field_data']);
1125
1126
		$tabs[] = $data;
1127
1128
		// Past deployments
1129
		$data = [
1130
			'id' => ++$id,
1131
			'name' => 'Redeploy a release that was previously deployed (to any environment)',
1132
			'field_type' => 'dropdown',
1133
			'field_label' => 'Choose a previously deployed release',
1134
			'field_id' => 'release',
1135
			'field_data' => [],
1136
			'options' => $options
1137
		];
1138
		// We are aiming at the format:
1139
		// [{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...
1140
		$redeploy = [];
1141 View Code Duplication
		foreach ($project->DNEnvironmentList() as $dnEnvironment) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
1142
			$envName = $dnEnvironment->Name;
1143
			$perEnvDeploys = [];
1144
1145
			foreach ($dnEnvironment->DeployHistory() as $deploy) {
1146
				$sha = $deploy->SHA;
1147
1148
				// Check if exists to make sure the newest deployment date is used.
1149
				if (!isset($perEnvDeploys[$sha])) {
1150
					$pastValue = sprintf("%s (deployed %s)",
1151
						substr($sha, 0, 8),
1152
						$deploy->obj('LastEdited')->Ago()
1153
					);
1154
					$perEnvDeploys[$sha] = [
1155
						'id' => $sha,
1156
						'text' => $pastValue
1157
					];
1158
				}
1159
			}
1160
1161
			if (!empty($perEnvDeploys)) {
1162
				$redeploy[$envName] = array_values($perEnvDeploys);
1163
			}
1164
		}
1165
		// Convert the array to the frontend format (i.e. keyed to regular array)
1166
		foreach ($redeploy as $name => $descr) {
1167
			$data['field_data'][] = ['text' => $name, 'children' => $descr];
1168
		}
1169
		$tabs[] = $data;
1170
1171
		$data = [
1172
			'id' => ++$id,
1173
			'name' => 'Deploy a specific SHA',
1174
			'field_type' => 'textfield',
1175
			'field_label' => 'Choose a SHA',
1176
			'field_id' => 'SHA',
1177
			'field_data' => [],
1178
			'options' => $options
1179
		];
1180
		$tabs[] = $data;
1181
1182
		// get the last time git fetch was run
1183
		$lastFetched = 'never';
1184
		$fetch = DNGitFetch::get()
1185
			->filter('ProjectID', $project->ID)
1186
			->sort('LastEdited', 'DESC')
1187
			->first();
1188
		if ($fetch) {
1189
			$lastFetched = $fetch->dbObject('LastEdited')->Ago();
1190
		}
1191
1192
		$data = [
1193
			'Tabs' => $tabs,
1194
			'last_fetched' => $lastFetched
1195
		];
1196
1197
		$this->applyRedeploy($request, $data);
1198
1199
		return json_encode($data, JSON_PRETTY_PRINT);
1200
	}
1201
1202
	/**
1203
	 * @deprecated 2.0.0 - moved to PlanDispatcher
1204
	 *
1205
	 * @param \SS_HTTPRequest $request
1206
	 *
1207
	 * @return string
1208
	 */
1209
	public function deploySummary(\SS_HTTPRequest $request) {
1210
1211
		// Performs canView permission check by limiting visible projects
1212
		$project = $this->getCurrentProject();
1213
		if (!$project) {
1214
			return $this->project404Response();
1215
		}
1216
1217
		// Performs canView permission check by limiting visible projects
1218
		$environment = $this->getCurrentEnvironment($project);
1219
		if (!$environment) {
1220
			return $this->environment404Response();
1221
		}
1222
1223
		// Plan the deployment.
1224
		$strategy = $environment->getDeployStrategy($request);
1225
		$data = $strategy->toArray();
1226
1227
		// Add in a URL for comparing from->to code changes. Ensure that we have
1228
		// two proper 40 character SHAs, otherwise we can't show the compare link.
1229
		$interface = $project->getRepositoryInterface();
1230
		if (
1231
			!empty($interface) && !empty($interface->URL)
1232
			&& !empty($data['changes']['Code version']['from'])
1233
			&& strlen($data['changes']['Code version']['from']) == '40'
1234
			&& !empty($data['changes']['Code version']['to'])
1235
			&& strlen($data['changes']['Code version']['to']) == '40'
1236
		) {
1237
			$compareurl = sprintf(
1238
				'%s/compare/%s...%s',
1239
				$interface->URL,
1240
				$data['changes']['Code version']['from'],
1241
				$data['changes']['Code version']['to']
1242
			);
1243
			$data['changes']['Code version']['compareUrl'] = $compareurl;
1244
		}
1245
1246
		// Append json to response
1247
		$token = SecurityToken::inst();
1248
		$data['SecurityID'] = $token->getValue();
1249
1250
		$this->extend('updateDeploySummary', $data);
1251
1252
		return json_encode($data);
1253
	}
1254
1255
	/**
1256
	 * Deployment form submission handler.
1257
	 *
1258
	 * @deprecated 2.0.0 - moved to DeployDispatcher
1259
	 *
1260
	 * Initiate a DNDeployment record and redirect to it for status polling
1261
	 *
1262
	 * @param \SS_HTTPRequest $request
1263
	 *
1264
	 * @return SS_HTTPResponse
1265
	 * @throws ValidationException
1266
	 * @throws null
1267
	 */
1268
	public function startDeploy(\SS_HTTPRequest $request) {
1269
1270
		$token = SecurityToken::inst();
1271
1272
		// Ensure the submitted token has a value
1273
		$submittedToken = $request->postVar(\Dispatcher::SECURITY_TOKEN_NAME);
1274
		if (!$submittedToken) {
1275
			return false;
1276
		}
1277
		// Do the actual check.
1278
		$check = $token->check($submittedToken);
1279
		// Ensure the CSRF Token is correct
1280
		if (!$check) {
1281
			// CSRF token didn't match
1282
			return $this->httpError(400, 'Bad Request');
1283
		}
1284
1285
		// Performs canView permission check by limiting visible projects
1286
		$project = $this->getCurrentProject();
1287
		if (!$project) {
1288
			return $this->project404Response();
1289
		}
1290
1291
		// Performs canView permission check by limiting visible projects
1292
		$environment = $this->getCurrentEnvironment($project);
1293
		if (!$environment) {
1294
			return $this->environment404Response();
1295
		}
1296
1297
		// Initiate the deployment
1298
		// The extension point should pass in: Project, Environment, SelectRelease, buildName
1299
		$this->extend('doDeploy', $project, $environment, $buildName, $data);
1300
1301
		// Start the deployment based on the approved strategy.
1302
		$strategy = new DeploymentStrategy($environment);
1303
		$strategy->fromArray($request->requestVar('strategy'));
1304
		$deployment = $strategy->createDeployment();
1305
		// Bypass approval by going straight to Queued.
1306
		$deployment->getMachine()->apply(DNDeployment::TR_QUEUE);
1307
1308
		return json_encode([
1309
			'url' => Director::absoluteBaseURL() . $deployment->Link()
1310
		], JSON_PRETTY_PRINT);
1311
	}
1312
1313
	/**
1314
	 * @deprecated 2.0.0 - moved to DeployDispatcher
1315
	 *
1316
	 * Action - Do the actual deploy
1317
	 *
1318
	 * @param \SS_HTTPRequest $request
1319
	 *
1320
	 * @return SS_HTTPResponse|string
1321
	 * @throws SS_HTTPResponse_Exception
1322
	 */
1323
	public function deploy(\SS_HTTPRequest $request) {
1324
		$params = $request->params();
1325
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1326
1327
		if (!$deployment || !$deployment->ID) {
1328
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1329
		}
1330
		if (!$deployment->canView()) {
1331
			return Security::permissionFailure();
1332
		}
1333
1334
		$environment = $deployment->Environment();
1335
		$project = $environment->Project();
1336
1337
		if ($environment->Name != $params['Environment']) {
1338
			throw new LogicException("Environment in URL doesn't match this deploy");
1339
		}
1340
		if ($project->Name != $params['Project']) {
1341
			throw new LogicException("Project in URL doesn't match this deploy");
1342
		}
1343
1344
		return $this->render([
1345
			'Deployment' => $deployment,
1346
		]);
1347
	}
1348
1349
	/**
1350
	 * @deprecated 2.0.0 - moved to DeployDispatcher
1351
	 *
1352
	 * Action - Get the latest deploy log
1353
	 *
1354
	 * @param \SS_HTTPRequest $request
1355
	 *
1356
	 * @return string
1357
	 * @throws SS_HTTPResponse_Exception
1358
	 */
1359
	public function deploylog(\SS_HTTPRequest $request) {
1360
		$params = $request->params();
1361
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1362
1363
		if (!$deployment || !$deployment->ID) {
1364
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1365
		}
1366
		if (!$deployment->canView()) {
1367
			return Security::permissionFailure();
1368
		}
1369
1370
		$environment = $deployment->Environment();
1371
		$project = $environment->Project();
1372
1373
		if ($environment->Name != $params['Environment']) {
1374
			throw new LogicException("Environment in URL doesn't match this deploy");
1375
		}
1376
		if ($project->Name != $params['Project']) {
1377
			throw new LogicException("Project in URL doesn't match this deploy");
1378
		}
1379
1380
		$log = $deployment->log();
1381
		if ($log->exists()) {
1382
			$content = $log->content();
1383
		} else {
1384
			$content = 'Waiting for action to start';
1385
		}
1386
1387
		return $this->sendResponse($deployment->ResqueStatus(), $content);
1388
	}
1389
1390
	public function abortDeploy(\SS_HTTPRequest $request) {
1391
		$params = $request->params();
1392
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1393
1394
		if (!$deployment || !$deployment->ID) {
1395
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1396
		}
1397
		if (!$deployment->canView()) {
1398
			return Security::permissionFailure();
1399
		}
1400
1401
		// For now restrict to ADMINs only.
1402
		if (!Permission::check('ADMIN')) {
1403
			return Security::permissionFailure();
1404
		}
1405
1406
		$environment = $deployment->Environment();
1407
		$project = $environment->Project();
1408
1409
		if ($environment->Name != $params['Environment']) {
1410
			throw new LogicException("Environment in URL doesn't match this deploy");
1411
		}
1412
		if ($project->Name != $params['Project']) {
1413
			throw new LogicException("Project in URL doesn't match this deploy");
1414
		}
1415
1416
		if (!in_array($deployment->Status, ['Queued', 'Deploying', 'Aborting'])) {
1417
			throw new LogicException(sprintf("Cannot abort from %s state.", $deployment->Status));
1418
		}
1419
1420
		$deployment->getMachine()->apply(DNDeployment::TR_ABORT);
1421
1422
		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...
1423
	}
1424
1425
	/**
1426
	 * @param \SS_HTTPRequest|null $request
1427
	 *
1428
	 * @return Form
1429
	 */
1430
	public function getDataTransferForm(\SS_HTTPRequest $request = null) {
1431
		// Performs canView permission check by limiting visible projects
1432
		$envs = $this->getCurrentProject()->DNEnvironmentList()->filterByCallback(function ($item) {
1433
			return $item->canBackup();
1434
		});
1435
1436
		if (!$envs) {
1437
			return $this->environment404Response();
1438
		}
1439
1440
		$items = [];
1441
		$disabledEnvironments = [];
1442 View Code Duplication
		foreach($envs as $env) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
1443
			$items[$env->ID] = $env->Title;
1444
			if ($env->CurrentBuild() === false) {
1445
				$items[$env->ID] = sprintf("%s - requires initial deployment", $env->Title);
1446
				$disabledEnvironments[] = $env->ID;
1447
			}
1448
		}
1449
1450
		$envsField =  DropdownField::create('EnvironmentID', 'Environment', $items)
1451
			->setEmptyString('Select an environment');
1452
		$envsField->setDisabledItems($disabledEnvironments);
1453
1454
		$formAction = FormAction::create('doDataTransfer', 'Create')
1455
			->addExtraClass('btn');
1456
1457
		if (count($disabledEnvironments) === $envs->count()) {
1458
			$formAction->setDisabled(true);
1459
		}
1460
1461
		$form = Form::create(
1462
			$this,
1463
			'DataTransferForm',
1464
			FieldList::create(
1465
				HiddenField::create('Direction', null, 'get'),
1466
				$envsField,
1467
				DropdownField::create('Mode', 'Transfer', DNDataArchive::get_mode_map())
1468
			),
1469
			FieldList::create($formAction)
1470
		);
1471
		$form->setFormAction($this->getRequest()->getURL() . '/DataTransferForm');
1472
1473
		return $form;
1474
	}
1475
1476
	/**
1477
	 * @param array $data
1478
	 * @param Form $form
1479
	 *
1480
	 * @return SS_HTTPResponse
1481
	 * @throws SS_HTTPResponse_Exception
1482
	 */
1483
	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...
1484
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1485
1486
		// Performs canView permission check by limiting visible projects
1487
		$project = $this->getCurrentProject();
1488
		if (!$project) {
1489
			return $this->project404Response();
1490
		}
1491
1492
		$dataArchive = null;
1493
1494
		// Validate direction.
1495
		if ($data['Direction'] == 'get') {
1496
			$validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1497
				->filterByCallback(function ($item) {
1498
					return $item->canBackup();
1499
				});
1500
		} else if ($data['Direction'] == 'push') {
1501
			$validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1502
				->filterByCallback(function ($item) {
1503
					return $item->canRestore();
1504
				});
1505
		} else {
1506
			throw new LogicException('Invalid direction');
1507
		}
1508
1509
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
1510
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
1511
		if (!$environment) {
1512
			throw new LogicException('Invalid environment');
1513
		}
1514
1515
		$this->validateSnapshotMode($data['Mode']);
1516
1517
		// Only 'push' direction is allowed an association with an existing archive.
1518
		if (
1519
			$data['Direction'] == 'push'
1520
			&& isset($data['DataArchiveID'])
1521
			&& is_numeric($data['DataArchiveID'])
1522
		) {
1523
			$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1524
			if (!$dataArchive) {
1525
				throw new LogicException('Invalid data archive');
1526
			}
1527
1528
			if (!$dataArchive->canDownload()) {
1529
				throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1530
			}
1531
		}
1532
1533
		$transfer = DNDataTransfer::create();
1534
		$transfer->EnvironmentID = $environment->ID;
1535
		$transfer->Direction = $data['Direction'];
1536
		$transfer->Mode = $data['Mode'];
1537
		$transfer->DataArchiveID = $dataArchive ? $dataArchive->ID : null;
1538
		if ($data['Direction'] == 'push') {
1539
			$transfer->setBackupBeforePush(!empty($data['BackupBeforePush']));
1540
		}
1541
		$transfer->write();
1542
		$transfer->start();
1543
1544
		return $this->redirect($transfer->Link());
1545
	}
1546
1547
	/**
1548
	 * View into the log for a {@link DNDataTransfer}.
1549
	 *
1550
	 * @param \SS_HTTPRequest $request
1551
	 *
1552
	 * @return SS_HTTPResponse|string
1553
	 * @throws SS_HTTPResponse_Exception
1554
	 */
1555
	public function transfer(\SS_HTTPRequest $request) {
1556
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1557
1558
		$params = $request->params();
1559
		$transfer = DNDataTransfer::get()->byId($params['Identifier']);
1560
1561
		if (!$transfer || !$transfer->ID) {
1562
			throw new SS_HTTPResponse_Exception('Transfer not found', 404);
1563
		}
1564
		if (!$transfer->canView()) {
1565
			return Security::permissionFailure();
1566
		}
1567
1568
		$environment = $transfer->Environment();
1569
		$project = $environment->Project();
1570
1571
		if ($project->Name != $params['Project']) {
1572
			throw new LogicException("Project in URL doesn't match this deploy");
1573
		}
1574
1575
		return $this->render([
1576
			'CurrentTransfer' => $transfer,
1577
			'SnapshotsSection' => 1,
1578
		]);
1579
	}
1580
1581
	/**
1582
	 * Action - Get the latest deploy log
1583
	 *
1584
	 * @param \SS_HTTPRequest $request
1585
	 *
1586
	 * @return string
1587
	 * @throws SS_HTTPResponse_Exception
1588
	 */
1589
	public function transferlog(\SS_HTTPRequest $request) {
1590
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1591
1592
		$params = $request->params();
1593
		$transfer = DNDataTransfer::get()->byId($params['Identifier']);
1594
1595
		if (!$transfer || !$transfer->ID) {
1596
			throw new SS_HTTPResponse_Exception('Transfer not found', 404);
1597
		}
1598
		if (!$transfer->canView()) {
1599
			return Security::permissionFailure();
1600
		}
1601
1602
		$environment = $transfer->Environment();
1603
		$project = $environment->Project();
1604
1605
		if ($project->Name != $params['Project']) {
1606
			throw new LogicException("Project in URL doesn't match this deploy");
1607
		}
1608
1609
		$log = $transfer->log();
1610
		if ($log->exists()) {
1611
			$content = $log->content();
1612
		} else {
1613
			$content = 'Waiting for action to start';
1614
		}
1615
1616
		return $this->sendResponse($transfer->ResqueStatus(), $content);
1617
	}
1618
1619
	/**
1620
	 * Note: Submits to the same action as {@link getDataTransferForm()},
1621
	 * but with a Direction=push and an archive reference.
1622
	 *
1623
	 * @param \SS_HTTPRequest $request
1624
	 * @param \DNDataArchive|null $dataArchive Only set when method is called manually in {@link restore()},
1625
	 *                            otherwise the state is inferred from the request data.
1626
	 * @return Form
1627
	 */
1628
	public function getDataTransferRestoreForm(\SS_HTTPRequest $request, \DNDataArchive $dataArchive = null) {
1629
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1630
1631
		// Performs canView permission check by limiting visible projects
1632
		$project = $this->getCurrentProject();
1633
		$envs = $project->DNEnvironmentList()->filterByCallback(function ($item) {
1634
			return $item->canRestore();
1635
		});
1636
1637
		if (!$envs) {
1638
			return $this->environment404Response();
1639
		}
1640
1641
		$modesMap = [];
1642
		if (in_array($dataArchive->Mode, ['all'])) {
1643
			$modesMap['all'] = 'Database and Assets';
1644
		};
1645
		if (in_array($dataArchive->Mode, ['all', 'db'])) {
1646
			$modesMap['db'] = 'Database only';
1647
		};
1648
		if (in_array($dataArchive->Mode, ['all', 'assets'])) {
1649
			$modesMap['assets'] = 'Assets only';
1650
		};
1651
1652
		$alertMessage = '<div class="alert alert-warning"><strong>Warning:</strong> '
1653
			. 'This restore will overwrite the data on the chosen environment below</div>';
1654
1655
1656
		$items = [];
1657
		$disabledEnvironments = [];
1658 View Code Duplication
		foreach($envs as $env) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
1659
			$items[$env->ID] = $env->Title;
1660
			if ($env->CurrentBuild() === false) {
1661
				$items[$env->ID] = sprintf("%s - requires initial deployment", $env->Title);
1662
				$disabledEnvironments[] = $env->ID;
1663
			}
1664
		}
1665
1666
		$envsField = DropdownField::create('EnvironmentID', 'Environment', $items)
1667
			->setEmptyString('Select an environment');
1668
		$envsField->setDisabledItems($disabledEnvironments);
1669
		$formAction = FormAction::create('doDataTransfer', 'Restore Data')->addExtraClass('btn');
1670
1671
		if (count($disabledEnvironments) == $envs->count()) {
1672
			$formAction->setDisabled(true);
1673
		}
1674
1675
		$form = Form::create(
1676
			$this,
1677
			'DataTransferRestoreForm',
1678
			FieldList::create(
1679
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1680
				HiddenField::create('Direction', null, 'push'),
1681
				LiteralField::create('Warning', $alertMessage),
1682
				$envsField,
1683
				DropdownField::create('Mode', 'Transfer', $modesMap),
1684
				CheckboxField::create('BackupBeforePush', 'Backup existing data', '1')
1685
			),
1686
			FieldList::create($formAction)
1687
		);
1688
		$form->setFormAction($project->Link() . '/DataTransferRestoreForm');
1689
1690
		return $form;
1691
	}
1692
1693
	/**
1694
	 * View a form to restore a specific {@link DataArchive}.
1695
	 * Permission checks are handled in {@link DataArchives()}.
1696
	 * Submissions are handled through {@link doDataTransfer()}, same as backup operations.
1697
	 *
1698
	 * @param \SS_HTTPRequest $request
1699
	 *
1700
	 * @return HTMLText
1701
	 * @throws SS_HTTPResponse_Exception
1702
	 */
1703 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...
1704
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1705
1706
		/** @var DNDataArchive $dataArchive */
1707
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1708
1709
		if (!$dataArchive) {
1710
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1711
		}
1712
1713
		// We check for canDownload because that implies access to the data.
1714
		// canRestore is later checked on the actual restore action per environment.
1715
		if (!$dataArchive->canDownload()) {
1716
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1717
		}
1718
1719
		$form = $this->getDataTransferRestoreForm($this->request, $dataArchive);
1720
1721
		// View currently only available via ajax
1722
		return $form->forTemplate();
1723
	}
1724
1725
	/**
1726
	 * View a form to delete a specific {@link DataArchive}.
1727
	 * Permission checks are handled in {@link DataArchives()}.
1728
	 * Submissions are handled through {@link doDelete()}.
1729
	 *
1730
	 * @param \SS_HTTPRequest $request
1731
	 *
1732
	 * @return HTMLText
1733
	 * @throws SS_HTTPResponse_Exception
1734
	 */
1735 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...
1736
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1737
1738
		/** @var DNDataArchive $dataArchive */
1739
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1740
1741
		if (!$dataArchive) {
1742
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1743
		}
1744
1745
		if (!$dataArchive->canDelete()) {
1746
			throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403);
1747
		}
1748
1749
		$form = $this->getDeleteForm($this->request, $dataArchive);
1750
1751
		// View currently only available via ajax
1752
		return $form->forTemplate();
1753
	}
1754
1755
	/**
1756
	 * @param \SS_HTTPRequest $request
1757
	 * @param DNDataArchive|null $dataArchive Only set when method is called manually, otherwise the state is inferred
1758
	 *        from the request data.
1759
	 * @return Form
1760
	 */
1761
	public function getDeleteForm(\SS_HTTPRequest $request, \DNDataArchive $dataArchive = null) {
1762
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1763
1764
		// Performs canView permission check by limiting visible projects
1765
		$project = $this->getCurrentProject();
1766
		if (!$project) {
1767
			return $this->project404Response();
1768
		}
1769
1770
		$snapshotDeleteWarning = '<div class="alert alert-warning">'
1771
			. 'Are you sure you want to permanently delete this snapshot from this archive area?'
1772
			. '</div>';
1773
1774
		$form = Form::create(
1775
			$this,
1776
			'DeleteForm',
1777
			FieldList::create(
1778
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1779
				LiteralField::create('Warning', $snapshotDeleteWarning)
1780
			),
1781
			FieldList::create(
1782
				FormAction::create('doDelete', 'Delete')
1783
					->addExtraClass('btn')
1784
			)
1785
		);
1786
		$form->setFormAction($project->Link() . '/DeleteForm');
1787
1788
		return $form;
1789
	}
1790
1791
	/**
1792
	 * @param array $data
1793
	 * @param Form $form
1794
	 *
1795
	 * @return bool|SS_HTTPResponse
1796
	 * @throws SS_HTTPResponse_Exception
1797
	 */
1798
	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...
1799
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1800
1801
		// Performs canView permission check by limiting visible projects
1802
		$project = $this->getCurrentProject();
1803
		if (!$project) {
1804
			return $this->project404Response();
1805
		}
1806
1807
		$dataArchive = null;
1808
1809
		if (
1810
			isset($data['DataArchiveID'])
1811
			&& is_numeric($data['DataArchiveID'])
1812
		) {
1813
			$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1814
		}
1815
1816
		if (!$dataArchive) {
1817
			throw new LogicException('Invalid data archive');
1818
		}
1819
1820
		if (!$dataArchive->canDelete()) {
1821
			throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403);
1822
		}
1823
1824
		$dataArchive->delete();
1825
1826
		return $this->redirectBack();
1827
	}
1828
1829
	/**
1830
	 * View a form to move a specific {@link DataArchive}.
1831
	 *
1832
	 * @param \SS_HTTPRequest $request
1833
	 *
1834
	 * @return HTMLText
1835
	 * @throws SS_HTTPResponse_Exception
1836
	 */
1837 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...
1838
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1839
1840
		/** @var DNDataArchive $dataArchive */
1841
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1842
1843
		if (!$dataArchive) {
1844
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1845
		}
1846
1847
		// We check for canDownload because that implies access to the data.
1848
		if (!$dataArchive->canDownload()) {
1849
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1850
		}
1851
1852
		$form = $this->getMoveForm($this->request, $dataArchive);
1853
1854
		// View currently only available via ajax
1855
		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...
1856
	}
1857
1858
	/**
1859
	 * Build snapshot move form.
1860
	 *
1861
	 * @param \SS_HTTPRequest $request
1862
	 * @param DNDataArchive|null $dataArchive
1863
	 *
1864
	 * @return Form|SS_HTTPResponse
1865
	 */
1866
	public function getMoveForm(\SS_HTTPRequest $request, \DNDataArchive $dataArchive = null) {
1867
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1868
1869
		$envs = $dataArchive->validTargetEnvironments();
1870
		if (!$envs) {
1871
			return $this->environment404Response();
1872
		}
1873
1874
		$warningMessage = '<div class="alert alert-warning"><strong>Warning:</strong> This will make the snapshot '
1875
			. 'available to people with access to the target environment.<br>By pressing "Change ownership" you '
1876
			. 'confirm that you have considered data confidentiality regulations.</div>';
1877
1878
		$form = Form::create(
1879
			$this,
1880
			'MoveForm',
1881
			FieldList::create(
1882
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1883
				LiteralField::create('Warning', $warningMessage),
1884
				DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1885
					->setEmptyString('Select an environment')
1886
			),
1887
			FieldList::create(
1888
				FormAction::create('doMove', 'Change ownership')
1889
					->addExtraClass('btn')
1890
			)
1891
		);
1892
		$form->setFormAction($this->getCurrentProject()->Link() . '/MoveForm');
1893
1894
		return $form;
1895
	}
1896
1897
	/**
1898
	 * @param array $data
1899
	 * @param Form $form
1900
	 *
1901
	 * @return bool|SS_HTTPResponse
1902
	 * @throws SS_HTTPResponse_Exception
1903
	 * @throws ValidationException
1904
	 * @throws null
1905
	 */
1906
	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...
1907
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1908
1909
		// Performs canView permission check by limiting visible projects
1910
		$project = $this->getCurrentProject();
1911
		if (!$project) {
1912
			return $this->project404Response();
1913
		}
1914
1915
		/** @var DNDataArchive $dataArchive */
1916
		$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1917
		if (!$dataArchive) {
1918
			throw new LogicException('Invalid data archive');
1919
		}
1920
1921
		// We check for canDownload because that implies access to the data.
1922
		if (!$dataArchive->canDownload()) {
1923
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1924
		}
1925
1926
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
1927
		$validEnvs = $dataArchive->validTargetEnvironments();
1928
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
1929
		if (!$environment) {
1930
			throw new LogicException('Invalid environment');
1931
		}
1932
1933
		$dataArchive->EnvironmentID = $environment->ID;
1934
		$dataArchive->write();
1935
1936
		return $this->redirectBack();
1937
	}
1938
1939
	/**
1940
	 * Returns an error message if redis is unavailable
1941
	 *
1942
	 * @return string
1943
	 */
1944
	public static function RedisUnavailable() {
1945
		try {
1946
			Resque::queues();
1947
		} catch (Exception $e) {
1948
			return $e->getMessage();
1949
		}
1950
		return '';
1951
	}
1952
1953
	/**
1954
	 * Returns the number of connected Redis workers
1955
	 *
1956
	 * @return int
1957
	 */
1958
	public static function RedisWorkersCount() {
1959
		return count(Resque_Worker::all());
1960
	}
1961
1962
	/**
1963
	 * @return array
1964
	 */
1965
	public function providePermissions() {
1966
		return [
1967
			self::DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS => [
1968
				'name' => "Access to advanced deploy options",
1969
				'category' => "Deploynaut",
1970
			],
1971
1972
			// Permissions that are intended to be added to the roles.
1973
			self::ALLOW_PROD_DEPLOYMENT => [
1974
				'name' => "Ability to deploy to production environments",
1975
				'category' => "Deploynaut",
1976
			],
1977
			self::ALLOW_NON_PROD_DEPLOYMENT => [
1978
				'name' => "Ability to deploy to non-production environments",
1979
				'category' => "Deploynaut",
1980
			],
1981
			self::ALLOW_PROD_SNAPSHOT => [
1982
				'name' => "Ability to make production snapshots",
1983
				'category' => "Deploynaut",
1984
			],
1985
			self::ALLOW_NON_PROD_SNAPSHOT => [
1986
				'name' => "Ability to make non-production snapshots",
1987
				'category' => "Deploynaut",
1988
			],
1989
			self::ALLOW_CREATE_ENVIRONMENT => [
1990
				'name' => "Ability to create environments",
1991
				'category' => "Deploynaut",
1992
			],
1993
		];
1994
	}
1995
1996
	/**
1997
	 * @return DNProject|null
1998
	 */
1999
	public function getCurrentProject() {
2000
		$projectName = trim($this->getRequest()->param('Project'));
2001
		if (!$projectName) {
2002
			return null;
2003
		}
2004
		if (empty(self::$_project_cache[$projectName])) {
2005
			self::$_project_cache[$projectName] = $this->DNProjectList()->filter('Name', $projectName)->First();
2006
		}
2007
		return self::$_project_cache[$projectName];
2008
	}
2009
2010
	/**
2011
	 * @param \DNProject|null $project
2012
	 * @return \DNEnvironment|null
2013
	 */
2014
	public function getCurrentEnvironment(\DNProject $project = null) {
2015
		if ($this->getRequest()->param('Environment') === null) {
2016
			return null;
2017
		}
2018
		if ($project === null) {
2019
			$project = $this->getCurrentProject();
2020
		}
2021
		// project can still be null
2022
		if ($project === null) {
2023
			return null;
2024
		}
2025
		return $project->DNEnvironmentList()->filter('Name', $this->getRequest()->param('Environment'))->First();
2026
	}
2027
2028
	/**
2029
	 * This will return a const that indicates the class of action currently being performed
2030
	 *
2031
	 * Until DNRoot is de-godded, it does a bunch of different actions all in the same class.
2032
	 * So we just have each action handler calll setCurrentActionType to define what sort of
2033
	 * action it is.
2034
	 *
2035
	 * @return string - one of the consts from self::$action_types
2036
	 */
2037
	public function getCurrentActionType() {
2038
		return $this->actionType;
2039
	}
2040
2041
	/**
2042
	 * Sets the current action type
2043
	 *
2044
	 * @param string $actionType string - one of the consts from self::$action_types
2045
	 */
2046
	public function setCurrentActionType($actionType) {
2047
		$this->actionType = $actionType;
2048
	}
2049
2050
	/**
2051
	 * Helper method to allow templates to know whether they should show the 'Archive List' include or not.
2052
	 * The actual permissions are set on a per-environment level, so we need to find out if this $member can upload to
2053
	 * or download from *any* {@link DNEnvironment} that (s)he has access to.
2054
	 *
2055
	 * TODO To be replaced with a method that just returns the list of archives this {@link Member} has access to.
2056
	 *
2057
	 * @param Member|null $member The {@link Member} to check (or null to check the currently logged in Member)
2058
	 * @return boolean|null true if $member has access to upload or download to at least one {@link DNEnvironment}.
2059
	 */
2060
	public function CanViewArchives(\Member $member = null) {
2061
		if ($member === null) {
2062
			$member = Member::currentUser();
2063
		}
2064
2065
		if (Permission::checkMember($member, 'ADMIN')) {
2066
			return true;
2067
		}
2068
2069
		$allProjects = $this->DNProjectList();
2070
		if (!$allProjects) {
2071
			return false;
2072
		}
2073
2074
		foreach ($allProjects as $project) {
2075
			if ($project->Environments()) {
2076
				foreach ($project->Environments() as $environment) {
2077
					if (
2078
						$environment->canRestore($member) ||
2079
						$environment->canBackup($member) ||
2080
						$environment->canUploadArchive($member) ||
2081
						$environment->canDownloadArchive($member)
2082
					) {
2083
						// We can return early as we only need to know that we can access one environment
2084
						return true;
2085
					}
2086
				}
2087
			}
2088
		}
2089
	}
2090
2091
	/**
2092
	 * Returns a list of attempted environment creations.
2093
	 *
2094
	 * @return PaginatedList
2095
	 */
2096
	public function CreateEnvironmentList() {
2097
		$project = $this->getCurrentProject();
2098
		if ($project) {
2099
			$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...
2100
		} else {
2101
			$dataList = new ArrayList();
2102
		}
2103
2104
		$this->extend('updateCreateEnvironmentList', $dataList);
2105
		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...
2106
	}
2107
2108
	/**
2109
	 * Returns a list of all archive files that can be accessed by the currently logged-in {@link Member}
2110
	 *
2111
	 * @return PaginatedList
2112
	 */
2113
	public function CompleteDataArchives() {
2114
		$project = $this->getCurrentProject();
2115
		$archives = new ArrayList();
2116
2117
		$archiveList = $project->Environments()->relation("DataArchives");
2118
		if ($archiveList->count() > 0) {
2119
			foreach ($archiveList as $archive) {
2120
				if (!$archive->isPending()) {
2121
					$archives->push($archive);
2122
				}
2123
			}
2124
		}
2125
		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...
2126
	}
2127
2128
	/**
2129
	 * @return PaginatedList The list of "pending" data archives which are waiting for a file
2130
	 * to be delivered offline by post, and manually uploaded into the system.
2131
	 */
2132
	public function PendingDataArchives() {
2133
		$project = $this->getCurrentProject();
2134
		$archives = new ArrayList();
2135
		foreach ($project->DNEnvironmentList() as $env) {
2136
			foreach ($env->DataArchives() as $archive) {
2137
				if ($archive->isPending()) {
2138
					$archives->push($archive);
2139
				}
2140
			}
2141
		}
2142
		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...
2143
	}
2144
2145
	/**
2146
	 * @return PaginatedList
2147
	 */
2148
	public function DataTransferLogs() {
2149
		$environments = $this->getCurrentProject()->Environments()->column('ID');
2150
		$transfers = DNDataTransfer::get()
2151
			->filter('EnvironmentID', $environments)
2152
			->filterByCallback(
2153
				function ($record) {
2154
					return
2155
						$record->Environment()->canRestore() || // Ensure member can perform an action on the transfers env
2156
						$record->Environment()->canBackup() ||
2157
						$record->Environment()->canUploadArchive() ||
2158
						$record->Environment()->canDownloadArchive();
2159
				});
2160
2161
		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...
2162
	}
2163
2164
	/**
2165
	 * @deprecated 2.0.0 - moved to DeployDispatcher
2166
	 *
2167
	 * @return null|PaginatedList
2168
	 */
2169
	public function DeployHistory() {
2170
		if ($env = $this->getCurrentEnvironment()) {
2171
			$history = $env->DeployHistory();
2172
			if ($history->count() > 0) {
2173
				$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...
2174
				$pagination->setPageLength(4);
2175
				return $pagination;
2176
			}
2177
		}
2178
		return null;
2179
	}
2180
2181
	/**
2182
	 * @param string $status
2183
	 * @param string $content
2184
	 *
2185
	 * @return string
2186
	 */
2187
	public function sendResponse($status, $content) {
2188
		// strip excessive newlines
2189
		$content = preg_replace('/(?:(?:\r\n|\r|\n)\s*){2}/s', "\n", $content);
2190
2191
		$sendJSON = (strpos($this->getRequest()->getHeader('Accept'), 'application/json') !== false)
2192
			|| $this->getRequest()->getExtension() == 'json';
2193
2194
		if (!$sendJSON) {
2195
			$this->response->addHeader("Content-type", "text/plain");
2196
			return $content;
2197
		}
2198
		$this->response->addHeader("Content-type", "application/json");
2199
		return json_encode([
2200
			'status' => $status,
2201
			'content' => $content,
2202
		]);
2203
	}
2204
2205
	/**
2206
	 * Get items for the ambient menu that should be accessible from all pages.
2207
	 *
2208
	 * @return ArrayList
2209
	 */
2210
	public function AmbientMenu() {
2211
		$list = new ArrayList();
2212
2213
		if (Member::currentUserID()) {
2214
			$list->push(new ArrayData([
2215
				'Classes' => 'logout',
2216
				'FaIcon' => 'sign-out',
2217
				'Link' => 'Security/logout',
2218
				'Title' => 'Log out',
2219
				'IsCurrent' => false,
2220
				'IsSection' => false
2221
			]));
2222
		}
2223
2224
		$this->extend('updateAmbientMenu', $list);
2225
		return $list;
2226
	}
2227
2228
	/**
2229
	 * Checks whether the user can create a project.
2230
	 *
2231
	 * @return bool
2232
	 */
2233
	public function canCreateProjects($member = null) {
2234
		if (!$member) {
2235
			$member = Member::currentUser();
2236
		}
2237
		if (!$member) {
2238
			return false;
2239
		}
2240
2241
		return singleton('DNProject')->canCreate($member);
2242
	}
2243
2244
	protected function applyRedeploy(\SS_HTTPRequest $request, &$data) {
2245
		if (!$request->getVar('redeploy')) {
2246
			return;
2247
		}
2248
2249
		$project = $this->getCurrentProject();
2250
		if (!$project) {
2251
			return $this->project404Response();
2252
		}
2253
2254
		// Performs canView permission check by limiting visible projects
2255
		$env = $this->getCurrentEnvironment($project);
2256
		if (!$env) {
2257
			return $this->environment404Response();
2258
		}
2259
2260
		$current = $env->CurrentBuild();
2261
		if ($current && $current->exists()) {
2262
			$data['preselect_tab'] = 3;
2263
			$data['preselect_sha'] = $current->SHA;
2264
		} else {
2265
			$master = $project->DNBranchList()->byName('master');
2266
			if ($master) {
2267
				$data['preselect_tab'] = 1;
2268
				$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...
2269
			}
2270
		}
2271
	}
2272
2273
	/**
2274
	 * @return SS_HTTPResponse
2275
	 */
2276
	protected function project404Response() {
2277
		return new SS_HTTPResponse(
2278
			"Project '" . Convert::raw2xml($this->getRequest()->param('Project')) . "' not found.",
2279
			404
2280
		);
2281
	}
2282
2283
	/**
2284
	 * @return SS_HTTPResponse
2285
	 */
2286
	protected function environment404Response() {
2287
		$envName = Convert::raw2xml($this->getRequest()->param('Environment'));
2288
		return new SS_HTTPResponse("Environment '" . $envName . "' not found.", 404);
2289
	}
2290
2291
	/**
2292
	 * Validate the snapshot mode
2293
	 *
2294
	 * @param string $mode
2295
	 */
2296
	protected function validateSnapshotMode($mode) {
2297
		if (!in_array($mode, ['all', 'assets', 'db'])) {
2298
			throw new LogicException('Invalid mode');
2299
		}
2300
	}
2301
2302
	/**
2303
	 * @param string $sectionName
2304
	 * @param string $title
2305
	 *
2306
	 * @return SS_HTTPResponse
2307
	 */
2308
	protected function getCustomisedViewSection($sectionName, $title = '', $data = []) {
2309
		// Performs canView permission check by limiting visible projects
2310
		$project = $this->getCurrentProject();
2311
		if (!$project) {
2312
			return $this->project404Response();
2313
		}
2314
		$data[$sectionName] = 1;
2315
2316
		if ($this !== '') {
2317
			$data['Title'] = $title;
2318
		}
2319
2320
		return $this->render($data);
2321
	}
2322
2323
}
2324
2325