Completed
Push — master ( 8ede81...9b7c85 )
by Sean
06:53 queued 05:40
created

DNRoot::getDataTransferRestoreForm()   C

Complexity

Conditions 9
Paths 98

Size

Total Lines 64
Code Lines 42

Duplication

Lines 7
Ratio 10.94 %

Importance

Changes 0
Metric Value
dl 7
loc 64
rs 6.5449
c 0
b 0
f 0
cc 9
eloc 42
nc 98
nop 2

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
		'metrics',
67
		'createenvlog',
68
		'createenv',
69
		'getDeployForm',
70
		'doDeploy',
71
		'deploy',
72
		'deploylog',
73
		'abortDeploy',
74
		'getDataTransferForm',
75
		'transfer',
76
		'transferlog',
77
		'snapshots',
78
		'createsnapshot',
79
		'snapshotslog',
80
		'uploadsnapshot',
81
		'getCreateEnvironmentForm',
82
		'getUploadSnapshotForm',
83
		'getPostSnapshotForm',
84
		'getDataTransferRestoreForm',
85
		'getDeleteForm',
86
		'getMoveForm',
87
		'restoresnapshot',
88
		'deletesnapshot',
89
		'movesnapshot',
90
		'postsnapshotsuccess',
91
		'gitRevisions',
92
		'deploySummary',
93
		'startDeploy'
94
	];
95
96
	/**
97
	 * URL handlers pretending that we have a deep URL structure.
98
	 */
99
	private static $url_handlers = [
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...
100
		'project/$Project/environment/$Environment/DeployForm' => 'getDeployForm',
101
		'project/$Project/createsnapshot/DataTransferForm' => 'getDataTransferForm',
102
		'project/$Project/DataTransferForm' => 'getDataTransferForm',
103
		'project/$Project/DataTransferRestoreForm' => 'getDataTransferRestoreForm',
104
		'project/$Project/DeleteForm' => 'getDeleteForm',
105
		'project/$Project/MoveForm' => 'getMoveForm',
106
		'project/$Project/UploadSnapshotForm' => 'getUploadSnapshotForm',
107
		'project/$Project/PostSnapshotForm' => 'getPostSnapshotForm',
108
		'project/$Project/environment/$Environment/metrics' => 'metrics',
109
		'project/$Project/environment/$Environment/deploy_summary' => 'deploySummary',
110
		'project/$Project/environment/$Environment/git_revisions' => 'gitRevisions',
111
		'project/$Project/environment/$Environment/start-deploy' => 'startDeploy',
112
		'project/$Project/environment/$Environment/deploy/$Identifier/log' => 'deploylog',
113
		'project/$Project/environment/$Environment/deploy/$Identifier/abort-deploy' => 'abortDeploy',
114
		'project/$Project/environment/$Environment/deploy/$Identifier' => 'deploy',
115
		'project/$Project/transfer/$Identifier/log' => 'transferlog',
116
		'project/$Project/transfer/$Identifier' => 'transfer',
117
		'project/$Project/environment/$Environment' => 'environment',
118
		'project/$Project/createenv/$Identifier/log' => 'createenvlog',
119
		'project/$Project/createenv/$Identifier' => 'createenv',
120
		'project/$Project/CreateEnvironmentForm' => 'getCreateEnvironmentForm',
121
		'project/$Project/branch' => 'branch',
122
		'project/$Project/build/$Build' => 'build',
123
		'project/$Project/restoresnapshot/$DataArchiveID' => 'restoresnapshot',
124
		'project/$Project/deletesnapshot/$DataArchiveID' => 'deletesnapshot',
125
		'project/$Project/movesnapshot/$DataArchiveID' => 'movesnapshot',
126
		'project/$Project/update' => 'update',
127
		'project/$Project/snapshots' => 'snapshots',
128
		'project/$Project/createsnapshot' => 'createsnapshot',
129
		'project/$Project/uploadsnapshot' => 'uploadsnapshot',
130
		'project/$Project/snapshotslog' => 'snapshotslog',
131
		'project/$Project/postsnapshotsuccess/$DataArchiveID' => 'postsnapshotsuccess',
132
		'project/$Project/star' => 'toggleprojectstar',
133
		'project/$Project' => 'project',
134
		'nav/$Project' => 'nav',
135
		'projects' => 'projects',
136
	];
137
138
	/**
139
	 * @var array
140
	 */
141
	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...
142
143
	/**
144
	 * @var array
145
	 */
146
	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...
147
148
	/**
149
	 * @var array
150
	 */
151
	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...
152
		self::ACTION_DEPLOY,
153
		self::ACTION_SNAPSHOT,
154
		self::PROJECT_OVERVIEW
155
	];
156
157
	/**
158
	 * Include requirements that deploynaut needs, such as javascript.
159
	 */
160
	public static function include_requirements() {
161
162
		// JS should always go to the bottom, otherwise there's the risk that Requirements
163
		// puts them halfway through the page to the nearest <script> tag. We don't want that.
164
		Requirements::set_force_js_to_bottom(true);
165
166
		// todo these should be bundled into the same JS as the others in "static" below.
167
		// We've deliberately not used combined_files as it can mess with some of the JS used
168
		// here and cause sporadic errors.
169
		Requirements::javascript('deploynaut/javascript/jquery.js');
170
		Requirements::javascript('deploynaut/javascript/bootstrap.js');
171
		Requirements::javascript('deploynaut/javascript/q.js');
172
		Requirements::javascript('deploynaut/javascript/tablefilter.js');
173
		Requirements::javascript('deploynaut/javascript/deploynaut.js');
174
175
		Requirements::javascript('deploynaut/javascript/bootstrap.file-input.js');
176
		Requirements::javascript('deploynaut/thirdparty/select2/dist/js/select2.min.js');
177
		Requirements::javascript('deploynaut/javascript/selectize.js');
178
		Requirements::javascript('deploynaut/thirdparty/bootstrap-switch/dist/js/bootstrap-switch.min.js');
179
		Requirements::javascript('deploynaut/javascript/material.js');
180
181
		// Load the buildable dependencies only if not loaded centrally.
182
		if (!is_dir(BASE_PATH . DIRECTORY_SEPARATOR . 'static')) {
183
			if (\Director::isDev()) {
184
				\Requirements::javascript('deploynaut/static/bundle-debug.js');
185
			} else {
186
				\Requirements::javascript('deploynaut/static/bundle.js');
187
			}
188
		}
189
190
		Requirements::css('deploynaut/static/style.css');
191
	}
192
193
	/**
194
	 * Check for feature flags:
195
	 * - FLAG_SNAPSHOTS_ENABLED: set to true to enable globally
196
	 * - FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS: set to semicolon-separated list of email addresses of allowed users.
197
	 *
198
	 * @return boolean
199
	 */
200
	public static function FlagSnapshotsEnabled() {
201
		if (defined('FLAG_SNAPSHOTS_ENABLED') && FLAG_SNAPSHOTS_ENABLED) {
202
			return true;
203
		}
204
		if (defined('FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS') && FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS) {
205
			$allowedMembers = explode(';', FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS);
206
			$member = Member::currentUser();
207
			if ($allowedMembers && $member && in_array($member->Email, $allowedMembers)) {
208
				return true;
209
			}
210
		}
211
		return false;
212
	}
213
214
	/**
215
	 * @return ArrayList
216
	 */
217
	public static function get_support_links() {
218
		$supportLinks = self::config()->support_links;
219
		if ($supportLinks) {
220
			return new ArrayList($supportLinks);
221
		}
222
	}
223
224
	/**
225
	 * @return array
226
	 */
227
	public static function get_template_global_variables() {
228
		return [
229
			'RedisUnavailable' => 'RedisUnavailable',
230
			'RedisWorkersCount' => 'RedisWorkersCount',
231
			'SidebarLinks' => 'SidebarLinks',
232
			"SupportLinks" => 'get_support_links'
233
		];
234
	}
235
236
	/**
237
	 */
238
	public function init() {
239
		parent::init();
240
241
		if (!Member::currentUser() && !Session::get('AutoLoginHash')) {
242
			return Security::permissionFailure();
243
		}
244
245
		// Block framework jquery
246
		Requirements::block(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
247
248
		self::include_requirements();
249
	}
250
251
	/**
252
	 * @return string
253
	 */
254
	public function Link() {
255
		return "naut/";
256
	}
257
258
	/**
259
	 * Actions
260
	 *
261
	 * @param SS_HTTPRequest $request
262
	 * @return \SS_HTTPResponse
263
	 */
264
	public function index(SS_HTTPRequest $request) {
265
		return $this->redirect($this->Link() . 'projects/');
266
	}
267
268
	/**
269
	 * Action
270
	 *
271
	 * @param SS_HTTPRequest $request
272
	 * @return string - HTML
273
	 */
274
	public function projects(SS_HTTPRequest $request) {
275
		// Performs canView permission check by limiting visible projects in DNProjectsList() call.
276
		return $this->customise([
277
			'Title' => 'Projects',
278
		])->render();
279
	}
280
281
	/**
282
	 * @param SS_HTTPRequest $request
283
	 * @return HTMLText
284
	 */
285
	public function nav(SS_HTTPRequest $request) {
286
		return $this->renderWith('Nav');
287
	}
288
289
	/**
290
	 * Return a link to the navigation template used for AJAX requests.
291
	 * @return string
292
	 */
293
	public function NavLink() {
294
		$currentProject = $this->getCurrentProject();
295
		$projectName = $currentProject ? $currentProject->Name : null;
296
		return Controller::join_links(Director::absoluteBaseURL(), 'naut', 'nav', $projectName);
297
	}
298
299
	/**
300
	 * Action
301
	 *
302
	 * @param SS_HTTPRequest $request
303
	 * @return SS_HTTPResponse - HTML
304
	 */
305
	public function snapshots(SS_HTTPRequest $request) {
306
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
307
		return $this->getCustomisedViewSection('SnapshotsSection', 'Data Snapshots');
308
	}
309
310
	/**
311
	 * Action
312
	 *
313
	 * @param SS_HTTPRequest $request
314
	 * @return string - HTML
315
	 */
316 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...
317
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
318
319
		// Performs canView permission check by limiting visible projects
320
		$project = $this->getCurrentProject();
321
		if (!$project) {
322
			return $this->project404Response();
323
		}
324
325
		if (!$project->canBackup()) {
326
			return new SS_HTTPResponse("Not allowed to create snapshots on any environments", 401);
327
		}
328
329
		return $this->customise([
330
			'Title' => 'Create Data Snapshot',
331
			'SnapshotsSection' => 1,
332
			'DataTransferForm' => $this->getDataTransferForm($request)
333
		])->render();
334
	}
335
336
	/**
337
	 * Action
338
	 *
339
	 * @param SS_HTTPRequest $request
340
	 * @return string - HTML
341
	 */
342 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...
343
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
344
345
		// Performs canView permission check by limiting visible projects
346
		$project = $this->getCurrentProject();
347
		if (!$project) {
348
			return $this->project404Response();
349
		}
350
351
		if (!$project->canUploadArchive()) {
352
			return new SS_HTTPResponse("Not allowed to upload", 401);
353
		}
354
355
		return $this->customise([
356
			'SnapshotsSection' => 1,
357
			'UploadSnapshotForm' => $this->getUploadSnapshotForm($request),
358
			'PostSnapshotForm' => $this->getPostSnapshotForm($request)
359
		])->render();
360
	}
361
362
	/**
363
	 * Return the upload limit for snapshot uploads
364
	 * @return string
365
	 */
366
	public function UploadLimit() {
367
		return File::format_size(min(
368
			File::ini2bytes(ini_get('upload_max_filesize')),
369
			File::ini2bytes(ini_get('post_max_size'))
370
		));
371
	}
372
373
	/**
374
	 * Construct the upload form.
375
	 *
376
	 * @param SS_HTTPRequest $request
377
	 * @return Form
378
	 */
379
	public function getUploadSnapshotForm(SS_HTTPRequest $request) {
380
		// Performs canView permission check by limiting visible projects
381
		$project = $this->getCurrentProject();
382
		if (!$project) {
383
			return $this->project404Response();
384
		}
385
386
		if (!$project->canUploadArchive()) {
387
			return new SS_HTTPResponse("Not allowed to upload", 401);
388
		}
389
390
		// Framing an environment as a "group of people with download access"
391
		// makes more sense to the user here, while still allowing us to enforce
392
		// environment specific restrictions on downloading the file later on.
393
		$envs = $project->DNEnvironmentList()->filterByCallback(function ($item) {
394
			return $item->canUploadArchive();
395
		});
396
		$envsMap = [];
397
		foreach ($envs as $env) {
398
			$envsMap[$env->ID] = $env->Name;
399
		}
400
401
		$maxSize = min(File::ini2bytes(ini_get('upload_max_filesize')), File::ini2bytes(ini_get('post_max_size')));
402
		$fileField = DataArchiveFileField::create('ArchiveFile', 'File');
403
		$fileField->getValidator()->setAllowedExtensions(['sspak']);
404
		$fileField->getValidator()->setAllowedMaxFileSize(['*' => $maxSize]);
405
406
		$form = Form::create(
407
			$this,
408
			'UploadSnapshotForm',
409
			FieldList::create(
410
				$fileField,
411
				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
412
				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
413
					->setEmptyString('Select an environment')
414
			),
415
			FieldList::create(
416
				FormAction::create('doUploadSnapshot', 'Upload File')
417
					->addExtraClass('btn')
418
			),
419
			RequiredFields::create('ArchiveFile')
420
		);
421
422
		$form->disableSecurityToken();
423
		$form->addExtraClass('fields-wide');
424
		// Tweak the action so it plays well with our fake URL structure.
425
		$form->setFormAction($project->Link() . '/UploadSnapshotForm');
426
427
		return $form;
428
	}
429
430
	/**
431
	 * @param array $data
432
	 * @param Form $form
433
	 *
434
	 * @return bool|HTMLText|SS_HTTPResponse
435
	 */
436
	public function doUploadSnapshot($data, Form $form) {
437
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
438
439
		// Performs canView permission check by limiting visible projects
440
		$project = $this->getCurrentProject();
441
		if (!$project) {
442
			return $this->project404Response();
443
		}
444
445
		$validEnvs = $project->DNEnvironmentList()
446
			->filterByCallback(function ($item) {
447
				return $item->canUploadArchive();
448
			});
449
450
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
451
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
452
		if (!$environment) {
453
			throw new LogicException('Invalid environment');
454
		}
455
456
		$this->validateSnapshotMode($data['Mode']);
457
458
		$dataArchive = DNDataArchive::create([
459
			'AuthorID' => Member::currentUserID(),
460
			'EnvironmentID' => $data['EnvironmentID'],
461
			'IsManualUpload' => true,
462
		]);
463
		// needs an ID and transfer to determine upload path
464
		$dataArchive->write();
465
		$dataTransfer = DNDataTransfer::create([
466
			'AuthorID' => Member::currentUserID(),
467
			'Mode' => $data['Mode'],
468
			'Origin' => 'ManualUpload',
469
			'EnvironmentID' => $data['EnvironmentID']
470
		]);
471
		$dataTransfer->write();
472
		$dataArchive->DataTransfers()->add($dataTransfer);
473
		$form->saveInto($dataArchive);
474
		$dataArchive->write();
475
		$workingDir = TEMP_FOLDER . DIRECTORY_SEPARATOR . 'deploynaut-transfer-' . $dataTransfer->ID;
476
477 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...
478
			$process = new AbortableProcess(sprintf('rm -rf %s', escapeshellarg($workingDir)));
479
			$process->setTimeout(120);
480
			$process->run();
481
			$dataTransfer->delete();
482
			$dataArchive->delete();
483
		};
484
485
		// extract the sspak contents so we can inspect them
486
		try {
487
			$dataArchive->extractArchive($workingDir);
488
		} catch (Exception $e) {
489
			$cleanupFn();
490
			$form->sessionMessage(
491
				'There was a problem trying to open your snapshot for processing. Please try uploading again',
492
				'bad'
493
			);
494
			return $this->redirectBack();
495
		}
496
497
		// validate that the sspak contents match the declared contents
498
		$result = $dataArchive->validateArchiveContents();
499
		if (!$result->valid()) {
500
			$cleanupFn();
501
			$form->sessionMessage($result->message(), 'bad');
502
			return $this->redirectBack();
503
		}
504
505
		// fix file permissions of extracted sspak files then re-build the sspak
506
		try {
507
			$dataArchive->fixArchivePermissions($workingDir);
508
			$dataArchive->setArchiveFromFiles($workingDir);
509
		} catch (Exception $e) {
510
			$cleanupFn();
511
			$form->sessionMessage(
512
				'There was a problem processing your snapshot. Please try uploading again',
513
				'bad'
514
			);
515
			return $this->redirectBack();
516
		}
517
518
		// cleanup any extracted sspak contents lying around
519
		$process = new AbortableProcess(sprintf('rm -rf %s', escapeshellarg($workingDir)));
520
		$process->setTimeout(120);
521
		$process->run();
522
523
		return $this->customise([
524
			'Project' => $project,
525
			'CurrentProject' => $project,
526
			'SnapshotsSection' => 1,
527
			'DataArchive' => $dataArchive,
528
			'DataTransferRestoreForm' => $this->getDataTransferRestoreForm($this->request, $dataArchive),
529
			'BackURL' => $project->Link('snapshots')
530
		])->renderWith(['DNRoot_uploadsnapshot', 'DNRoot']);
531
	}
532
533
	/**
534
	 * @param SS_HTTPRequest $request
535
	 * @return Form
536
	 */
537
	public function getPostSnapshotForm(SS_HTTPRequest $request) {
538
		// Performs canView permission check by limiting visible projects
539
		$project = $this->getCurrentProject();
540
		if (!$project) {
541
			return $this->project404Response();
542
		}
543
544
		if (!$project->canUploadArchive()) {
545
			return new SS_HTTPResponse("Not allowed to upload", 401);
546
		}
547
548
		// Framing an environment as a "group of people with download access"
549
		// makes more sense to the user here, while still allowing us to enforce
550
		// environment specific restrictions on downloading the file later on.
551
		$envs = $project->DNEnvironmentList()->filterByCallback(function ($item) {
552
			return $item->canUploadArchive();
553
		});
554
		$envsMap = [];
555
		foreach ($envs as $env) {
556
			$envsMap[$env->ID] = $env->Name;
557
		}
558
559
		$form = Form::create(
560
			$this,
561
			'PostSnapshotForm',
562
			FieldList::create(
563
				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
564
				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
565
					->setEmptyString('Select an environment')
566
			),
567
			FieldList::create(
568
				FormAction::create('doPostSnapshot', 'Submit request')
569
					->addExtraClass('btn')
570
			),
571
			RequiredFields::create('File')
572
		);
573
574
		$form->disableSecurityToken();
575
		$form->addExtraClass('fields-wide');
576
		// Tweak the action so it plays well with our fake URL structure.
577
		$form->setFormAction($project->Link() . '/PostSnapshotForm');
578
579
		return $form;
580
	}
581
582
	/**
583
	 * @param array $data
584
	 * @param Form $form
585
	 *
586
	 * @return SS_HTTPResponse
587
	 */
588
	public function doPostSnapshot($data, $form) {
589
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
590
591
		$project = $this->getCurrentProject();
592
		if (!$project) {
593
			return $this->project404Response();
594
		}
595
596
		$validEnvs = $project->DNEnvironmentList()->filterByCallback(function ($item) {
597
			return $item->canUploadArchive();
598
		});
599
600
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
601
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
602
		if (!$environment) {
603
			throw new LogicException('Invalid environment');
604
		}
605
606
		$dataArchive = DNDataArchive::create([
607
			'UploadToken' => DNDataArchive::generate_upload_token(),
608
		]);
609
		$form->saveInto($dataArchive);
610
		$dataArchive->write();
611
612
		return $this->redirect(Controller::join_links(
613
			$project->Link(),
614
			'postsnapshotsuccess',
615
			$dataArchive->ID
616
		));
617
	}
618
619
	/**
620
	 * Action
621
	 *
622
	 * @param SS_HTTPRequest $request
623
	 * @return SS_HTTPResponse - HTML
624
	 */
625
	public function snapshotslog(SS_HTTPRequest $request) {
626
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
627
		return $this->getCustomisedViewSection('SnapshotsSection', 'Snapshots log');
628
	}
629
630
	/**
631
	 * @param SS_HTTPRequest $request
632
	 * @return SS_HTTPResponse|string
633
	 * @throws SS_HTTPResponse_Exception
634
	 */
635
	public function postsnapshotsuccess(SS_HTTPRequest $request) {
636
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
637
638
		// Performs canView permission check by limiting visible projects
639
		$project = $this->getCurrentProject();
640
		if (!$project) {
641
			return $this->project404Response();
642
		}
643
644
		if (!$project->canUploadArchive()) {
645
			return new SS_HTTPResponse("Not allowed to upload", 401);
646
		}
647
648
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
649
		if (!$dataArchive) {
650
			return new SS_HTTPResponse("Archive not found.", 404);
651
		}
652
653
		if (!$dataArchive->canRestore()) {
654
			throw new SS_HTTPResponse_Exception('Not allowed to restore archive', 403);
655
		}
656
657
		return $this->render([
658
			'Title' => 'How to send us your Data Snapshot by post',
659
			'DataArchive' => $dataArchive,
660
			'Address' => Config::inst()->get('Deploynaut', 'snapshot_post_address'),
661
			'BackURL' => $project->Link(),
662
		]);
663
	}
664
665
	/**
666
	 * @param SS_HTTPRequest $request
667
	 * @return \SS_HTTPResponse
668
	 */
669
	public function project(SS_HTTPRequest $request) {
670
		$this->setCurrentActionType(self::PROJECT_OVERVIEW);
671
		return $this->getCustomisedViewSection('ProjectOverview', '', ['IsAdmin' => Permission::check('ADMIN')]);
672
	}
673
674
	/**
675
	 * This action will star / unstar a project for the current member
676
	 *
677
	 * @param SS_HTTPRequest $request
678
	 *
679
	 * @return SS_HTTPResponse
680
	 */
681
	public function toggleprojectstar(SS_HTTPRequest $request) {
682
		$project = $this->getCurrentProject();
683
		if (!$project) {
684
			return $this->project404Response();
685
		}
686
687
		$member = Member::currentUser();
688
		if ($member === null) {
689
			return $this->project404Response();
690
		}
691
		$favProject = $member->StarredProjects()
692
			->filter('DNProjectID', $project->ID)
693
			->first();
694
695
		if ($favProject) {
696
			$member->StarredProjects()->remove($favProject);
697
		} else {
698
			$member->StarredProjects()->add($project);
699
		}
700
		return $this->redirectBack();
701
	}
702
703
	/**
704
	 * @param SS_HTTPRequest $request
705
	 * @return \SS_HTTPResponse
706
	 */
707
	public function branch(SS_HTTPRequest $request) {
708
		$project = $this->getCurrentProject();
709
		if (!$project) {
710
			return $this->project404Response();
711
		}
712
713
		$branchName = $request->getVar('name');
714
		$branch = $project->DNBranchList()->byName($branchName);
715
		if (!$branch) {
716
			return new SS_HTTPResponse("Branch '" . Convert::raw2xml($branchName) . "' not found.", 404);
717
		}
718
719
		return $this->render([
720
			'CurrentBranch' => $branch,
721
		]);
722
	}
723
724
	/**
725
	 * @param SS_HTTPRequest $request
726
	 * @return \SS_HTTPResponse
727
	 */
728
	public function environment(SS_HTTPRequest $request) {
729
		// Performs canView permission check by limiting visible projects
730
		$project = $this->getCurrentProject();
731
		if (!$project) {
732
			return $this->project404Response();
733
		}
734
735
		// Performs canView permission check by limiting visible projects
736
		$env = $this->getCurrentEnvironment($project);
737
		if (!$env) {
738
			return $this->environment404Response();
739
		}
740
741
		return $this->render([
742
			'DNEnvironmentList' => $this->getCurrentProject()->DNEnvironmentList(),
743
			'FlagSnapshotsEnabled' => $this->FlagSnapshotsEnabled(),
744
			'Redeploy' => (bool) $request->getVar('redeploy')
745
		]);
746
	}
747
748
	/**
749
	 * Shows the creation log.
750
	 *
751
	 * @param SS_HTTPRequest $request
752
	 * @return string
753
	 */
754
	public function createenv(SS_HTTPRequest $request) {
755
		$params = $request->params();
756
		if ($params['Identifier']) {
757
			$record = DNCreateEnvironment::get()->byId($params['Identifier']);
758
759
			if (!$record || !$record->ID) {
760
				throw new SS_HTTPResponse_Exception('Create environment not found', 404);
761
			}
762
			if (!$record->canView()) {
763
				return Security::permissionFailure();
764
			}
765
766
			$project = $this->getCurrentProject();
767
			if (!$project) {
768
				return $this->project404Response();
769
			}
770
771
			if ($project->Name != $params['Project']) {
772
				throw new LogicException("Project in URL doesn't match this creation");
773
			}
774
775
			return $this->render([
776
				'CreateEnvironment' => $record,
777
			]);
778
		}
779
		return $this->render(['CurrentTitle' => 'Create an environment']);
780
	}
781
782
	public function createenvlog(SS_HTTPRequest $request) {
783
		$params = $request->params();
784
		$env = DNCreateEnvironment::get()->byId($params['Identifier']);
785
786
		if (!$env || !$env->ID) {
787
			throw new SS_HTTPResponse_Exception('Log not found', 404);
788
		}
789
		if (!$env->canView()) {
790
			return Security::permissionFailure();
791
		}
792
793
		$project = $env->Project();
794
795
		if ($project->Name != $params['Project']) {
796
			throw new LogicException("Project in URL doesn't match this deploy");
797
		}
798
799
		$log = $env->log();
800
		if ($log->exists()) {
801
			$content = $log->content();
802
		} else {
803
			$content = 'Waiting for action to start';
804
		}
805
806
		return $this->sendResponse($env->ResqueStatus(), $content);
807
	}
808
809
	/**
810
	 * @param SS_HTTPRequest $request
811
	 * @return Form
812
	 */
813
	public function getCreateEnvironmentForm(SS_HTTPRequest $request) {
814
		$this->setCurrentActionType(self::ACTION_ENVIRONMENTS);
815
816
		$project = $this->getCurrentProject();
817
		if (!$project) {
818
			return $this->project404Response();
819
		}
820
821
		$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...
822
		if (!$envType || !class_exists($envType)) {
823
			return null;
824
		}
825
826
		$backend = Injector::inst()->get($envType);
827
		if (!($backend instanceof EnvironmentCreateBackend)) {
828
			// Only allow this for supported backends.
829
			return null;
830
		}
831
832
		$fields = $backend->getCreateEnvironmentFields($project);
833
		if (!$fields) {
834
			return null;
835
		}
836
837
		if (!$project->canCreateEnvironments()) {
838
			return new SS_HTTPResponse('Not allowed to create environments for this project', 401);
839
		}
840
841
		$form = Form::create(
842
			$this,
843
			'CreateEnvironmentForm',
844
			$fields,
845
			FieldList::create(
846
				FormAction::create('doCreateEnvironment', 'Create')
847
					->addExtraClass('btn')
848
			),
849
			$backend->getCreateEnvironmentValidator()
850
		);
851
852
		// Tweak the action so it plays well with our fake URL structure.
853
		$form->setFormAction($project->Link() . '/CreateEnvironmentForm');
854
855
		return $form;
856
	}
857
858
	/**
859
	 * @param array $data
860
	 * @param Form $form
861
	 *
862
	 * @return bool|HTMLText|SS_HTTPResponse
863
	 */
864
	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...
865
		$this->setCurrentActionType(self::ACTION_ENVIRONMENTS);
866
867
		$project = $this->getCurrentProject();
868
		if (!$project) {
869
			return $this->project404Response();
870
		}
871
872
		if (!$project->canCreateEnvironments()) {
873
			return new SS_HTTPResponse('Not allowed to create environments for this project', 401);
874
		}
875
876
		// Set the environment type so we know what we're creating.
877
		$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...
878
879
		$job = DNCreateEnvironment::create();
880
881
		$job->Data = serialize($data);
882
		$job->ProjectID = $project->ID;
883
		$job->write();
884
		$job->start();
885
886
		return $this->redirect($project->Link('createenv') . '/' . $job->ID);
887
	}
888
889
	/**
890
	 *
891
	 * @param SS_HTTPRequest $request
892
	 * @return \SS_HTTPResponse
893
	 */
894
	public function metrics(SS_HTTPRequest $request) {
895
		// Performs canView permission check by limiting visible projects
896
		$project = $this->getCurrentProject();
897
		if (!$project) {
898
			return $this->project404Response();
899
		}
900
901
		// Performs canView permission check by limiting visible projects
902
		$env = $this->getCurrentEnvironment($project);
903
		if (!$env) {
904
			return $this->environment404Response();
905
		}
906
907
		return $this->render();
908
	}
909
910
	/**
911
	 * Get the DNData object.
912
	 *
913
	 * @return DNData
914
	 */
915
	public function DNData() {
916
		return DNData::inst();
917
	}
918
919
	/**
920
	 * Provide a list of all projects.
921
	 *
922
	 * @return SS_List
923
	 */
924
	public function DNProjectList() {
925
		$memberId = Member::currentUserID();
926
		if (!$memberId) {
927
			return new ArrayList();
928
		}
929
930
		if (Permission::check('ADMIN')) {
931
			return DNProject::get();
932
		}
933
934
		$projects = Member::get()->filter('ID', $memberId)
935
			->relation('Groups')
936
			->relation('Projects');
937
938
		$this->extend('updateDNProjectList', $projects);
939
		return $projects;
940
	}
941
942
	/**
943
	 * @return ArrayList
944
	 */
945
	public function getPlatformSpecificStrings() {
946
		$strings = $this->config()->platform_specific_strings;
947
		if ($strings) {
948
			return new ArrayList($strings);
949
		}
950
	}
951
952
	/**
953
	 * Provide a list of all starred projects for the currently logged in member
954
	 *
955
	 * @return SS_List
956
	 */
957
	public function getStarredProjects() {
958
		$member = Member::currentUser();
959
		if ($member === null) {
960
			return new ArrayList();
961
		}
962
963
		$favProjects = $member->StarredProjects();
964
965
		$list = new ArrayList();
966
		foreach ($favProjects as $project) {
967
			if ($project->canView($member)) {
968
				$list->add($project);
969
			}
970
		}
971
		return $list;
972
	}
973
974
	/**
975
	 * Returns top level navigation of projects.
976
	 *
977
	 * @param int $limit
978
	 *
979
	 * @return ArrayList
980
	 */
981
	public function Navigation($limit = 5) {
982
		$navigation = new ArrayList();
983
984
		$currentProject = $this->getCurrentProject();
985
		$currentEnvironment = $this->getCurrentEnvironment();
986
		$actionType = $this->getCurrentActionType();
987
988
		$projects = $this->getStarredProjects();
989
		if ($projects->count() < 1) {
990
			$projects = $this->DNProjectList();
991
		} else {
992
			$limit = -1;
993
		}
994
995
		if ($projects->count() > 0) {
996
			$activeProject = false;
997
998
			if ($limit > 0) {
999
				$limitedProjects = $projects->limit($limit);
1000
			} else {
1001
				$limitedProjects = $projects;
1002
			}
1003
1004
			foreach ($limitedProjects as $project) {
1005
				$isActive = $currentProject && $currentProject->ID == $project->ID;
1006
				if ($isActive) {
1007
					$activeProject = true;
1008
				}
1009
1010
				$isCurrentEnvironment = false;
1011
				if ($project && $currentEnvironment) {
1012
					$isCurrentEnvironment = (bool) $project->DNEnvironmentList()->find('ID', $currentEnvironment->ID);
1013
				}
1014
1015
				$navigation->push([
1016
					'Project' => $project,
1017
					'IsCurrentEnvironment' => $isCurrentEnvironment,
1018
					'IsActive' => $currentProject && $currentProject->ID == $project->ID,
1019
					'IsOverview' => $actionType == self::PROJECT_OVERVIEW && $currentProject->ID == $project->ID
1020
				]);
1021
			}
1022
1023
			// Ensure the current project is in the list
1024
			if (!$activeProject && $currentProject) {
1025
				$navigation->unshift([
1026
					'Project' => $currentProject,
1027
					'IsActive' => true,
1028
					'IsCurrentEnvironment' => $currentEnvironment,
1029
					'IsOverview' => $actionType == self::PROJECT_OVERVIEW
1030
				]);
1031
				if ($limit > 0 && $navigation->count() > $limit) {
1032
					$navigation->pop();
1033
				}
1034
			}
1035
		}
1036
1037
		return $navigation;
1038
	}
1039
1040
	/**
1041
	 * Construct the deployment form
1042
	 *
1043
	 * @deprecated 2.0.0 - moved to DeployDispatcher
1044
	 *
1045
	 * @return Form
1046
	 */
1047
	public function getDeployForm($request = null) {
1048
1049
		// Performs canView permission check by limiting visible projects
1050
		$project = $this->getCurrentProject();
1051
		if (!$project) {
1052
			return $this->project404Response();
1053
		}
1054
1055
		// Performs canView permission check by limiting visible projects
1056
		$environment = $this->getCurrentEnvironment($project);
1057
		if (!$environment) {
1058
			return $this->environment404Response();
1059
		}
1060
1061
		if (!$environment->canDeploy()) {
1062
			return new SS_HTTPResponse("Not allowed to deploy", 401);
1063
		}
1064
1065
		// Generate the form
1066
		$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...
1067
1068
		// If this is an ajax request we don't want to submit the form - we just want to retrieve the markup.
1069
		if (
1070
			$request &&
1071
			!$request->requestVar('action_showDeploySummary') &&
1072
			$this->getRequest()->isAjax() &&
1073
			$this->getRequest()->isGET()
1074
		) {
1075
			// We can just use the URL we're accessing
1076
			$form->setFormAction($this->getRequest()->getURL());
1077
1078
			$body = json_encode(['Content' => $form->forAjaxTemplate()->forTemplate()]);
1079
			$this->getResponse()->addHeader('Content-Type', 'application/json');
1080
			$this->getResponse()->setBody($body);
1081
			return $body;
1082
		}
1083
1084
		$form->setFormAction($this->getRequest()->getURL() . '/DeployForm');
1085
		return $form;
1086
	}
1087
1088
	/**
1089
	 * @deprecated 2.0.0 - moved to GitDispatcher
1090
	 *
1091
	 * @param SS_HTTPRequest $request
1092
	 *
1093
	 * @return SS_HTTPResponse|string
1094
	 */
1095
	public function gitRevisions(SS_HTTPRequest $request) {
1096
1097
		// Performs canView permission check by limiting visible projects
1098
		$project = $this->getCurrentProject();
1099
		if (!$project) {
1100
			return $this->project404Response();
1101
		}
1102
1103
		// Performs canView permission check by limiting visible projects
1104
		$env = $this->getCurrentEnvironment($project);
1105
		if (!$env) {
1106
			return $this->environment404Response();
1107
		}
1108
1109
		$options = [];
1110
		foreach ($env->Backend()->getDeployOptions($env) as $option) {
1111
			$options[] = [
1112
				'name' => $option->getName(),
1113
				'title' => $option->getTitle(),
1114
				'defaultValue' => $option->getDefaultValue()
1115
			];
1116
		}
1117
1118
		$tabs = [];
1119
		$id = 0;
1120
		$data = [
1121
			'id' => ++$id,
1122
			'name' => 'Deploy the latest version of a branch',
1123
			'field_type' => 'dropdown',
1124
			'field_label' => 'Choose a branch',
1125
			'field_id' => 'branch',
1126
			'field_data' => [],
1127
			'options' => $options
1128
		];
1129
		foreach ($project->DNBranchList() as $branch) {
1130
			$sha = $branch->SHA();
1131
			$name = $branch->Name();
1132
			$branchValue = sprintf("%s (%s, %s old)",
1133
				$name,
1134
				substr($sha, 0, 8),
1135
				$branch->LastUpdated()->TimeDiff()
1136
			);
1137
			$data['field_data'][] = [
1138
				'id' => $sha,
1139
				'text' => $branchValue,
1140
				'branch_name' => $name // the raw branch name, not including the time etc
1141
			];
1142
		}
1143
		$tabs[] = $data;
1144
1145
		$data = [
1146
			'id' => ++$id,
1147
			'name' => 'Deploy a tagged release',
1148
			'field_type' => 'dropdown',
1149
			'field_label' => 'Choose a tag',
1150
			'field_id' => 'tag',
1151
			'field_data' => [],
1152
			'options' => $options
1153
		];
1154
1155
		foreach ($project->DNTagList()->setLimit(null) as $tag) {
1156
			$name = $tag->Name();
1157
			$data['field_data'][] = [
1158
				'id' => $tag->SHA(),
1159
				'text' => sprintf("%s", $name)
1160
			];
1161
		}
1162
1163
		// show newest tags first.
1164
		$data['field_data'] = array_reverse($data['field_data']);
1165
1166
		$tabs[] = $data;
1167
1168
		// Past deployments
1169
		$data = [
1170
			'id' => ++$id,
1171
			'name' => 'Redeploy a release that was previously deployed (to any environment)',
1172
			'field_type' => 'dropdown',
1173
			'field_label' => 'Choose a previously deployed release',
1174
			'field_id' => 'release',
1175
			'field_data' => [],
1176
			'options' => $options
1177
		];
1178
		// We are aiming at the format:
1179
		// [{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...
1180
		$redeploy = [];
1181 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...
1182
			$envName = $dnEnvironment->Name;
1183
			$perEnvDeploys = [];
1184
1185
			foreach ($dnEnvironment->DeployHistory() as $deploy) {
1186
				$sha = $deploy->SHA;
1187
1188
				// Check if exists to make sure the newest deployment date is used.
1189
				if (!isset($perEnvDeploys[$sha])) {
1190
					$pastValue = sprintf("%s (deployed %s)",
1191
						substr($sha, 0, 8),
1192
						$deploy->obj('LastEdited')->Ago()
1193
					);
1194
					$perEnvDeploys[$sha] = [
1195
						'id' => $sha,
1196
						'text' => $pastValue
1197
					];
1198
				}
1199
			}
1200
1201
			if (!empty($perEnvDeploys)) {
1202
				$redeploy[$envName] = array_values($perEnvDeploys);
1203
			}
1204
		}
1205
		// Convert the array to the frontend format (i.e. keyed to regular array)
1206
		foreach ($redeploy as $name => $descr) {
1207
			$data['field_data'][] = ['text' => $name, 'children' => $descr];
1208
		}
1209
		$tabs[] = $data;
1210
1211
		$data = [
1212
			'id' => ++$id,
1213
			'name' => 'Deploy a specific SHA',
1214
			'field_type' => 'textfield',
1215
			'field_label' => 'Choose a SHA',
1216
			'field_id' => 'SHA',
1217
			'field_data' => [],
1218
			'options' => $options
1219
		];
1220
		$tabs[] = $data;
1221
1222
		// get the last time git fetch was run
1223
		$lastFetched = 'never';
1224
		$fetch = DNGitFetch::get()
1225
			->filter('ProjectID', $project->ID)
1226
			->sort('LastEdited', 'DESC')
1227
			->first();
1228
		if ($fetch) {
1229
			$lastFetched = $fetch->dbObject('LastEdited')->Ago();
1230
		}
1231
1232
		$data = [
1233
			'Tabs' => $tabs,
1234
			'last_fetched' => $lastFetched
1235
		];
1236
1237
		$this->applyRedeploy($request, $data);
1238
1239
		return json_encode($data, JSON_PRETTY_PRINT);
1240
	}
1241
1242
	/**
1243
	 * @deprecated 2.0.0 - moved to PlanDispatcher
1244
	 *
1245
	 * @param SS_HTTPRequest $request
1246
	 *
1247
	 * @return string
1248
	 */
1249
	public function deploySummary(SS_HTTPRequest $request) {
1250
1251
		// Performs canView permission check by limiting visible projects
1252
		$project = $this->getCurrentProject();
1253
		if (!$project) {
1254
			return $this->project404Response();
1255
		}
1256
1257
		// Performs canView permission check by limiting visible projects
1258
		$environment = $this->getCurrentEnvironment($project);
1259
		if (!$environment) {
1260
			return $this->environment404Response();
1261
		}
1262
1263
		// Plan the deployment.
1264
		$strategy = $environment->getDeployStrategy($request);
1265
		$data = $strategy->toArray();
1266
1267
		// Add in a URL for comparing from->to code changes. Ensure that we have
1268
		// two proper 40 character SHAs, otherwise we can't show the compare link.
1269
		$interface = $project->getRepositoryInterface();
1270
		if (
1271
			!empty($interface) && !empty($interface->URL)
1272
			&& !empty($data['changes']['Code version']['from'])
1273
			&& strlen($data['changes']['Code version']['from']) == '40'
1274
			&& !empty($data['changes']['Code version']['to'])
1275
			&& strlen($data['changes']['Code version']['to']) == '40'
1276
		) {
1277
			$compareurl = sprintf(
1278
				'%s/compare/%s...%s',
1279
				$interface->URL,
1280
				$data['changes']['Code version']['from'],
1281
				$data['changes']['Code version']['to']
1282
			);
1283
			$data['changes']['Code version']['compareUrl'] = $compareurl;
1284
		}
1285
1286
		// Append json to response
1287
		$token = SecurityToken::inst();
1288
		$data['SecurityID'] = $token->getValue();
1289
1290
		$this->extend('updateDeploySummary', $data);
1291
1292
		return json_encode($data);
1293
	}
1294
1295
	/**
1296
	 * Deployment form submission handler.
1297
	 *
1298
	 * @deprecated 2.0.0 - moved to DeployDispatcher
1299
	 *
1300
	 * Initiate a DNDeployment record and redirect to it for status polling
1301
	 *
1302
	 * @param SS_HTTPRequest $request
1303
	 *
1304
	 * @return SS_HTTPResponse
1305
	 * @throws ValidationException
1306
	 * @throws null
1307
	 */
1308
	public function startDeploy(SS_HTTPRequest $request) {
1309
1310
		$token = SecurityToken::inst();
1311
1312
		// Ensure the submitted token has a value
1313
		$submittedToken = $request->postVar(\Dispatcher::SECURITY_TOKEN_NAME);
1314
		if (!$submittedToken) {
1315
			return false;
1316
		}
1317
		// Do the actual check.
1318
		$check = $token->check($submittedToken);
1319
		// Ensure the CSRF Token is correct
1320
		if (!$check) {
1321
			// CSRF token didn't match
1322
			return $this->httpError(400, 'Bad Request');
1323
		}
1324
1325
		// Performs canView permission check by limiting visible projects
1326
		$project = $this->getCurrentProject();
1327
		if (!$project) {
1328
			return $this->project404Response();
1329
		}
1330
1331
		// Performs canView permission check by limiting visible projects
1332
		$environment = $this->getCurrentEnvironment($project);
1333
		if (!$environment) {
1334
			return $this->environment404Response();
1335
		}
1336
1337
		// Initiate the deployment
1338
		// The extension point should pass in: Project, Environment, SelectRelease, buildName
1339
		$this->extend('doDeploy', $project, $environment, $buildName, $data);
1340
1341
		// Start the deployment based on the approved strategy.
1342
		$strategy = new DeploymentStrategy($environment);
1343
		$strategy->fromArray($request->requestVar('strategy'));
1344
		$deployment = $strategy->createDeployment();
1345
		// Skip through the approval state for now.
1346
		$deployment->getMachine()->apply(DNDeployment::TR_SUBMIT);
1347
		$deployment->getMachine()->apply(DNDeployment::TR_QUEUE);
1348
1349
		return json_encode([
1350
			'url' => Director::absoluteBaseURL() . $deployment->Link()
1351
		], JSON_PRETTY_PRINT);
1352
	}
1353
1354
	/**
1355
	 * @deprecated 2.0.0 - moved to DeployDispatcher
1356
	 *
1357
	 * Action - Do the actual deploy
1358
	 *
1359
	 * @param SS_HTTPRequest $request
1360
	 *
1361
	 * @return SS_HTTPResponse|string
1362
	 * @throws SS_HTTPResponse_Exception
1363
	 */
1364
	public function deploy(SS_HTTPRequest $request) {
1365
		$params = $request->params();
1366
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1367
1368
		if (!$deployment || !$deployment->ID) {
1369
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1370
		}
1371
		if (!$deployment->canView()) {
1372
			return Security::permissionFailure();
1373
		}
1374
1375
		$environment = $deployment->Environment();
1376
		$project = $environment->Project();
1377
1378
		if ($environment->Name != $params['Environment']) {
1379
			throw new LogicException("Environment in URL doesn't match this deploy");
1380
		}
1381
		if ($project->Name != $params['Project']) {
1382
			throw new LogicException("Project in URL doesn't match this deploy");
1383
		}
1384
1385
		return $this->render([
1386
			'Deployment' => $deployment,
1387
		]);
1388
	}
1389
1390
	/**
1391
	 * @deprecated 2.0.0 - moved to DeployDispatcher
1392
	 *
1393
	 * Action - Get the latest deploy log
1394
	 *
1395
	 * @param SS_HTTPRequest $request
1396
	 *
1397
	 * @return string
1398
	 * @throws SS_HTTPResponse_Exception
1399
	 */
1400
	public function deploylog(SS_HTTPRequest $request) {
1401
		$params = $request->params();
1402
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1403
1404
		if (!$deployment || !$deployment->ID) {
1405
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1406
		}
1407
		if (!$deployment->canView()) {
1408
			return Security::permissionFailure();
1409
		}
1410
1411
		$environment = $deployment->Environment();
1412
		$project = $environment->Project();
1413
1414
		if ($environment->Name != $params['Environment']) {
1415
			throw new LogicException("Environment in URL doesn't match this deploy");
1416
		}
1417
		if ($project->Name != $params['Project']) {
1418
			throw new LogicException("Project in URL doesn't match this deploy");
1419
		}
1420
1421
		$log = $deployment->log();
1422
		if ($log->exists()) {
1423
			$content = $log->content();
1424
		} else {
1425
			$content = 'Waiting for action to start';
1426
		}
1427
1428
		return $this->sendResponse($deployment->ResqueStatus(), $content);
1429
	}
1430
1431
	public function abortDeploy(SS_HTTPRequest $request) {
1432
		$params = $request->params();
1433
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1434
1435
		if (!$deployment || !$deployment->ID) {
1436
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1437
		}
1438
		if (!$deployment->canView()) {
1439
			return Security::permissionFailure();
1440
		}
1441
1442
		// For now restrict to ADMINs only.
1443
		if (!Permission::check('ADMIN')) {
1444
			return Security::permissionFailure();
1445
		}
1446
1447
		$environment = $deployment->Environment();
1448
		$project = $environment->Project();
1449
1450
		if ($environment->Name != $params['Environment']) {
1451
			throw new LogicException("Environment in URL doesn't match this deploy");
1452
		}
1453
		if ($project->Name != $params['Project']) {
1454
			throw new LogicException("Project in URL doesn't match this deploy");
1455
		}
1456
1457
		if (!in_array($deployment->Status, ['Queued', 'Deploying', 'Aborting'])) {
1458
			throw new LogicException(sprintf("Cannot abort from %s state.", $deployment->Status));
1459
		}
1460
1461
		$deployment->getMachine()->apply(DNDeployment::TR_ABORT);
1462
1463
		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...
1464
	}
1465
1466
	/**
1467
	 * @param SS_HTTPRequest|null $request
1468
	 *
1469
	 * @return Form
1470
	 */
1471
	public function getDataTransferForm(SS_HTTPRequest $request = null) {
1472
		// Performs canView permission check by limiting visible projects
1473
		$envs = $this->getCurrentProject()->DNEnvironmentList()->filterByCallback(function ($item) {
1474
			return $item->canBackup();
1475
		});
1476
1477
		if (!$envs) {
1478
			return $this->environment404Response();
1479
		}
1480
1481
		$items = [];
1482
		$disabledEnvironments = [];
1483 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...
1484
			$items[$env->ID] = $env->Title;
1485
			if ($env->CurrentBuild() === false) {
1486
				$items[$env->ID] = sprintf("%s - requires initial deployment", $env->Title);
1487
				$disabledEnvironments[] = $env->ID;
1488
			}
1489
		}
1490
1491
		$envsField =  DropdownField::create('EnvironmentID', 'Environment', $items)
1492
			->setEmptyString('Select an environment');
1493
		$envsField->setDisabledItems($disabledEnvironments);
1494
1495
		$formAction = FormAction::create('doDataTransfer', 'Create')
1496
			->addExtraClass('btn');
1497
1498
		if (count($disabledEnvironments) === $envs->count()) {
1499
			$formAction->setDisabled(true);
1500
		}
1501
1502
		$form = Form::create(
1503
			$this,
1504
			'DataTransferForm',
1505
			FieldList::create(
1506
				HiddenField::create('Direction', null, 'get'),
1507
				$envsField,
1508
				DropdownField::create('Mode', 'Transfer', DNDataArchive::get_mode_map())
1509
			),
1510
			FieldList::create($formAction)
1511
		);
1512
		$form->setFormAction($this->getRequest()->getURL() . '/DataTransferForm');
1513
1514
		return $form;
1515
	}
1516
1517
	/**
1518
	 * @param array $data
1519
	 * @param Form $form
1520
	 *
1521
	 * @return SS_HTTPResponse
1522
	 * @throws SS_HTTPResponse_Exception
1523
	 */
1524
	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...
1525
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1526
1527
		// Performs canView permission check by limiting visible projects
1528
		$project = $this->getCurrentProject();
1529
		if (!$project) {
1530
			return $this->project404Response();
1531
		}
1532
1533
		$dataArchive = null;
1534
1535
		// Validate direction.
1536
		if ($data['Direction'] == 'get') {
1537
			$validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1538
				->filterByCallback(function ($item) {
1539
					return $item->canBackup();
1540
				});
1541
		} else if ($data['Direction'] == 'push') {
1542
			$validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1543
				->filterByCallback(function ($item) {
1544
					return $item->canRestore();
1545
				});
1546
		} else {
1547
			throw new LogicException('Invalid direction');
1548
		}
1549
1550
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
1551
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
1552
		if (!$environment) {
1553
			throw new LogicException('Invalid environment');
1554
		}
1555
1556
		$this->validateSnapshotMode($data['Mode']);
1557
1558
		// Only 'push' direction is allowed an association with an existing archive.
1559
		if (
1560
			$data['Direction'] == 'push'
1561
			&& isset($data['DataArchiveID'])
1562
			&& is_numeric($data['DataArchiveID'])
1563
		) {
1564
			$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1565
			if (!$dataArchive) {
1566
				throw new LogicException('Invalid data archive');
1567
			}
1568
1569
			if (!$dataArchive->canDownload()) {
1570
				throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1571
			}
1572
		}
1573
1574
		$transfer = DNDataTransfer::create();
1575
		$transfer->EnvironmentID = $environment->ID;
1576
		$transfer->Direction = $data['Direction'];
1577
		$transfer->Mode = $data['Mode'];
1578
		$transfer->DataArchiveID = $dataArchive ? $dataArchive->ID : null;
1579
		if ($data['Direction'] == 'push') {
1580
			$transfer->setBackupBeforePush(!empty($data['BackupBeforePush']));
1581
		}
1582
		$transfer->write();
1583
		$transfer->start();
1584
1585
		return $this->redirect($transfer->Link());
1586
	}
1587
1588
	/**
1589
	 * View into the log for a {@link DNDataTransfer}.
1590
	 *
1591
	 * @param SS_HTTPRequest $request
1592
	 *
1593
	 * @return SS_HTTPResponse|string
1594
	 * @throws SS_HTTPResponse_Exception
1595
	 */
1596
	public function transfer(SS_HTTPRequest $request) {
1597
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1598
1599
		$params = $request->params();
1600
		$transfer = DNDataTransfer::get()->byId($params['Identifier']);
1601
1602
		if (!$transfer || !$transfer->ID) {
1603
			throw new SS_HTTPResponse_Exception('Transfer not found', 404);
1604
		}
1605
		if (!$transfer->canView()) {
1606
			return Security::permissionFailure();
1607
		}
1608
1609
		$environment = $transfer->Environment();
1610
		$project = $environment->Project();
1611
1612
		if ($project->Name != $params['Project']) {
1613
			throw new LogicException("Project in URL doesn't match this deploy");
1614
		}
1615
1616
		return $this->render([
1617
			'CurrentTransfer' => $transfer,
1618
			'SnapshotsSection' => 1,
1619
		]);
1620
	}
1621
1622
	/**
1623
	 * Action - Get the latest deploy log
1624
	 *
1625
	 * @param SS_HTTPRequest $request
1626
	 *
1627
	 * @return string
1628
	 * @throws SS_HTTPResponse_Exception
1629
	 */
1630
	public function transferlog(SS_HTTPRequest $request) {
1631
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1632
1633
		$params = $request->params();
1634
		$transfer = DNDataTransfer::get()->byId($params['Identifier']);
1635
1636
		if (!$transfer || !$transfer->ID) {
1637
			throw new SS_HTTPResponse_Exception('Transfer not found', 404);
1638
		}
1639
		if (!$transfer->canView()) {
1640
			return Security::permissionFailure();
1641
		}
1642
1643
		$environment = $transfer->Environment();
1644
		$project = $environment->Project();
1645
1646
		if ($project->Name != $params['Project']) {
1647
			throw new LogicException("Project in URL doesn't match this deploy");
1648
		}
1649
1650
		$log = $transfer->log();
1651
		if ($log->exists()) {
1652
			$content = $log->content();
1653
		} else {
1654
			$content = 'Waiting for action to start';
1655
		}
1656
1657
		return $this->sendResponse($transfer->ResqueStatus(), $content);
1658
	}
1659
1660
	/**
1661
	 * Note: Submits to the same action as {@link getDataTransferForm()},
1662
	 * but with a Direction=push and an archive reference.
1663
	 *
1664
	 * @param SS_HTTPRequest $request
1665
	 * @param DNDataArchive|null $dataArchive Only set when method is called manually in {@link restore()},
1666
	 *                            otherwise the state is inferred from the request data.
1667
	 * @return Form
1668
	 */
1669
	public function getDataTransferRestoreForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) {
1670
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1671
1672
		// Performs canView permission check by limiting visible projects
1673
		$project = $this->getCurrentProject();
1674
		$envs = $project->DNEnvironmentList()->filterByCallback(function ($item) {
1675
			return $item->canRestore();
1676
		});
1677
1678
		if (!$envs) {
1679
			return $this->environment404Response();
1680
		}
1681
1682
		$modesMap = [];
1683
		if (in_array($dataArchive->Mode, ['all'])) {
1684
			$modesMap['all'] = 'Database and Assets';
1685
		};
1686
		if (in_array($dataArchive->Mode, ['all', 'db'])) {
1687
			$modesMap['db'] = 'Database only';
1688
		};
1689
		if (in_array($dataArchive->Mode, ['all', 'assets'])) {
1690
			$modesMap['assets'] = 'Assets only';
1691
		};
1692
1693
		$alertMessage = '<div class="alert alert-warning"><strong>Warning:</strong> '
1694
			. 'This restore will overwrite the data on the chosen environment below</div>';
1695
1696
1697
		$items = [];
1698
		$disabledEnvironments = [];
1699 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...
1700
			$items[$env->ID] = $env->Title;
1701
			if ($env->CurrentBuild() === false) {
1702
				$items[$env->ID] = sprintf("%s - requires initial deployment", $env->Title);
1703
				$disabledEnvironments[] = $env->ID;
1704
			}
1705
		}
1706
1707
		$envsField = DropdownField::create('EnvironmentID', 'Environment', $items)
1708
			->setEmptyString('Select an environment');
1709
		$envsField->setDisabledItems($disabledEnvironments);
1710
		$formAction = FormAction::create('doDataTransfer', 'Restore Data')->addExtraClass('btn');
1711
1712
		if (count($disabledEnvironments) == $envs->count()) {
1713
			$formAction->setDisabled(true);
1714
		}
1715
1716
		$form = Form::create(
1717
			$this,
1718
			'DataTransferRestoreForm',
1719
			FieldList::create(
1720
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1721
				HiddenField::create('Direction', null, 'push'),
1722
				LiteralField::create('Warning', $alertMessage),
1723
				$envsField,
1724
				DropdownField::create('Mode', 'Transfer', $modesMap),
1725
				CheckboxField::create('BackupBeforePush', 'Backup existing data', '1')
1726
			),
1727
			FieldList::create($formAction)
1728
		);
1729
		$form->setFormAction($project->Link() . '/DataTransferRestoreForm');
1730
1731
		return $form;
1732
	}
1733
1734
	/**
1735
	 * View a form to restore a specific {@link DataArchive}.
1736
	 * Permission checks are handled in {@link DataArchives()}.
1737
	 * Submissions are handled through {@link doDataTransfer()}, same as backup operations.
1738
	 *
1739
	 * @param SS_HTTPRequest $request
1740
	 *
1741
	 * @return HTMLText
1742
	 * @throws SS_HTTPResponse_Exception
1743
	 */
1744 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...
1745
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1746
1747
		/** @var DNDataArchive $dataArchive */
1748
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1749
1750
		if (!$dataArchive) {
1751
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1752
		}
1753
1754
		// We check for canDownload because that implies access to the data.
1755
		// canRestore is later checked on the actual restore action per environment.
1756
		if (!$dataArchive->canDownload()) {
1757
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1758
		}
1759
1760
		$form = $this->getDataTransferRestoreForm($this->request, $dataArchive);
1761
1762
		// View currently only available via ajax
1763
		return $form->forTemplate();
1764
	}
1765
1766
	/**
1767
	 * View a form to delete a specific {@link DataArchive}.
1768
	 * Permission checks are handled in {@link DataArchives()}.
1769
	 * Submissions are handled through {@link doDelete()}.
1770
	 *
1771
	 * @param SS_HTTPRequest $request
1772
	 *
1773
	 * @return HTMLText
1774
	 * @throws SS_HTTPResponse_Exception
1775
	 */
1776 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...
1777
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1778
1779
		/** @var DNDataArchive $dataArchive */
1780
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1781
1782
		if (!$dataArchive) {
1783
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1784
		}
1785
1786
		if (!$dataArchive->canDelete()) {
1787
			throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403);
1788
		}
1789
1790
		$form = $this->getDeleteForm($this->request, $dataArchive);
1791
1792
		// View currently only available via ajax
1793
		return $form->forTemplate();
1794
	}
1795
1796
	/**
1797
	 * @param SS_HTTPRequest $request
1798
	 * @param DNDataArchive|null $dataArchive Only set when method is called manually, otherwise the state is inferred
1799
	 *        from the request data.
1800
	 * @return Form
1801
	 */
1802
	public function getDeleteForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) {
1803
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1804
1805
		// Performs canView permission check by limiting visible projects
1806
		$project = $this->getCurrentProject();
1807
		if (!$project) {
1808
			return $this->project404Response();
1809
		}
1810
1811
		$snapshotDeleteWarning = '<div class="alert alert-warning">'
1812
			. 'Are you sure you want to permanently delete this snapshot from this archive area?'
1813
			. '</div>';
1814
1815
		$form = Form::create(
1816
			$this,
1817
			'DeleteForm',
1818
			FieldList::create(
1819
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1820
				LiteralField::create('Warning', $snapshotDeleteWarning)
1821
			),
1822
			FieldList::create(
1823
				FormAction::create('doDelete', 'Delete')
1824
					->addExtraClass('btn')
1825
			)
1826
		);
1827
		$form->setFormAction($project->Link() . '/DeleteForm');
1828
1829
		return $form;
1830
	}
1831
1832
	/**
1833
	 * @param array $data
1834
	 * @param Form $form
1835
	 *
1836
	 * @return bool|SS_HTTPResponse
1837
	 * @throws SS_HTTPResponse_Exception
1838
	 */
1839
	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...
1840
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1841
1842
		// Performs canView permission check by limiting visible projects
1843
		$project = $this->getCurrentProject();
1844
		if (!$project) {
1845
			return $this->project404Response();
1846
		}
1847
1848
		$dataArchive = null;
1849
1850
		if (
1851
			isset($data['DataArchiveID'])
1852
			&& is_numeric($data['DataArchiveID'])
1853
		) {
1854
			$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1855
		}
1856
1857
		if (!$dataArchive) {
1858
			throw new LogicException('Invalid data archive');
1859
		}
1860
1861
		if (!$dataArchive->canDelete()) {
1862
			throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403);
1863
		}
1864
1865
		$dataArchive->delete();
1866
1867
		return $this->redirectBack();
1868
	}
1869
1870
	/**
1871
	 * View a form to move a specific {@link DataArchive}.
1872
	 *
1873
	 * @param SS_HTTPRequest $request
1874
	 *
1875
	 * @return HTMLText
1876
	 * @throws SS_HTTPResponse_Exception
1877
	 */
1878 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...
1879
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1880
1881
		/** @var DNDataArchive $dataArchive */
1882
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1883
1884
		if (!$dataArchive) {
1885
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1886
		}
1887
1888
		// We check for canDownload because that implies access to the data.
1889
		if (!$dataArchive->canDownload()) {
1890
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1891
		}
1892
1893
		$form = $this->getMoveForm($this->request, $dataArchive);
1894
1895
		// View currently only available via ajax
1896
		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...
1897
	}
1898
1899
	/**
1900
	 * Build snapshot move form.
1901
	 *
1902
	 * @param SS_HTTPRequest $request
1903
	 * @param DNDataArchive|null $dataArchive
1904
	 *
1905
	 * @return Form|SS_HTTPResponse
1906
	 */
1907
	public function getMoveForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) {
1908
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1909
1910
		$envs = $dataArchive->validTargetEnvironments();
1911
		if (!$envs) {
1912
			return $this->environment404Response();
1913
		}
1914
1915
		$warningMessage = '<div class="alert alert-warning"><strong>Warning:</strong> This will make the snapshot '
1916
			. 'available to people with access to the target environment.<br>By pressing "Change ownership" you '
1917
			. 'confirm that you have considered data confidentiality regulations.</div>';
1918
1919
		$form = Form::create(
1920
			$this,
1921
			'MoveForm',
1922
			FieldList::create(
1923
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1924
				LiteralField::create('Warning', $warningMessage),
1925
				DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1926
					->setEmptyString('Select an environment')
1927
			),
1928
			FieldList::create(
1929
				FormAction::create('doMove', 'Change ownership')
1930
					->addExtraClass('btn')
1931
			)
1932
		);
1933
		$form->setFormAction($this->getCurrentProject()->Link() . '/MoveForm');
1934
1935
		return $form;
1936
	}
1937
1938
	/**
1939
	 * @param array $data
1940
	 * @param Form $form
1941
	 *
1942
	 * @return bool|SS_HTTPResponse
1943
	 * @throws SS_HTTPResponse_Exception
1944
	 * @throws ValidationException
1945
	 * @throws null
1946
	 */
1947
	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...
1948
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1949
1950
		// Performs canView permission check by limiting visible projects
1951
		$project = $this->getCurrentProject();
1952
		if (!$project) {
1953
			return $this->project404Response();
1954
		}
1955
1956
		/** @var DNDataArchive $dataArchive */
1957
		$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1958
		if (!$dataArchive) {
1959
			throw new LogicException('Invalid data archive');
1960
		}
1961
1962
		// We check for canDownload because that implies access to the data.
1963
		if (!$dataArchive->canDownload()) {
1964
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1965
		}
1966
1967
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
1968
		$validEnvs = $dataArchive->validTargetEnvironments();
1969
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
1970
		if (!$environment) {
1971
			throw new LogicException('Invalid environment');
1972
		}
1973
1974
		$dataArchive->EnvironmentID = $environment->ID;
1975
		$dataArchive->write();
1976
1977
		return $this->redirectBack();
1978
	}
1979
1980
	/**
1981
	 * Returns an error message if redis is unavailable
1982
	 *
1983
	 * @return string
1984
	 */
1985
	public static function RedisUnavailable() {
1986
		try {
1987
			Resque::queues();
1988
		} catch (Exception $e) {
1989
			return $e->getMessage();
1990
		}
1991
		return '';
1992
	}
1993
1994
	/**
1995
	 * Returns the number of connected Redis workers
1996
	 *
1997
	 * @return int
1998
	 */
1999
	public static function RedisWorkersCount() {
2000
		return count(Resque_Worker::all());
2001
	}
2002
2003
	/**
2004
	 * @return array
2005
	 */
2006
	public function providePermissions() {
2007
		return [
2008
			self::DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS => [
2009
				'name' => "Access to advanced deploy options",
2010
				'category' => "Deploynaut",
2011
			],
2012
2013
			// Permissions that are intended to be added to the roles.
2014
			self::ALLOW_PROD_DEPLOYMENT => [
2015
				'name' => "Ability to deploy to production environments",
2016
				'category' => "Deploynaut",
2017
			],
2018
			self::ALLOW_NON_PROD_DEPLOYMENT => [
2019
				'name' => "Ability to deploy to non-production environments",
2020
				'category' => "Deploynaut",
2021
			],
2022
			self::ALLOW_PROD_SNAPSHOT => [
2023
				'name' => "Ability to make production snapshots",
2024
				'category' => "Deploynaut",
2025
			],
2026
			self::ALLOW_NON_PROD_SNAPSHOT => [
2027
				'name' => "Ability to make non-production snapshots",
2028
				'category' => "Deploynaut",
2029
			],
2030
			self::ALLOW_CREATE_ENVIRONMENT => [
2031
				'name' => "Ability to create environments",
2032
				'category' => "Deploynaut",
2033
			],
2034
		];
2035
	}
2036
2037
	/**
2038
	 * @return DNProject|null
2039
	 */
2040
	public function getCurrentProject() {
2041
		$projectName = trim($this->getRequest()->param('Project'));
2042
		if (!$projectName) {
2043
			return null;
2044
		}
2045
		if (empty(self::$_project_cache[$projectName])) {
2046
			self::$_project_cache[$projectName] = $this->DNProjectList()->filter('Name', $projectName)->First();
2047
		}
2048
		return self::$_project_cache[$projectName];
2049
	}
2050
2051
	/**
2052
	 * @param DNProject|null $project
2053
	 * @return DNEnvironment|null
2054
	 */
2055
	public function getCurrentEnvironment(DNProject $project = null) {
2056
		if ($this->getRequest()->param('Environment') === null) {
2057
			return null;
2058
		}
2059
		if ($project === null) {
2060
			$project = $this->getCurrentProject();
2061
		}
2062
		// project can still be null
2063
		if ($project === null) {
2064
			return null;
2065
		}
2066
		return $project->DNEnvironmentList()->filter('Name', $this->getRequest()->param('Environment'))->First();
2067
	}
2068
2069
	/**
2070
	 * This will return a const that indicates the class of action currently being performed
2071
	 *
2072
	 * Until DNRoot is de-godded, it does a bunch of different actions all in the same class.
2073
	 * So we just have each action handler calll setCurrentActionType to define what sort of
2074
	 * action it is.
2075
	 *
2076
	 * @return string - one of the consts from self::$action_types
2077
	 */
2078
	public function getCurrentActionType() {
2079
		return $this->actionType;
2080
	}
2081
2082
	/**
2083
	 * Sets the current action type
2084
	 *
2085
	 * @param string $actionType string - one of the consts from self::$action_types
2086
	 */
2087
	public function setCurrentActionType($actionType) {
2088
		$this->actionType = $actionType;
2089
	}
2090
2091
	/**
2092
	 * Helper method to allow templates to know whether they should show the 'Archive List' include or not.
2093
	 * The actual permissions are set on a per-environment level, so we need to find out if this $member can upload to
2094
	 * or download from *any* {@link DNEnvironment} that (s)he has access to.
2095
	 *
2096
	 * TODO To be replaced with a method that just returns the list of archives this {@link Member} has access to.
2097
	 *
2098
	 * @param Member|null $member The {@link Member} to check (or null to check the currently logged in Member)
2099
	 * @return boolean|null true if $member has access to upload or download to at least one {@link DNEnvironment}.
2100
	 */
2101
	public function CanViewArchives(Member $member = null) {
2102
		if ($member === null) {
2103
			$member = Member::currentUser();
2104
		}
2105
2106
		if (Permission::checkMember($member, 'ADMIN')) {
2107
			return true;
2108
		}
2109
2110
		$allProjects = $this->DNProjectList();
2111
		if (!$allProjects) {
2112
			return false;
2113
		}
2114
2115
		foreach ($allProjects as $project) {
2116
			if ($project->Environments()) {
2117
				foreach ($project->Environments() as $environment) {
2118
					if (
2119
						$environment->canRestore($member) ||
2120
						$environment->canBackup($member) ||
2121
						$environment->canUploadArchive($member) ||
2122
						$environment->canDownloadArchive($member)
2123
					) {
2124
						// We can return early as we only need to know that we can access one environment
2125
						return true;
2126
					}
2127
				}
2128
			}
2129
		}
2130
	}
2131
2132
	/**
2133
	 * Returns a list of attempted environment creations.
2134
	 *
2135
	 * @return PaginatedList
2136
	 */
2137
	public function CreateEnvironmentList() {
2138
		$project = $this->getCurrentProject();
2139
		if ($project) {
2140
			$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...
2141
		} else {
2142
			$dataList = new ArrayList();
2143
		}
2144
2145
		$this->extend('updateCreateEnvironmentList', $dataList);
2146
		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...
2147
	}
2148
2149
	/**
2150
	 * Returns a list of all archive files that can be accessed by the currently logged-in {@link Member}
2151
	 *
2152
	 * @return PaginatedList
2153
	 */
2154
	public function CompleteDataArchives() {
2155
		$project = $this->getCurrentProject();
2156
		$archives = new ArrayList();
2157
2158
		$archiveList = $project->Environments()->relation("DataArchives");
2159
		if ($archiveList->count() > 0) {
2160
			foreach ($archiveList as $archive) {
2161
				if (!$archive->isPending()) {
2162
					$archives->push($archive);
2163
				}
2164
			}
2165
		}
2166
		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...
2167
	}
2168
2169
	/**
2170
	 * @return PaginatedList The list of "pending" data archives which are waiting for a file
2171
	 * to be delivered offline by post, and manually uploaded into the system.
2172
	 */
2173
	public function PendingDataArchives() {
2174
		$project = $this->getCurrentProject();
2175
		$archives = new ArrayList();
2176
		foreach ($project->DNEnvironmentList() as $env) {
2177
			foreach ($env->DataArchives() as $archive) {
2178
				if ($archive->isPending()) {
2179
					$archives->push($archive);
2180
				}
2181
			}
2182
		}
2183
		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...
2184
	}
2185
2186
	/**
2187
	 * @return PaginatedList
2188
	 */
2189
	public function DataTransferLogs() {
2190
		$environments = $this->getCurrentProject()->Environments()->column('ID');
2191
		$transfers = DNDataTransfer::get()
2192
			->filter('EnvironmentID', $environments)
2193
			->filterByCallback(
2194
				function ($record) {
2195
					return
2196
						$record->Environment()->canRestore() || // Ensure member can perform an action on the transfers env
2197
						$record->Environment()->canBackup() ||
2198
						$record->Environment()->canUploadArchive() ||
2199
						$record->Environment()->canDownloadArchive();
2200
				});
2201
2202
		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...
2203
	}
2204
2205
	/**
2206
	 * @deprecated 2.0.0 - moved to DeployDispatcher
2207
	 *
2208
	 * @return null|PaginatedList
2209
	 */
2210
	public function DeployHistory() {
2211
		if ($env = $this->getCurrentEnvironment()) {
2212
			$history = $env->DeployHistory();
2213
			if ($history->count() > 0) {
2214
				$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...
2215
				$pagination->setPageLength(4);
2216
				return $pagination;
2217
			}
2218
		}
2219
		return null;
2220
	}
2221
2222
	/**
2223
	 * @param string $status
2224
	 * @param string $content
2225
	 *
2226
	 * @return string
2227
	 */
2228
	public function sendResponse($status, $content) {
2229
		// strip excessive newlines
2230
		$content = preg_replace('/(?:(?:\r\n|\r|\n)\s*){2}/s', "\n", $content);
2231
2232
		$sendJSON = (strpos($this->getRequest()->getHeader('Accept'), 'application/json') !== false)
2233
			|| $this->getRequest()->getExtension() == 'json';
2234
2235
		if (!$sendJSON) {
2236
			$this->response->addHeader("Content-type", "text/plain");
2237
			return $content;
2238
		}
2239
		$this->response->addHeader("Content-type", "application/json");
2240
		return json_encode([
2241
			'status' => $status,
2242
			'content' => $content,
2243
		]);
2244
	}
2245
2246
	/**
2247
	 * Get items for the ambient menu that should be accessible from all pages.
2248
	 *
2249
	 * @return ArrayList
2250
	 */
2251
	public function AmbientMenu() {
2252
		$list = new ArrayList();
2253
2254
		if (Member::currentUserID()) {
2255
			$list->push(new ArrayData([
2256
				'Classes' => 'logout',
2257
				'FaIcon' => 'sign-out',
2258
				'Link' => 'Security/logout',
2259
				'Title' => 'Log out',
2260
				'IsCurrent' => false,
2261
				'IsSection' => false
2262
			]));
2263
		}
2264
2265
		$this->extend('updateAmbientMenu', $list);
2266
		return $list;
2267
	}
2268
2269
	/**
2270
	 * Checks whether the user can create a project.
2271
	 *
2272
	 * @return bool
2273
	 */
2274
	public function canCreateProjects($member = null) {
2275
		if (!$member) {
2276
			$member = Member::currentUser();
2277
		}
2278
		if (!$member) {
2279
			return false;
2280
		}
2281
2282
		return singleton('DNProject')->canCreate($member);
2283
	}
2284
2285
	protected function applyRedeploy(SS_HTTPRequest $request, &$data) {
2286
		if (!$request->getVar('redeploy')) {
2287
			return;
2288
		}
2289
2290
		$project = $this->getCurrentProject();
2291
		if (!$project) {
2292
			return $this->project404Response();
2293
		}
2294
2295
		// Performs canView permission check by limiting visible projects
2296
		$env = $this->getCurrentEnvironment($project);
2297
		if (!$env) {
2298
			return $this->environment404Response();
2299
		}
2300
2301
		$current = $env->CurrentBuild();
2302
		if ($current && $current->exists()) {
2303
			$data['preselect_tab'] = 3;
2304
			$data['preselect_sha'] = $current->SHA;
2305
		} else {
2306
			$master = $project->DNBranchList()->byName('master');
2307
			if ($master) {
2308
				$data['preselect_tab'] = 1;
2309
				$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...
2310
			}
2311
		}
2312
	}
2313
2314
	/**
2315
	 * @return SS_HTTPResponse
2316
	 */
2317
	protected function project404Response() {
2318
		return new SS_HTTPResponse(
2319
			"Project '" . Convert::raw2xml($this->getRequest()->param('Project')) . "' not found.",
2320
			404
2321
		);
2322
	}
2323
2324
	/**
2325
	 * @return SS_HTTPResponse
2326
	 */
2327
	protected function environment404Response() {
2328
		$envName = Convert::raw2xml($this->getRequest()->param('Environment'));
2329
		return new SS_HTTPResponse("Environment '" . $envName . "' not found.", 404);
2330
	}
2331
2332
	/**
2333
	 * Validate the snapshot mode
2334
	 *
2335
	 * @param string $mode
2336
	 */
2337
	protected function validateSnapshotMode($mode) {
2338
		if (!in_array($mode, ['all', 'assets', 'db'])) {
2339
			throw new LogicException('Invalid mode');
2340
		}
2341
	}
2342
2343
	/**
2344
	 * @param string $sectionName
2345
	 * @param string $title
2346
	 *
2347
	 * @return SS_HTTPResponse
2348
	 */
2349
	protected function getCustomisedViewSection($sectionName, $title = '', $data = []) {
2350
		// Performs canView permission check by limiting visible projects
2351
		$project = $this->getCurrentProject();
2352
		if (!$project) {
2353
			return $this->project404Response();
2354
		}
2355
		$data[$sectionName] = 1;
2356
2357
		if ($this !== '') {
2358
			$data['Title'] = $title;
2359
		}
2360
2361
		return $this->render($data);
2362
	}
2363
2364
}
2365
2366