Completed
Pull Request — master (#741)
by Sean
07:46 queued 03:20
created

DNRoot::environment()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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