Completed
Push — master ( 163f02...ecf5fa )
by Sean
1360:37 queued 1357:19
created

DNRoot   F

Complexity

Total Complexity 304

Size/Duplication

Total Lines 2397
Duplicated Lines 4.09 %

Coupling/Cohesion

Components 1
Dependencies 54

Importance

Changes 17
Bugs 1 Features 2
Metric Value
wmc 304
c 17
b 1
f 2
lcom 1
cbo 54
dl 98
loc 2397
rs 0.5217

78 Methods

Rating   Name   Duplication   Size   Complexity  
A doDryRun() 0 3 1
A startPipeline() 0 3 1
B beginPipeline() 0 33 6
C pipeline() 0 24 7
C createenv() 0 27 7
C getCreateEnvironmentForm() 0 42 7
B doCreateEnvironment() 0 24 3
A metrics() 0 15 3
A DNData() 0 3 1
A getPlatformSpecificStrings() 0 6 2
A getStarredProjects() 0 16 4
C getDeployForm() 0 40 8
B FlagSnapshotsEnabled() 0 13 8
A get_support_links() 0 6 2
A get_template_global_variables() 0 8 1
A init() 0 12 3
A Link() 0 3 1
A index() 0 3 1
A projects() 0 6 1
A nav() 0 3 1
A NavLink() 0 5 2
A snapshots() 0 4 1
A createsnapshot() 19 19 3
A uploadsnapshot() 19 19 3
A UploadLimit() 0 6 1
B getUploadSnapshotForm() 0 50 4
B doUploadSnapshot() 0 94 6
B getPostSnapshotForm() 0 44 4
B doPostSnapshot() 0 30 3
A snapshotslog() 0 4 1
B postsnapshotsuccess() 0 29 5
A toggleprojectstar() 0 21 4
A branch() 0 16 3
A environment() 0 19 3
B createenvlog() 0 26 6
F gitRevisions() 0 145 13
C applyRedeploy() 0 26 7
A checkCsrfToken() 0 20 3
B startDeploy() 0 34 4
B deploy() 0 25 6
C deploylog() 0 30 7
B getDataTransferForm() 0 28 2
C doDataTransfer() 0 64 12
B transfer() 0 25 5
B transferlog() 0 29 6
B getDataTransferRestoreForm() 0 48 6
A restoresnapshot() 21 21 3
A deletesnapshot() 19 19 3
B getDeleteForm() 0 29 3
B doDelete() 0 30 6
A movesnapshot() 20 20 3
B getMoveForm() 0 30 3
B doMove() 0 32 5
A RedisUnavailable() 0 8 2
A RedisWorkersCount() 0 3 1
B providePermissions() 0 40 1
A getCurrentProject() 0 10 3
A getCurrentEnvironment() 0 13 4
A getCurrentActionType() 0 3 1
A setCurrentActionType() 0 3 1
C CanViewArchives() 0 30 11
A CreateEnvironmentList() 0 11 2
B CompleteDataArchives() 0 14 5
B PendingDataArchives() 0 12 5
B DataTransferLogs() 0 16 5
A DeployHistory() 0 11 3
A project404Response() 0 6 1
A environment404Response() 0 4 1
A sendResponse() 0 17 3
A validateSnapshotMode() 0 5 2
A getCustomisedViewSection() 0 14 3
A AmbientMenu() 0 17 2
B include_requirements() 0 31 3
A project() 0 4 1
A DNProjectList() 0 17 3
C Navigation() 0 58 15
D deploySummary() 0 43 9
A canCreateProjects() 0 6 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like DNRoot often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DNRoot, and based on these observations, apply Extract Interface, too.

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