Completed
Pull Request — master (#684)
by Stig
11:09 queued 05:06
created

DNRoot::getPlatformSpecificStrings()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
152
		self::ACTION_DEPLOY,
153
		self::ACTION_SNAPSHOT,
154
		self::PROJECT_OVERVIEW
155
	];
156
157
	/**
158
	 * Include requirements that deploynaut needs, such as javascript.
159
	 */
160
	public static function include_requirements() {
161
162
		// JS should always go to the bottom, otherwise there's the risk that Requirements
163
		// puts them halfway through the page to the nearest <script> tag. We don't want that.
164
		Requirements::set_force_js_to_bottom(true);
165
166
		// todo these should be bundled into the same JS as the others in "static" below.
167
		// We've deliberately not used combined_files as it can mess with some of the JS used
168
		// here and cause sporadic errors.
169
		Requirements::javascript('deploynaut/javascript/jquery.js');
170
		Requirements::javascript('deploynaut/javascript/bootstrap.js');
171
		Requirements::javascript('deploynaut/javascript/q.js');
172
		Requirements::javascript('deploynaut/javascript/tablefilter.js');
173
		Requirements::javascript('deploynaut/javascript/deploynaut.js');
174
175
		Requirements::javascript('deploynaut/javascript/bootstrap.file-input.js');
176
		Requirements::javascript('deploynaut/thirdparty/select2/dist/js/select2.min.js');
177
		Requirements::javascript('deploynaut/javascript/selectize.js');
178
		Requirements::javascript('deploynaut/thirdparty/bootstrap-switch/dist/js/bootstrap-switch.min.js');
179
		Requirements::javascript('deploynaut/javascript/material.js');
180
181
		// Load the buildable dependencies only if not loaded centrally.
182
		if (!is_dir(BASE_PATH . DIRECTORY_SEPARATOR . 'static')) {
183
			if (\Director::isDev()) {
184
				\Requirements::javascript('deploynaut/static/bundle-debug.js');
185
			} else {
186
				\Requirements::javascript('deploynaut/static/bundle.js');
187
			}
188
		}
189
190
		Requirements::css('deploynaut/static/style.css');
191
	}
192
193
	/**
194
	 * Check for feature flags:
195
	 * - FLAG_SNAPSHOTS_ENABLED: set to true to enable globally
196
	 * - FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS: set to semicolon-separated list of email addresses of allowed users.
197
	 *
198
	 * @return boolean
199
	 */
200
	public static function FlagSnapshotsEnabled() {
201
		if (defined('FLAG_SNAPSHOTS_ENABLED') && FLAG_SNAPSHOTS_ENABLED) {
202
			return true;
203
		}
204
		if (defined('FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS') && FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS) {
205
			$allowedMembers = explode(';', FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS);
206
			$member = Member::currentUser();
207
			if ($allowedMembers && $member && in_array($member->Email, $allowedMembers)) {
208
				return true;
209
			}
210
		}
211
		return false;
212
	}
213
214
	/**
215
	 * @return ArrayList
216
	 */
217
	public static function get_support_links() {
218
		$supportLinks = self::config()->support_links;
219
		if ($supportLinks) {
220
			return new ArrayList($supportLinks);
221
		}
222
	}
223
224
	/**
225
	 * @return array
226
	 */
227
	public static function get_template_global_variables() {
228
		return [
229
			'RedisUnavailable' => 'RedisUnavailable',
230
			'RedisWorkersCount' => 'RedisWorkersCount',
231
			'SidebarLinks' => 'SidebarLinks',
232
			"SupportLinks" => 'get_support_links'
233
		];
234
	}
235
236
	/**
237
	 */
238
	public function init() {
239
		parent::init();
240
241
		if (!Member::currentUser() && !Session::get('AutoLoginHash')) {
242
			return Security::permissionFailure();
243
		}
244
245
		// Block framework jquery
246
		Requirements::block(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
247
248
		self::include_requirements();
249
	}
250
251
	/**
252
	 * @return string
253
	 */
254
	public function Link() {
255
		return "naut/";
256
	}
257
258
	/**
259
	 * Actions
260
	 *
261
	 * @param SS_HTTPRequest $request
262
	 * @return \SS_HTTPResponse
263
	 */
264
	public function index(SS_HTTPRequest $request) {
265
		return $this->redirect($this->Link() . 'projects/');
266
	}
267
268
	/**
269
	 * Action
270
	 *
271
	 * @param SS_HTTPRequest $request
272
	 * @return string - HTML
273
	 */
274
	public function projects(SS_HTTPRequest $request) {
275
		// Performs canView permission check by limiting visible projects in DNProjectsList() call.
276
		return $this->customise([
277
			'Title' => 'Projects',
278
		])->render();
279
	}
280
281
	/**
282
	 * @param SS_HTTPRequest $request
283
	 * @return HTMLText
284
	 */
285
	public function nav(SS_HTTPRequest $request) {
286
		return $this->renderWith('Nav');
287
	}
288
289
	/**
290
	 * Return a link to the navigation template used for AJAX requests.
291
	 * @return string
292
	 */
293
	public function NavLink() {
294
		$currentProject = $this->getCurrentProject();
295
		$projectName = $currentProject ? $currentProject->Name : null;
296
		return Controller::join_links(Director::absoluteBaseURL(), 'naut', 'nav', $projectName);
297
	}
298
299
	/**
300
	 * Action
301
	 *
302
	 * @param SS_HTTPRequest $request
303
	 * @return SS_HTTPResponse - HTML
304
	 */
305
	public function snapshots(SS_HTTPRequest $request) {
306
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
307
		return $this->getCustomisedViewSection('SnapshotsSection', 'Data Snapshots');
308
	}
309
310
	/**
311
	 * Action
312
	 *
313
	 * @param SS_HTTPRequest $request
314
	 * @return string - HTML
315
	 */
316 View Code Duplication
	public function createsnapshot(SS_HTTPRequest $request) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

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

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

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

Loading history...
343
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
344
345
		// Performs canView permission check by limiting visible projects
346
		$project = $this->getCurrentProject();
347
		if (!$project) {
348
			return $this->project404Response();
349
		}
350
351
		if (!$project->canUploadArchive()) {
352
			return new SS_HTTPResponse("Not allowed to upload", 401);
353
		}
354
355
		return $this->customise([
356
			'SnapshotsSection' => 1,
357
			'UploadSnapshotForm' => $this->getUploadSnapshotForm($request),
358
			'PostSnapshotForm' => $this->getPostSnapshotForm($request)
359
		])->render();
360
	}
361
362
	/**
363
	 * Return the upload limit for snapshot uploads
364
	 * @return string
365
	 */
366
	public function UploadLimit() {
367
		return File::format_size(min(
368
			File::ini2bytes(ini_get('upload_max_filesize')),
369
			File::ini2bytes(ini_get('post_max_size'))
370
		));
371
	}
372
373
	/**
374
	 * Construct the upload form.
375
	 *
376
	 * @param SS_HTTPRequest $request
377
	 * @return Form
378
	 */
379
	public function getUploadSnapshotForm(SS_HTTPRequest $request) {
380
		// Performs canView permission check by limiting visible projects
381
		$project = $this->getCurrentProject();
382
		if (!$project) {
383
			return $this->project404Response();
384
		}
385
386
		if (!$project->canUploadArchive()) {
387
			return new SS_HTTPResponse("Not allowed to upload", 401);
388
		}
389
390
		// Framing an environment as a "group of people with download access"
391
		// makes more sense to the user here, while still allowing us to enforce
392
		// environment specific restrictions on downloading the file later on.
393
		$envs = $project->DNEnvironmentList()->filterByCallback(function ($item) {
394
			return $item->canUploadArchive();
395
		});
396
		$envsMap = [];
397
		foreach ($envs as $env) {
398
			$envsMap[$env->ID] = $env->Name;
399
		}
400
401
		$maxSize = min(File::ini2bytes(ini_get('upload_max_filesize')), File::ini2bytes(ini_get('post_max_size')));
402
		$fileField = DataArchiveFileField::create('ArchiveFile', 'File');
403
		$fileField->getValidator()->setAllowedExtensions(['sspak']);
404
		$fileField->getValidator()->setAllowedMaxFileSize(['*' => $maxSize]);
405
406
		$form = Form::create(
407
			$this,
408
			'UploadSnapshotForm',
409
			FieldList::create(
410
				$fileField,
411
				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
412
				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
413
					->setEmptyString('Select an environment')
414
			),
415
			FieldList::create(
416
				FormAction::create('doUploadSnapshot', 'Upload File')
417
					->addExtraClass('btn')
418
			),
419
			RequiredFields::create('ArchiveFile')
420
		);
421
422
		$form->disableSecurityToken();
423
		$form->addExtraClass('fields-wide');
424
		// Tweak the action so it plays well with our fake URL structure.
425
		$form->setFormAction($project->Link() . '/UploadSnapshotForm');
426
427
		return $form;
428
	}
429
430
	/**
431
	 * @param array $data
432
	 * @param Form $form
433
	 *
434
	 * @return bool|HTMLText|SS_HTTPResponse
435
	 */
436
	public function doUploadSnapshot($data, Form $form) {
437
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
438
439
		// Performs canView permission check by limiting visible projects
440
		$project = $this->getCurrentProject();
441
		if (!$project) {
442
			return $this->project404Response();
443
		}
444
445
		$validEnvs = $project->DNEnvironmentList()
446
			->filterByCallback(function ($item) {
447
				return $item->canUploadArchive();
448
			});
449
450
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
451
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
452
		if (!$environment) {
453
			throw new LogicException('Invalid environment');
454
		}
455
456
		$this->validateSnapshotMode($data['Mode']);
457
458
		$dataArchive = DNDataArchive::create([
459
			'AuthorID' => Member::currentUserID(),
460
			'EnvironmentID' => $data['EnvironmentID'],
461
			'IsManualUpload' => true,
462
		]);
463
		// needs an ID and transfer to determine upload path
464
		$dataArchive->write();
465
		$dataTransfer = DNDataTransfer::create([
466
			'AuthorID' => Member::currentUserID(),
467
			'Mode' => $data['Mode'],
468
			'Origin' => 'ManualUpload',
469
			'EnvironmentID' => $data['EnvironmentID']
470
		]);
471
		$dataTransfer->write();
472
		$dataArchive->DataTransfers()->add($dataTransfer);
473
		$form->saveInto($dataArchive);
474
		$dataArchive->write();
475
		$workingDir = TEMP_FOLDER . DIRECTORY_SEPARATOR . 'deploynaut-transfer-' . $dataTransfer->ID;
476
477 View Code Duplication
		$cleanupFn = function () use ($workingDir, $dataTransfer, $dataArchive) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
478
			$process = new AbortableProcess(sprintf('rm -rf %s', escapeshellarg($workingDir)));
479
			$process->setTimeout(120);
480
			$process->run();
481
			$dataTransfer->delete();
482
			$dataArchive->delete();
483
		};
484
485
		// extract the sspak contents so we can inspect them
486
		try {
487
			$dataArchive->extractArchive($workingDir);
488
		} catch (Exception $e) {
489
			$cleanupFn();
490
			$form->sessionMessage(
491
				'There was a problem trying to open your snapshot for processing. Please try uploading again',
492
				'bad'
493
			);
494
			return $this->redirectBack();
495
		}
496
497
		// validate that the sspak contents match the declared contents
498
		$result = $dataArchive->validateArchiveContents();
499
		if (!$result->valid()) {
500
			$cleanupFn();
501
			$form->sessionMessage($result->message(), 'bad');
502
			return $this->redirectBack();
503
		}
504
505
		// fix file permissions of extracted sspak files then re-build the sspak
506
		try {
507
			$dataArchive->fixArchivePermissions($workingDir);
508
			$dataArchive->setArchiveFromFiles($workingDir);
509
		} catch (Exception $e) {
510
			$cleanupFn();
511
			$form->sessionMessage(
512
				'There was a problem processing your snapshot. Please try uploading again',
513
				'bad'
514
			);
515
			return $this->redirectBack();
516
		}
517
518
		// cleanup any extracted sspak contents lying around
519
		$process = new AbortableProcess(sprintf('rm -rf %s', escapeshellarg($workingDir)));
520
		$process->setTimeout(120);
521
		$process->run();
522
523
		return $this->customise([
524
			'Project' => $project,
525
			'CurrentProject' => $project,
526
			'SnapshotsSection' => 1,
527
			'DataArchive' => $dataArchive,
528
			'DataTransferRestoreForm' => $this->getDataTransferRestoreForm($this->request, $dataArchive),
529
			'BackURL' => $project->Link('snapshots')
530
		])->renderWith(['DNRoot_uploadsnapshot', 'DNRoot']);
531
	}
532
533
	/**
534
	 * @param SS_HTTPRequest $request
535
	 * @return Form
536
	 */
537
	public function getPostSnapshotForm(SS_HTTPRequest $request) {
538
		// Performs canView permission check by limiting visible projects
539
		$project = $this->getCurrentProject();
540
		if (!$project) {
541
			return $this->project404Response();
542
		}
543
544
		if (!$project->canUploadArchive()) {
545
			return new SS_HTTPResponse("Not allowed to upload", 401);
546
		}
547
548
		// Framing an environment as a "group of people with download access"
549
		// makes more sense to the user here, while still allowing us to enforce
550
		// environment specific restrictions on downloading the file later on.
551
		$envs = $project->DNEnvironmentList()->filterByCallback(function ($item) {
552
			return $item->canUploadArchive();
553
		});
554
		$envsMap = [];
555
		foreach ($envs as $env) {
556
			$envsMap[$env->ID] = $env->Name;
557
		}
558
559
		$form = Form::create(
560
			$this,
561
			'PostSnapshotForm',
562
			FieldList::create(
563
				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
564
				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
565
					->setEmptyString('Select an environment')
566
			),
567
			FieldList::create(
568
				FormAction::create('doPostSnapshot', 'Submit request')
569
					->addExtraClass('btn')
570
			),
571
			RequiredFields::create('File')
572
		);
573
574
		$form->disableSecurityToken();
575
		$form->addExtraClass('fields-wide');
576
		// Tweak the action so it plays well with our fake URL structure.
577
		$form->setFormAction($project->Link() . '/PostSnapshotForm');
578
579
		return $form;
580
	}
581
582
	/**
583
	 * @param array $data
584
	 * @param Form $form
585
	 *
586
	 * @return SS_HTTPResponse
587
	 */
588
	public function doPostSnapshot($data, $form) {
589
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
590
591
		$project = $this->getCurrentProject();
592
		if (!$project) {
593
			return $this->project404Response();
594
		}
595
596
		$validEnvs = $project->DNEnvironmentList()->filterByCallback(function ($item) {
597
			return $item->canUploadArchive();
598
		});
599
600
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
601
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
602
		if (!$environment) {
603
			throw new LogicException('Invalid environment');
604
		}
605
606
		$dataArchive = DNDataArchive::create([
607
			'UploadToken' => DNDataArchive::generate_upload_token(),
608
		]);
609
		$form->saveInto($dataArchive);
610
		$dataArchive->write();
611
612
		return $this->redirect(Controller::join_links(
613
			$project->Link(),
614
			'postsnapshotsuccess',
615
			$dataArchive->ID
616
		));
617
	}
618
619
	/**
620
	 * Action
621
	 *
622
	 * @param SS_HTTPRequest $request
623
	 * @return SS_HTTPResponse - HTML
624
	 */
625
	public function snapshotslog(SS_HTTPRequest $request) {
626
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
627
		return $this->getCustomisedViewSection('SnapshotsSection', 'Snapshots log');
628
	}
629
630
	/**
631
	 * @param SS_HTTPRequest $request
632
	 * @return SS_HTTPResponse|string
633
	 * @throws SS_HTTPResponse_Exception
634
	 */
635
	public function postsnapshotsuccess(SS_HTTPRequest $request) {
636
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
637
638
		// Performs canView permission check by limiting visible projects
639
		$project = $this->getCurrentProject();
640
		if (!$project) {
641
			return $this->project404Response();
642
		}
643
644
		if (!$project->canUploadArchive()) {
645
			return new SS_HTTPResponse("Not allowed to upload", 401);
646
		}
647
648
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
649
		if (!$dataArchive) {
650
			return new SS_HTTPResponse("Archive not found.", 404);
651
		}
652
653
		if (!$dataArchive->canRestore()) {
654
			throw new SS_HTTPResponse_Exception('Not allowed to restore archive', 403);
655
		}
656
657
		return $this->render([
658
			'Title' => 'How to send us your Data Snapshot by post',
659
			'DataArchive' => $dataArchive,
660
			'Address' => Config::inst()->get('Deploynaut', 'snapshot_post_address'),
661
			'BackURL' => $project->Link(),
662
		]);
663
	}
664
665
	/**
666
	 * @param SS_HTTPRequest $request
667
	 * @return \SS_HTTPResponse
668
	 */
669
	public function project(SS_HTTPRequest $request) {
670
		$this->setCurrentActionType(self::PROJECT_OVERVIEW);
671
		return $this->getCustomisedViewSection('ProjectOverview', '', ['IsAdmin' => Permission::check('ADMIN')]);
672
	}
673
674
	/**
675
	 * This action will star / unstar a project for the current member
676
	 *
677
	 * @param SS_HTTPRequest $request
678
	 *
679
	 * @return SS_HTTPResponse
680
	 */
681
	public function toggleprojectstar(SS_HTTPRequest $request) {
682
		$project = $this->getCurrentProject();
683
		if (!$project) {
684
			return $this->project404Response();
685
		}
686
687
		$member = Member::currentUser();
688
		if ($member === null) {
689
			return $this->project404Response();
690
		}
691
		$favProject = $member->StarredProjects()
692
			->filter('DNProjectID', $project->ID)
693
			->first();
694
695
		if ($favProject) {
696
			$member->StarredProjects()->remove($favProject);
697
		} else {
698
			$member->StarredProjects()->add($project);
699
		}
700
		return $this->redirectBack();
701
	}
702
703
	/**
704
	 * @param SS_HTTPRequest $request
705
	 * @return \SS_HTTPResponse
706
	 */
707
	public function branch(SS_HTTPRequest $request) {
708
		$project = $this->getCurrentProject();
709
		if (!$project) {
710
			return $this->project404Response();
711
		}
712
713
		$branchName = $request->getVar('name');
714
		$branch = $project->DNBranchList()->byName($branchName);
715
		if (!$branch) {
716
			return new SS_HTTPResponse("Branch '" . Convert::raw2xml($branchName) . "' not found.", 404);
717
		}
718
719
		return $this->render([
720
			'CurrentBranch' => $branch,
721
		]);
722
	}
723
724
	/**
725
	 * @param SS_HTTPRequest $request
726
	 * @return \SS_HTTPResponse
727
	 */
728
	public function environment(SS_HTTPRequest $request) {
729
		// Performs canView permission check by limiting visible projects
730
		$project = $this->getCurrentProject();
731
		if (!$project) {
732
			return $this->project404Response();
733
		}
734
735
		// Performs canView permission check by limiting visible projects
736
		$env = $this->getCurrentEnvironment($project);
737
		if (!$env) {
738
			return $this->environment404Response();
739
		}
740
741
		return $this->render([
742
			'DNEnvironmentList' => $this->getCurrentProject()->DNEnvironmentList(),
743
			'FlagSnapshotsEnabled' => $this->FlagSnapshotsEnabled(),
744
			'Redeploy' => (bool) $request->getVar('redeploy')
745
		]);
746
	}
747
748
	/**
749
	 * Shows the creation log.
750
	 *
751
	 * @param SS_HTTPRequest $request
752
	 * @return string
753
	 */
754
	public function createenv(SS_HTTPRequest $request) {
755
		$params = $request->params();
756
		if ($params['Identifier']) {
757
			$record = DNCreateEnvironment::get()->byId($params['Identifier']);
758
759
			if (!$record || !$record->ID) {
760
				throw new SS_HTTPResponse_Exception('Create environment not found', 404);
761
			}
762
			if (!$record->canView()) {
763
				return Security::permissionFailure();
764
			}
765
766
			$project = $this->getCurrentProject();
767
			if (!$project) {
768
				return $this->project404Response();
769
			}
770
771
			if ($project->Name != $params['Project']) {
772
				throw new LogicException("Project in URL doesn't match this creation");
773
			}
774
775
			return $this->render([
776
				'CreateEnvironment' => $record,
777
			]);
778
		}
779
		return $this->render(['CurrentTitle' => 'Create an environment']);
780
	}
781
782
	public function createenvlog(SS_HTTPRequest $request) {
783
		$params = $request->params();
784
		$env = DNCreateEnvironment::get()->byId($params['Identifier']);
785
786
		if (!$env || !$env->ID) {
787
			throw new SS_HTTPResponse_Exception('Log not found', 404);
788
		}
789
		if (!$env->canView()) {
790
			return Security::permissionFailure();
791
		}
792
793
		$project = $env->Project();
794
795
		if ($project->Name != $params['Project']) {
796
			throw new LogicException("Project in URL doesn't match this deploy");
797
		}
798
799
		$log = $env->log();
800
		if ($log->exists()) {
801
			$content = $log->content();
802
		} else {
803
			$content = 'Waiting for action to start';
804
		}
805
806
		return $this->sendResponse($env->ResqueStatus(), $content);
807
	}
808
809
	/**
810
	 * @param SS_HTTPRequest $request
811
	 * @return Form
812
	 */
813
	public function getCreateEnvironmentForm(SS_HTTPRequest $request) {
814
		$this->setCurrentActionType(self::ACTION_ENVIRONMENTS);
815
816
		$project = $this->getCurrentProject();
817
		if (!$project) {
818
			return $this->project404Response();
819
		}
820
821
		$envType = $project->AllowedEnvironmentType;
0 ignored issues
show
Documentation introduced by
The property AllowedEnvironmentType does not exist on object<DNProject>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
822
		if (!$envType || !class_exists($envType)) {
823
			return null;
824
		}
825
826
		$backend = Injector::inst()->get($envType);
827
		if (!($backend instanceof EnvironmentCreateBackend)) {
828
			// Only allow this for supported backends.
829
			return null;
830
		}
831
832
		$fields = $backend->getCreateEnvironmentFields($project);
833
		if (!$fields) {
834
			return null;
835
		}
836
837
		if (!$project->canCreateEnvironments()) {
838
			return new SS_HTTPResponse('Not allowed to create environments for this project', 401);
839
		}
840
841
		$form = Form::create(
842
			$this,
843
			'CreateEnvironmentForm',
844
			$fields,
845
			FieldList::create(
846
				FormAction::create('doCreateEnvironment', 'Create')
847
					->addExtraClass('btn')
848
			),
849
			$backend->getCreateEnvironmentValidator()
850
		);
851
852
		// Tweak the action so it plays well with our fake URL structure.
853
		$form->setFormAction($project->Link() . '/CreateEnvironmentForm');
854
855
		return $form;
856
	}
857
858
	/**
859
	 * @param array $data
860
	 * @param Form $form
861
	 *
862
	 * @return bool|HTMLText|SS_HTTPResponse
863
	 */
864
	public function doCreateEnvironment($data, Form $form) {
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

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

Loading history...
865
		$this->setCurrentActionType(self::ACTION_ENVIRONMENTS);
866
867
		$project = $this->getCurrentProject();
868
		if (!$project) {
869
			return $this->project404Response();
870
		}
871
872
		if (!$project->canCreateEnvironments()) {
873
			return new SS_HTTPResponse('Not allowed to create environments for this project', 401);
874
		}
875
876
		// Set the environment type so we know what we're creating.
877
		$data['EnvironmentType'] = $project->AllowedEnvironmentType;
0 ignored issues
show
Documentation introduced by
The property AllowedEnvironmentType does not exist on object<DNProject>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
878
879
		$job = DNCreateEnvironment::create();
880
881
		$job->Data = serialize($data);
882
		$job->ProjectID = $project->ID;
883
		$job->write();
884
		$job->start();
885
886
		return $this->redirect($project->Link('createenv') . '/' . $job->ID);
887
	}
888
889
	/**
890
	 *
891
	 * @param SS_HTTPRequest $request
892
	 * @return \SS_HTTPResponse
893
	 */
894
	public function metrics(SS_HTTPRequest $request) {
895
		// Performs canView permission check by limiting visible projects
896
		$project = $this->getCurrentProject();
897
		if (!$project) {
898
			return $this->project404Response();
899
		}
900
901
		// Performs canView permission check by limiting visible projects
902
		$env = $this->getCurrentEnvironment($project);
903
		if (!$env) {
904
			return $this->environment404Response();
905
		}
906
907
		return $this->render();
908
	}
909
910
	/**
911
	 * Get the DNData object.
912
	 *
913
	 * @return DNData
914
	 */
915
	public function DNData() {
916
		return DNData::inst();
917
	}
918
919
	/**
920
	 * Provide a list of all projects.
921
	 *
922
	 * @return SS_List
923
	 */
924
	public function DNProjectList() {
925
		$memberId = Member::currentUserID();
926
		if (!$memberId) {
927
			return new ArrayList();
928
		}
929
930
		if (Permission::check('ADMIN')) {
931
			return DNProject::get();
932
		}
933
934
		$projects = Member::get()->filter('ID', $memberId)
935
			->relation('Groups')
936
			->relation('Projects');
937
938
		$this->extend('updateDNProjectList', $projects);
939
		return $projects;
940
	}
941
942
	/**
943
	 * @return ArrayList
944
	 */
945
	public function getPlatformSpecificStrings() {
946
		$strings = $this->config()->platform_specific_strings;
947
		if ($strings) {
948
			return new ArrayList($strings);
949
		}
950
	}
951
952
	/**
953
	 * Provide a list of all starred projects for the currently logged in member
954
	 *
955
	 * @return SS_List
956
	 */
957
	public function getStarredProjects() {
958
		$member = Member::currentUser();
959
		if ($member === null) {
960
			return new ArrayList();
961
		}
962
963
		$favProjects = $member->StarredProjects();
964
965
		$list = new ArrayList();
966
		foreach ($favProjects as $project) {
967
			if ($project->canView($member)) {
968
				$list->add($project);
969
			}
970
		}
971
		return $list;
972
	}
973
974
	/**
975
	 * Returns top level navigation of projects.
976
	 *
977
	 * @param int $limit
978
	 *
979
	 * @return ArrayList
980
	 */
981
	public function Navigation($limit = 5) {
982
		$navigation = new ArrayList();
983
984
		$currentProject = $this->getCurrentProject();
985
		$currentEnvironment = $this->getCurrentEnvironment();
986
		$actionType = $this->getCurrentActionType();
987
988
		$projects = $this->getStarredProjects();
989
		if ($projects->count() < 1) {
990
			$projects = $this->DNProjectList();
991
		} else {
992
			$limit = -1;
993
		}
994
995
		if ($projects->count() > 0) {
996
			$activeProject = false;
997
998
			if ($limit > 0) {
999
				$limitedProjects = $projects->limit($limit);
1000
			} else {
1001
				$limitedProjects = $projects;
1002
			}
1003
1004
			foreach ($limitedProjects as $project) {
1005
				$isActive = $currentProject && $currentProject->ID == $project->ID;
1006
				if ($isActive) {
1007
					$activeProject = true;
1008
				}
1009
1010
				$isCurrentEnvironment = false;
1011
				if ($project && $currentEnvironment) {
1012
					$isCurrentEnvironment = (bool) $project->DNEnvironmentList()->find('ID', $currentEnvironment->ID);
1013
				}
1014
1015
				$navigation->push([
1016
					'Project' => $project,
1017
					'IsCurrentEnvironment' => $isCurrentEnvironment,
1018
					'IsActive' => $currentProject && $currentProject->ID == $project->ID,
1019
					'IsOverview' => $actionType == self::PROJECT_OVERVIEW && $currentProject->ID == $project->ID
1020
				]);
1021
			}
1022
1023
			// Ensure the current project is in the list
1024
			if (!$activeProject && $currentProject) {
1025
				$navigation->unshift([
1026
					'Project' => $currentProject,
1027
					'IsActive' => true,
1028
					'IsCurrentEnvironment' => $currentEnvironment,
1029
					'IsOverview' => $actionType == self::PROJECT_OVERVIEW
1030
				]);
1031
				if ($limit > 0 && $navigation->count() > $limit) {
1032
					$navigation->pop();
1033
				}
1034
			}
1035
		}
1036
1037
		return $navigation;
1038
	}
1039
1040
	/**
1041
	 * Construct the deployment form
1042
	 *
1043
	 * @deprecated 2.0.0 - moved to DeployDispatcher
1044
	 *
1045
	 * @return Form
1046
	 */
1047
	public function getDeployForm($request = null) {
1048
1049
		// Performs canView permission check by limiting visible projects
1050
		$project = $this->getCurrentProject();
1051
		if (!$project) {
1052
			return $this->project404Response();
1053
		}
1054
1055
		// Performs canView permission check by limiting visible projects
1056
		$environment = $this->getCurrentEnvironment($project);
1057
		if (!$environment) {
1058
			return $this->environment404Response();
1059
		}
1060
1061
		if (!$environment->canDeploy()) {
1062
			return new SS_HTTPResponse("Not allowed to deploy", 401);
1063
		}
1064
1065
		// Generate the form
1066
		$form = new DeployForm($this, 'DeployForm', $environment, $project);
0 ignored issues
show
Deprecated Code introduced by
The class DeployForm has been deprecated with message: 2.0.0 - moved to Dispatchers and frontend Form for generating deployments from a specified commit

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

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

Loading history...
1067
1068
		// If this is an ajax request we don't want to submit the form - we just want to retrieve the markup.
1069
		if (
1070
			$request &&
1071
			!$request->requestVar('action_showDeploySummary') &&
1072
			$this->getRequest()->isAjax() &&
1073
			$this->getRequest()->isGET()
1074
		) {
1075
			// We can just use the URL we're accessing
1076
			$form->setFormAction($this->getRequest()->getURL());
1077
1078
			$body = json_encode(['Content' => $form->forAjaxTemplate()->forTemplate()]);
1079
			$this->getResponse()->addHeader('Content-Type', 'application/json');
1080
			$this->getResponse()->setBody($body);
1081
			return $body;
1082
		}
1083
1084
		$form->setFormAction($this->getRequest()->getURL() . '/DeployForm');
1085
		return $form;
1086
	}
1087
1088
	/**
1089
	 * @deprecated 2.0.0 - moved to GitDispatcher
1090
	 *
1091
	 * @param SS_HTTPRequest $request
1092
	 *
1093
	 * @return SS_HTTPResponse|string
1094
	 */
1095
	public function gitRevisions(SS_HTTPRequest $request) {
1096
1097
		// Performs canView permission check by limiting visible projects
1098
		$project = $this->getCurrentProject();
1099
		if (!$project) {
1100
			return $this->project404Response();
1101
		}
1102
1103
		// Performs canView permission check by limiting visible projects
1104
		$env = $this->getCurrentEnvironment($project);
1105
		if (!$env) {
1106
			return $this->environment404Response();
1107
		}
1108
1109
		$options = [];
1110
		foreach ($env->Backend()->getDeployOptions($env) as $option) {
1111
			$options[] = [
1112
				'name' => $option->getName(),
1113
				'title' => $option->getTitle(),
1114
				'defaultValue' => $option->getDefaultValue()
1115
			];
1116
		}
1117
1118
		$tabs = [];
1119
		$id = 0;
1120
		$data = [
1121
			'id' => ++$id,
1122
			'name' => 'Deploy the latest version of a branch',
1123
			'field_type' => 'dropdown',
1124
			'field_label' => 'Choose a branch',
1125
			'field_id' => 'branch',
1126
			'field_data' => [],
1127
			'options' => $options
1128
		];
1129
		foreach ($project->DNBranchList() as $branch) {
1130
			$sha = $branch->SHA();
1131
			$name = $branch->Name();
1132
			$branchValue = sprintf("%s (%s, %s old)",
1133
				$name,
1134
				substr($sha, 0, 8),
1135
				$branch->LastUpdated()->TimeDiff()
1136
			);
1137
			$data['field_data'][] = [
1138
				'id' => $sha,
1139
				'text' => $branchValue,
1140
				'branch_name' => $name // the raw branch name, not including the time etc
1141
			];
1142
		}
1143
		$tabs[] = $data;
1144
1145
		$data = [
1146
			'id' => ++$id,
1147
			'name' => 'Deploy a tagged release',
1148
			'field_type' => 'dropdown',
1149
			'field_label' => 'Choose a tag',
1150
			'field_id' => 'tag',
1151
			'field_data' => [],
1152
			'options' => $options
1153
		];
1154
1155
		foreach ($project->DNTagList()->setLimit(null) as $tag) {
1156
			$name = $tag->Name();
1157
			$data['field_data'][] = [
1158
				'id' => $tag->SHA(),
1159
				'text' => sprintf("%s", $name)
1160
			];
1161
		}
1162
1163
		// show newest tags first.
1164
		$data['field_data'] = array_reverse($data['field_data']);
1165
1166
		$tabs[] = $data;
1167
1168
		// Past deployments
1169
		$data = [
1170
			'id' => ++$id,
1171
			'name' => 'Redeploy a release that was previously deployed (to any environment)',
1172
			'field_type' => 'dropdown',
1173
			'field_label' => 'Choose a previously deployed release',
1174
			'field_id' => 'release',
1175
			'field_data' => [],
1176
			'options' => $options
1177
		];
1178
		// We are aiming at the format:
1179
		// [{text: 'optgroup text', children: [{id: '<sha>', text: '<inner text>'}]}]
0 ignored issues
show
Unused Code Comprehensibility introduced by
55% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
1180
		$redeploy = [];
1181 View Code Duplication
		foreach ($project->DNEnvironmentList() as $dnEnvironment) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1182
			$envName = $dnEnvironment->Name;
1183
			$perEnvDeploys = [];
1184
1185
			foreach ($dnEnvironment->DeployHistory() as $deploy) {
1186
				$sha = $deploy->SHA;
1187
1188
				// Check if exists to make sure the newest deployment date is used.
1189
				if (!isset($perEnvDeploys[$sha])) {
1190
					$pastValue = sprintf("%s (deployed %s)",
1191
						substr($sha, 0, 8),
1192
						$deploy->obj('LastEdited')->Ago()
1193
					);
1194
					$perEnvDeploys[$sha] = [
1195
						'id' => $sha,
1196
						'text' => $pastValue
1197
					];
1198
				}
1199
			}
1200
1201
			if (!empty($perEnvDeploys)) {
1202
				$redeploy[$envName] = array_values($perEnvDeploys);
1203
			}
1204
		}
1205
		// Convert the array to the frontend format (i.e. keyed to regular array)
1206
		foreach ($redeploy as $name => $descr) {
1207
			$data['field_data'][] = ['text' => $name, 'children' => $descr];
1208
		}
1209
		$tabs[] = $data;
1210
1211
		$data = [
1212
			'id' => ++$id,
1213
			'name' => 'Deploy a specific SHA',
1214
			'field_type' => 'textfield',
1215
			'field_label' => 'Choose a SHA',
1216
			'field_id' => 'SHA',
1217
			'field_data' => [],
1218
			'options' => $options
1219
		];
1220
		$tabs[] = $data;
1221
1222
		// get the last time git fetch was run
1223
		$lastFetched = 'never';
1224
		$fetch = DNGitFetch::get()
1225
			->filter('ProjectID', $project->ID)
1226
			->sort('LastEdited', 'DESC')
1227
			->first();
1228
		if ($fetch) {
1229
			$lastFetched = $fetch->dbObject('LastEdited')->Ago();
1230
		}
1231
1232
		$data = [
1233
			'Tabs' => $tabs,
1234
			'last_fetched' => $lastFetched
1235
		];
1236
1237
		$this->applyRedeploy($request, $data);
1238
1239
		return json_encode($data, JSON_PRETTY_PRINT);
1240
	}
1241
1242
	/**
1243
	 * @deprecated 2.0.0 - moved to PlanDispatcher
1244
	 *
1245
	 * @param SS_HTTPRequest $request
1246
	 *
1247
	 * @return string
1248
	 */
1249
	public function deploySummary(SS_HTTPRequest $request) {
1250
1251
		// Performs canView permission check by limiting visible projects
1252
		$project = $this->getCurrentProject();
1253
		if (!$project) {
1254
			return $this->project404Response();
1255
		}
1256
1257
		// Performs canView permission check by limiting visible projects
1258
		$environment = $this->getCurrentEnvironment($project);
1259
		if (!$environment) {
1260
			return $this->environment404Response();
1261
		}
1262
1263
		// Plan the deployment.
1264
		$strategy = $environment->getDeployStrategy($request);
1265
		$data = $strategy->toArray();
1266
1267
		// Add in a URL for comparing from->to code changes. Ensure that we have
1268
		// two proper 40 character SHAs, otherwise we can't show the compare link.
1269
		$interface = $project->getRepositoryInterface();
1270
		if (
1271
			!empty($interface) && !empty($interface->URL)
1272
			&& !empty($data['changes']['Code version']['from'])
1273
			&& strlen($data['changes']['Code version']['from']) == '40'
1274
			&& !empty($data['changes']['Code version']['to'])
1275
			&& strlen($data['changes']['Code version']['to']) == '40'
1276
		) {
1277
			$compareurl = sprintf(
1278
				'%s/compare/%s...%s',
1279
				$interface->URL,
1280
				$data['changes']['Code version']['from'],
1281
				$data['changes']['Code version']['to']
1282
			);
1283
			$data['changes']['Code version']['compareUrl'] = $compareurl;
1284
		}
1285
1286
		// Append json to response
1287
		$token = SecurityToken::inst();
1288
		$data['SecurityID'] = $token->getValue();
1289
1290
		$this->extend('updateDeploySummary', $data);
1291
1292
		return json_encode($data);
1293
	}
1294
1295
	/**
1296
	 * Deployment form submission handler.
1297
	 *
1298
	 * @deprecated 2.0.0 - moved to DeployDispatcher
1299
	 *
1300
	 * Initiate a DNDeployment record and redirect to it for status polling
1301
	 *
1302
	 * @param SS_HTTPRequest $request
1303
	 *
1304
	 * @return SS_HTTPResponse
1305
	 * @throws ValidationException
1306
	 * @throws null
1307
	 */
1308
	public function startDeploy(SS_HTTPRequest $request) {
1309
1310
		$token = SecurityToken::inst();
1311
1312
		// Ensure the submitted token has a value
1313
		$submittedToken = $request->postVar(\Dispatcher::SECURITY_TOKEN_NAME);
1314
		if (!$submittedToken) {
1315
			return false;
1316
		}
1317
		// Do the actual check.
1318
		$check = $token->check($submittedToken);
1319
		// Ensure the CSRF Token is correct
1320
		if (!$check) {
1321
			// CSRF token didn't match
1322
			return $this->httpError(400, 'Bad Request');
1323
		}
1324
1325
		// Performs canView permission check by limiting visible projects
1326
		$project = $this->getCurrentProject();
1327
		if (!$project) {
1328
			return $this->project404Response();
1329
		}
1330
1331
		// Performs canView permission check by limiting visible projects
1332
		$environment = $this->getCurrentEnvironment($project);
1333
		if (!$environment) {
1334
			return $this->environment404Response();
1335
		}
1336
1337
		// Initiate the deployment
1338
		// The extension point should pass in: Project, Environment, SelectRelease, buildName
1339
		$this->extend('doDeploy', $project, $environment, $buildName, $data);
1340
1341
		// Start the deployment based on the approved strategy.
1342
		$strategy = new DeploymentStrategy($environment);
1343
		$strategy->fromArray($request->requestVar('strategy'));
1344
		$deployment = $strategy->createDeployment();
1345
		// Skip through the approval state for now.
1346
		$deployment->getMachine()->apply(DNDeployment::TR_SUBMIT);
1347
		$deployment->getMachine()->apply(DNDeployment::TR_QUEUE);
1348
1349
		return json_encode([
1350
			'url' => Director::absoluteBaseURL() . $deployment->Link()
1351
		], JSON_PRETTY_PRINT);
1352
	}
1353
1354
	/**
1355
	 * @deprecated 2.0.0 - moved to DeployDispatcher
1356
	 *
1357
	 * Action - Do the actual deploy
1358
	 *
1359
	 * @param SS_HTTPRequest $request
1360
	 *
1361
	 * @return SS_HTTPResponse|string
1362
	 * @throws SS_HTTPResponse_Exception
1363
	 */
1364
	public function deploy(SS_HTTPRequest $request) {
1365
		$params = $request->params();
1366
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1367
1368
		if (!$deployment || !$deployment->ID) {
1369
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1370
		}
1371
		if (!$deployment->canView()) {
1372
			return Security::permissionFailure();
1373
		}
1374
1375
		$environment = $deployment->Environment();
1376
		$project = $environment->Project();
1377
1378
		if ($environment->Name != $params['Environment']) {
1379
			throw new LogicException("Environment in URL doesn't match this deploy");
1380
		}
1381
		if ($project->Name != $params['Project']) {
1382
			throw new LogicException("Project in URL doesn't match this deploy");
1383
		}
1384
1385
		return $this->render([
1386
			'Deployment' => $deployment,
1387
		]);
1388
	}
1389
1390
	/**
1391
	 * @deprecated 2.0.0 - moved to DeployDispatcher
1392
	 *
1393
	 * Action - Get the latest deploy log
1394
	 *
1395
	 * @param SS_HTTPRequest $request
1396
	 *
1397
	 * @return string
1398
	 * @throws SS_HTTPResponse_Exception
1399
	 */
1400
	public function deploylog(SS_HTTPRequest $request) {
1401
		$params = $request->params();
1402
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1403
1404
		if (!$deployment || !$deployment->ID) {
1405
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1406
		}
1407
		if (!$deployment->canView()) {
1408
			return Security::permissionFailure();
1409
		}
1410
1411
		$environment = $deployment->Environment();
1412
		$project = $environment->Project();
1413
1414
		if ($environment->Name != $params['Environment']) {
1415
			throw new LogicException("Environment in URL doesn't match this deploy");
1416
		}
1417
		if ($project->Name != $params['Project']) {
1418
			throw new LogicException("Project in URL doesn't match this deploy");
1419
		}
1420
1421
		$log = $deployment->log();
1422
		if ($log->exists()) {
1423
			$content = $log->content();
1424
		} else {
1425
			$content = 'Waiting for action to start';
1426
		}
1427
1428
		return $this->sendResponse($deployment->ResqueStatus(), $content);
1429
	}
1430
1431
	public function abortDeploy(SS_HTTPRequest $request) {
1432
		$params = $request->params();
1433
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1434
1435
		if (!$deployment || !$deployment->ID) {
1436
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1437
		}
1438
		if (!$deployment->canView()) {
1439
			return Security::permissionFailure();
1440
		}
1441
1442
		// For now restrict to ADMINs only.
1443
		if (!Permission::check('ADMIN')) {
1444
			return Security::permissionFailure();
1445
		}
1446
1447
		$environment = $deployment->Environment();
1448
		$project = $environment->Project();
1449
1450
		if ($environment->Name != $params['Environment']) {
1451
			throw new LogicException("Environment in URL doesn't match this deploy");
1452
		}
1453
		if ($project->Name != $params['Project']) {
1454
			throw new LogicException("Project in URL doesn't match this deploy");
1455
		}
1456
1457
		if (!in_array($deployment->Status, ['Queued', 'Deploying', 'Aborting'])) {
1458
			throw new LogicException(sprintf("Cannot abort from %s state.", $deployment->Status));
1459
		}
1460
1461
		$deployment->getMachine()->apply(DNDeployment::TR_ABORT);
1462
1463
		return $this->sendResponse($deployment->ResqueStatus(), []);
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1464
	}
1465
1466
	/**
1467
	 * @param SS_HTTPRequest|null $request
1468
	 *
1469
	 * @return Form
1470
	 */
1471
	public function getDataTransferForm(SS_HTTPRequest $request = null) {
1472
		// Performs canView permission check by limiting visible projects
1473
		$envs = $this->getCurrentProject()->DNEnvironmentList()->filterByCallback(function ($item) {
1474
			return $item->canBackup();
1475
		});
1476
1477
		if (!$envs) {
1478
			return $this->environment404Response();
1479
		}
1480
1481
		$items = [];
1482
		$disabledEnvironments = [];
1483 View Code Duplication
		foreach($envs as $env) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1484
			$items[$env->ID] = $env->Title;
1485
			if ($env->CurrentBuild() === false) {
1486
				$items[$env->ID] = sprintf("%s - requires initial deployment", $env->Title);
1487
				$disabledEnvironments[] = $env->ID;
1488
			}
1489
		}
1490
1491
		$envsField =  DropdownField::create('EnvironmentID', 'Environment', $items)
1492
			->setEmptyString('Select an environment');
1493
		$envsField->setDisabledItems($disabledEnvironments);
1494
1495
		$formAction = FormAction::create('doDataTransfer', 'Create')
1496
			->addExtraClass('btn');
1497
1498
		if (count($disabledEnvironments) === $envs->count()) {
1499
			$formAction->setDisabled(true);
1500
		}
1501
1502
		$form = Form::create(
1503
			$this,
1504
			'DataTransferForm',
1505
			FieldList::create(
1506
				HiddenField::create('Direction', null, 'get'),
1507
				$envsField,
1508
				DropdownField::create('Mode', 'Transfer', DNDataArchive::get_mode_map())
1509
			),
1510
			FieldList::create($formAction)
1511
		);
1512
		$form->setFormAction($this->getRequest()->getURL() . '/DataTransferForm');
1513
1514
		return $form;
1515
	}
1516
1517
	/**
1518
	 * @param array $data
1519
	 * @param Form $form
1520
	 *
1521
	 * @return SS_HTTPResponse
1522
	 * @throws SS_HTTPResponse_Exception
1523
	 */
1524
	public function doDataTransfer($data, Form $form) {
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

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

Loading history...
1525
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1526
1527
		// Performs canView permission check by limiting visible projects
1528
		$project = $this->getCurrentProject();
1529
		if (!$project) {
1530
			return $this->project404Response();
1531
		}
1532
1533
		$dataArchive = null;
1534
1535
		// Validate direction.
1536
		if ($data['Direction'] == 'get') {
1537
			$validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1538
				->filterByCallback(function ($item) {
1539
					return $item->canBackup();
1540
				});
1541
		} else if ($data['Direction'] == 'push') {
1542
			$validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1543
				->filterByCallback(function ($item) {
1544
					return $item->canRestore();
1545
				});
1546
		} else {
1547
			throw new LogicException('Invalid direction');
1548
		}
1549
1550
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
1551
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
1552
		if (!$environment) {
1553
			throw new LogicException('Invalid environment');
1554
		}
1555
1556
		$this->validateSnapshotMode($data['Mode']);
1557
1558
		// Only 'push' direction is allowed an association with an existing archive.
1559
		if (
1560
			$data['Direction'] == 'push'
1561
			&& isset($data['DataArchiveID'])
1562
			&& is_numeric($data['DataArchiveID'])
1563
		) {
1564
			$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1565
			if (!$dataArchive) {
1566
				throw new LogicException('Invalid data archive');
1567
			}
1568
1569
			if (!$dataArchive->canDownload()) {
1570
				throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1571
			}
1572
		}
1573
1574
		$transfer = DNDataTransfer::create();
1575
		$transfer->EnvironmentID = $environment->ID;
1576
		$transfer->Direction = $data['Direction'];
1577
		$transfer->Mode = $data['Mode'];
1578
		$transfer->DataArchiveID = $dataArchive ? $dataArchive->ID : null;
1579
		if ($data['Direction'] == 'push') {
1580
			$transfer->setBackupBeforePush(!empty($data['BackupBeforePush']));
1581
		}
1582
		$transfer->write();
1583
		$transfer->start();
1584
1585
		return $this->redirect($transfer->Link());
1586
	}
1587
1588
	/**
1589
	 * View into the log for a {@link DNDataTransfer}.
1590
	 *
1591
	 * @param SS_HTTPRequest $request
1592
	 *
1593
	 * @return SS_HTTPResponse|string
1594
	 * @throws SS_HTTPResponse_Exception
1595
	 */
1596
	public function transfer(SS_HTTPRequest $request) {
1597
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1598
1599
		$params = $request->params();
1600
		$transfer = DNDataTransfer::get()->byId($params['Identifier']);
1601
1602
		if (!$transfer || !$transfer->ID) {
1603
			throw new SS_HTTPResponse_Exception('Transfer not found', 404);
1604
		}
1605
		if (!$transfer->canView()) {
1606
			return Security::permissionFailure();
1607
		}
1608
1609
		$environment = $transfer->Environment();
1610
		$project = $environment->Project();
1611
1612
		if ($project->Name != $params['Project']) {
1613
			throw new LogicException("Project in URL doesn't match this deploy");
1614
		}
1615
1616
		return $this->render([
1617
			'CurrentTransfer' => $transfer,
1618
			'SnapshotsSection' => 1,
1619
		]);
1620
	}
1621
1622
	/**
1623
	 * Action - Get the latest deploy log
1624
	 *
1625
	 * @param SS_HTTPRequest $request
1626
	 *
1627
	 * @return string
1628
	 * @throws SS_HTTPResponse_Exception
1629
	 */
1630
	public function transferlog(SS_HTTPRequest $request) {
1631
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1632
1633
		$params = $request->params();
1634
		$transfer = DNDataTransfer::get()->byId($params['Identifier']);
1635
1636
		if (!$transfer || !$transfer->ID) {
1637
			throw new SS_HTTPResponse_Exception('Transfer not found', 404);
1638
		}
1639
		if (!$transfer->canView()) {
1640
			return Security::permissionFailure();
1641
		}
1642
1643
		$environment = $transfer->Environment();
1644
		$project = $environment->Project();
1645
1646
		if ($project->Name != $params['Project']) {
1647
			throw new LogicException("Project in URL doesn't match this deploy");
1648
		}
1649
1650
		$log = $transfer->log();
1651
		if ($log->exists()) {
1652
			$content = $log->content();
1653
		} else {
1654
			$content = 'Waiting for action to start';
1655
		}
1656
1657
		return $this->sendResponse($transfer->ResqueStatus(), $content);
1658
	}
1659
1660
	/**
1661
	 * Note: Submits to the same action as {@link getDataTransferForm()},
1662
	 * but with a Direction=push and an archive reference.
1663
	 *
1664
	 * @param SS_HTTPRequest $request
1665
	 * @param DNDataArchive|null $dataArchive Only set when method is called manually in {@link restore()},
1666
	 *                            otherwise the state is inferred from the request data.
1667
	 * @return Form
1668
	 */
1669
	public function getDataTransferRestoreForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) {
1670
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1671
1672
		// Performs canView permission check by limiting visible projects
1673
		$project = $this->getCurrentProject();
1674
		$envs = $project->DNEnvironmentList()->filterByCallback(function ($item) {
1675
			return $item->canRestore();
1676
		});
1677
1678
		if (!$envs) {
1679
			return $this->environment404Response();
1680
		}
1681
1682
		$modesMap = [];
1683
		if (in_array($dataArchive->Mode, ['all'])) {
1684
			$modesMap['all'] = 'Database and Assets';
1685
		};
1686
		if (in_array($dataArchive->Mode, ['all', 'db'])) {
1687
			$modesMap['db'] = 'Database only';
1688
		};
1689
		if (in_array($dataArchive->Mode, ['all', 'assets'])) {
1690
			$modesMap['assets'] = 'Assets only';
1691
		};
1692
1693
		$alertMessage = '<div class="alert alert-warning"><strong>Warning:</strong> '
1694
			. 'This restore will overwrite the data on the chosen environment below</div>';
1695
1696
1697
		$items = [];
1698
		$disabledEnvironments = [];
1699 View Code Duplication
		foreach($envs as $env) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1700
			$items[$env->ID] = $env->Title;
1701
			if ($env->CurrentBuild() === false) {
1702
				$items[$env->ID] = sprintf("%s - requires initial deployment", $env->Title);
1703
				$disabledEnvironments[] = $env->ID;
1704
			}
1705
		}
1706
1707
		$envsField = DropdownField::create('EnvironmentID', 'Environment', $items)
1708
			->setEmptyString('Select an environment');
1709
		$formAction = FormAction::create('doDataTransfer', 'Restore Data')->addExtraClass('btn');
1710
1711
		if (count($disabledEnvironments) == $envs->count()) {
1712
			$formAction->setDisabled(true);
1713
		}
1714
1715
		$form = Form::create(
1716
			$this,
1717
			'DataTransferRestoreForm',
1718
			FieldList::create(
1719
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1720
				HiddenField::create('Direction', null, 'push'),
1721
				LiteralField::create('Warning', $alertMessage),
1722
				$envsField,
1723
				DropdownField::create('Mode', 'Transfer', $modesMap),
1724
				CheckboxField::create('BackupBeforePush', 'Backup existing data', '1')
1725
			),
1726
			FieldList::create($formAction)
1727
		);
1728
		$form->setFormAction($project->Link() . '/DataTransferRestoreForm');
1729
1730
		return $form;
1731
	}
1732
1733
	/**
1734
	 * View a form to restore a specific {@link DataArchive}.
1735
	 * Permission checks are handled in {@link DataArchives()}.
1736
	 * Submissions are handled through {@link doDataTransfer()}, same as backup operations.
1737
	 *
1738
	 * @param SS_HTTPRequest $request
1739
	 *
1740
	 * @return HTMLText
1741
	 * @throws SS_HTTPResponse_Exception
1742
	 */
1743 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...
1744
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1745
1746
		/** @var DNDataArchive $dataArchive */
1747
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1748
1749
		if (!$dataArchive) {
1750
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1751
		}
1752
1753
		// We check for canDownload because that implies access to the data.
1754
		// canRestore is later checked on the actual restore action per environment.
1755
		if (!$dataArchive->canDownload()) {
1756
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1757
		}
1758
1759
		$form = $this->getDataTransferRestoreForm($this->request, $dataArchive);
1760
1761
		// View currently only available via ajax
1762
		return $form->forTemplate();
1763
	}
1764
1765
	/**
1766
	 * View a form to delete a specific {@link DataArchive}.
1767
	 * Permission checks are handled in {@link DataArchives()}.
1768
	 * Submissions are handled through {@link doDelete()}.
1769
	 *
1770
	 * @param SS_HTTPRequest $request
1771
	 *
1772
	 * @return HTMLText
1773
	 * @throws SS_HTTPResponse_Exception
1774
	 */
1775 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...
1776
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1777
1778
		/** @var DNDataArchive $dataArchive */
1779
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1780
1781
		if (!$dataArchive) {
1782
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1783
		}
1784
1785
		if (!$dataArchive->canDelete()) {
1786
			throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403);
1787
		}
1788
1789
		$form = $this->getDeleteForm($this->request, $dataArchive);
1790
1791
		// View currently only available via ajax
1792
		return $form->forTemplate();
1793
	}
1794
1795
	/**
1796
	 * @param SS_HTTPRequest $request
1797
	 * @param DNDataArchive|null $dataArchive Only set when method is called manually, otherwise the state is inferred
1798
	 *        from the request data.
1799
	 * @return Form
1800
	 */
1801
	public function getDeleteForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) {
1802
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1803
1804
		// Performs canView permission check by limiting visible projects
1805
		$project = $this->getCurrentProject();
1806
		if (!$project) {
1807
			return $this->project404Response();
1808
		}
1809
1810
		$snapshotDeleteWarning = '<div class="alert alert-warning">'
1811
			. 'Are you sure you want to permanently delete this snapshot from this archive area?'
1812
			. '</div>';
1813
1814
		$form = Form::create(
1815
			$this,
1816
			'DeleteForm',
1817
			FieldList::create(
1818
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1819
				LiteralField::create('Warning', $snapshotDeleteWarning)
1820
			),
1821
			FieldList::create(
1822
				FormAction::create('doDelete', 'Delete')
1823
					->addExtraClass('btn')
1824
			)
1825
		);
1826
		$form->setFormAction($project->Link() . '/DeleteForm');
1827
1828
		return $form;
1829
	}
1830
1831
	/**
1832
	 * @param array $data
1833
	 * @param Form $form
1834
	 *
1835
	 * @return bool|SS_HTTPResponse
1836
	 * @throws SS_HTTPResponse_Exception
1837
	 */
1838
	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...
1839
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1840
1841
		// Performs canView permission check by limiting visible projects
1842
		$project = $this->getCurrentProject();
1843
		if (!$project) {
1844
			return $this->project404Response();
1845
		}
1846
1847
		$dataArchive = null;
1848
1849
		if (
1850
			isset($data['DataArchiveID'])
1851
			&& is_numeric($data['DataArchiveID'])
1852
		) {
1853
			$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1854
		}
1855
1856
		if (!$dataArchive) {
1857
			throw new LogicException('Invalid data archive');
1858
		}
1859
1860
		if (!$dataArchive->canDelete()) {
1861
			throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403);
1862
		}
1863
1864
		$dataArchive->delete();
1865
1866
		return $this->redirectBack();
1867
	}
1868
1869
	/**
1870
	 * View a form to move a specific {@link DataArchive}.
1871
	 *
1872
	 * @param SS_HTTPRequest $request
1873
	 *
1874
	 * @return HTMLText
1875
	 * @throws SS_HTTPResponse_Exception
1876
	 */
1877 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...
1878
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1879
1880
		/** @var DNDataArchive $dataArchive */
1881
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1882
1883
		if (!$dataArchive) {
1884
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1885
		}
1886
1887
		// We check for canDownload because that implies access to the data.
1888
		if (!$dataArchive->canDownload()) {
1889
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1890
		}
1891
1892
		$form = $this->getMoveForm($this->request, $dataArchive);
1893
1894
		// View currently only available via ajax
1895
		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...
1896
	}
1897
1898
	/**
1899
	 * Build snapshot move form.
1900
	 *
1901
	 * @param SS_HTTPRequest $request
1902
	 * @param DNDataArchive|null $dataArchive
1903
	 *
1904
	 * @return Form|SS_HTTPResponse
1905
	 */
1906
	public function getMoveForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) {
1907
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1908
1909
		$envs = $dataArchive->validTargetEnvironments();
1910
		if (!$envs) {
1911
			return $this->environment404Response();
1912
		}
1913
1914
		$warningMessage = '<div class="alert alert-warning"><strong>Warning:</strong> This will make the snapshot '
1915
			. 'available to people with access to the target environment.<br>By pressing "Change ownership" you '
1916
			. 'confirm that you have considered data confidentiality regulations.</div>';
1917
1918
		$form = Form::create(
1919
			$this,
1920
			'MoveForm',
1921
			FieldList::create(
1922
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1923
				LiteralField::create('Warning', $warningMessage),
1924
				DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1925
					->setEmptyString('Select an environment')
1926
			),
1927
			FieldList::create(
1928
				FormAction::create('doMove', 'Change ownership')
1929
					->addExtraClass('btn')
1930
			)
1931
		);
1932
		$form->setFormAction($this->getCurrentProject()->Link() . '/MoveForm');
1933
1934
		return $form;
1935
	}
1936
1937
	/**
1938
	 * @param array $data
1939
	 * @param Form $form
1940
	 *
1941
	 * @return bool|SS_HTTPResponse
1942
	 * @throws SS_HTTPResponse_Exception
1943
	 * @throws ValidationException
1944
	 * @throws null
1945
	 */
1946
	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...
1947
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1948
1949
		// Performs canView permission check by limiting visible projects
1950
		$project = $this->getCurrentProject();
1951
		if (!$project) {
1952
			return $this->project404Response();
1953
		}
1954
1955
		/** @var DNDataArchive $dataArchive */
1956
		$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1957
		if (!$dataArchive) {
1958
			throw new LogicException('Invalid data archive');
1959
		}
1960
1961
		// We check for canDownload because that implies access to the data.
1962
		if (!$dataArchive->canDownload()) {
1963
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1964
		}
1965
1966
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
1967
		$validEnvs = $dataArchive->validTargetEnvironments();
1968
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
1969
		if (!$environment) {
1970
			throw new LogicException('Invalid environment');
1971
		}
1972
1973
		$dataArchive->EnvironmentID = $environment->ID;
1974
		$dataArchive->write();
1975
1976
		return $this->redirectBack();
1977
	}
1978
1979
	/**
1980
	 * Returns an error message if redis is unavailable
1981
	 *
1982
	 * @return string
1983
	 */
1984
	public static function RedisUnavailable() {
1985
		try {
1986
			Resque::queues();
1987
		} catch (Exception $e) {
1988
			return $e->getMessage();
1989
		}
1990
		return '';
1991
	}
1992
1993
	/**
1994
	 * Returns the number of connected Redis workers
1995
	 *
1996
	 * @return int
1997
	 */
1998
	public static function RedisWorkersCount() {
1999
		return count(Resque_Worker::all());
2000
	}
2001
2002
	/**
2003
	 * @return array
2004
	 */
2005
	public function providePermissions() {
2006
		return [
2007
			self::DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS => [
2008
				'name' => "Access to advanced deploy options",
2009
				'category' => "Deploynaut",
2010
			],
2011
2012
			// Permissions that are intended to be added to the roles.
2013
			self::ALLOW_PROD_DEPLOYMENT => [
2014
				'name' => "Ability to deploy to production environments",
2015
				'category' => "Deploynaut",
2016
			],
2017
			self::ALLOW_NON_PROD_DEPLOYMENT => [
2018
				'name' => "Ability to deploy to non-production environments",
2019
				'category' => "Deploynaut",
2020
			],
2021
			self::ALLOW_PROD_SNAPSHOT => [
2022
				'name' => "Ability to make production snapshots",
2023
				'category' => "Deploynaut",
2024
			],
2025
			self::ALLOW_NON_PROD_SNAPSHOT => [
2026
				'name' => "Ability to make non-production snapshots",
2027
				'category' => "Deploynaut",
2028
			],
2029
			self::ALLOW_CREATE_ENVIRONMENT => [
2030
				'name' => "Ability to create environments",
2031
				'category' => "Deploynaut",
2032
			],
2033
		];
2034
	}
2035
2036
	/**
2037
	 * @return DNProject|null
2038
	 */
2039
	public function getCurrentProject() {
2040
		$projectName = trim($this->getRequest()->param('Project'));
2041
		if (!$projectName) {
2042
			return null;
2043
		}
2044
		if (empty(self::$_project_cache[$projectName])) {
2045
			self::$_project_cache[$projectName] = $this->DNProjectList()->filter('Name', $projectName)->First();
2046
		}
2047
		return self::$_project_cache[$projectName];
2048
	}
2049
2050
	/**
2051
	 * @param DNProject|null $project
2052
	 * @return DNEnvironment|null
2053
	 */
2054
	public function getCurrentEnvironment(DNProject $project = null) {
2055
		if ($this->getRequest()->param('Environment') === null) {
2056
			return null;
2057
		}
2058
		if ($project === null) {
2059
			$project = $this->getCurrentProject();
2060
		}
2061
		// project can still be null
2062
		if ($project === null) {
2063
			return null;
2064
		}
2065
		return $project->DNEnvironmentList()->filter('Name', $this->getRequest()->param('Environment'))->First();
2066
	}
2067
2068
	/**
2069
	 * This will return a const that indicates the class of action currently being performed
2070
	 *
2071
	 * Until DNRoot is de-godded, it does a bunch of different actions all in the same class.
2072
	 * So we just have each action handler calll setCurrentActionType to define what sort of
2073
	 * action it is.
2074
	 *
2075
	 * @return string - one of the consts from self::$action_types
2076
	 */
2077
	public function getCurrentActionType() {
2078
		return $this->actionType;
2079
	}
2080
2081
	/**
2082
	 * Sets the current action type
2083
	 *
2084
	 * @param string $actionType string - one of the consts from self::$action_types
2085
	 */
2086
	public function setCurrentActionType($actionType) {
2087
		$this->actionType = $actionType;
2088
	}
2089
2090
	/**
2091
	 * Helper method to allow templates to know whether they should show the 'Archive List' include or not.
2092
	 * The actual permissions are set on a per-environment level, so we need to find out if this $member can upload to
2093
	 * or download from *any* {@link DNEnvironment} that (s)he has access to.
2094
	 *
2095
	 * TODO To be replaced with a method that just returns the list of archives this {@link Member} has access to.
2096
	 *
2097
	 * @param Member|null $member The {@link Member} to check (or null to check the currently logged in Member)
2098
	 * @return boolean|null true if $member has access to upload or download to at least one {@link DNEnvironment}.
2099
	 */
2100
	public function CanViewArchives(Member $member = null) {
2101
		if ($member === null) {
2102
			$member = Member::currentUser();
2103
		}
2104
2105
		if (Permission::checkMember($member, 'ADMIN')) {
2106
			return true;
2107
		}
2108
2109
		$allProjects = $this->DNProjectList();
2110
		if (!$allProjects) {
2111
			return false;
2112
		}
2113
2114
		foreach ($allProjects as $project) {
2115
			if ($project->Environments()) {
2116
				foreach ($project->Environments() as $environment) {
2117
					if (
2118
						$environment->canRestore($member) ||
2119
						$environment->canBackup($member) ||
2120
						$environment->canUploadArchive($member) ||
2121
						$environment->canDownloadArchive($member)
2122
					) {
2123
						// We can return early as we only need to know that we can access one environment
2124
						return true;
2125
					}
2126
				}
2127
			}
2128
		}
2129
	}
2130
2131
	/**
2132
	 * Returns a list of attempted environment creations.
2133
	 *
2134
	 * @return PaginatedList
2135
	 */
2136
	public function CreateEnvironmentList() {
2137
		$project = $this->getCurrentProject();
2138
		if ($project) {
2139
			$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...
2140
		} else {
2141
			$dataList = new ArrayList();
2142
		}
2143
2144
		$this->extend('updateCreateEnvironmentList', $dataList);
2145
		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...
2146
	}
2147
2148
	/**
2149
	 * Returns a list of all archive files that can be accessed by the currently logged-in {@link Member}
2150
	 *
2151
	 * @return PaginatedList
2152
	 */
2153
	public function CompleteDataArchives() {
2154
		$project = $this->getCurrentProject();
2155
		$archives = new ArrayList();
2156
2157
		$archiveList = $project->Environments()->relation("DataArchives");
2158
		if ($archiveList->count() > 0) {
2159
			foreach ($archiveList as $archive) {
2160
				if (!$archive->isPending()) {
2161
					$archives->push($archive);
2162
				}
2163
			}
2164
		}
2165
		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...
2166
	}
2167
2168
	/**
2169
	 * @return PaginatedList The list of "pending" data archives which are waiting for a file
2170
	 * to be delivered offline by post, and manually uploaded into the system.
2171
	 */
2172
	public function PendingDataArchives() {
2173
		$project = $this->getCurrentProject();
2174
		$archives = new ArrayList();
2175
		foreach ($project->DNEnvironmentList() as $env) {
2176
			foreach ($env->DataArchives() as $archive) {
2177
				if ($archive->isPending()) {
2178
					$archives->push($archive);
2179
				}
2180
			}
2181
		}
2182
		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...
2183
	}
2184
2185
	/**
2186
	 * @return PaginatedList
2187
	 */
2188
	public function DataTransferLogs() {
2189
		$environments = $this->getCurrentProject()->Environments()->column('ID');
2190
		$transfers = DNDataTransfer::get()
2191
			->filter('EnvironmentID', $environments)
2192
			->filterByCallback(
2193
				function ($record) {
2194
					return
2195
						$record->Environment()->canRestore() || // Ensure member can perform an action on the transfers env
2196
						$record->Environment()->canBackup() ||
2197
						$record->Environment()->canUploadArchive() ||
2198
						$record->Environment()->canDownloadArchive();
2199
				});
2200
2201
		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...
2202
	}
2203
2204
	/**
2205
	 * @deprecated 2.0.0 - moved to DeployDispatcher
2206
	 *
2207
	 * @return null|PaginatedList
2208
	 */
2209
	public function DeployHistory() {
2210
		if ($env = $this->getCurrentEnvironment()) {
2211
			$history = $env->DeployHistory();
2212
			if ($history->count() > 0) {
2213
				$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...
2214
				$pagination->setPageLength(4);
2215
				return $pagination;
2216
			}
2217
		}
2218
		return null;
2219
	}
2220
2221
	/**
2222
	 * @param string $status
2223
	 * @param string $content
2224
	 *
2225
	 * @return string
2226
	 */
2227
	public function sendResponse($status, $content) {
2228
		// strip excessive newlines
2229
		$content = preg_replace('/(?:(?:\r\n|\r|\n)\s*){2}/s', "\n", $content);
2230
2231
		$sendJSON = (strpos($this->getRequest()->getHeader('Accept'), 'application/json') !== false)
2232
			|| $this->getRequest()->getExtension() == 'json';
2233
2234
		if (!$sendJSON) {
2235
			$this->response->addHeader("Content-type", "text/plain");
2236
			return $content;
2237
		}
2238
		$this->response->addHeader("Content-type", "application/json");
2239
		return json_encode([
2240
			'status' => $status,
2241
			'content' => $content,
2242
		]);
2243
	}
2244
2245
	/**
2246
	 * Get items for the ambient menu that should be accessible from all pages.
2247
	 *
2248
	 * @return ArrayList
2249
	 */
2250
	public function AmbientMenu() {
2251
		$list = new ArrayList();
2252
2253
		if (Member::currentUserID()) {
2254
			$list->push(new ArrayData([
2255
				'Classes' => 'logout',
2256
				'FaIcon' => 'sign-out',
2257
				'Link' => 'Security/logout',
2258
				'Title' => 'Log out',
2259
				'IsCurrent' => false,
2260
				'IsSection' => false
2261
			]));
2262
		}
2263
2264
		$this->extend('updateAmbientMenu', $list);
2265
		return $list;
2266
	}
2267
2268
	/**
2269
	 * Checks whether the user can create a project.
2270
	 *
2271
	 * @return bool
2272
	 */
2273
	public function canCreateProjects($member = null) {
2274
		if (!$member) {
2275
			$member = Member::currentUser();
2276
		}
2277
		if (!$member) {
2278
			return false;
2279
		}
2280
2281
		return singleton('DNProject')->canCreate($member);
2282
	}
2283
2284
	protected function applyRedeploy(SS_HTTPRequest $request, &$data) {
2285
		if (!$request->getVar('redeploy')) {
2286
			return;
2287
		}
2288
2289
		$project = $this->getCurrentProject();
2290
		if (!$project) {
2291
			return $this->project404Response();
2292
		}
2293
2294
		// Performs canView permission check by limiting visible projects
2295
		$env = $this->getCurrentEnvironment($project);
2296
		if (!$env) {
2297
			return $this->environment404Response();
2298
		}
2299
2300
		$current = $env->CurrentBuild();
2301
		if ($current && $current->exists()) {
2302
			$data['preselect_tab'] = 3;
2303
			$data['preselect_sha'] = $current->SHA;
2304
		} else {
2305
			$master = $project->DNBranchList()->byName('master');
2306
			if ($master) {
2307
				$data['preselect_tab'] = 1;
2308
				$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...
2309
			}
2310
		}
2311
	}
2312
2313
	/**
2314
	 * @return SS_HTTPResponse
2315
	 */
2316
	protected function project404Response() {
2317
		return new SS_HTTPResponse(
2318
			"Project '" . Convert::raw2xml($this->getRequest()->param('Project')) . "' not found.",
2319
			404
2320
		);
2321
	}
2322
2323
	/**
2324
	 * @return SS_HTTPResponse
2325
	 */
2326
	protected function environment404Response() {
2327
		$envName = Convert::raw2xml($this->getRequest()->param('Environment'));
2328
		return new SS_HTTPResponse("Environment '" . $envName . "' not found.", 404);
2329
	}
2330
2331
	/**
2332
	 * Validate the snapshot mode
2333
	 *
2334
	 * @param string $mode
2335
	 */
2336
	protected function validateSnapshotMode($mode) {
2337
		if (!in_array($mode, ['all', 'assets', 'db'])) {
2338
			throw new LogicException('Invalid mode');
2339
		}
2340
	}
2341
2342
	/**
2343
	 * @param string $sectionName
2344
	 * @param string $title
2345
	 *
2346
	 * @return SS_HTTPResponse
2347
	 */
2348
	protected function getCustomisedViewSection($sectionName, $title = '', $data = []) {
2349
		// Performs canView permission check by limiting visible projects
2350
		$project = $this->getCurrentProject();
2351
		if (!$project) {
2352
			return $this->project404Response();
2353
		}
2354
		$data[$sectionName] = 1;
2355
2356
		if ($this !== '') {
2357
			$data['Title'] = $title;
2358
		}
2359
2360
		return $this->render($data);
2361
	}
2362
2363
}
2364
2365