Completed
Pull Request — master (#684)
by Stig
06:00
created

DNRoot::getMoveForm()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 30
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1464
	}
1465
1466
	/**
1467
	 * @param SS_HTTPRequest|null $request
1468
	 *
1469
	 * @return Form
1470
	 */
1471
	public function getDataTransferForm(SS_HTTPRequest $request = null) {
1472
		// Performs canView permission check by limiting visible projects
1473
		$envs = $this->getCurrentProject()->DNEnvironmentList()->filterByCallback(function ($item) {
1474
			return $item->canBackup();
1475
		});
1476
1477
		if (!$envs) {
1478
			return $this->environment404Response();
1479
		}
1480
1481
		// remove environments that hasn't yet been deployed to
1482
		$validEnvs = ArrayList::create();
1483
		foreach($envs as $env) {
1484
			if ($env->CurrentBuild() !== false) {
1485
				$validEnvs->Add($env);
1486
			}
1487
		}
1488
1489
		$envsField =  DropdownField::create('EnvironmentID', 'Environment', $validEnvs->map())
1490
			->setEmptyString('Select an environment');
1491
		$formAction = FormAction::create('doDataTransfer', 'Create')
1492
			->addExtraClass('btn');
1493
1494 View Code Duplication
		if ($validEnvs->count() < 1) {
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...
1495
			$envsField->setEmptyString('You need to deploy to an environment before creating a snapshot');
1496
			$envsField->setDisabled(true);
1497
			$formAction->setDisabled(true);
1498
		}
1499
1500
		$form = Form::create(
1501
			$this,
1502
			'DataTransferForm',
1503
			FieldList::create(
1504
				HiddenField::create('Direction', null, 'get'),
1505
				$envsField,
1506
				DropdownField::create('Mode', 'Transfer', DNDataArchive::get_mode_map())
1507
			),
1508
			FieldList::create($formAction)
1509
		);
1510
		$form->setFormAction($this->getRequest()->getURL() . '/DataTransferForm');
1511
1512
		return $form;
1513
	}
1514
1515
	/**
1516
	 * @param array $data
1517
	 * @param Form $form
1518
	 *
1519
	 * @return SS_HTTPResponse
1520
	 * @throws SS_HTTPResponse_Exception
1521
	 */
1522
	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...
1523
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1524
1525
		// Performs canView permission check by limiting visible projects
1526
		$project = $this->getCurrentProject();
1527
		if (!$project) {
1528
			return $this->project404Response();
1529
		}
1530
1531
		$dataArchive = null;
1532
1533
		// Validate direction.
1534
		if ($data['Direction'] == 'get') {
1535
			$validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1536
				->filterByCallback(function ($item) {
1537
					return $item->canBackup();
1538
				});
1539
		} else if ($data['Direction'] == 'push') {
1540
			$validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1541
				->filterByCallback(function ($item) {
1542
					return $item->canRestore();
1543
				});
1544
		} else {
1545
			throw new LogicException('Invalid direction');
1546
		}
1547
1548
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
1549
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
1550
		if (!$environment) {
1551
			throw new LogicException('Invalid environment');
1552
		}
1553
1554
		$this->validateSnapshotMode($data['Mode']);
1555
1556
		// Only 'push' direction is allowed an association with an existing archive.
1557
		if (
1558
			$data['Direction'] == 'push'
1559
			&& isset($data['DataArchiveID'])
1560
			&& is_numeric($data['DataArchiveID'])
1561
		) {
1562
			$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1563
			if (!$dataArchive) {
1564
				throw new LogicException('Invalid data archive');
1565
			}
1566
1567
			if (!$dataArchive->canDownload()) {
1568
				throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1569
			}
1570
		}
1571
1572
		$transfer = DNDataTransfer::create();
1573
		$transfer->EnvironmentID = $environment->ID;
1574
		$transfer->Direction = $data['Direction'];
1575
		$transfer->Mode = $data['Mode'];
1576
		$transfer->DataArchiveID = $dataArchive ? $dataArchive->ID : null;
1577
		if ($data['Direction'] == 'push') {
1578
			$transfer->setBackupBeforePush(!empty($data['BackupBeforePush']));
1579
		}
1580
		$transfer->write();
1581
		$transfer->start();
1582
1583
		return $this->redirect($transfer->Link());
1584
	}
1585
1586
	/**
1587
	 * View into the log for a {@link DNDataTransfer}.
1588
	 *
1589
	 * @param SS_HTTPRequest $request
1590
	 *
1591
	 * @return SS_HTTPResponse|string
1592
	 * @throws SS_HTTPResponse_Exception
1593
	 */
1594
	public function transfer(SS_HTTPRequest $request) {
1595
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1596
1597
		$params = $request->params();
1598
		$transfer = DNDataTransfer::get()->byId($params['Identifier']);
1599
1600
		if (!$transfer || !$transfer->ID) {
1601
			throw new SS_HTTPResponse_Exception('Transfer not found', 404);
1602
		}
1603
		if (!$transfer->canView()) {
1604
			return Security::permissionFailure();
1605
		}
1606
1607
		$environment = $transfer->Environment();
1608
		$project = $environment->Project();
1609
1610
		if ($project->Name != $params['Project']) {
1611
			throw new LogicException("Project in URL doesn't match this deploy");
1612
		}
1613
1614
		return $this->render([
1615
			'CurrentTransfer' => $transfer,
1616
			'SnapshotsSection' => 1,
1617
		]);
1618
	}
1619
1620
	/**
1621
	 * Action - Get the latest deploy log
1622
	 *
1623
	 * @param SS_HTTPRequest $request
1624
	 *
1625
	 * @return string
1626
	 * @throws SS_HTTPResponse_Exception
1627
	 */
1628
	public function transferlog(SS_HTTPRequest $request) {
1629
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1630
1631
		$params = $request->params();
1632
		$transfer = DNDataTransfer::get()->byId($params['Identifier']);
1633
1634
		if (!$transfer || !$transfer->ID) {
1635
			throw new SS_HTTPResponse_Exception('Transfer not found', 404);
1636
		}
1637
		if (!$transfer->canView()) {
1638
			return Security::permissionFailure();
1639
		}
1640
1641
		$environment = $transfer->Environment();
1642
		$project = $environment->Project();
1643
1644
		if ($project->Name != $params['Project']) {
1645
			throw new LogicException("Project in URL doesn't match this deploy");
1646
		}
1647
1648
		$log = $transfer->log();
1649
		if ($log->exists()) {
1650
			$content = $log->content();
1651
		} else {
1652
			$content = 'Waiting for action to start';
1653
		}
1654
1655
		return $this->sendResponse($transfer->ResqueStatus(), $content);
1656
	}
1657
1658
	/**
1659
	 * Note: Submits to the same action as {@link getDataTransferForm()},
1660
	 * but with a Direction=push and an archive reference.
1661
	 *
1662
	 * @param SS_HTTPRequest $request
1663
	 * @param DNDataArchive|null $dataArchive Only set when method is called manually in {@link restore()},
1664
	 *                            otherwise the state is inferred from the request data.
1665
	 * @return Form
1666
	 */
1667
	public function getDataTransferRestoreForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) {
1668
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1669
1670
		// Performs canView permission check by limiting visible projects
1671
		$project = $this->getCurrentProject();
1672
		$envs = $project->DNEnvironmentList()->filterByCallback(function ($item) {
1673
			return $item->canRestore();
1674
		});
1675
1676
		if (!$envs) {
1677
			return $this->environment404Response();
1678
		}
1679
1680
		$modesMap = [];
1681
		if (in_array($dataArchive->Mode, ['all'])) {
1682
			$modesMap['all'] = 'Database and Assets';
1683
		};
1684
		if (in_array($dataArchive->Mode, ['all', 'db'])) {
1685
			$modesMap['db'] = 'Database only';
1686
		};
1687
		if (in_array($dataArchive->Mode, ['all', 'assets'])) {
1688
			$modesMap['assets'] = 'Assets only';
1689
		};
1690
1691
		$alertMessage = '<div class="alert alert-warning"><strong>Warning:</strong> '
1692
			. 'This restore will overwrite the data on the chosen environment below</div>';
1693
1694
1695
		// remove environments that hasn't yet been deployed to
1696
		$validEnvs = ArrayList::create();
1697
		foreach($envs as $env) {
1698
			if ($env->CurrentBuild() !== false) {
1699
				$validEnvs->Add($env);
1700
			}
1701
		}
1702
1703
		$envsField = DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1704
			->setEmptyString('Select an environment');
1705
		$formAction = FormAction::create('doDataTransfer', 'Restore Data')->addExtraClass('btn');
1706 View Code Duplication
		if ($validEnvs->count() < 1) {
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...
1707
			$envsField->setEmptyString('You need to deploy to an environment before restoring a snapshot');
1708
			$envsField->setDisabled(true);
1709
			$formAction->setDisabled(true);
1710
		}
1711
1712
		$form = Form::create(
1713
			$this,
1714
			'DataTransferRestoreForm',
1715
			FieldList::create(
1716
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1717
				HiddenField::create('Direction', null, 'push'),
1718
				LiteralField::create('Warning', $alertMessage),
1719
				$envsField,
1720
				DropdownField::create('Mode', 'Transfer', $modesMap),
1721
				CheckboxField::create('BackupBeforePush', 'Backup existing data', '1')
1722
			),
1723
			FieldList::create($formAction)
1724
		);
1725
		$form->setFormAction($project->Link() . '/DataTransferRestoreForm');
1726
1727
		return $form;
1728
	}
1729
1730
	/**
1731
	 * View a form to restore a specific {@link DataArchive}.
1732
	 * Permission checks are handled in {@link DataArchives()}.
1733
	 * Submissions are handled through {@link doDataTransfer()}, same as backup operations.
1734
	 *
1735
	 * @param SS_HTTPRequest $request
1736
	 *
1737
	 * @return HTMLText
1738
	 * @throws SS_HTTPResponse_Exception
1739
	 */
1740 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...
1741
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1742
1743
		/** @var DNDataArchive $dataArchive */
1744
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1745
1746
		if (!$dataArchive) {
1747
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1748
		}
1749
1750
		// We check for canDownload because that implies access to the data.
1751
		// canRestore is later checked on the actual restore action per environment.
1752
		if (!$dataArchive->canDownload()) {
1753
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1754
		}
1755
1756
		$form = $this->getDataTransferRestoreForm($this->request, $dataArchive);
1757
1758
		// View currently only available via ajax
1759
		return $form->forTemplate();
1760
	}
1761
1762
	/**
1763
	 * View a form to delete a specific {@link DataArchive}.
1764
	 * Permission checks are handled in {@link DataArchives()}.
1765
	 * Submissions are handled through {@link doDelete()}.
1766
	 *
1767
	 * @param SS_HTTPRequest $request
1768
	 *
1769
	 * @return HTMLText
1770
	 * @throws SS_HTTPResponse_Exception
1771
	 */
1772 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...
1773
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1774
1775
		/** @var DNDataArchive $dataArchive */
1776
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1777
1778
		if (!$dataArchive) {
1779
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1780
		}
1781
1782
		if (!$dataArchive->canDelete()) {
1783
			throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403);
1784
		}
1785
1786
		$form = $this->getDeleteForm($this->request, $dataArchive);
1787
1788
		// View currently only available via ajax
1789
		return $form->forTemplate();
1790
	}
1791
1792
	/**
1793
	 * @param SS_HTTPRequest $request
1794
	 * @param DNDataArchive|null $dataArchive Only set when method is called manually, otherwise the state is inferred
1795
	 *        from the request data.
1796
	 * @return Form
1797
	 */
1798
	public function getDeleteForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) {
1799
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1800
1801
		// Performs canView permission check by limiting visible projects
1802
		$project = $this->getCurrentProject();
1803
		if (!$project) {
1804
			return $this->project404Response();
1805
		}
1806
1807
		$snapshotDeleteWarning = '<div class="alert alert-warning">'
1808
			. 'Are you sure you want to permanently delete this snapshot from this archive area?'
1809
			. '</div>';
1810
1811
		$form = Form::create(
1812
			$this,
1813
			'DeleteForm',
1814
			FieldList::create(
1815
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1816
				LiteralField::create('Warning', $snapshotDeleteWarning)
1817
			),
1818
			FieldList::create(
1819
				FormAction::create('doDelete', 'Delete')
1820
					->addExtraClass('btn')
1821
			)
1822
		);
1823
		$form->setFormAction($project->Link() . '/DeleteForm');
1824
1825
		return $form;
1826
	}
1827
1828
	/**
1829
	 * @param array $data
1830
	 * @param Form $form
1831
	 *
1832
	 * @return bool|SS_HTTPResponse
1833
	 * @throws SS_HTTPResponse_Exception
1834
	 */
1835
	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...
1836
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1837
1838
		// Performs canView permission check by limiting visible projects
1839
		$project = $this->getCurrentProject();
1840
		if (!$project) {
1841
			return $this->project404Response();
1842
		}
1843
1844
		$dataArchive = null;
1845
1846
		if (
1847
			isset($data['DataArchiveID'])
1848
			&& is_numeric($data['DataArchiveID'])
1849
		) {
1850
			$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1851
		}
1852
1853
		if (!$dataArchive) {
1854
			throw new LogicException('Invalid data archive');
1855
		}
1856
1857
		if (!$dataArchive->canDelete()) {
1858
			throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403);
1859
		}
1860
1861
		$dataArchive->delete();
1862
1863
		return $this->redirectBack();
1864
	}
1865
1866
	/**
1867
	 * View a form to move a specific {@link DataArchive}.
1868
	 *
1869
	 * @param SS_HTTPRequest $request
1870
	 *
1871
	 * @return HTMLText
1872
	 * @throws SS_HTTPResponse_Exception
1873
	 */
1874 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...
1875
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1876
1877
		/** @var DNDataArchive $dataArchive */
1878
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1879
1880
		if (!$dataArchive) {
1881
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1882
		}
1883
1884
		// We check for canDownload because that implies access to the data.
1885
		if (!$dataArchive->canDownload()) {
1886
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1887
		}
1888
1889
		$form = $this->getMoveForm($this->request, $dataArchive);
1890
1891
		// View currently only available via ajax
1892
		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...
1893
	}
1894
1895
	/**
1896
	 * Build snapshot move form.
1897
	 *
1898
	 * @param SS_HTTPRequest $request
1899
	 * @param DNDataArchive|null $dataArchive
1900
	 *
1901
	 * @return Form|SS_HTTPResponse
1902
	 */
1903
	public function getMoveForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) {
1904
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1905
1906
		$envs = $dataArchive->validTargetEnvironments();
1907
		if (!$envs) {
1908
			return $this->environment404Response();
1909
		}
1910
1911
		$warningMessage = '<div class="alert alert-warning"><strong>Warning:</strong> This will make the snapshot '
1912
			. 'available to people with access to the target environment.<br>By pressing "Change ownership" you '
1913
			. 'confirm that you have considered data confidentiality regulations.</div>';
1914
1915
		$form = Form::create(
1916
			$this,
1917
			'MoveForm',
1918
			FieldList::create(
1919
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1920
				LiteralField::create('Warning', $warningMessage),
1921
				DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1922
					->setEmptyString('Select an environment')
1923
			),
1924
			FieldList::create(
1925
				FormAction::create('doMove', 'Change ownership')
1926
					->addExtraClass('btn')
1927
			)
1928
		);
1929
		$form->setFormAction($this->getCurrentProject()->Link() . '/MoveForm');
1930
1931
		return $form;
1932
	}
1933
1934
	/**
1935
	 * @param array $data
1936
	 * @param Form $form
1937
	 *
1938
	 * @return bool|SS_HTTPResponse
1939
	 * @throws SS_HTTPResponse_Exception
1940
	 * @throws ValidationException
1941
	 * @throws null
1942
	 */
1943
	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...
1944
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1945
1946
		// Performs canView permission check by limiting visible projects
1947
		$project = $this->getCurrentProject();
1948
		if (!$project) {
1949
			return $this->project404Response();
1950
		}
1951
1952
		/** @var DNDataArchive $dataArchive */
1953
		$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1954
		if (!$dataArchive) {
1955
			throw new LogicException('Invalid data archive');
1956
		}
1957
1958
		// We check for canDownload because that implies access to the data.
1959
		if (!$dataArchive->canDownload()) {
1960
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1961
		}
1962
1963
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
1964
		$validEnvs = $dataArchive->validTargetEnvironments();
1965
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
1966
		if (!$environment) {
1967
			throw new LogicException('Invalid environment');
1968
		}
1969
1970
		$dataArchive->EnvironmentID = $environment->ID;
1971
		$dataArchive->write();
1972
1973
		return $this->redirectBack();
1974
	}
1975
1976
	/**
1977
	 * Returns an error message if redis is unavailable
1978
	 *
1979
	 * @return string
1980
	 */
1981
	public static function RedisUnavailable() {
1982
		try {
1983
			Resque::queues();
1984
		} catch (Exception $e) {
1985
			return $e->getMessage();
1986
		}
1987
		return '';
1988
	}
1989
1990
	/**
1991
	 * Returns the number of connected Redis workers
1992
	 *
1993
	 * @return int
1994
	 */
1995
	public static function RedisWorkersCount() {
1996
		return count(Resque_Worker::all());
1997
	}
1998
1999
	/**
2000
	 * @return array
2001
	 */
2002
	public function providePermissions() {
2003
		return [
2004
			self::DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS => [
2005
				'name' => "Access to advanced deploy options",
2006
				'category' => "Deploynaut",
2007
			],
2008
2009
			// Permissions that are intended to be added to the roles.
2010
			self::ALLOW_PROD_DEPLOYMENT => [
2011
				'name' => "Ability to deploy to production environments",
2012
				'category' => "Deploynaut",
2013
			],
2014
			self::ALLOW_NON_PROD_DEPLOYMENT => [
2015
				'name' => "Ability to deploy to non-production environments",
2016
				'category' => "Deploynaut",
2017
			],
2018
			self::ALLOW_PROD_SNAPSHOT => [
2019
				'name' => "Ability to make production snapshots",
2020
				'category' => "Deploynaut",
2021
			],
2022
			self::ALLOW_NON_PROD_SNAPSHOT => [
2023
				'name' => "Ability to make non-production snapshots",
2024
				'category' => "Deploynaut",
2025
			],
2026
			self::ALLOW_CREATE_ENVIRONMENT => [
2027
				'name' => "Ability to create environments",
2028
				'category' => "Deploynaut",
2029
			],
2030
		];
2031
	}
2032
2033
	/**
2034
	 * @return DNProject|null
2035
	 */
2036
	public function getCurrentProject() {
2037
		$projectName = trim($this->getRequest()->param('Project'));
2038
		if (!$projectName) {
2039
			return null;
2040
		}
2041
		if (empty(self::$_project_cache[$projectName])) {
2042
			self::$_project_cache[$projectName] = $this->DNProjectList()->filter('Name', $projectName)->First();
2043
		}
2044
		return self::$_project_cache[$projectName];
2045
	}
2046
2047
	/**
2048
	 * @param DNProject|null $project
2049
	 * @return DNEnvironment|null
2050
	 */
2051
	public function getCurrentEnvironment(DNProject $project = null) {
2052
		if ($this->getRequest()->param('Environment') === null) {
2053
			return null;
2054
		}
2055
		if ($project === null) {
2056
			$project = $this->getCurrentProject();
2057
		}
2058
		// project can still be null
2059
		if ($project === null) {
2060
			return null;
2061
		}
2062
		return $project->DNEnvironmentList()->filter('Name', $this->getRequest()->param('Environment'))->First();
2063
	}
2064
2065
	/**
2066
	 * This will return a const that indicates the class of action currently being performed
2067
	 *
2068
	 * Until DNRoot is de-godded, it does a bunch of different actions all in the same class.
2069
	 * So we just have each action handler calll setCurrentActionType to define what sort of
2070
	 * action it is.
2071
	 *
2072
	 * @return string - one of the consts from self::$action_types
2073
	 */
2074
	public function getCurrentActionType() {
2075
		return $this->actionType;
2076
	}
2077
2078
	/**
2079
	 * Sets the current action type
2080
	 *
2081
	 * @param string $actionType string - one of the consts from self::$action_types
2082
	 */
2083
	public function setCurrentActionType($actionType) {
2084
		$this->actionType = $actionType;
2085
	}
2086
2087
	/**
2088
	 * Helper method to allow templates to know whether they should show the 'Archive List' include or not.
2089
	 * The actual permissions are set on a per-environment level, so we need to find out if this $member can upload to
2090
	 * or download from *any* {@link DNEnvironment} that (s)he has access to.
2091
	 *
2092
	 * TODO To be replaced with a method that just returns the list of archives this {@link Member} has access to.
2093
	 *
2094
	 * @param Member|null $member The {@link Member} to check (or null to check the currently logged in Member)
2095
	 * @return boolean|null true if $member has access to upload or download to at least one {@link DNEnvironment}.
2096
	 */
2097
	public function CanViewArchives(Member $member = null) {
2098
		if ($member === null) {
2099
			$member = Member::currentUser();
2100
		}
2101
2102
		if (Permission::checkMember($member, 'ADMIN')) {
2103
			return true;
2104
		}
2105
2106
		$allProjects = $this->DNProjectList();
2107
		if (!$allProjects) {
2108
			return false;
2109
		}
2110
2111
		foreach ($allProjects as $project) {
2112
			if ($project->Environments()) {
2113
				foreach ($project->Environments() as $environment) {
2114
					if (
2115
						$environment->canRestore($member) ||
2116
						$environment->canBackup($member) ||
2117
						$environment->canUploadArchive($member) ||
2118
						$environment->canDownloadArchive($member)
2119
					) {
2120
						// We can return early as we only need to know that we can access one environment
2121
						return true;
2122
					}
2123
				}
2124
			}
2125
		}
2126
	}
2127
2128
	/**
2129
	 * Returns a list of attempted environment creations.
2130
	 *
2131
	 * @return PaginatedList
2132
	 */
2133
	public function CreateEnvironmentList() {
2134
		$project = $this->getCurrentProject();
2135
		if ($project) {
2136
			$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...
2137
		} else {
2138
			$dataList = new ArrayList();
2139
		}
2140
2141
		$this->extend('updateCreateEnvironmentList', $dataList);
2142
		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...
2143
	}
2144
2145
	/**
2146
	 * Returns a list of all archive files that can be accessed by the currently logged-in {@link Member}
2147
	 *
2148
	 * @return PaginatedList
2149
	 */
2150
	public function CompleteDataArchives() {
2151
		$project = $this->getCurrentProject();
2152
		$archives = new ArrayList();
2153
2154
		$archiveList = $project->Environments()->relation("DataArchives");
2155
		if ($archiveList->count() > 0) {
2156
			foreach ($archiveList as $archive) {
2157
				if (!$archive->isPending()) {
2158
					$archives->push($archive);
2159
				}
2160
			}
2161
		}
2162
		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...
2163
	}
2164
2165
	/**
2166
	 * @return PaginatedList The list of "pending" data archives which are waiting for a file
2167
	 * to be delivered offline by post, and manually uploaded into the system.
2168
	 */
2169
	public function PendingDataArchives() {
2170
		$project = $this->getCurrentProject();
2171
		$archives = new ArrayList();
2172
		foreach ($project->DNEnvironmentList() as $env) {
2173
			foreach ($env->DataArchives() as $archive) {
2174
				if ($archive->isPending()) {
2175
					$archives->push($archive);
2176
				}
2177
			}
2178
		}
2179
		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...
2180
	}
2181
2182
	/**
2183
	 * @return PaginatedList
2184
	 */
2185
	public function DataTransferLogs() {
2186
		$environments = $this->getCurrentProject()->Environments()->column('ID');
2187
		$transfers = DNDataTransfer::get()
2188
			->filter('EnvironmentID', $environments)
2189
			->filterByCallback(
2190
				function ($record) {
2191
					return
2192
						$record->Environment()->canRestore() || // Ensure member can perform an action on the transfers env
2193
						$record->Environment()->canBackup() ||
2194
						$record->Environment()->canUploadArchive() ||
2195
						$record->Environment()->canDownloadArchive();
2196
				});
2197
2198
		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...
2199
	}
2200
2201
	/**
2202
	 * @deprecated 2.0.0 - moved to DeployDispatcher
2203
	 *
2204
	 * @return null|PaginatedList
2205
	 */
2206
	public function DeployHistory() {
2207
		if ($env = $this->getCurrentEnvironment()) {
2208
			$history = $env->DeployHistory();
2209
			if ($history->count() > 0) {
2210
				$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...
2211
				$pagination->setPageLength(4);
2212
				return $pagination;
2213
			}
2214
		}
2215
		return null;
2216
	}
2217
2218
	/**
2219
	 * @param string $status
2220
	 * @param string $content
2221
	 *
2222
	 * @return string
2223
	 */
2224
	public function sendResponse($status, $content) {
2225
		// strip excessive newlines
2226
		$content = preg_replace('/(?:(?:\r\n|\r|\n)\s*){2}/s', "\n", $content);
2227
2228
		$sendJSON = (strpos($this->getRequest()->getHeader('Accept'), 'application/json') !== false)
2229
			|| $this->getRequest()->getExtension() == 'json';
2230
2231
		if (!$sendJSON) {
2232
			$this->response->addHeader("Content-type", "text/plain");
2233
			return $content;
2234
		}
2235
		$this->response->addHeader("Content-type", "application/json");
2236
		return json_encode([
2237
			'status' => $status,
2238
			'content' => $content,
2239
		]);
2240
	}
2241
2242
	/**
2243
	 * Get items for the ambient menu that should be accessible from all pages.
2244
	 *
2245
	 * @return ArrayList
2246
	 */
2247
	public function AmbientMenu() {
2248
		$list = new ArrayList();
2249
2250
		if (Member::currentUserID()) {
2251
			$list->push(new ArrayData([
2252
				'Classes' => 'logout',
2253
				'FaIcon' => 'sign-out',
2254
				'Link' => 'Security/logout',
2255
				'Title' => 'Log out',
2256
				'IsCurrent' => false,
2257
				'IsSection' => false
2258
			]));
2259
		}
2260
2261
		$this->extend('updateAmbientMenu', $list);
2262
		return $list;
2263
	}
2264
2265
	/**
2266
	 * Checks whether the user can create a project.
2267
	 *
2268
	 * @return bool
2269
	 */
2270
	public function canCreateProjects($member = null) {
2271
		if (!$member) {
2272
			$member = Member::currentUser();
2273
		}
2274
		if (!$member) {
2275
			return false;
2276
		}
2277
2278
		return singleton('DNProject')->canCreate($member);
2279
	}
2280
2281
	protected function applyRedeploy(SS_HTTPRequest $request, &$data) {
2282
		if (!$request->getVar('redeploy')) {
2283
			return;
2284
		}
2285
2286
		$project = $this->getCurrentProject();
2287
		if (!$project) {
2288
			return $this->project404Response();
2289
		}
2290
2291
		// Performs canView permission check by limiting visible projects
2292
		$env = $this->getCurrentEnvironment($project);
2293
		if (!$env) {
2294
			return $this->environment404Response();
2295
		}
2296
2297
		$current = $env->CurrentBuild();
2298
		if ($current && $current->exists()) {
2299
			$data['preselect_tab'] = 3;
2300
			$data['preselect_sha'] = $current->SHA;
2301
		} else {
2302
			$master = $project->DNBranchList()->byName('master');
2303
			if ($master) {
2304
				$data['preselect_tab'] = 1;
2305
				$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...
2306
			}
2307
		}
2308
	}
2309
2310
	/**
2311
	 * @return SS_HTTPResponse
2312
	 */
2313
	protected function project404Response() {
2314
		return new SS_HTTPResponse(
2315
			"Project '" . Convert::raw2xml($this->getRequest()->param('Project')) . "' not found.",
2316
			404
2317
		);
2318
	}
2319
2320
	/**
2321
	 * @return SS_HTTPResponse
2322
	 */
2323
	protected function environment404Response() {
2324
		$envName = Convert::raw2xml($this->getRequest()->param('Environment'));
2325
		return new SS_HTTPResponse("Environment '" . $envName . "' not found.", 404);
2326
	}
2327
2328
	/**
2329
	 * Validate the snapshot mode
2330
	 *
2331
	 * @param string $mode
2332
	 */
2333
	protected function validateSnapshotMode($mode) {
2334
		if (!in_array($mode, ['all', 'assets', 'db'])) {
2335
			throw new LogicException('Invalid mode');
2336
		}
2337
	}
2338
2339
	/**
2340
	 * @param string $sectionName
2341
	 * @param string $title
2342
	 *
2343
	 * @return SS_HTTPResponse
2344
	 */
2345
	protected function getCustomisedViewSection($sectionName, $title = '', $data = []) {
2346
		// Performs canView permission check by limiting visible projects
2347
		$project = $this->getCurrentProject();
2348
		if (!$project) {
2349
			return $this->project404Response();
2350
		}
2351
		$data[$sectionName] = 1;
2352
2353
		if ($this !== '') {
2354
			$data['Title'] = $title;
2355
		}
2356
2357
		return $this->render($data);
2358
	}
2359
2360
}
2361
2362