Completed
Pull Request — master (#740)
by Sean
04:17
created

DNRoot::metrics()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
150
		self::ACTION_DEPLOY,
151
		self::ACTION_SNAPSHOT,
152
		self::PROJECT_OVERVIEW
153
	];
154
155
	/**
156
	 * Include requirements that deploynaut needs, such as javascript.
157
	 */
158
	public static function include_requirements() {
159
160
		// JS should always go to the bottom, otherwise there's the risk that Requirements
161
		// puts them halfway through the page to the nearest <script> tag. We don't want that.
162
		Requirements::set_force_js_to_bottom(true);
163
164
		// todo these should be bundled into the same JS as the others in "static" below.
165
		// We've deliberately not used combined_files as it can mess with some of the JS used
166
		// here and cause sporadic errors.
167
		Requirements::javascript('deploynaut/javascript/jquery.js');
168
		Requirements::javascript('deploynaut/javascript/bootstrap.js');
169
		Requirements::javascript('deploynaut/javascript/q.js');
170
		Requirements::javascript('deploynaut/javascript/tablefilter.js');
171
		Requirements::javascript('deploynaut/javascript/deploynaut.js');
172
173
		Requirements::javascript('deploynaut/javascript/bootstrap.file-input.js');
174
		Requirements::javascript('deploynaut/thirdparty/select2/dist/js/select2.min.js');
175
		Requirements::javascript('deploynaut/javascript/selectize.js');
176
		Requirements::javascript('deploynaut/thirdparty/bootstrap-switch/dist/js/bootstrap-switch.min.js');
177
		Requirements::javascript('deploynaut/javascript/material.js');
178
179
		// Load the buildable dependencies only if not loaded centrally.
180
		if (!is_dir(BASE_PATH . DIRECTORY_SEPARATOR . 'static')) {
181
			if (\Director::isDev()) {
182
				\Requirements::javascript('deploynaut/static/bundle-debug.js');
183
			} else {
184
				\Requirements::javascript('deploynaut/static/bundle.js');
185
			}
186
		}
187
188
		Requirements::css('deploynaut/static/style.css');
189
	}
190
191
	/**
192
	 * Check for feature flags:
193
	 * - FLAG_SNAPSHOTS_ENABLED: set to true to enable globally
194
	 * - FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS: set to semicolon-separated list of email addresses of allowed users.
195
	 *
196
	 * @return boolean
197
	 */
198 View Code Duplication
	public static function FlagSnapshotsEnabled() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
199
		if (defined('FLAG_SNAPSHOTS_ENABLED') && FLAG_SNAPSHOTS_ENABLED) {
200
			return true;
201
		}
202
		if (defined('FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS') && FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS) {
203
			$allowedMembers = explode(';', FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS);
204
			$member = Member::currentUser();
205
			if ($allowedMembers && $member && in_array($member->Email, $allowedMembers)) {
206
				return true;
207
			}
208
		}
209
		return false;
210
	}
211
212
	/**
213
	 * @return ArrayList
214
	 */
215
	public static function get_support_links() {
216
		$supportLinks = self::config()->support_links;
217
		if ($supportLinks) {
218
			return new ArrayList($supportLinks);
219
		}
220
	}
221
222
	/**
223
	 * @return array
224
	 */
225
	public static function get_template_global_variables() {
226
		return [
227
			'RedisUnavailable' => 'RedisUnavailable',
228
			'RedisWorkersCount' => 'RedisWorkersCount',
229
			'SidebarLinks' => 'SidebarLinks',
230
			"SupportLinks" => 'get_support_links'
231
		];
232
	}
233
234
	/**
235
	 */
236
	public function init() {
237
		parent::init();
238
239
		if (!Member::currentUser() && !Session::get('AutoLoginHash')) {
240
			return Security::permissionFailure();
241
		}
242
243
		// Block framework jquery
244
		Requirements::block(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
245
246
		self::include_requirements();
247
	}
248
249
	/**
250
	 * @return string
251
	 */
252
	public function Link() {
253
		return "naut/";
254
	}
255
256
	/**
257
	 * Actions
258
	 *
259
	 * @param \SS_HTTPRequest $request
260
	 * @return \SS_HTTPResponse
261
	 */
262
	public function index(\SS_HTTPRequest $request) {
263
		return $this->redirect($this->Link() . 'projects/');
264
	}
265
266
	/**
267
	 * Action
268
	 *
269
	 * @param \SS_HTTPRequest $request
270
	 * @return string - HTML
271
	 */
272
	public function projects(\SS_HTTPRequest $request) {
273
		// Performs canView permission check by limiting visible projects in DNProjectsList() call.
274
		return $this->customise([
275
			'Title' => 'Projects',
276
		])->render();
277
	}
278
279
	/**
280
	 * @param \SS_HTTPRequest $request
281
	 * @return HTMLText
282
	 */
283
	public function nav(\SS_HTTPRequest $request) {
284
		return $this->renderWith('Nav');
285
	}
286
287
	/**
288
	 * Return a link to the navigation template used for AJAX requests.
289
	 * @return string
290
	 */
291
	public function NavLink() {
292
		$currentProject = $this->getCurrentProject();
293
		$projectName = $currentProject ? $currentProject->Name : null;
294
		return Controller::join_links(Director::absoluteBaseURL(), 'naut', 'nav', $projectName);
295
	}
296
297
	/**
298
	 * Action
299
	 *
300
	 * @param \SS_HTTPRequest $request
301
	 * @return SS_HTTPResponse - HTML
302
	 */
303
	public function snapshots(\SS_HTTPRequest $request) {
304
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
305
		return $this->getCustomisedViewSection('SnapshotsSection', 'Data Snapshots');
306
	}
307
308
	/**
309
	 * Action
310
	 *
311
	 * @param \SS_HTTPRequest $request
312
	 * @return string - HTML
313
	 */
314 View Code Duplication
	public function createsnapshot(\SS_HTTPRequest $request) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

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

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

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

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

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

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

Loading history...
476
			$process = new AbortableProcess(sprintf('rm -rf %s', escapeshellarg($workingDir)));
477
			$process->setTimeout(120);
478
			$process->run();
479
			$dataTransfer->delete();
480
			$dataArchive->delete();
481
		};
482
483
		// extract the sspak contents so we can inspect them
484
		try {
485
			$dataArchive->extractArchive($workingDir);
486
		} catch (Exception $e) {
487
			$cleanupFn();
488
			$form->sessionMessage(
489
				'There was a problem trying to open your snapshot for processing. Please try uploading again',
490
				'bad'
491
			);
492
			return $this->redirectBack();
493
		}
494
495
		// validate that the sspak contents match the declared contents
496
		$result = $dataArchive->validateArchiveContents();
497
		if (!$result->valid()) {
498
			$cleanupFn();
499
			$form->sessionMessage($result->message(), 'bad');
500
			return $this->redirectBack();
501
		}
502
503
		// fix file permissions of extracted sspak files then re-build the sspak
504
		try {
505
			$dataArchive->fixArchivePermissions($workingDir);
506
			$dataArchive->setArchiveFromFiles($workingDir);
507
		} catch (Exception $e) {
508
			$cleanupFn();
509
			$form->sessionMessage(
510
				'There was a problem processing your snapshot. Please try uploading again',
511
				'bad'
512
			);
513
			return $this->redirectBack();
514
		}
515
516
		// cleanup any extracted sspak contents lying around
517
		$process = new AbortableProcess(sprintf('rm -rf %s', escapeshellarg($workingDir)));
518
		$process->setTimeout(120);
519
		$process->run();
520
521
		return $this->customise([
522
			'Project' => $project,
523
			'CurrentProject' => $project,
524
			'SnapshotsSection' => 1,
525
			'DataArchive' => $dataArchive,
526
			'DataTransferRestoreForm' => $this->getDataTransferRestoreForm($this->request, $dataArchive),
527
			'BackURL' => $project->Link('snapshots')
528
		])->renderWith(['DNRoot_uploadsnapshot', 'DNRoot']);
529
	}
530
531
	/**
532
	 * @param \SS_HTTPRequest $request
533
	 * @return Form
534
	 */
535
	public function getPostSnapshotForm(\SS_HTTPRequest $request) {
536
		// Performs canView permission check by limiting visible projects
537
		$project = $this->getCurrentProject();
538
		if (!$project) {
539
			return $this->project404Response();
540
		}
541
542
		if (!$project->canUploadArchive()) {
543
			return new SS_HTTPResponse("Not allowed to upload", 401);
544
		}
545
546
		// Framing an environment as a "group of people with download access"
547
		// makes more sense to the user here, while still allowing us to enforce
548
		// environment specific restrictions on downloading the file later on.
549
		$envs = $project->DNEnvironmentList()->filterByCallback(function ($item) {
550
			return $item->canUploadArchive();
551
		});
552
		$envsMap = [];
553
		foreach ($envs as $env) {
554
			$envsMap[$env->ID] = $env->Name;
555
		}
556
557
		$form = Form::create(
558
			$this,
559
			'PostSnapshotForm',
560
			FieldList::create(
561
				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
562
				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
563
					->setEmptyString('Select an environment')
564
			),
565
			FieldList::create(
566
				FormAction::create('doPostSnapshot', 'Submit request')
567
					->addExtraClass('btn')
568
			),
569
			RequiredFields::create('File')
570
		);
571
572
		$form->disableSecurityToken();
573
		$form->addExtraClass('fields-wide');
574
		// Tweak the action so it plays well with our fake URL structure.
575
		$form->setFormAction($project->Link() . '/PostSnapshotForm');
576
577
		return $form;
578
	}
579
580
	/**
581
	 * @param array $data
582
	 * @param Form $form
583
	 *
584
	 * @return SS_HTTPResponse
585
	 */
586
	public function doPostSnapshot($data, $form) {
587
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
588
589
		$project = $this->getCurrentProject();
590
		if (!$project) {
591
			return $this->project404Response();
592
		}
593
594
		$validEnvs = $project->DNEnvironmentList()->filterByCallback(function ($item) {
595
			return $item->canUploadArchive();
596
		});
597
598
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
599
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
600
		if (!$environment) {
601
			throw new LogicException('Invalid environment');
602
		}
603
604
		$dataArchive = DNDataArchive::create([
605
			'UploadToken' => DNDataArchive::generate_upload_token(),
606
		]);
607
		$form->saveInto($dataArchive);
608
		$dataArchive->write();
609
610
		return $this->redirect(Controller::join_links(
611
			$project->Link(),
612
			'postsnapshotsuccess',
613
			$dataArchive->ID
614
		));
615
	}
616
617
	/**
618
	 * Action
619
	 *
620
	 * @param \SS_HTTPRequest $request
621
	 * @return SS_HTTPResponse - HTML
622
	 */
623
	public function snapshotslog(\SS_HTTPRequest $request) {
624
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
625
		return $this->getCustomisedViewSection('SnapshotsSection', 'Snapshots log');
626
	}
627
628
	/**
629
	 * @param \SS_HTTPRequest $request
630
	 * @return SS_HTTPResponse|string
631
	 * @throws SS_HTTPResponse_Exception
632
	 */
633
	public function postsnapshotsuccess(\SS_HTTPRequest $request) {
634
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
635
636
		// Performs canView permission check by limiting visible projects
637
		$project = $this->getCurrentProject();
638
		if (!$project) {
639
			return $this->project404Response();
640
		}
641
642
		if (!$project->canUploadArchive()) {
643
			return new SS_HTTPResponse("Not allowed to upload", 401);
644
		}
645
646
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
647
		if (!$dataArchive) {
648
			return new SS_HTTPResponse("Archive not found.", 404);
649
		}
650
651
		if (!$dataArchive->canRestore()) {
652
			throw new SS_HTTPResponse_Exception('Not allowed to restore archive', 403);
653
		}
654
655
		return $this->render([
656
			'Title' => 'How to send us your Data Snapshot by post',
657
			'DataArchive' => $dataArchive,
658
			'Address' => Config::inst()->get('Deploynaut', 'snapshot_post_address'),
659
			'BackURL' => $project->Link(),
660
		]);
661
	}
662
663
	/**
664
	 * @param \SS_HTTPRequest $request
665
	 * @return \SS_HTTPResponse
666
	 */
667
	public function project(\SS_HTTPRequest $request) {
668
		$this->setCurrentActionType(self::PROJECT_OVERVIEW);
669
		return $this->getCustomisedViewSection('ProjectOverview', '', ['IsAdmin' => Permission::check('ADMIN')]);
670
	}
671
672
	/**
673
	 * This action will star / unstar a project for the current member
674
	 *
675
	 * @param \SS_HTTPRequest $request
676
	 *
677
	 * @return SS_HTTPResponse
678
	 */
679
	public function toggleprojectstar(\SS_HTTPRequest $request) {
680
		$project = $this->getCurrentProject();
681
		if (!$project) {
682
			return $this->project404Response();
683
		}
684
685
		$member = Member::currentUser();
686
		if ($member === null) {
687
			return $this->project404Response();
688
		}
689
		$favProject = $member->StarredProjects()
690
			->filter('DNProjectID', $project->ID)
691
			->first();
692
693
		if ($favProject) {
694
			$member->StarredProjects()->remove($favProject);
695
		} else {
696
			$member->StarredProjects()->add($project);
697
		}
698
		return $this->redirectBack();
699
	}
700
701
	/**
702
	 * @param \SS_HTTPRequest $request
703
	 * @return \SS_HTTPResponse
704
	 */
705
	public function branch(\SS_HTTPRequest $request) {
706
		$project = $this->getCurrentProject();
707
		if (!$project) {
708
			return $this->project404Response();
709
		}
710
711
		$branchName = $request->getVar('name');
712
		$branch = $project->DNBranchList()->byName($branchName);
713
		if (!$branch) {
714
			return new SS_HTTPResponse("Branch '" . Convert::raw2xml($branchName) . "' not found.", 404);
715
		}
716
717
		return $this->render([
718
			'CurrentBranch' => $branch,
719
		]);
720
	}
721
722
	/**
723
	 * @param \SS_HTTPRequest $request
724
	 * @return \SS_HTTPResponse
725
	 */
726
	public function environment(\SS_HTTPRequest $request) {
727
		// Performs canView permission check by limiting visible projects
728
		$project = $this->getCurrentProject();
729
		if (!$project) {
730
			return $this->project404Response();
731
		}
732
733
		// Performs canView permission check by limiting visible projects
734
		$env = $this->getCurrentEnvironment($project);
735
		if (!$env) {
736
			return $this->environment404Response();
737
		}
738
739
		return $this->render([
740
			'DNEnvironmentList' => $this->getCurrentProject()->DNEnvironmentList(),
741
			'FlagSnapshotsEnabled' => $this->FlagSnapshotsEnabled(),
742
			'Redeploy' => (bool) $request->getVar('redeploy')
743
		]);
744
	}
745
746
	/**
747
	 * Shows the creation log.
748
	 *
749
	 * @param \SS_HTTPRequest $request
750
	 * @return string
751
	 */
752
	public function createenv(\SS_HTTPRequest $request) {
753
		$params = $request->params();
754
		if ($params['Identifier']) {
755
			$record = DNCreateEnvironment::get()->byId($params['Identifier']);
756
757
			if (!$record || !$record->ID) {
758
				throw new SS_HTTPResponse_Exception('Create environment not found', 404);
759
			}
760
			if (!$record->canView()) {
761
				return Security::permissionFailure();
762
			}
763
764
			$project = $this->getCurrentProject();
765
			if (!$project) {
766
				return $this->project404Response();
767
			}
768
769
			if ($project->Name != $params['Project']) {
770
				throw new LogicException("Project in URL doesn't match this creation");
771
			}
772
773
			return $this->render([
774
				'CreateEnvironment' => $record,
775
			]);
776
		}
777
		return $this->render(['CurrentTitle' => 'Create an environment']);
778
	}
779
780
	public function createenvlog(\SS_HTTPRequest $request) {
781
		$params = $request->params();
782
		$env = DNCreateEnvironment::get()->byId($params['Identifier']);
783
784
		if (!$env || !$env->ID) {
785
			throw new SS_HTTPResponse_Exception('Log not found', 404);
786
		}
787
		if (!$env->canView()) {
788
			return Security::permissionFailure();
789
		}
790
791
		$project = $env->Project();
792
793
		if ($project->Name != $params['Project']) {
794
			throw new LogicException("Project in URL doesn't match this deploy");
795
		}
796
797
		$log = $env->log();
798
		if ($log->exists()) {
799
			$content = $log->content();
800
		} else {
801
			$content = 'Waiting for action to start';
802
		}
803
804
		return $this->sendResponse($env->ResqueStatus(), $content);
805
	}
806
807
	/**
808
	 * @param \SS_HTTPRequest $request
809
	 * @return Form
810
	 */
811
	public function getCreateEnvironmentForm(\SS_HTTPRequest $request) {
812
		$this->setCurrentActionType(self::ACTION_ENVIRONMENTS);
813
814
		$project = $this->getCurrentProject();
815
		if (!$project) {
816
			return $this->project404Response();
817
		}
818
819
		$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...
820
		if (!$envType || !class_exists($envType)) {
821
			return null;
822
		}
823
824
		$backend = Injector::inst()->get($envType);
825
		if (!($backend instanceof EnvironmentCreateBackend)) {
826
			// Only allow this for supported backends.
827
			return null;
828
		}
829
830
		$fields = $backend->getCreateEnvironmentFields($project);
831
		if (!$fields) {
832
			return null;
833
		}
834
835
		if (!$project->canCreateEnvironments()) {
836
			return new SS_HTTPResponse('Not allowed to create environments for this project', 401);
837
		}
838
839
		$form = Form::create(
840
			$this,
841
			'CreateEnvironmentForm',
842
			$fields,
843
			FieldList::create(
844
				FormAction::create('doCreateEnvironment', 'Create')
845
					->addExtraClass('btn')
846
			),
847
			$backend->getCreateEnvironmentValidator()
848
		);
849
850
		// Tweak the action so it plays well with our fake URL structure.
851
		$form->setFormAction($project->Link() . '/CreateEnvironmentForm');
852
853
		return $form;
854
	}
855
856
	/**
857
	 * @param array $data
858
	 * @param Form $form
859
	 *
860
	 * @return bool|HTMLText|SS_HTTPResponse
861
	 */
862
	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...
863
		$this->setCurrentActionType(self::ACTION_ENVIRONMENTS);
864
865
		$project = $this->getCurrentProject();
866
		if (!$project) {
867
			return $this->project404Response();
868
		}
869
870
		if (!$project->canCreateEnvironments()) {
871
			return new SS_HTTPResponse('Not allowed to create environments for this project', 401);
872
		}
873
874
		// Set the environment type so we know what we're creating.
875
		$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...
876
877
		$job = DNCreateEnvironment::create();
878
879
		$job->Data = serialize($data);
880
		$job->ProjectID = $project->ID;
881
		$job->write();
882
		$job->start();
883
884
		return $this->redirect($project->Link('createenv') . '/' . $job->ID);
885
	}
886
887
	/**
888
	 * Get the DNData object.
889
	 *
890
	 * @return DNData
891
	 */
892
	public function DNData() {
893
		return DNData::inst();
894
	}
895
896
	/**
897
	 * Provide a list of all projects.
898
	 *
899
	 * @return SS_List
900
	 */
901
	public function DNProjectList() {
902
		$memberId = Member::currentUserID();
903
		if (!$memberId) {
904
			return new ArrayList();
905
		}
906
907
		if (Permission::check('ADMIN')) {
908
			return DNProject::get();
909
		}
910
911
		$projects = Member::get()->filter('ID', $memberId)
912
			->relation('Groups')
913
			->relation('Projects');
914
915
		$this->extend('updateDNProjectList', $projects);
916
		return $projects;
917
	}
918
919
	/**
920
	 * @return ArrayList
921
	 */
922
	public function getPlatformSpecificStrings() {
923
		$strings = $this->config()->platform_specific_strings;
924
		if ($strings) {
925
			return new ArrayList($strings);
926
		}
927
	}
928
929
	/**
930
	 * Provide a list of all starred projects for the currently logged in member
931
	 *
932
	 * @return SS_List
933
	 */
934
	public function getStarredProjects() {
935
		$member = Member::currentUser();
936
		if ($member === null) {
937
			return new ArrayList();
938
		}
939
940
		$favProjects = $member->StarredProjects();
941
942
		$list = new ArrayList();
943
		foreach ($favProjects as $project) {
944
			if ($project->canView($member)) {
945
				$list->add($project);
946
			}
947
		}
948
		return $list;
949
	}
950
951
	/**
952
	 * Returns top level navigation of projects.
953
	 *
954
	 * @param int $limit
955
	 *
956
	 * @return ArrayList
957
	 */
958
	public function Navigation($limit = 5) {
959
		$navigation = new ArrayList();
960
961
		$currentProject = $this->getCurrentProject();
962
		$currentEnvironment = $this->getCurrentEnvironment();
963
		$actionType = $this->getCurrentActionType();
964
965
		$projects = $this->getStarredProjects();
966
		if ($projects->count() < 1) {
967
			$projects = $this->DNProjectList();
968
		} else {
969
			$limit = -1;
970
		}
971
972
		if ($projects->count() > 0) {
973
			$activeProject = false;
974
975
			if ($limit > 0) {
976
				$limitedProjects = $projects->limit($limit);
977
			} else {
978
				$limitedProjects = $projects;
979
			}
980
981
			foreach ($limitedProjects as $project) {
982
				$isActive = $currentProject && $currentProject->ID == $project->ID;
983
				if ($isActive) {
984
					$activeProject = true;
985
				}
986
987
				$isCurrentEnvironment = false;
988
				if ($project && $currentEnvironment) {
989
					$isCurrentEnvironment = (bool) $project->DNEnvironmentList()->find('ID', $currentEnvironment->ID);
990
				}
991
992
				$navigation->push([
993
					'Project' => $project,
994
					'IsCurrentEnvironment' => $isCurrentEnvironment,
995
					'IsActive' => $currentProject && $currentProject->ID == $project->ID,
996
					'IsOverview' => $actionType == self::PROJECT_OVERVIEW && $currentProject->ID == $project->ID
997
				]);
998
			}
999
1000
			// Ensure the current project is in the list
1001
			if (!$activeProject && $currentProject) {
1002
				$navigation->unshift([
1003
					'Project' => $currentProject,
1004
					'IsActive' => true,
1005
					'IsCurrentEnvironment' => $currentEnvironment,
1006
					'IsOverview' => $actionType == self::PROJECT_OVERVIEW
1007
				]);
1008
				if ($limit > 0 && $navigation->count() > $limit) {
1009
					$navigation->pop();
1010
				}
1011
			}
1012
		}
1013
1014
		return $navigation;
1015
	}
1016
1017
	/**
1018
	 * Construct the deployment form
1019
	 *
1020
	 * @deprecated 2.0.0 - moved to DeployDispatcher
1021
	 *
1022
	 * @return Form
1023
	 */
1024
	public function getDeployForm($request = null) {
1025
1026
		// Performs canView permission check by limiting visible projects
1027
		$project = $this->getCurrentProject();
1028
		if (!$project) {
1029
			return $this->project404Response();
1030
		}
1031
1032
		// Performs canView permission check by limiting visible projects
1033
		$environment = $this->getCurrentEnvironment($project);
1034
		if (!$environment) {
1035
			return $this->environment404Response();
1036
		}
1037
1038
		if (!$environment->canDeploy()) {
1039
			return new SS_HTTPResponse("Not allowed to deploy", 401);
1040
		}
1041
1042
		// Generate the form
1043
		$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...
1044
1045
		// If this is an ajax request we don't want to submit the form - we just want to retrieve the markup.
1046
		if (
1047
			$request &&
1048
			!$request->requestVar('action_showDeploySummary') &&
1049
			$this->getRequest()->isAjax() &&
1050
			$this->getRequest()->isGET()
1051
		) {
1052
			// We can just use the URL we're accessing
1053
			$form->setFormAction($this->getRequest()->getURL());
1054
1055
			$body = json_encode(['Content' => $form->forAjaxTemplate()->forTemplate()]);
1056
			$this->getResponse()->addHeader('Content-Type', 'application/json');
1057
			$this->getResponse()->setBody($body);
1058
			return $body;
1059
		}
1060
1061
		$form->setFormAction($this->getRequest()->getURL() . '/DeployForm');
1062
		return $form;
1063
	}
1064
1065
	/**
1066
	 * @deprecated 2.0.0 - moved to GitDispatcher
1067
	 *
1068
	 * @param \SS_HTTPRequest $request
1069
	 *
1070
	 * @return SS_HTTPResponse|string
1071
	 */
1072
	public function gitRevisions(\SS_HTTPRequest $request) {
1073
1074
		// Performs canView permission check by limiting visible projects
1075
		$project = $this->getCurrentProject();
1076
		if (!$project) {
1077
			return $this->project404Response();
1078
		}
1079
1080
		// Performs canView permission check by limiting visible projects
1081
		$env = $this->getCurrentEnvironment($project);
1082
		if (!$env) {
1083
			return $this->environment404Response();
1084
		}
1085
1086
		$options = [];
1087
		foreach ($env->getSupportedOptions() as $option) {
1088
			$options[] = [
1089
				'name' => $option->getName(),
1090
				'title' => $option->getTitle(),
1091
				'defaultValue' => $option->getDefaultValue()
1092
			];
1093
		}
1094
1095
		$tabs = [];
1096
		$id = 0;
1097
		$data = [
1098
			'id' => ++$id,
1099
			'name' => 'Deploy the latest version of a branch',
1100
			'field_type' => 'dropdown',
1101
			'field_label' => 'Choose a branch',
1102
			'field_id' => 'branch',
1103
			'field_data' => [],
1104
			'options' => $options
1105
		];
1106
		foreach ($project->DNBranchList() as $branch) {
1107
			$sha = $branch->SHA();
1108
			$name = $branch->Name();
1109
			$branchValue = sprintf("%s (%s, %s old)",
1110
				$name,
1111
				substr($sha, 0, 8),
1112
				$branch->LastUpdated()->TimeDiff()
1113
			);
1114
			$data['field_data'][] = [
1115
				'id' => $sha,
1116
				'text' => $branchValue,
1117
				'branch_name' => $name // the raw branch name, not including the time etc
1118
			];
1119
		}
1120
		$tabs[] = $data;
1121
1122
		$data = [
1123
			'id' => ++$id,
1124
			'name' => 'Deploy a tagged release',
1125
			'field_type' => 'dropdown',
1126
			'field_label' => 'Choose a tag',
1127
			'field_id' => 'tag',
1128
			'field_data' => [],
1129
			'options' => $options
1130
		];
1131
1132
		foreach ($project->DNTagList()->setLimit(null) as $tag) {
1133
			$name = $tag->Name();
1134
			$data['field_data'][] = [
1135
				'id' => $tag->SHA(),
1136
				'text' => sprintf("%s", $name)
1137
			];
1138
		}
1139
1140
		// show newest tags first.
1141
		$data['field_data'] = array_reverse($data['field_data']);
1142
1143
		$tabs[] = $data;
1144
1145
		// Past deployments
1146
		$data = [
1147
			'id' => ++$id,
1148
			'name' => 'Redeploy a release that was previously deployed (to any environment)',
1149
			'field_type' => 'dropdown',
1150
			'field_label' => 'Choose a previously deployed release',
1151
			'field_id' => 'release',
1152
			'field_data' => [],
1153
			'options' => $options
1154
		];
1155
		// We are aiming at the format:
1156
		// [{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...
1157
		$redeploy = [];
1158 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...
1159
			$envName = $dnEnvironment->Name;
1160
			$perEnvDeploys = [];
1161
1162
			foreach ($dnEnvironment->DeployHistory() as $deploy) {
1163
				$sha = $deploy->SHA;
1164
1165
				// Check if exists to make sure the newest deployment date is used.
1166
				if (!isset($perEnvDeploys[$sha])) {
1167
					$pastValue = sprintf("%s (deployed %s)",
1168
						substr($sha, 0, 8),
1169
						$deploy->obj('LastEdited')->Ago()
1170
					);
1171
					$perEnvDeploys[$sha] = [
1172
						'id' => $sha,
1173
						'text' => $pastValue
1174
					];
1175
				}
1176
			}
1177
1178
			if (!empty($perEnvDeploys)) {
1179
				$redeploy[$envName] = array_values($perEnvDeploys);
1180
			}
1181
		}
1182
		// Convert the array to the frontend format (i.e. keyed to regular array)
1183
		foreach ($redeploy as $name => $descr) {
1184
			$data['field_data'][] = ['text' => $name, 'children' => $descr];
1185
		}
1186
		$tabs[] = $data;
1187
1188
		$data = [
1189
			'id' => ++$id,
1190
			'name' => 'Deploy a specific SHA',
1191
			'field_type' => 'textfield',
1192
			'field_label' => 'Choose a SHA',
1193
			'field_id' => 'SHA',
1194
			'field_data' => [],
1195
			'options' => $options
1196
		];
1197
		$tabs[] = $data;
1198
1199
		// get the last time git fetch was run
1200
		$lastFetched = 'never';
1201
		$fetch = DNGitFetch::get()
1202
			->filter('ProjectID', $project->ID)
1203
			->sort('LastEdited', 'DESC')
1204
			->first();
1205
		if ($fetch) {
1206
			$lastFetched = $fetch->dbObject('LastEdited')->Ago();
1207
		}
1208
1209
		$data = [
1210
			'Tabs' => $tabs,
1211
			'last_fetched' => $lastFetched
1212
		];
1213
1214
		$this->applyRedeploy($request, $data);
1215
1216
		return json_encode($data, JSON_PRETTY_PRINT);
1217
	}
1218
1219
	/**
1220
	 * @deprecated 2.0.0 - moved to PlanDispatcher
1221
	 *
1222
	 * @param \SS_HTTPRequest $request
1223
	 *
1224
	 * @return string
1225
	 */
1226
	public function deploySummary(\SS_HTTPRequest $request) {
1227
1228
		// Performs canView permission check by limiting visible projects
1229
		$project = $this->getCurrentProject();
1230
		if (!$project) {
1231
			return $this->project404Response();
1232
		}
1233
1234
		// Performs canView permission check by limiting visible projects
1235
		$environment = $this->getCurrentEnvironment($project);
1236
		if (!$environment) {
1237
			return $this->environment404Response();
1238
		}
1239
1240
		// Plan the deployment.
1241
		$strategy = $environment->getDeployStrategy($request);
1242
		$data = $strategy->toArray();
1243
1244
		// Add in a URL for comparing from->to code changes. Ensure that we have
1245
		// two proper 40 character SHAs, otherwise we can't show the compare link.
1246
		$interface = $project->getRepositoryInterface();
1247
		if (
1248
			!empty($interface) && !empty($interface->URL)
1249
			&& !empty($data['changes']['Code version']['from'])
1250
			&& strlen($data['changes']['Code version']['from']) == '40'
1251
			&& !empty($data['changes']['Code version']['to'])
1252
			&& strlen($data['changes']['Code version']['to']) == '40'
1253
		) {
1254
			$compareurl = sprintf(
1255
				'%s/compare/%s...%s',
1256
				$interface->URL,
1257
				$data['changes']['Code version']['from'],
1258
				$data['changes']['Code version']['to']
1259
			);
1260
			$data['changes']['Code version']['compareUrl'] = $compareurl;
1261
		}
1262
1263
		// Append json to response
1264
		$token = SecurityToken::inst();
1265
		$data['SecurityID'] = $token->getValue();
1266
1267
		$this->extend('updateDeploySummary', $data);
1268
1269
		return json_encode($data);
1270
	}
1271
1272
	/**
1273
	 * Deployment form submission handler.
1274
	 *
1275
	 * @deprecated 2.0.0 - moved to DeployDispatcher
1276
	 *
1277
	 * Initiate a DNDeployment record and redirect to it for status polling
1278
	 *
1279
	 * @param \SS_HTTPRequest $request
1280
	 *
1281
	 * @return SS_HTTPResponse
1282
	 * @throws ValidationException
1283
	 * @throws null
1284
	 */
1285
	public function startDeploy(\SS_HTTPRequest $request) {
1286
1287
		$token = SecurityToken::inst();
1288
1289
		// Ensure the submitted token has a value
1290
		$submittedToken = $request->postVar(\Dispatcher::SECURITY_TOKEN_NAME);
1291
		if (!$submittedToken) {
1292
			return false;
1293
		}
1294
		// Do the actual check.
1295
		$check = $token->check($submittedToken);
1296
		// Ensure the CSRF Token is correct
1297
		if (!$check) {
1298
			// CSRF token didn't match
1299
			return $this->httpError(400, 'Bad Request');
1300
		}
1301
1302
		// Performs canView permission check by limiting visible projects
1303
		$project = $this->getCurrentProject();
1304
		if (!$project) {
1305
			return $this->project404Response();
1306
		}
1307
1308
		// Performs canView permission check by limiting visible projects
1309
		$environment = $this->getCurrentEnvironment($project);
1310
		if (!$environment) {
1311
			return $this->environment404Response();
1312
		}
1313
1314
		// Initiate the deployment
1315
		// The extension point should pass in: Project, Environment, SelectRelease, buildName
1316
		$this->extend('doDeploy', $project, $environment, $buildName, $data);
1317
1318
		// Start the deployment based on the approved strategy.
1319
		$strategy = new DeploymentStrategy($environment);
1320
		$strategy->fromArray($request->requestVar('strategy'));
1321
		$deployment = $strategy->createDeployment();
1322
		// Bypass approval by going straight to Queued.
1323
		$deployment->getMachine()->apply(DNDeployment::TR_QUEUE);
1324
1325
		return json_encode([
1326
			'url' => Director::absoluteBaseURL() . $deployment->Link()
1327
		], JSON_PRETTY_PRINT);
1328
	}
1329
1330
	/**
1331
	 * @deprecated 2.0.0 - moved to DeployDispatcher
1332
	 *
1333
	 * Action - Do the actual deploy
1334
	 *
1335
	 * @param \SS_HTTPRequest $request
1336
	 *
1337
	 * @return SS_HTTPResponse|string
1338
	 * @throws SS_HTTPResponse_Exception
1339
	 */
1340
	public function deploy(\SS_HTTPRequest $request) {
1341
		$params = $request->params();
1342
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1343
1344
		if (!$deployment || !$deployment->ID) {
1345
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1346
		}
1347
		if (!$deployment->canView()) {
1348
			return Security::permissionFailure();
1349
		}
1350
1351
		$environment = $deployment->Environment();
1352
		$project = $environment->Project();
1353
1354
		if ($environment->Name != $params['Environment']) {
1355
			throw new LogicException("Environment in URL doesn't match this deploy");
1356
		}
1357
		if ($project->Name != $params['Project']) {
1358
			throw new LogicException("Project in URL doesn't match this deploy");
1359
		}
1360
1361
		return $this->render([
1362
			'Deployment' => $deployment,
1363
		]);
1364
	}
1365
1366
	/**
1367
	 * @deprecated 2.0.0 - moved to DeployDispatcher
1368
	 *
1369
	 * Action - Get the latest deploy log
1370
	 *
1371
	 * @param \SS_HTTPRequest $request
1372
	 *
1373
	 * @return string
1374
	 * @throws SS_HTTPResponse_Exception
1375
	 */
1376
	public function deploylog(\SS_HTTPRequest $request) {
1377
		$params = $request->params();
1378
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1379
1380
		if (!$deployment || !$deployment->ID) {
1381
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1382
		}
1383
		if (!$deployment->canView()) {
1384
			return Security::permissionFailure();
1385
		}
1386
1387
		$environment = $deployment->Environment();
1388
		$project = $environment->Project();
1389
1390
		if ($environment->Name != $params['Environment']) {
1391
			throw new LogicException("Environment in URL doesn't match this deploy");
1392
		}
1393
		if ($project->Name != $params['Project']) {
1394
			throw new LogicException("Project in URL doesn't match this deploy");
1395
		}
1396
1397
		$log = $deployment->log();
1398
		if ($log->exists()) {
1399
			$content = $log->content();
1400
		} else {
1401
			$content = 'Waiting for action to start';
1402
		}
1403
1404
		return $this->sendResponse($deployment->ResqueStatus(), $content);
1405
	}
1406
1407
	public function abortDeploy(\SS_HTTPRequest $request) {
1408
		$params = $request->params();
1409
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1410
1411
		if (!$deployment || !$deployment->ID) {
1412
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1413
		}
1414
		if (!$deployment->canView()) {
1415
			return Security::permissionFailure();
1416
		}
1417
1418
		// For now restrict to ADMINs only.
1419
		if (!Permission::check('ADMIN')) {
1420
			return Security::permissionFailure();
1421
		}
1422
1423
		$environment = $deployment->Environment();
1424
		$project = $environment->Project();
1425
1426
		if ($environment->Name != $params['Environment']) {
1427
			throw new LogicException("Environment in URL doesn't match this deploy");
1428
		}
1429
		if ($project->Name != $params['Project']) {
1430
			throw new LogicException("Project in URL doesn't match this deploy");
1431
		}
1432
1433
		if (!in_array($deployment->Status, ['Queued', 'Deploying', 'Aborting'])) {
1434
			throw new LogicException(sprintf("Cannot abort from %s state.", $deployment->Status));
1435
		}
1436
1437
		$deployment->getMachine()->apply(DNDeployment::TR_ABORT);
1438
1439
		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...
1440
	}
1441
1442
	/**
1443
	 * @param \SS_HTTPRequest|null $request
1444
	 *
1445
	 * @return Form
1446
	 */
1447
	public function getDataTransferForm(\SS_HTTPRequest $request = null) {
1448
		// Performs canView permission check by limiting visible projects
1449
		$envs = $this->getCurrentProject()->DNEnvironmentList()->filterByCallback(function ($item) {
1450
			return $item->canBackup();
1451
		});
1452
1453
		if (!$envs) {
1454
			return $this->environment404Response();
1455
		}
1456
1457
		$items = [];
1458
		$disabledEnvironments = [];
1459 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...
1460
			$items[$env->ID] = $env->Title;
1461
			if ($env->CurrentBuild() === false) {
1462
				$items[$env->ID] = sprintf("%s - requires initial deployment", $env->Title);
1463
				$disabledEnvironments[] = $env->ID;
1464
			}
1465
		}
1466
1467
		$envsField =  DropdownField::create('EnvironmentID', 'Environment', $items)
1468
			->setEmptyString('Select an environment');
1469
		$envsField->setDisabledItems($disabledEnvironments);
1470
1471
		$formAction = FormAction::create('doDataTransfer', 'Create')
1472
			->addExtraClass('btn');
1473
1474
		if (count($disabledEnvironments) === $envs->count()) {
1475
			$formAction->setDisabled(true);
1476
		}
1477
1478
		$form = Form::create(
1479
			$this,
1480
			'DataTransferForm',
1481
			FieldList::create(
1482
				HiddenField::create('Direction', null, 'get'),
1483
				$envsField,
1484
				DropdownField::create('Mode', 'Transfer', DNDataArchive::get_mode_map())
1485
			),
1486
			FieldList::create($formAction)
1487
		);
1488
		$form->setFormAction($this->getRequest()->getURL() . '/DataTransferForm');
1489
1490
		return $form;
1491
	}
1492
1493
	/**
1494
	 * @param array $data
1495
	 * @param Form $form
1496
	 *
1497
	 * @return SS_HTTPResponse
1498
	 * @throws SS_HTTPResponse_Exception
1499
	 */
1500
	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...
1501
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1502
1503
		// Performs canView permission check by limiting visible projects
1504
		$project = $this->getCurrentProject();
1505
		if (!$project) {
1506
			return $this->project404Response();
1507
		}
1508
1509
		$dataArchive = null;
1510
1511
		// Validate direction.
1512
		if ($data['Direction'] == 'get') {
1513
			$validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1514
				->filterByCallback(function ($item) {
1515
					return $item->canBackup();
1516
				});
1517
		} else if ($data['Direction'] == 'push') {
1518
			$validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1519
				->filterByCallback(function ($item) {
1520
					return $item->canRestore();
1521
				});
1522
		} else {
1523
			throw new LogicException('Invalid direction');
1524
		}
1525
1526
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
1527
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
1528
		if (!$environment) {
1529
			throw new LogicException('Invalid environment');
1530
		}
1531
1532
		$this->validateSnapshotMode($data['Mode']);
1533
1534
		// Only 'push' direction is allowed an association with an existing archive.
1535
		if (
1536
			$data['Direction'] == 'push'
1537
			&& isset($data['DataArchiveID'])
1538
			&& is_numeric($data['DataArchiveID'])
1539
		) {
1540
			$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1541
			if (!$dataArchive) {
1542
				throw new LogicException('Invalid data archive');
1543
			}
1544
1545
			if (!$dataArchive->canDownload()) {
1546
				throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1547
			}
1548
		}
1549
1550
		$transfer = DNDataTransfer::create();
1551
		$transfer->EnvironmentID = $environment->ID;
1552
		$transfer->Direction = $data['Direction'];
1553
		$transfer->Mode = $data['Mode'];
1554
		$transfer->DataArchiveID = $dataArchive ? $dataArchive->ID : null;
1555
		if ($data['Direction'] == 'push') {
1556
			$transfer->setBackupBeforePush(!empty($data['BackupBeforePush']));
1557
		}
1558
		$transfer->write();
1559
		$transfer->start();
1560
1561
		return $this->redirect($transfer->Link());
1562
	}
1563
1564
	/**
1565
	 * View into the log for a {@link DNDataTransfer}.
1566
	 *
1567
	 * @param \SS_HTTPRequest $request
1568
	 *
1569
	 * @return SS_HTTPResponse|string
1570
	 * @throws SS_HTTPResponse_Exception
1571
	 */
1572
	public function transfer(\SS_HTTPRequest $request) {
1573
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1574
1575
		$params = $request->params();
1576
		$transfer = DNDataTransfer::get()->byId($params['Identifier']);
1577
1578
		if (!$transfer || !$transfer->ID) {
1579
			throw new SS_HTTPResponse_Exception('Transfer not found', 404);
1580
		}
1581
		if (!$transfer->canView()) {
1582
			return Security::permissionFailure();
1583
		}
1584
1585
		$environment = $transfer->Environment();
1586
		$project = $environment->Project();
1587
1588
		if ($project->Name != $params['Project']) {
1589
			throw new LogicException("Project in URL doesn't match this deploy");
1590
		}
1591
1592
		return $this->render([
1593
			'CurrentTransfer' => $transfer,
1594
			'SnapshotsSection' => 1,
1595
		]);
1596
	}
1597
1598
	/**
1599
	 * Action - Get the latest deploy log
1600
	 *
1601
	 * @param \SS_HTTPRequest $request
1602
	 *
1603
	 * @return string
1604
	 * @throws SS_HTTPResponse_Exception
1605
	 */
1606
	public function transferlog(\SS_HTTPRequest $request) {
1607
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1608
1609
		$params = $request->params();
1610
		$transfer = DNDataTransfer::get()->byId($params['Identifier']);
1611
1612
		if (!$transfer || !$transfer->ID) {
1613
			throw new SS_HTTPResponse_Exception('Transfer not found', 404);
1614
		}
1615
		if (!$transfer->canView()) {
1616
			return Security::permissionFailure();
1617
		}
1618
1619
		$environment = $transfer->Environment();
1620
		$project = $environment->Project();
1621
1622
		if ($project->Name != $params['Project']) {
1623
			throw new LogicException("Project in URL doesn't match this deploy");
1624
		}
1625
1626
		$log = $transfer->log();
1627
		if ($log->exists()) {
1628
			$content = $log->content();
1629
		} else {
1630
			$content = 'Waiting for action to start';
1631
		}
1632
1633
		return $this->sendResponse($transfer->ResqueStatus(), $content);
1634
	}
1635
1636
	/**
1637
	 * Note: Submits to the same action as {@link getDataTransferForm()},
1638
	 * but with a Direction=push and an archive reference.
1639
	 *
1640
	 * @param \SS_HTTPRequest $request
1641
	 * @param \DNDataArchive|null $dataArchive Only set when method is called manually in {@link restore()},
1642
	 *                            otherwise the state is inferred from the request data.
1643
	 * @return Form
1644
	 */
1645
	public function getDataTransferRestoreForm(\SS_HTTPRequest $request, \DNDataArchive $dataArchive = null) {
1646
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1647
1648
		// Performs canView permission check by limiting visible projects
1649
		$project = $this->getCurrentProject();
1650
		$envs = $project->DNEnvironmentList()->filterByCallback(function ($item) {
1651
			return $item->canRestore();
1652
		});
1653
1654
		if (!$envs) {
1655
			return $this->environment404Response();
1656
		}
1657
1658
		$modesMap = [];
1659
		if (in_array($dataArchive->Mode, ['all'])) {
1660
			$modesMap['all'] = 'Database and Assets';
1661
		};
1662
		if (in_array($dataArchive->Mode, ['all', 'db'])) {
1663
			$modesMap['db'] = 'Database only';
1664
		};
1665
		if (in_array($dataArchive->Mode, ['all', 'assets'])) {
1666
			$modesMap['assets'] = 'Assets only';
1667
		};
1668
1669
		$alertMessage = '<div class="alert alert-warning"><strong>Warning:</strong> '
1670
			. 'This restore will overwrite the data on the chosen environment below</div>';
1671
1672
1673
		$items = [];
1674
		$disabledEnvironments = [];
1675 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...
1676
			$items[$env->ID] = $env->Title;
1677
			if ($env->CurrentBuild() === false) {
1678
				$items[$env->ID] = sprintf("%s - requires initial deployment", $env->Title);
1679
				$disabledEnvironments[] = $env->ID;
1680
			}
1681
		}
1682
1683
		$envsField = DropdownField::create('EnvironmentID', 'Environment', $items)
1684
			->setEmptyString('Select an environment');
1685
		$envsField->setDisabledItems($disabledEnvironments);
1686
		$formAction = FormAction::create('doDataTransfer', 'Restore Data')->addExtraClass('btn');
1687
1688
		if (count($disabledEnvironments) == $envs->count()) {
1689
			$formAction->setDisabled(true);
1690
		}
1691
1692
		$form = Form::create(
1693
			$this,
1694
			'DataTransferRestoreForm',
1695
			FieldList::create(
1696
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1697
				HiddenField::create('Direction', null, 'push'),
1698
				LiteralField::create('Warning', $alertMessage),
1699
				$envsField,
1700
				DropdownField::create('Mode', 'Transfer', $modesMap),
1701
				CheckboxField::create('BackupBeforePush', 'Backup existing data', '1')
1702
			),
1703
			FieldList::create($formAction)
1704
		);
1705
		$form->setFormAction($project->Link() . '/DataTransferRestoreForm');
1706
1707
		return $form;
1708
	}
1709
1710
	/**
1711
	 * View a form to restore a specific {@link DataArchive}.
1712
	 * Permission checks are handled in {@link DataArchives()}.
1713
	 * Submissions are handled through {@link doDataTransfer()}, same as backup operations.
1714
	 *
1715
	 * @param \SS_HTTPRequest $request
1716
	 *
1717
	 * @return HTMLText
1718
	 * @throws SS_HTTPResponse_Exception
1719
	 */
1720 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...
1721
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1722
1723
		/** @var DNDataArchive $dataArchive */
1724
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1725
1726
		if (!$dataArchive) {
1727
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1728
		}
1729
1730
		// We check for canDownload because that implies access to the data.
1731
		// canRestore is later checked on the actual restore action per environment.
1732
		if (!$dataArchive->canDownload()) {
1733
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1734
		}
1735
1736
		$form = $this->getDataTransferRestoreForm($this->request, $dataArchive);
1737
1738
		// View currently only available via ajax
1739
		return $form->forTemplate();
1740
	}
1741
1742
	/**
1743
	 * View a form to delete a specific {@link DataArchive}.
1744
	 * Permission checks are handled in {@link DataArchives()}.
1745
	 * Submissions are handled through {@link doDelete()}.
1746
	 *
1747
	 * @param \SS_HTTPRequest $request
1748
	 *
1749
	 * @return HTMLText
1750
	 * @throws SS_HTTPResponse_Exception
1751
	 */
1752 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...
1753
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1754
1755
		/** @var DNDataArchive $dataArchive */
1756
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1757
1758
		if (!$dataArchive) {
1759
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1760
		}
1761
1762
		if (!$dataArchive->canDelete()) {
1763
			throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403);
1764
		}
1765
1766
		$form = $this->getDeleteForm($this->request, $dataArchive);
1767
1768
		// View currently only available via ajax
1769
		return $form->forTemplate();
1770
	}
1771
1772
	/**
1773
	 * @param \SS_HTTPRequest $request
1774
	 * @param DNDataArchive|null $dataArchive Only set when method is called manually, otherwise the state is inferred
1775
	 *        from the request data.
1776
	 * @return Form
1777
	 */
1778
	public function getDeleteForm(\SS_HTTPRequest $request, \DNDataArchive $dataArchive = null) {
1779
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1780
1781
		// Performs canView permission check by limiting visible projects
1782
		$project = $this->getCurrentProject();
1783
		if (!$project) {
1784
			return $this->project404Response();
1785
		}
1786
1787
		$snapshotDeleteWarning = '<div class="alert alert-warning">'
1788
			. 'Are you sure you want to permanently delete this snapshot from this archive area?'
1789
			. '</div>';
1790
1791
		$form = Form::create(
1792
			$this,
1793
			'DeleteForm',
1794
			FieldList::create(
1795
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1796
				LiteralField::create('Warning', $snapshotDeleteWarning)
1797
			),
1798
			FieldList::create(
1799
				FormAction::create('doDelete', 'Delete')
1800
					->addExtraClass('btn')
1801
			)
1802
		);
1803
		$form->setFormAction($project->Link() . '/DeleteForm');
1804
1805
		return $form;
1806
	}
1807
1808
	/**
1809
	 * @param array $data
1810
	 * @param Form $form
1811
	 *
1812
	 * @return bool|SS_HTTPResponse
1813
	 * @throws SS_HTTPResponse_Exception
1814
	 */
1815
	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...
1816
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1817
1818
		// Performs canView permission check by limiting visible projects
1819
		$project = $this->getCurrentProject();
1820
		if (!$project) {
1821
			return $this->project404Response();
1822
		}
1823
1824
		$dataArchive = null;
1825
1826
		if (
1827
			isset($data['DataArchiveID'])
1828
			&& is_numeric($data['DataArchiveID'])
1829
		) {
1830
			$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1831
		}
1832
1833
		if (!$dataArchive) {
1834
			throw new LogicException('Invalid data archive');
1835
		}
1836
1837
		if (!$dataArchive->canDelete()) {
1838
			throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403);
1839
		}
1840
1841
		$dataArchive->delete();
1842
1843
		return $this->redirectBack();
1844
	}
1845
1846
	/**
1847
	 * View a form to move a specific {@link DataArchive}.
1848
	 *
1849
	 * @param \SS_HTTPRequest $request
1850
	 *
1851
	 * @return HTMLText
1852
	 * @throws SS_HTTPResponse_Exception
1853
	 */
1854 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...
1855
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1856
1857
		/** @var DNDataArchive $dataArchive */
1858
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1859
1860
		if (!$dataArchive) {
1861
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1862
		}
1863
1864
		// We check for canDownload because that implies access to the data.
1865
		if (!$dataArchive->canDownload()) {
1866
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1867
		}
1868
1869
		$form = $this->getMoveForm($this->request, $dataArchive);
1870
1871
		// View currently only available via ajax
1872
		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...
1873
	}
1874
1875
	/**
1876
	 * Build snapshot move form.
1877
	 *
1878
	 * @param \SS_HTTPRequest $request
1879
	 * @param DNDataArchive|null $dataArchive
1880
	 *
1881
	 * @return Form|SS_HTTPResponse
1882
	 */
1883
	public function getMoveForm(\SS_HTTPRequest $request, \DNDataArchive $dataArchive = null) {
1884
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1885
1886
		$envs = $dataArchive->validTargetEnvironments();
1887
		if (!$envs) {
1888
			return $this->environment404Response();
1889
		}
1890
1891
		$warningMessage = '<div class="alert alert-warning"><strong>Warning:</strong> This will make the snapshot '
1892
			. 'available to people with access to the target environment.<br>By pressing "Change ownership" you '
1893
			. 'confirm that you have considered data confidentiality regulations.</div>';
1894
1895
		$form = Form::create(
1896
			$this,
1897
			'MoveForm',
1898
			FieldList::create(
1899
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1900
				LiteralField::create('Warning', $warningMessage),
1901
				DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1902
					->setEmptyString('Select an environment')
1903
			),
1904
			FieldList::create(
1905
				FormAction::create('doMove', 'Change ownership')
1906
					->addExtraClass('btn')
1907
			)
1908
		);
1909
		$form->setFormAction($this->getCurrentProject()->Link() . '/MoveForm');
1910
1911
		return $form;
1912
	}
1913
1914
	/**
1915
	 * @param array $data
1916
	 * @param Form $form
1917
	 *
1918
	 * @return bool|SS_HTTPResponse
1919
	 * @throws SS_HTTPResponse_Exception
1920
	 * @throws ValidationException
1921
	 * @throws null
1922
	 */
1923
	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...
1924
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1925
1926
		// Performs canView permission check by limiting visible projects
1927
		$project = $this->getCurrentProject();
1928
		if (!$project) {
1929
			return $this->project404Response();
1930
		}
1931
1932
		/** @var DNDataArchive $dataArchive */
1933
		$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1934
		if (!$dataArchive) {
1935
			throw new LogicException('Invalid data archive');
1936
		}
1937
1938
		// We check for canDownload because that implies access to the data.
1939
		if (!$dataArchive->canDownload()) {
1940
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1941
		}
1942
1943
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
1944
		$validEnvs = $dataArchive->validTargetEnvironments();
1945
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
1946
		if (!$environment) {
1947
			throw new LogicException('Invalid environment');
1948
		}
1949
1950
		$dataArchive->EnvironmentID = $environment->ID;
1951
		$dataArchive->write();
1952
1953
		return $this->redirectBack();
1954
	}
1955
1956
	/**
1957
	 * Returns an error message if redis is unavailable
1958
	 *
1959
	 * @return string
1960
	 */
1961
	public static function RedisUnavailable() {
1962
		try {
1963
			Resque::queues();
1964
		} catch (Exception $e) {
1965
			return $e->getMessage();
1966
		}
1967
		return '';
1968
	}
1969
1970
	/**
1971
	 * Returns the number of connected Redis workers
1972
	 *
1973
	 * @return int
1974
	 */
1975
	public static function RedisWorkersCount() {
1976
		return count(Resque_Worker::all());
1977
	}
1978
1979
	/**
1980
	 * @return array
1981
	 */
1982
	public function providePermissions() {
1983
		return [
1984
			self::DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS => [
1985
				'name' => "Access to advanced deploy options",
1986
				'category' => "Deploynaut",
1987
			],
1988
1989
			// Permissions that are intended to be added to the roles.
1990
			self::ALLOW_PROD_DEPLOYMENT => [
1991
				'name' => "Ability to deploy to production environments",
1992
				'category' => "Deploynaut",
1993
			],
1994
			self::ALLOW_NON_PROD_DEPLOYMENT => [
1995
				'name' => "Ability to deploy to non-production environments",
1996
				'category' => "Deploynaut",
1997
			],
1998
			self::ALLOW_PROD_SNAPSHOT => [
1999
				'name' => "Ability to make production snapshots",
2000
				'category' => "Deploynaut",
2001
			],
2002
			self::ALLOW_NON_PROD_SNAPSHOT => [
2003
				'name' => "Ability to make non-production snapshots",
2004
				'category' => "Deploynaut",
2005
			],
2006
			self::ALLOW_CREATE_ENVIRONMENT => [
2007
				'name' => "Ability to create environments",
2008
				'category' => "Deploynaut",
2009
			],
2010
		];
2011
	}
2012
2013
	/**
2014
	 * @return DNProject|null
2015
	 */
2016
	public function getCurrentProject() {
2017
		$projectName = trim($this->getRequest()->param('Project'));
2018
		if (!$projectName) {
2019
			return null;
2020
		}
2021
		if (empty(self::$_project_cache[$projectName])) {
2022
			self::$_project_cache[$projectName] = $this->DNProjectList()->filter('Name', $projectName)->First();
2023
		}
2024
		return self::$_project_cache[$projectName];
2025
	}
2026
2027
	/**
2028
	 * @param \DNProject|null $project
2029
	 * @return \DNEnvironment|null
2030
	 */
2031
	public function getCurrentEnvironment(\DNProject $project = null) {
2032
		if ($this->getRequest()->param('Environment') === null) {
2033
			return null;
2034
		}
2035
		if ($project === null) {
2036
			$project = $this->getCurrentProject();
2037
		}
2038
		// project can still be null
2039
		if ($project === null) {
2040
			return null;
2041
		}
2042
		return $project->DNEnvironmentList()->filter('Name', $this->getRequest()->param('Environment'))->First();
2043
	}
2044
2045
	/**
2046
	 * This will return a const that indicates the class of action currently being performed
2047
	 *
2048
	 * Until DNRoot is de-godded, it does a bunch of different actions all in the same class.
2049
	 * So we just have each action handler calll setCurrentActionType to define what sort of
2050
	 * action it is.
2051
	 *
2052
	 * @return string - one of the consts from self::$action_types
2053
	 */
2054
	public function getCurrentActionType() {
2055
		return $this->actionType;
2056
	}
2057
2058
	/**
2059
	 * Sets the current action type
2060
	 *
2061
	 * @param string $actionType string - one of the consts from self::$action_types
2062
	 */
2063
	public function setCurrentActionType($actionType) {
2064
		$this->actionType = $actionType;
2065
	}
2066
2067
	/**
2068
	 * Helper method to allow templates to know whether they should show the 'Archive List' include or not.
2069
	 * The actual permissions are set on a per-environment level, so we need to find out if this $member can upload to
2070
	 * or download from *any* {@link DNEnvironment} that (s)he has access to.
2071
	 *
2072
	 * TODO To be replaced with a method that just returns the list of archives this {@link Member} has access to.
2073
	 *
2074
	 * @param Member|null $member The {@link Member} to check (or null to check the currently logged in Member)
2075
	 * @return boolean|null true if $member has access to upload or download to at least one {@link DNEnvironment}.
2076
	 */
2077
	public function CanViewArchives(\Member $member = null) {
2078
		if ($member === null) {
2079
			$member = Member::currentUser();
2080
		}
2081
2082
		if (Permission::checkMember($member, 'ADMIN')) {
2083
			return true;
2084
		}
2085
2086
		$allProjects = $this->DNProjectList();
2087
		if (!$allProjects) {
2088
			return false;
2089
		}
2090
2091
		foreach ($allProjects as $project) {
2092
			if ($project->Environments()) {
2093
				foreach ($project->Environments() as $environment) {
2094
					if (
2095
						$environment->canRestore($member) ||
2096
						$environment->canBackup($member) ||
2097
						$environment->canUploadArchive($member) ||
2098
						$environment->canDownloadArchive($member)
2099
					) {
2100
						// We can return early as we only need to know that we can access one environment
2101
						return true;
2102
					}
2103
				}
2104
			}
2105
		}
2106
	}
2107
2108
	/**
2109
	 * Returns a list of attempted environment creations.
2110
	 *
2111
	 * @return PaginatedList
2112
	 */
2113
	public function CreateEnvironmentList() {
2114
		$project = $this->getCurrentProject();
2115
		if ($project) {
2116
			$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...
2117
		} else {
2118
			$dataList = new ArrayList();
2119
		}
2120
2121
		$this->extend('updateCreateEnvironmentList', $dataList);
2122
		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...
2123
	}
2124
2125
	/**
2126
	 * Returns a list of all archive files that can be accessed by the currently logged-in {@link Member}
2127
	 *
2128
	 * @return PaginatedList
2129
	 */
2130
	public function CompleteDataArchives() {
2131
		$project = $this->getCurrentProject();
2132
		$archives = new ArrayList();
2133
2134
		$archiveList = $project->Environments()->relation("DataArchives");
2135
		if ($archiveList->count() > 0) {
2136
			foreach ($archiveList as $archive) {
2137
				if (!$archive->isPending()) {
2138
					$archives->push($archive);
2139
				}
2140
			}
2141
		}
2142
		return new PaginatedList($archives->sort("Created", "DESC"), $this->request);
0 ignored issues
show
Documentation introduced by
$this->request is of type object<SS_HTTPRequest>, but the function expects a array.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2143
	}
2144
2145
	/**
2146
	 * @return PaginatedList The list of "pending" data archives which are waiting for a file
2147
	 * to be delivered offline by post, and manually uploaded into the system.
2148
	 */
2149
	public function PendingDataArchives() {
2150
		$project = $this->getCurrentProject();
2151
		$archives = new ArrayList();
2152
		foreach ($project->DNEnvironmentList() as $env) {
2153
			foreach ($env->DataArchives() as $archive) {
2154
				if ($archive->isPending()) {
2155
					$archives->push($archive);
2156
				}
2157
			}
2158
		}
2159
		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...
2160
	}
2161
2162
	/**
2163
	 * @return PaginatedList
2164
	 */
2165
	public function DataTransferLogs() {
2166
		$environments = $this->getCurrentProject()->Environments()->column('ID');
2167
		$transfers = DNDataTransfer::get()
2168
			->filter('EnvironmentID', $environments)
2169
			->filterByCallback(
2170
				function ($record) {
2171
					return
2172
						$record->Environment()->canRestore() || // Ensure member can perform an action on the transfers env
2173
						$record->Environment()->canBackup() ||
2174
						$record->Environment()->canUploadArchive() ||
2175
						$record->Environment()->canDownloadArchive();
2176
				});
2177
2178
		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...
2179
	}
2180
2181
	/**
2182
	 * @deprecated 2.0.0 - moved to DeployDispatcher
2183
	 *
2184
	 * @return null|PaginatedList
2185
	 */
2186
	public function DeployHistory() {
2187
		if ($env = $this->getCurrentEnvironment()) {
2188
			$history = $env->DeployHistory();
2189
			if ($history->count() > 0) {
2190
				$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...
2191
				$pagination->setPageLength(4);
2192
				return $pagination;
2193
			}
2194
		}
2195
		return null;
2196
	}
2197
2198
	/**
2199
	 * @param string $status
2200
	 * @param string $content
2201
	 *
2202
	 * @return string
2203
	 */
2204
	public function sendResponse($status, $content) {
2205
		// strip excessive newlines
2206
		$content = preg_replace('/(?:(?:\r\n|\r|\n)\s*){2}/s', "\n", $content);
2207
2208
		$sendJSON = (strpos($this->getRequest()->getHeader('Accept'), 'application/json') !== false)
2209
			|| $this->getRequest()->getExtension() == 'json';
2210
2211
		if (!$sendJSON) {
2212
			$this->response->addHeader("Content-type", "text/plain");
2213
			return $content;
2214
		}
2215
		$this->response->addHeader("Content-type", "application/json");
2216
		return json_encode([
2217
			'status' => $status,
2218
			'content' => $content,
2219
		]);
2220
	}
2221
2222
	/**
2223
	 * Get items for the ambient menu that should be accessible from all pages.
2224
	 *
2225
	 * @return ArrayList
2226
	 */
2227
	public function AmbientMenu() {
2228
		$list = new ArrayList();
2229
2230
		if (Member::currentUserID()) {
2231
			$list->push(new ArrayData([
2232
				'Classes' => 'logout',
2233
				'FaIcon' => 'sign-out',
2234
				'Link' => 'Security/logout',
2235
				'Title' => 'Log out',
2236
				'IsCurrent' => false,
2237
				'IsSection' => false
2238
			]));
2239
		}
2240
2241
		$this->extend('updateAmbientMenu', $list);
2242
		return $list;
2243
	}
2244
2245
	/**
2246
	 * Checks whether the user can create a project.
2247
	 *
2248
	 * @return bool
2249
	 */
2250
	public function canCreateProjects($member = null) {
2251
		if (!$member) {
2252
			$member = Member::currentUser();
2253
		}
2254
		if (!$member) {
2255
			return false;
2256
		}
2257
2258
		return singleton('DNProject')->canCreate($member);
2259
	}
2260
2261
	protected function applyRedeploy(\SS_HTTPRequest $request, &$data) {
2262
		if (!$request->getVar('redeploy')) {
2263
			return;
2264
		}
2265
2266
		$project = $this->getCurrentProject();
2267
		if (!$project) {
2268
			return $this->project404Response();
2269
		}
2270
2271
		// Performs canView permission check by limiting visible projects
2272
		$env = $this->getCurrentEnvironment($project);
2273
		if (!$env) {
2274
			return $this->environment404Response();
2275
		}
2276
2277
		$current = $env->CurrentBuild();
2278
		if ($current && $current->exists()) {
2279
			$data['preselect_tab'] = 3;
2280
			$data['preselect_sha'] = $current->SHA;
2281
		} else {
2282
			$master = $project->DNBranchList()->byName('master');
2283
			if ($master) {
2284
				$data['preselect_tab'] = 1;
2285
				$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...
2286
			}
2287
		}
2288
	}
2289
2290
	/**
2291
	 * @return SS_HTTPResponse
2292
	 */
2293
	protected function project404Response() {
2294
		return new SS_HTTPResponse(
2295
			"Project '" . Convert::raw2xml($this->getRequest()->param('Project')) . "' not found.",
2296
			404
2297
		);
2298
	}
2299
2300
	/**
2301
	 * @return SS_HTTPResponse
2302
	 */
2303
	protected function environment404Response() {
2304
		$envName = Convert::raw2xml($this->getRequest()->param('Environment'));
2305
		return new SS_HTTPResponse("Environment '" . $envName . "' not found.", 404);
2306
	}
2307
2308
	/**
2309
	 * Validate the snapshot mode
2310
	 *
2311
	 * @param string $mode
2312
	 */
2313
	protected function validateSnapshotMode($mode) {
2314
		if (!in_array($mode, ['all', 'assets', 'db'])) {
2315
			throw new LogicException('Invalid mode');
2316
		}
2317
	}
2318
2319
	/**
2320
	 * @param string $sectionName
2321
	 * @param string $title
2322
	 *
2323
	 * @return SS_HTTPResponse
2324
	 */
2325
	protected function getCustomisedViewSection($sectionName, $title = '', $data = []) {
2326
		// Performs canView permission check by limiting visible projects
2327
		$project = $this->getCurrentProject();
2328
		if (!$project) {
2329
			return $this->project404Response();
2330
		}
2331
		$data[$sectionName] = 1;
2332
2333
		if ($this !== '') {
2334
			$data['Title'] = $title;
2335
		}
2336
2337
		return $this->render($data);
2338
	}
2339
2340
}
2341
2342