Completed
Pull Request — master (#741)
by Sean
04:30
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
		'createenvlog',
67
		'createenv',
68
		'getDeployForm',
69
		'doDeploy',
70
		'deploy',
71
		'deploylog',
72
		'abortDeploy',
73
		'getDataTransferForm',
74
		'transfer',
75
		'transferlog',
76
		'snapshots',
77
		'createsnapshot',
78
		'snapshotslog',
79
		'uploadsnapshot',
80
		'getCreateEnvironmentForm',
81
		'getUploadSnapshotForm',
82
		'getPostSnapshotForm',
83
		'getDataTransferRestoreForm',
84
		'getDeleteForm',
85
		'getMoveForm',
86
		'restoresnapshot',
87
		'deletesnapshot',
88
		'movesnapshot',
89
		'postsnapshotsuccess',
90
		'gitRevisions',
91
		'deploySummary',
92
		'startDeploy'
93
	];
94
95
	/**
96
	 * URL handlers pretending that we have a deep URL structure.
97
	 */
98
	private static $url_handlers = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
99
		'project/$Project/environment/$Environment/DeployForm' => 'getDeployForm',
100
		'project/$Project/createsnapshot/DataTransferForm' => 'getDataTransferForm',
101
		'project/$Project/DataTransferForm' => 'getDataTransferForm',
102
		'project/$Project/DataTransferRestoreForm' => 'getDataTransferRestoreForm',
103
		'project/$Project/DeleteForm' => 'getDeleteForm',
104
		'project/$Project/MoveForm' => 'getMoveForm',
105
		'project/$Project/UploadSnapshotForm' => 'getUploadSnapshotForm',
106
		'project/$Project/PostSnapshotForm' => 'getPostSnapshotForm',
107
		'project/$Project/environment/$Environment/deploy_summary' => 'deploySummary',
108
		'project/$Project/environment/$Environment/git_revisions' => 'gitRevisions',
109
		'project/$Project/environment/$Environment/start-deploy' => 'startDeploy',
110
		'project/$Project/environment/$Environment/deploy/$Identifier/log' => 'deploylog',
111
		'project/$Project/environment/$Environment/deploy/$Identifier/abort-deploy' => 'abortDeploy',
112
		'project/$Project/environment/$Environment/deploy/$Identifier' => 'deploy',
113
		'project/$Project/transfer/$Identifier/log' => 'transferlog',
114
		'project/$Project/transfer/$Identifier' => 'transfer',
115
		'project/$Project/environment/$Environment' => 'environment',
116
		'project/$Project/createenv/$Identifier/log' => 'createenvlog',
117
		'project/$Project/createenv/$Identifier' => 'createenv',
118
		'project/$Project/CreateEnvironmentForm' => 'getCreateEnvironmentForm',
119
		'project/$Project/branch' => 'branch',
120
		'project/$Project/build/$Build' => 'build',
121
		'project/$Project/restoresnapshot/$DataArchiveID' => 'restoresnapshot',
122
		'project/$Project/deletesnapshot/$DataArchiveID' => 'deletesnapshot',
123
		'project/$Project/movesnapshot/$DataArchiveID' => 'movesnapshot',
124
		'project/$Project/update' => 'update',
125
		'project/$Project/snapshots' => 'snapshots',
126
		'project/$Project/createsnapshot' => 'createsnapshot',
127
		'project/$Project/uploadsnapshot' => 'uploadsnapshot',
128
		'project/$Project/snapshotslog' => 'snapshotslog',
129
		'project/$Project/postsnapshotsuccess/$DataArchiveID' => 'postsnapshotsuccess',
130
		'project/$Project/star' => 'toggleprojectstar',
131
		'project/$Project' => 'project',
132
		'nav/$Project' => 'nav',
133
		'projects' => 'projects',
134
	];
135
136
	/**
137
	 * @var array
138
	 */
139
	private static $support_links = [];
0 ignored issues
show
Unused Code introduced by
The property $support_links is not used and could be removed.

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

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

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

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

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

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