Completed
Pull Request — master (#598)
by Sean
03:15
created

DNRoot::gitRevisions()   F

Complexity

Conditions 13
Paths 338

Size

Total Lines 145
Code Lines 95

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 145
rs 3.7737
cc 13
eloc 95
nc 338
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * God controller for the deploynaut interface
5
 *
6
 * @package deploynaut
7
 * @subpackage control
8
 */
9
class DNRoot extends Controller implements PermissionProvider, TemplateGlobalProvider {
10
11
	/**
12
	 * @const string - action type for actions that perform deployments
13
	 */
14
	const ACTION_DEPLOY = 'deploy';
15
16
	/**
17
	 * @const string - action type for actions that manipulate snapshots
18
	 */
19
	const ACTION_SNAPSHOT = 'snapshot';
20
21
	const ACTION_ENVIRONMENTS = 'createenv';
22
23
	const PROJECT_OVERVIEW = 'overview';
24
25
	/**
26
	 * @var string
27
	 */
28
	private $actionType = self::ACTION_DEPLOY;
29
30
	/**
31
	 * Allow advanced options on deployments
32
	 */
33
	const DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS = 'DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS';
34
35
	const ALLOW_PROD_DEPLOYMENT = 'ALLOW_PROD_DEPLOYMENT';
36
	const ALLOW_NON_PROD_DEPLOYMENT = 'ALLOW_NON_PROD_DEPLOYMENT';
37
	const ALLOW_PROD_SNAPSHOT = 'ALLOW_PROD_SNAPSHOT';
38
	const ALLOW_NON_PROD_SNAPSHOT = 'ALLOW_NON_PROD_SNAPSHOT';
39
	const ALLOW_CREATE_ENVIRONMENT = 'ALLOW_CREATE_ENVIRONMENT';
40
41
	/**
42
	 * @var array
43
	 */
44
	private static $allowed_actions = array(
45
		'projects',
46
		'nav',
47
		'update',
48
		'project',
49
		'toggleprojectstar',
50
		'branch',
51
		'environment',
52
		'metrics',
53
		'createenvlog',
54
		'createenv',
55
		'getDeployForm',
56
		'doDeploy',
57
		'deploy',
58
		'deploylog',
59
		'abortDeploy',
60
		'getDataTransferForm',
61
		'transfer',
62
		'transferlog',
63
		'snapshots',
64
		'createsnapshot',
65
		'snapshotslog',
66
		'uploadsnapshot',
67
		'getCreateEnvironmentForm',
68
		'getUploadSnapshotForm',
69
		'getPostSnapshotForm',
70
		'getDataTransferRestoreForm',
71
		'getDeleteForm',
72
		'getMoveForm',
73
		'restoresnapshot',
74
		'deletesnapshot',
75
		'movesnapshot',
76
		'postsnapshotsuccess',
77
		'gitRevisions',
78
		'deploySummary',
79
		'startDeploy'
80
	);
81
82
	/**
83
	 * URL handlers pretending that we have a deep URL structure.
84
	 */
85
	private static $url_handlers = array(
86
		'project/$Project/environment/$Environment/DeployForm' => 'getDeployForm',
87
		'project/$Project/createsnapshot/DataTransferForm' => 'getDataTransferForm',
88
		'project/$Project/DataTransferForm' => 'getDataTransferForm',
89
		'project/$Project/DataTransferRestoreForm' => 'getDataTransferRestoreForm',
90
		'project/$Project/DeleteForm' => 'getDeleteForm',
91
		'project/$Project/MoveForm' => 'getMoveForm',
92
		'project/$Project/UploadSnapshotForm' => 'getUploadSnapshotForm',
93
		'project/$Project/PostSnapshotForm' => 'getPostSnapshotForm',
94
		'project/$Project/environment/$Environment/metrics' => 'metrics',
95
		'project/$Project/environment/$Environment/deploy_summary' => 'deploySummary',
96
		'project/$Project/environment/$Environment/git_revisions' => 'gitRevisions',
97
		'project/$Project/environment/$Environment/start-deploy' => 'startDeploy',
98
		'project/$Project/environment/$Environment/deploy/$Identifier/log' => 'deploylog',
99
		'project/$Project/environment/$Environment/deploy/$Identifier/abort-deploy' => 'abortDeploy',
100
		'project/$Project/environment/$Environment/deploy/$Identifier' => 'deploy',
101
		'project/$Project/transfer/$Identifier/log' => 'transferlog',
102
		'project/$Project/transfer/$Identifier' => 'transfer',
103
		'project/$Project/environment/$Environment' => 'environment',
104
		'project/$Project/createenv/$Identifier/log' => 'createenvlog',
105
		'project/$Project/createenv/$Identifier' => 'createenv',
106
		'project/$Project/CreateEnvironmentForm' => 'getCreateEnvironmentForm',
107
		'project/$Project/branch' => 'branch',
108
		'project/$Project/build/$Build' => 'build',
109
		'project/$Project/restoresnapshot/$DataArchiveID' => 'restoresnapshot',
110
		'project/$Project/deletesnapshot/$DataArchiveID' => 'deletesnapshot',
111
		'project/$Project/movesnapshot/$DataArchiveID' => 'movesnapshot',
112
		'project/$Project/update' => 'update',
113
		'project/$Project/snapshots' => 'snapshots',
114
		'project/$Project/createsnapshot' => 'createsnapshot',
115
		'project/$Project/uploadsnapshot' => 'uploadsnapshot',
116
		'project/$Project/snapshotslog' => 'snapshotslog',
117
		'project/$Project/postsnapshotsuccess/$DataArchiveID' => 'postsnapshotsuccess',
118
		'project/$Project/star' => 'toggleprojectstar',
119
		'project/$Project' => 'project',
120
		'nav/$Project' => 'nav',
121
		'projects' => 'projects',
122
	);
123
124
	/**
125
	 * @var array
126
	 */
127
	protected static $_project_cache = array();
128
129
	/**
130
	 * @var array
131
	 */
132
	private static $support_links = array();
133
134
	/**
135
	 * @var array
136
	 */
137
	private static $platform_specific_strings = array();
138
139
	/**
140
	 * @var array
141
	 */
142
	private static $action_types = array(
143
		self::ACTION_DEPLOY,
144
		self::ACTION_SNAPSHOT,
145
		self::PROJECT_OVERVIEW
146
	);
147
148
	/**
149
	 * @var DNData
150
	 */
151
	protected $data;
152
153
	/**
154
	 * Include requirements that deploynaut needs, such as javascript.
155
	 */
156
	public static function include_requirements() {
157
158
		// JS should always go to the bottom, otherwise there's the risk that Requirements
159
		// puts them halfway through the page to the nearest <script> tag. We don't want that.
160
		Requirements::set_force_js_to_bottom(true);
161
162
		// todo these should be bundled into the same JS as the others in "static" below.
163
		// We've deliberately not used combined_files as it can mess with some of the JS used
164
		// here and cause sporadic errors.
165
		Requirements::javascript('deploynaut/javascript/jquery.js');
166
		Requirements::javascript('deploynaut/javascript/bootstrap.js');
167
		Requirements::javascript('deploynaut/javascript/q.js');
168
		Requirements::javascript('deploynaut/javascript/tablefilter.js');
169
		Requirements::javascript('deploynaut/javascript/deploynaut.js');
170
171
		Requirements::javascript('deploynaut/javascript/bootstrap.file-input.js');
172
		Requirements::javascript('deploynaut/thirdparty/select2/dist/js/select2.min.js');
173
		Requirements::javascript('deploynaut/thirdparty/bootstrap-switch/dist/js/bootstrap-switch.min.js');
174
		Requirements::javascript('deploynaut/javascript/material.js');
175
176
		// Load the buildable dependencies only if not loaded centrally.
177
		if (!is_dir(BASE_PATH . DIRECTORY_SEPARATOR . 'static')) {
178
			if (\Director::isDev()) {
179
				\Requirements::javascript('deploynaut/static/bundle-debug.js');
180
			} else {
181
				\Requirements::javascript('deploynaut/static/bundle.js');
182
			}
183
		}
184
185
		Requirements::css('deploynaut/static/style.css');
186
	}
187
188
	/**
189
	 * Check for feature flags:
190
	 * - FLAG_SNAPSHOTS_ENABLED: set to true to enable globally
191
	 * - FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS: set to semicolon-separated list of email addresses of allowed users.
192
	 *
193
	 * @return boolean
194
	 */
195
	public static function FlagSnapshotsEnabled() {
196
		if(defined('FLAG_SNAPSHOTS_ENABLED') && FLAG_SNAPSHOTS_ENABLED) {
197
			return true;
198
		}
199
		if(defined('FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS') && FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS) {
200
			$allowedMembers = explode(';', FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS);
201
			$member = Member::currentUser();
202
			if($allowedMembers && $member && in_array($member->Email, $allowedMembers)) {
203
				return true;
204
			}
205
		}
206
		return false;
207
	}
208
209
	/**
210
	 * @return ArrayList
211
	 */
212
	public static function get_support_links() {
213
		$supportLinks = self::config()->support_links;
214
		if($supportLinks) {
215
			return new ArrayList($supportLinks);
216
		}
217
	}
218
219
	/**
220
	 * @return array
221
	 */
222
	public static function get_template_global_variables() {
223
		return array(
224
			'RedisUnavailable' => 'RedisUnavailable',
225
			'RedisWorkersCount' => 'RedisWorkersCount',
226
			'SidebarLinks' => 'SidebarLinks',
227
			"SupportLinks" => 'get_support_links'
228
		);
229
	}
230
231
	/**
232
	 */
233
	public function init() {
234
		parent::init();
235
236
		if(!Member::currentUser() && !Session::get('AutoLoginHash')) {
237
			return Security::permissionFailure();
238
		}
239
240
		// Block framework jquery
241
		Requirements::block(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
242
243
		self::include_requirements();
244
	}
245
246
	/**
247
	 * @return string
248
	 */
249
	public function Link() {
250
		return "naut/";
251
	}
252
253
	/**
254
	 * Actions
255
	 *
256
	 * @param SS_HTTPRequest $request
257
	 * @return \SS_HTTPResponse
258
	 */
259
	public function index(SS_HTTPRequest $request) {
260
		return $this->redirect($this->Link() . 'projects/');
261
	}
262
263
	/**
264
	 * Action
265
	 *
266
	 * @param SS_HTTPRequest $request
267
	 * @return string - HTML
268
	 */
269
	public function projects(SS_HTTPRequest $request) {
270
		// Performs canView permission check by limiting visible projects in DNProjectsList() call.
271
		return $this->customise(array(
272
			'Title' => 'Projects',
273
		))->render();
274
	}
275
276
	/**
277
	 * @param SS_HTTPRequest $request
278
	 * @return HTMLText
279
	 */
280
	public function nav(SS_HTTPRequest $request) {
281
		return $this->renderWith('Nav');
282
	}
283
284
	/**
285
	 * Return a link to the navigation template used for AJAX requests.
286
	 * @return string
287
	 */
288
	public function NavLink() {
289
		$currentProject = $this->getCurrentProject();
290
		$projectName = $currentProject ? $currentProject->Name : null;
291
		return Controller::join_links(Director::absoluteBaseURL(), 'naut', 'nav', $projectName);
292
	}
293
294
	/**
295
	 * Action
296
	 *
297
	 * @param SS_HTTPRequest $request
298
	 * @return SS_HTTPResponse - HTML
299
	 */
300
	public function snapshots(SS_HTTPRequest $request) {
301
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
302
		return $this->getCustomisedViewSection('SnapshotsSection', 'Data Snapshots');
303
	}
304
305
	/**
306
	 * Action
307
	 *
308
	 * @param SS_HTTPRequest $request
309
	 * @return string - HTML
310
	 */
311 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...
312
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
313
314
		// Performs canView permission check by limiting visible projects
315
		$project = $this->getCurrentProject();
316
		if(!$project) {
317
			return $this->project404Response();
318
		}
319
320
		if(!$project->canBackup()) {
321
			return new SS_HTTPResponse("Not allowed to create snapshots on any environments", 401);
322
		}
323
324
		return $this->customise(array(
325
			'Title' => 'Create Data Snapshot',
326
			'SnapshotsSection' => 1,
327
			'DataTransferForm' => $this->getDataTransferForm($request)
328
		))->render();
329
	}
330
331
	/**
332
	 * Action
333
	 *
334
	 * @param SS_HTTPRequest $request
335
	 * @return string - HTML
336
	 */
337 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...
338
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
339
340
		// Performs canView permission check by limiting visible projects
341
		$project = $this->getCurrentProject();
342
		if(!$project) {
343
			return $this->project404Response();
344
		}
345
346
		if(!$project->canUploadArchive()) {
347
			return new SS_HTTPResponse("Not allowed to upload", 401);
348
		}
349
350
		return $this->customise(array(
351
			'SnapshotsSection' => 1,
352
			'UploadSnapshotForm' => $this->getUploadSnapshotForm($request),
353
			'PostSnapshotForm' => $this->getPostSnapshotForm($request)
354
		))->render();
355
	}
356
357
	/**
358
	 * Return the upload limit for snapshot uploads
359
	 * @return string
360
	 */
361
	public function UploadLimit() {
362
		return File::format_size(min(
363
			File::ini2bytes(ini_get('upload_max_filesize')),
364
			File::ini2bytes(ini_get('post_max_size'))
365
		));
366
	}
367
368
	/**
369
	 * Construct the upload form.
370
	 *
371
	 * @param SS_HTTPRequest $request
372
	 * @return Form
373
	 */
374
	public function getUploadSnapshotForm(SS_HTTPRequest $request) {
375
		// Performs canView permission check by limiting visible projects
376
		$project = $this->getCurrentProject();
377
		if(!$project) {
378
			return $this->project404Response();
379
		}
380
381
		if(!$project->canUploadArchive()) {
382
			return new SS_HTTPResponse("Not allowed to upload", 401);
383
		}
384
385
		// Framing an environment as a "group of people with download access"
386
		// makes more sense to the user here, while still allowing us to enforce
387
		// environment specific restrictions on downloading the file later on.
388
		$envs = $project->DNEnvironmentList()->filterByCallback(function($item) {
389
			return $item->canUploadArchive();
390
		});
391
		$envsMap = array();
392
		foreach($envs as $env) {
393
			$envsMap[$env->ID] = $env->Name;
394
		}
395
396
		$maxSize = min(File::ini2bytes(ini_get('upload_max_filesize')), File::ini2bytes(ini_get('post_max_size')));
397
		$fileField = DataArchiveFileField::create('ArchiveFile', 'File');
398
		$fileField->getValidator()->setAllowedExtensions(array('sspak'));
399
		$fileField->getValidator()->setAllowedMaxFileSize(array('*' => $maxSize));
400
401
		$form = Form::create(
402
			$this,
403
			'UploadSnapshotForm',
404
			FieldList::create(
405
				$fileField,
406
				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
407
				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
408
					->setEmptyString('Select an environment')
409
			),
410
			FieldList::create(
411
				FormAction::create('doUploadSnapshot', 'Upload File')
412
					->addExtraClass('btn')
413
			),
414
			RequiredFields::create('ArchiveFile')
415
		);
416
417
		$form->disableSecurityToken();
418
		$form->addExtraClass('fields-wide');
419
		// Tweak the action so it plays well with our fake URL structure.
420
		$form->setFormAction($project->Link() . '/UploadSnapshotForm');
421
422
		return $form;
423
	}
424
425
	/**
426
	 * @param array $data
427
	 * @param Form $form
428
	 *
429
	 * @return bool|HTMLText|SS_HTTPResponse
430
	 */
431
	public function doUploadSnapshot($data, Form $form) {
432
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
433
434
		// Performs canView permission check by limiting visible projects
435
		$project = $this->getCurrentProject();
436
		if(!$project) {
437
			return $this->project404Response();
438
		}
439
440
		$validEnvs = $project->DNEnvironmentList()
441
			->filterByCallback(function($item) {
442
				return $item->canUploadArchive();
443
			});
444
445
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
446
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
447
		if(!$environment) {
448
			throw new LogicException('Invalid environment');
449
		}
450
451
		$this->validateSnapshotMode($data['Mode']);
452
453
		$dataArchive = DNDataArchive::create(array(
454
			'AuthorID' => Member::currentUserID(),
455
			'EnvironmentID' => $data['EnvironmentID'],
456
			'IsManualUpload' => true,
457
		));
458
		// needs an ID and transfer to determine upload path
459
		$dataArchive->write();
460
		$dataTransfer = DNDataTransfer::create(array(
461
			'AuthorID' => Member::currentUserID(),
462
			'Mode' => $data['Mode'],
463
			'Origin' => 'ManualUpload',
464
			'EnvironmentID' => $data['EnvironmentID']
465
		));
466
		$dataTransfer->write();
467
		$dataArchive->DataTransfers()->add($dataTransfer);
468
		$form->saveInto($dataArchive);
469
		$dataArchive->write();
470
		$workingDir = TEMP_FOLDER . DIRECTORY_SEPARATOR . 'deploynaut-transfer-' . $dataTransfer->ID;
471
472 View Code Duplication
		$cleanupFn = function() use($workingDir, $dataTransfer, $dataArchive) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
473
			$process = new AbortableProcess(sprintf('rm -rf %s', escapeshellarg($workingDir)));
474
			$process->setTimeout(120);
475
			$process->run();
476
			$dataTransfer->delete();
477
			$dataArchive->delete();
478
		};
479
480
		// extract the sspak contents so we can inspect them
481
		try {
482
			$dataArchive->extractArchive($workingDir);
483
		} catch(Exception $e) {
484
			$cleanupFn();
485
			$form->sessionMessage(
486
				'There was a problem trying to open your snapshot for processing. Please try uploading again',
487
				'bad'
488
			);
489
			return $this->redirectBack();
490
		}
491
492
		// validate that the sspak contents match the declared contents
493
		$result = $dataArchive->validateArchiveContents();
494
		if(!$result->valid()) {
495
			$cleanupFn();
496
			$form->sessionMessage($result->message(), 'bad');
497
			return $this->redirectBack();
498
		}
499
500
		// fix file permissions of extracted sspak files then re-build the sspak
501
		try {
502
			$dataArchive->fixArchivePermissions($workingDir);
503
			$dataArchive->setArchiveFromFiles($workingDir);
504
		} catch(Exception $e) {
505
			$cleanupFn();
506
			$form->sessionMessage(
507
				'There was a problem processing your snapshot. Please try uploading again',
508
				'bad'
509
			);
510
			return $this->redirectBack();
511
		}
512
513
		// cleanup any extracted sspak contents lying around
514
		$process = new AbortableProcess(sprintf('rm -rf %s', escapeshellarg($workingDir)));
515
		$process->setTimeout(120);
516
		$process->run();
517
518
		return $this->customise(array(
519
			'Project' => $project,
520
			'CurrentProject' => $project,
521
			'SnapshotsSection' => 1,
522
			'DataArchive' => $dataArchive,
523
			'DataTransferRestoreForm' => $this->getDataTransferRestoreForm($this->request, $dataArchive),
524
			'BackURL' => $project->Link('snapshots')
525
		))->renderWith(array('DNRoot_uploadsnapshot', 'DNRoot'));
526
	}
527
528
	/**
529
	 * @param SS_HTTPRequest $request
530
	 * @return Form
531
	 */
532
	public function getPostSnapshotForm(SS_HTTPRequest $request) {
533
		// Performs canView permission check by limiting visible projects
534
		$project = $this->getCurrentProject();
535
		if(!$project) {
536
			return $this->project404Response();
537
		}
538
539
		if(!$project->canUploadArchive()) {
540
			return new SS_HTTPResponse("Not allowed to upload", 401);
541
		}
542
543
		// Framing an environment as a "group of people with download access"
544
		// makes more sense to the user here, while still allowing us to enforce
545
		// environment specific restrictions on downloading the file later on.
546
		$envs = $project->DNEnvironmentList()->filterByCallback(function($item) {
547
			return $item->canUploadArchive();
548
		});
549
		$envsMap = array();
550
		foreach($envs as $env) {
551
			$envsMap[$env->ID] = $env->Name;
552
		}
553
554
		$form = Form::create(
555
			$this,
556
			'PostSnapshotForm',
557
			FieldList::create(
558
				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
559
				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
560
					->setEmptyString('Select an environment')
561
			),
562
			FieldList::create(
563
				FormAction::create('doPostSnapshot', 'Submit request')
564
					->addExtraClass('btn')
565
			),
566
			RequiredFields::create('File')
567
		);
568
569
		$form->disableSecurityToken();
570
		$form->addExtraClass('fields-wide');
571
		// Tweak the action so it plays well with our fake URL structure.
572
		$form->setFormAction($project->Link() . '/PostSnapshotForm');
573
574
		return $form;
575
	}
576
577
	/**
578
	 * @param array $data
579
	 * @param Form $form
580
	 *
581
	 * @return SS_HTTPResponse
582
	 */
583
	public function doPostSnapshot($data, $form) {
584
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
585
586
		$project = $this->getCurrentProject();
587
		if(!$project) {
588
			return $this->project404Response();
589
		}
590
591
		$validEnvs = $project->DNEnvironmentList()->filterByCallback(function($item) {
592
				return $item->canUploadArchive();
593
		});
594
595
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
596
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
597
		if(!$environment) {
598
			throw new LogicException('Invalid environment');
599
		}
600
601
		$dataArchive = DNDataArchive::create(array(
602
			'UploadToken' => DNDataArchive::generate_upload_token(),
603
		));
604
		$form->saveInto($dataArchive);
605
		$dataArchive->write();
606
607
		return $this->redirect(Controller::join_links(
608
			$project->Link(),
609
			'postsnapshotsuccess',
610
			$dataArchive->ID
611
		));
612
	}
613
614
	/**
615
	 * Action
616
	 *
617
	 * @param SS_HTTPRequest $request
618
	 * @return SS_HTTPResponse - HTML
619
	 */
620
	public function snapshotslog(SS_HTTPRequest $request) {
621
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
622
		return $this->getCustomisedViewSection('SnapshotsSection', 'Snapshots log');
623
	}
624
625
	/**
626
	 * @param SS_HTTPRequest $request
627
	 * @return SS_HTTPResponse|string
628
	 * @throws SS_HTTPResponse_Exception
629
	 */
630
	public function postsnapshotsuccess(SS_HTTPRequest $request) {
631
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
632
633
		// Performs canView permission check by limiting visible projects
634
		$project = $this->getCurrentProject();
635
		if(!$project) {
636
			return $this->project404Response();
637
		}
638
639
		if(!$project->canUploadArchive()) {
640
			return new SS_HTTPResponse("Not allowed to upload", 401);
641
		}
642
643
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
644
		if(!$dataArchive) {
645
			return new SS_HTTPResponse("Archive not found.", 404);
646
		}
647
648
		if(!$dataArchive->canRestore()) {
649
			throw new SS_HTTPResponse_Exception('Not allowed to restore archive', 403);
650
		}
651
652
		return $this->render(array(
653
				'Title' => 'How to send us your Data Snapshot by post',
654
				'DataArchive' => $dataArchive,
655
				'Address' => Config::inst()->get('Deploynaut', 'snapshot_post_address'),
656
				'BackURL' => $project->Link(),
657
			));
658
	}
659
660
	/**
661
	 * @param SS_HTTPRequest $request
662
	 * @return \SS_HTTPResponse
663
	 */
664
	public function project(SS_HTTPRequest $request) {
665
		$this->setCurrentActionType(self::PROJECT_OVERVIEW);
666
		return $this->getCustomisedViewSection('ProjectOverview', '', array('IsAdmin' => Permission::check('ADMIN')));
667
	}
668
669
	/**
670
	 * This action will star / unstar a project for the current member
671
	 *
672
	 * @param SS_HTTPRequest $request
673
	 *
674
	 * @return SS_HTTPResponse
675
	 */
676
	public function toggleprojectstar(SS_HTTPRequest $request) {
677
		$project = $this->getCurrentProject();
678
		if(!$project) {
679
			return $this->project404Response();
680
		}
681
682
		$member = Member::currentUser();
683
		if($member === null) {
684
			return $this->project404Response();
685
		}
686
		$favProject = $member->StarredProjects()
687
			->filter('DNProjectID', $project->ID)
688
			->first();
689
690
		if($favProject) {
691
			$member->StarredProjects()->remove($favProject);
692
		} else {
693
			$member->StarredProjects()->add($project);
694
		}
695
		return $this->redirectBack();
696
	}
697
698
	/**
699
	 * @param SS_HTTPRequest $request
700
	 * @return \SS_HTTPResponse
701
	 */
702
	public function branch(SS_HTTPRequest $request) {
703
		$project = $this->getCurrentProject();
704
		if(!$project) {
705
			return $this->project404Response();
706
		}
707
708
		$branchName = $request->getVar('name');
709
		$branch = $project->DNBranchList()->byName($branchName);
710
		if(!$branch) {
711
			return new SS_HTTPResponse("Branch '" . Convert::raw2xml($branchName) . "' not found.", 404);
712
		}
713
714
		return $this->render(array(
715
			'CurrentBranch' => $branch,
716
		));
717
	}
718
719
	/**
720
	 * @param SS_HTTPRequest $request
721
	 * @return \SS_HTTPResponse
722
	 */
723
	public function environment(SS_HTTPRequest $request) {
724
		// Performs canView permission check by limiting visible projects
725
		$project = $this->getCurrentProject();
726
		if(!$project) {
727
			return $this->project404Response();
728
		}
729
730
		// Performs canView permission check by limiting visible projects
731
		$env = $this->getCurrentEnvironment($project);
732
		if(!$env) {
733
			return $this->environment404Response();
734
		}
735
736
		return $this->render(array(
737
			'DNEnvironmentList' => $this->getCurrentProject()->DNEnvironmentList(),
738
			'FlagSnapshotsEnabled' => $this->FlagSnapshotsEnabled(),
739
			'Redeploy' => (bool)$request->getVar('redeploy')
740
		));
741
	}
742
743
	/**
744
	 * Shows the creation log.
745
	 *
746
	 * @param SS_HTTPRequest $request
747
	 * @return string
748
	 */
749
	public function createenv(SS_HTTPRequest $request) {
750
		$params = $request->params();
751
		if($params['Identifier']) {
752
			$record = DNCreateEnvironment::get()->byId($params['Identifier']);
753
754
			if(!$record || !$record->ID) {
755
				throw new SS_HTTPResponse_Exception('Create environment not found', 404);
756
			}
757
			if(!$record->canView()) {
758
				return Security::permissionFailure();
759
			}
760
761
			$project = $this->getCurrentProject();
762
			if(!$project) {
763
				return $this->project404Response();
764
			}
765
766
			if($project->Name != $params['Project']) {
767
				throw new LogicException("Project in URL doesn't match this creation");
768
			}
769
770
			return $this->render(array(
771
				'CreateEnvironment' => $record,
772
			));
773
		}
774
		return $this->render(array('CurrentTitle' => 'Create an environment'));
775
	}
776
777
778
	public function createenvlog(SS_HTTPRequest $request) {
779
		$params = $request->params();
780
		$env = DNCreateEnvironment::get()->byId($params['Identifier']);
781
782
		if(!$env || !$env->ID) {
783
			throw new SS_HTTPResponse_Exception('Log not found', 404);
784
		}
785
		if(!$env->canView()) {
786
			return Security::permissionFailure();
787
		}
788
789
		$project = $env->Project();
790
791
		if($project->Name != $params['Project']) {
792
			throw new LogicException("Project in URL doesn't match this deploy");
793
		}
794
795
		$log = $env->log();
796
		if($log->exists()) {
797
			$content = $log->content();
798
		} else {
799
			$content = 'Waiting for action to start';
800
		}
801
802
		return $this->sendResponse($env->ResqueStatus(), $content);
803
	}
804
805
	/**
806
	 * @param SS_HTTPRequest $request
807
	 * @return Form
808
	 */
809
	public function getCreateEnvironmentForm(SS_HTTPRequest $request) {
810
		$this->setCurrentActionType(self::ACTION_ENVIRONMENTS);
811
812
		$project = $this->getCurrentProject();
813
		if(!$project) {
814
			return $this->project404Response();
815
		}
816
817
		$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...
818
		if(!$envType || !class_exists($envType)) {
819
			return null;
820
		}
821
822
		$backend = Injector::inst()->get($envType);
823
		if(!($backend instanceof EnvironmentCreateBackend)) {
824
			// Only allow this for supported backends.
825
			return null;
826
		}
827
828
		$fields = $backend->getCreateEnvironmentFields($project);
829
		if(!$fields) return null;
830
831
		if(!$project->canCreateEnvironments()) {
832
			return new SS_HTTPResponse('Not allowed to create environments for this project', 401);
833
		}
834
835
		$form = Form::create(
836
			$this,
837
			'CreateEnvironmentForm',
838
			$fields,
839
			FieldList::create(
840
				FormAction::create('doCreateEnvironment', 'Create')
841
					->addExtraClass('btn')
842
			),
843
			$backend->getCreateEnvironmentValidator()
844
		);
845
846
		// Tweak the action so it plays well with our fake URL structure.
847
		$form->setFormAction($project->Link() . '/CreateEnvironmentForm');
848
849
		return $form;
850
	}
851
852
	/**
853
	 * @param array $data
854
	 * @param Form $form
855
	 *
856
	 * @return bool|HTMLText|SS_HTTPResponse
857
	 */
858
	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...
859
		$this->setCurrentActionType(self::ACTION_ENVIRONMENTS);
860
861
		$project = $this->getCurrentProject();
862
		if(!$project) {
863
			return $this->project404Response();
864
		}
865
866
		if(!$project->canCreateEnvironments()) {
867
			return new SS_HTTPResponse('Not allowed to create environments for this project', 401);
868
		}
869
870
		// Set the environment type so we know what we're creating.
871
		$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...
872
873
		$job = DNCreateEnvironment::create();
874
875
		$job->Data = serialize($data);
876
		$job->ProjectID = $project->ID;
877
		$job->write();
878
		$job->start();
879
880
		return $this->redirect($project->Link('createenv') . '/' . $job->ID);
881
	}
882
883
	/**
884
	 *
885
	 * @param SS_HTTPRequest $request
886
	 * @return \SS_HTTPResponse
887
	 */
888
	public function metrics(SS_HTTPRequest $request) {
889
		// Performs canView permission check by limiting visible projects
890
		$project = $this->getCurrentProject();
891
		if(!$project) {
892
			return $this->project404Response();
893
		}
894
895
		// Performs canView permission check by limiting visible projects
896
		$env = $this->getCurrentEnvironment($project);
897
		if(!$env) {
898
			return $this->environment404Response();
899
		}
900
901
		return $this->render();
902
	}
903
904
	/**
905
	 * Get the DNData object.
906
	 *
907
	 * @return DNData
908
	 */
909
	public function DNData() {
910
		return DNData::inst();
911
	}
912
913
	/**
914
	 * Provide a list of all projects.
915
	 *
916
	 * @return SS_List
917
	 */
918
	public function DNProjectList() {
919
		$memberId = Member::currentUserID();
920
		if(!$memberId) {
921
			return new ArrayList();
922
		}
923
924
		if(Permission::check('ADMIN')) {
925
			return DNProject::get();
926
		}
927
928
		$projects = Member::get()->filter('ID', $memberId)
929
			->relation('Groups')
930
			->relation('Projects');
931
932
		$this->extend('updateDNProjectList', $projects);
933
		return $projects;
934
	}
935
936
	/**
937
	 * @return ArrayList
938
	 */
939
	public function getPlatformSpecificStrings() {
940
		$strings = $this->config()->platform_specific_strings;
941
		if ($strings) {
942
			return new ArrayList($strings);
943
		}
944
	}
945
946
	/**
947
	 * Provide a list of all starred projects for the currently logged in member
948
	 *
949
	 * @return SS_List
950
	 */
951
	public function getStarredProjects() {
952
		$member = Member::currentUser();
953
		if($member === null) {
954
			return new ArrayList();
955
		}
956
957
		$favProjects = $member->StarredProjects();
958
959
		$list = new ArrayList();
960
		foreach($favProjects as $project) {
961
			if($project->canView($member)) {
962
				$list->add($project);
963
			}
964
		}
965
		return $list;
966
	}
967
968
	/**
969
	 * Returns top level navigation of projects.
970
	 *
971
	 * @param int $limit
972
	 *
973
	 * @return ArrayList
974
	 */
975
	public function Navigation($limit = 5) {
976
		$navigation = new ArrayList();
977
978
		$currentProject = $this->getCurrentProject();
979
		$currentEnvironment = $this->getCurrentEnvironment();
980
		$actionType = $this->getCurrentActionType();
981
982
		$projects = $this->getStarredProjects();
983
		if($projects->count() < 1) {
984
			$projects = $this->DNProjectList();
985
		} else {
986
			$limit = -1;
987
		}
988
989
		if($projects->count() > 0) {
990
			$activeProject = false;
991
992
			if($limit > 0) {
993
				$limitedProjects = $projects->limit($limit);
994
			} else {
995
				$limitedProjects = $projects;
996
			}
997
998
			foreach($limitedProjects as $project) {
999
				$isActive = $currentProject && $currentProject->ID == $project->ID;
1000
				if($isActive) {
1001
					$activeProject = true;
1002
				}
1003
1004
				$isCurrentEnvironment = false;
1005
				if($project && $currentEnvironment) {
1006
					$isCurrentEnvironment = (bool) $project->DNEnvironmentList()->find('ID', $currentEnvironment->ID);
1007
				}
1008
1009
				$navigation->push(array(
1010
					'Project' => $project,
1011
					'IsCurrentEnvironment' => $isCurrentEnvironment,
1012
					'IsActive' => $currentProject && $currentProject->ID == $project->ID,
1013
					'IsOverview' => $actionType == self::PROJECT_OVERVIEW && $currentProject->ID == $project->ID
1014
				));
1015
			}
1016
1017
			// Ensure the current project is in the list
1018
			if(!$activeProject && $currentProject) {
1019
				$navigation->unshift(array(
1020
					'Project' => $currentProject,
1021
					'IsActive' => true,
1022
					'IsCurrentEnvironment' => $currentEnvironment,
1023
					'IsOverview' => $actionType == self::PROJECT_OVERVIEW
1024
				));
1025
				if($limit > 0 && $navigation->count() > $limit) {
1026
					$navigation->pop();
1027
				}
1028
			}
1029
		}
1030
1031
		return $navigation;
1032
	}
1033
1034
	/**
1035
	 * Construct the deployment form
1036
	 *
1037
	 * @return Form
1038
	 */
1039
	public function getDeployForm($request = null) {
1040
1041
		// Performs canView permission check by limiting visible projects
1042
		$project = $this->getCurrentProject();
1043
		if(!$project) {
1044
			return $this->project404Response();
1045
		}
1046
1047
		// Performs canView permission check by limiting visible projects
1048
		$environment = $this->getCurrentEnvironment($project);
1049
		if(!$environment) {
1050
			return $this->environment404Response();
1051
		}
1052
1053
		if(!$environment->canDeploy()) {
1054
			return new SS_HTTPResponse("Not allowed to deploy", 401);
1055
		}
1056
1057
		// Generate the form
1058
		$form = new DeployForm($this, 'DeployForm', $environment, $project);
1059
1060
		// If this is an ajax request we don't want to submit the form - we just want to retrieve the markup.
1061
		if(
1062
			$request &&
1063
			!$request->requestVar('action_showDeploySummary') &&
1064
			$this->getRequest()->isAjax() &&
1065
			$this->getRequest()->isGET()
1066
		) {
1067
			// We can just use the URL we're accessing
1068
			$form->setFormAction($this->getRequest()->getURL());
1069
1070
			$body = json_encode(array('Content' => $form->forAjaxTemplate()->forTemplate()));
1071
			$this->getResponse()->addHeader('Content-Type', 'application/json');
1072
			$this->getResponse()->setBody($body);
1073
			return $body;
1074
		}
1075
1076
		$form->setFormAction($this->getRequest()->getURL() . '/DeployForm');
1077
		return $form;
1078
	}
1079
1080
	/**
1081
	 * @param SS_HTTPRequest $request
1082
	 *
1083
	 * @return SS_HTTPResponse|string
1084
	 */
1085
	public function gitRevisions(SS_HTTPRequest $request) {
1086
1087
		// Performs canView permission check by limiting visible projects
1088
		$project = $this->getCurrentProject();
1089
		if(!$project) {
1090
			return $this->project404Response();
1091
		}
1092
1093
		// Performs canView permission check by limiting visible projects
1094
		$env = $this->getCurrentEnvironment($project);
1095
		if(!$env) {
1096
			return $this->environment404Response();
1097
		}
1098
1099
		// For now only permit advanced options on one environment type, because we hacked the "full-deploy"
1100
		// checkbox in. Other environments such as the fast or capistrano one wouldn't know what to do with it.
1101
		if(get_class($env) === 'RainforestEnvironment') {
1102
			$advanced = Permission::check('DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS') ? 'true' : 'false';
1103
		} else {
1104
			$advanced = 'false';
1105
		}
1106
1107
		$tabs = array();
1108
		$id = 0;
1109
		$data = array(
1110
			'id' => ++$id,
1111
			'name' => 'Deploy the latest version of a branch',
1112
			'field_type' => 'dropdown',
1113
			'field_label' => 'Choose a branch',
1114
			'field_id' => 'branch',
1115
			'field_data' => array(),
1116
			'advanced_opts' => $advanced
1117
		);
1118
		foreach($project->DNBranchList() as $branch) {
1119
			$sha = $branch->SHA();
1120
			$name = $branch->Name();
1121
			$branchValue = sprintf("%s (%s, %s old)",
1122
				$name,
1123
				substr($sha, 0, 8),
1124
				$branch->LastUpdated()->TimeDiff()
1125
			);
1126
			$data['field_data'][] = array(
1127
				'id' => $sha,
1128
				'text' => $branchValue,
1129
				'branch_name' => $name // the raw branch name, not including the time etc
1130
			);
1131
		}
1132
		$tabs[] = $data;
1133
1134
		$data = array(
1135
			'id' => ++$id,
1136
			'name' => 'Deploy a tagged release',
1137
			'field_type' => 'dropdown',
1138
			'field_label' => 'Choose a tag',
1139
			'field_id' => 'tag',
1140
			'field_data' => array(),
1141
			'advanced_opts' => $advanced
1142
		);
1143
1144
		foreach($project->DNTagList()->setLimit(null) as $tag) {
1145
			$name = $tag->Name();
1146
			$data['field_data'][] = array(
1147
				'id' => $tag->SHA(),
1148
				'text' => sprintf("%s", $name)
1149
			);
1150
		}
1151
1152
		// show newest tags first.
1153
		$data['field_data'] = array_reverse($data['field_data']);
1154
1155
		$tabs[] = $data;
1156
1157
		// Past deployments
1158
		$data = array(
1159
			'id' => ++$id,
1160
			'name' => 'Redeploy a release that was previously deployed (to any environment)',
1161
			'field_type' => 'dropdown',
1162
			'field_label' => 'Choose a previously deployed release',
1163
			'field_id' => 'release',
1164
			'field_data' => array(),
1165
			'advanced_opts' => $advanced
1166
		);
1167
		// We are aiming at the format:
1168
		// [{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...
1169
		$redeploy = array();
1170
		foreach($project->DNEnvironmentList() as $dnEnvironment) {
1171
			$envName = $dnEnvironment->Name;
1172
			$perEnvDeploys = array();
1173
1174
			foreach($dnEnvironment->DeployHistory() as $deploy) {
1175
				$sha = $deploy->SHA;
1176
1177
				// Check if exists to make sure the newest deployment date is used.
1178
				if(!isset($perEnvDeploys[$sha])) {
1179
					$pastValue = sprintf("%s (deployed %s)",
1180
						substr($sha, 0, 8),
1181
						$deploy->obj('LastEdited')->Ago()
1182
					);
1183
					$perEnvDeploys[$sha] = array(
1184
						'id' => $sha,
1185
						'text' => $pastValue
1186
					);
1187
				}
1188
			}
1189
1190
			if(!empty($perEnvDeploys)) {
1191
				$redeploy[$envName] = array_values($perEnvDeploys);
1192
			}
1193
		}
1194
		// Convert the array to the frontend format (i.e. keyed to regular array)
1195
		foreach($redeploy as $env => $descr) {
1196
			$data['field_data'][] = array('text'=>$env, 'children'=>$descr);
1197
		}
1198
		$tabs[] = $data;
1199
1200
		$data = array(
1201
			'id' => ++$id,
1202
			'name' => 'Deploy a specific SHA',
1203
			'field_type' => 'textfield',
1204
			'field_label' => 'Choose a SHA',
1205
			'field_id' => 'SHA',
1206
			'field_data' => array(),
1207
			'advanced_opts' => $advanced
1208
		);
1209
		$tabs[] = $data;
1210
1211
		// get the last time git fetch was run
1212
		$lastFetched = 'never';
1213
		$fetch = DNGitFetch::get()
1214
			->filter('ProjectID', $project->ID)
1215
			->sort('LastEdited', 'DESC')
1216
			->first();
1217
		if($fetch) {
1218
			$lastFetched = $fetch->dbObject('LastEdited')->Ago();
1219
		}
1220
1221
		$data = array(
1222
			'Tabs' => $tabs,
1223
			'last_fetched' => $lastFetched
1224
		);
1225
1226
		$this->applyRedeploy($request, $data);
1227
1228
		return json_encode($data, JSON_PRETTY_PRINT);
1229
	}
1230
1231
	protected function applyRedeploy(SS_HTTPRequest $request, &$data) {
1232
		if (!$request->getVar('redeploy')) return;
1233
1234
		$project = $this->getCurrentProject();
1235
		if(!$project) {
1236
			return $this->project404Response();
1237
		}
1238
1239
		// Performs canView permission check by limiting visible projects
1240
		$env = $this->getCurrentEnvironment($project);
1241
		if(!$env) {
1242
			return $this->environment404Response();
1243
		}
1244
1245
		$current = $env->CurrentBuild();
1246
		if ($current && $current->exists()) {
1247
			$data['preselect_tab'] = 3;
1248
			$data['preselect_sha'] = $current->SHA;
1249
		} else {
1250
			$master = $project->DNBranchList()->byName('master');
1251
			if ($master) {
1252
				$data['preselect_tab'] = 1;
1253
				$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...
1254
			}
1255
		}
1256
	}
1257
1258
	/**
1259
	 * Check and regenerate a global CSRF token
1260
	 *
1261
	 * @param SS_HTTPRequest $request
1262
	 * @param bool $resetToken
1263
	 *
1264
	 * @return bool
1265
	 */
1266
	protected function checkCsrfToken(SS_HTTPRequest $request, $resetToken = true) {
1267
		$token = SecurityToken::inst();
1268
1269
		// Ensure the submitted token has a value
1270
		$submittedToken = $request->postVar('SecurityID');
1271
		if(!$submittedToken) {
1272
			return false;
1273
		}
1274
1275
		// Do the actual check.
1276
		$check = $token->check($submittedToken);
1277
1278
		// Reset the token after we've checked the existing token
1279
		if($resetToken) {
1280
			$token->reset();
1281
		}
1282
1283
		// Return whether the token was correct or not
1284
		return $check;
1285
	}
1286
1287
	/**
1288
	 * @param SS_HTTPRequest $request
1289
	 *
1290
	 * @return string
1291
	 */
1292
	public function deploySummary(SS_HTTPRequest $request) {
1293
1294
		// Performs canView permission check by limiting visible projects
1295
		$project = $this->getCurrentProject();
1296
		if(!$project) {
1297
			return $this->project404Response();
1298
		}
1299
1300
		// Performs canView permission check by limiting visible projects
1301
		$environment = $this->getCurrentEnvironment($project);
1302
		if(!$environment) {
1303
			return $this->environment404Response();
1304
		}
1305
1306
		// Plan the deployment.
1307
		$strategy = $environment->getDeployStrategy($request);
1308
		$data = $strategy->toArray();
1309
1310
		// Add in a URL for comparing from->to code changes. Ensure that we have
1311
		// two proper 40 character SHAs, otherwise we can't show the compare link.
1312
		$interface = $project->getRepositoryInterface();
1313
		if(
1314
			!empty($interface) && !empty($interface->URL)
1315
			&& !empty($data['changes']['Code version']['from'])
1316
			&& strlen($data['changes']['Code version']['from']) == '40'
1317
			&& !empty($data['changes']['Code version']['to'])
1318
			&& strlen($data['changes']['Code version']['to']) == '40'
1319
		) {
1320
			$compareurl = sprintf(
1321
				'%s/compare/%s...%s',
1322
				$interface->URL,
1323
				$data['changes']['Code version']['from'],
1324
				$data['changes']['Code version']['to']
1325
			);
1326
			$data['changes']['Code version']['compareUrl'] = $compareurl;
1327
		}
1328
1329
		// Append json to response
1330
		$token = SecurityToken::inst();
1331
		$data['SecurityID'] = $token->getValue();
1332
1333
		$this->extend('updateDeploySummary', $data);
1334
1335
		return json_encode($data);
1336
	}
1337
1338
	/**
1339
	 * Deployment form submission handler.
1340
	 *
1341
	 * Initiate a DNDeployment record and redirect to it for status polling
1342
	 *
1343
	 * @param SS_HTTPRequest $request
1344
	 *
1345
	 * @return SS_HTTPResponse
1346
	 * @throws ValidationException
1347
	 * @throws null
1348
	 */
1349
	public function startDeploy(SS_HTTPRequest $request) {
1350
1351
		// Ensure the CSRF Token is correct
1352
		if(!$this->checkCsrfToken($request)) {
1353
			// CSRF token didn't match
1354
			return $this->httpError(400, 'Bad Request');
1355
		}
1356
1357
		// Performs canView permission check by limiting visible projects
1358
		$project = $this->getCurrentProject();
1359
		if(!$project) {
1360
			return $this->project404Response();
1361
		}
1362
1363
		// Performs canView permission check by limiting visible projects
1364
		$environment = $this->getCurrentEnvironment($project);
1365
		if(!$environment) {
1366
			return $this->environment404Response();
1367
		}
1368
1369
		// Initiate the deployment
1370
		// The extension point should pass in: Project, Environment, SelectRelease, buildName
1371
		$this->extend('doDeploy', $project, $environment, $buildName, $data);
1372
1373
		// Start the deployment based on the approved strategy.
1374
		$strategy = new DeploymentStrategy($environment);
1375
		$strategy->fromArray($request->requestVar('strategy'));
1376
		$deployment = $strategy->createDeployment();
1377
		// Skip through the approval state for now.
1378
		$deployment->getMachine()->apply(DNDeployment::TR_SUBMIT);
1379
		$deployment->getMachine()->apply(DNDeployment::TR_QUEUE);
1380
1381
		return json_encode(array(
1382
			'url' => Director::absoluteBaseURL() . $deployment->Link()
1383
		), JSON_PRETTY_PRINT);
1384
	}
1385
1386
	/**
1387
	 * Action - Do the actual deploy
1388
	 *
1389
	 * @param SS_HTTPRequest $request
1390
	 *
1391
	 * @return SS_HTTPResponse|string
1392
	 * @throws SS_HTTPResponse_Exception
1393
	 */
1394
	public function deploy(SS_HTTPRequest $request) {
1395
		$params = $request->params();
1396
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1397
1398
		if(!$deployment || !$deployment->ID) {
1399
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1400
		}
1401
		if(!$deployment->canView()) {
1402
			return Security::permissionFailure();
1403
		}
1404
1405
		$environment = $deployment->Environment();
1406
		$project = $environment->Project();
1407
1408
		if($environment->Name != $params['Environment']) {
1409
			throw new LogicException("Environment in URL doesn't match this deploy");
1410
		}
1411
		if($project->Name != $params['Project']) {
1412
			throw new LogicException("Project in URL doesn't match this deploy");
1413
		}
1414
1415
		return $this->render(array(
1416
			'Deployment' => $deployment,
1417
		));
1418
	}
1419
1420
1421
	/**
1422
	 * Action - Get the latest deploy log
1423
	 *
1424
	 * @param SS_HTTPRequest $request
1425
	 *
1426
	 * @return string
1427
	 * @throws SS_HTTPResponse_Exception
1428
	 */
1429
	public function deploylog(SS_HTTPRequest $request) {
1430
		$params = $request->params();
1431
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1432
1433
		if(!$deployment || !$deployment->ID) {
1434
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1435
		}
1436
		if(!$deployment->canView()) {
1437
			return Security::permissionFailure();
1438
		}
1439
1440
		$environment = $deployment->Environment();
1441
		$project = $environment->Project();
1442
1443
		if($environment->Name != $params['Environment']) {
1444
			throw new LogicException("Environment in URL doesn't match this deploy");
1445
		}
1446
		if($project->Name != $params['Project']) {
1447
			throw new LogicException("Project in URL doesn't match this deploy");
1448
		}
1449
1450
		$log = $deployment->log();
1451
		if($log->exists()) {
1452
			$content = $log->content();
1453
		} else {
1454
			$content = 'Waiting for action to start';
1455
		}
1456
1457
		return $this->sendResponse($deployment->ResqueStatus(), $content);
1458
	}
1459
1460
	public function abortDeploy(SS_HTTPRequest $request) {
1461
		$params = $request->params();
1462
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1463
1464
		if(!$deployment || !$deployment->ID) {
1465
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1466
		}
1467
		if(!$deployment->canView()) {
1468
			return Security::permissionFailure();
1469
		}
1470
1471
		// For now restrict to ADMINs only.
1472
		if(!Permission::check('ADMIN')) {
1473
			return Security::permissionFailure();
1474
		}
1475
1476
		$environment = $deployment->Environment();
1477
		$project = $environment->Project();
1478
1479
		if($environment->Name != $params['Environment']) {
1480
			throw new LogicException("Environment in URL doesn't match this deploy");
1481
		}
1482
		if($project->Name != $params['Project']) {
1483
			throw new LogicException("Project in URL doesn't match this deploy");
1484
		}
1485
1486
		if (!in_array($deployment->Status, ['Queued', 'Deploying', 'Aborting'])) {
1487
			throw new LogicException(sprintf("Cannot abort from %s state.", $deployment->Status));
1488
		}
1489
1490
		$deployment->getMachine()->apply(DNDeployment::TR_ABORT);
1491
1492
		return $this->sendResponse($deployment->ResqueStatus(), []);
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1493
	}
1494
1495
	/**
1496
	 * @param SS_HTTPRequest|null $request
1497
	 *
1498
	 * @return Form
1499
	 */
1500
	public function getDataTransferForm(SS_HTTPRequest $request = null) {
1501
		// Performs canView permission check by limiting visible projects
1502
		$envs = $this->getCurrentProject()->DNEnvironmentList()->filterByCallback(function($item) {
1503
			return $item->canBackup();
1504
		});
1505
1506
		if(!$envs) {
1507
			return $this->environment404Response();
1508
		}
1509
1510
		$form = Form::create(
1511
			$this,
1512
			'DataTransferForm',
1513
			FieldList::create(
1514
				HiddenField::create('Direction', null, 'get'),
1515
				DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1516
					->setEmptyString('Select an environment'),
1517
				DropdownField::create('Mode', 'Transfer', DNDataArchive::get_mode_map())
1518
			),
1519
			FieldList::create(
1520
				FormAction::create('doDataTransfer', 'Create')
1521
					->addExtraClass('btn')
1522
			)
1523
		);
1524
		$form->setFormAction($this->getRequest()->getURL() . '/DataTransferForm');
1525
1526
		return $form;
1527
	}
1528
1529
	/**
1530
	 * @param array $data
1531
	 * @param Form $form
1532
	 *
1533
	 * @return SS_HTTPResponse
1534
	 * @throws SS_HTTPResponse_Exception
1535
	 */
1536
	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...
1537
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1538
1539
		// Performs canView permission check by limiting visible projects
1540
		$project = $this->getCurrentProject();
1541
		if(!$project) {
1542
			return $this->project404Response();
1543
		}
1544
1545
		$dataArchive = null;
1546
1547
		// Validate direction.
1548
		if($data['Direction'] == 'get') {
1549
			$validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1550
				->filterByCallback(function($item) {
1551
					return $item->canBackup();
1552
				});
1553
		} else if($data['Direction'] == 'push') {
1554
			$validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1555
				->filterByCallback(function($item) {
1556
					return $item->canRestore();
1557
				});
1558
		} else {
1559
			throw new LogicException('Invalid direction');
1560
		}
1561
1562
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
1563
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
1564
		if(!$environment) {
1565
			throw new LogicException('Invalid environment');
1566
		}
1567
1568
		$this->validateSnapshotMode($data['Mode']);
1569
1570
		// Only 'push' direction is allowed an association with an existing archive.
1571
		if(
1572
			$data['Direction'] == 'push'
1573
			&& isset($data['DataArchiveID'])
1574
			&& is_numeric($data['DataArchiveID'])
1575
		) {
1576
			$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1577
			if(!$dataArchive) {
1578
				throw new LogicException('Invalid data archive');
1579
			}
1580
1581
			if(!$dataArchive->canDownload()) {
1582
				throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1583
			}
1584
		}
1585
1586
		$transfer = DNDataTransfer::create();
1587
		$transfer->EnvironmentID = $environment->ID;
1588
		$transfer->Direction = $data['Direction'];
1589
		$transfer->Mode = $data['Mode'];
1590
		$transfer->DataArchiveID = $dataArchive ? $dataArchive->ID : null;
1591
		$transfer->write();
1592
1593
		$backupMode = null;
1594
		// if we're only restoring the database, there's no need to backup the assets
1595
		if (!empty($data['BackupFirst']) && $data['Mode'] == 'db') {
1596
			$backupMode = 'db';
1597
		} elseif (!empty($data['BackupFirst'])) {
1598
			$backupMode = 'all';
1599
		}
1600
		if ($backupMode !== null && $data['Direction'] == 'push') {
1601
			$backupTransfer = DNDataTransfer::create();
1602
			$backupTransfer->EnvironmentID = $environment->ID;
1603
			$backupTransfer->Direction = 'get';
1604
			$backupTransfer->Mode = $backupMode;
1605
			$backupTransfer->AuthorID = $transfer->AuthorID;
1606
			$backupTransfer->write();
1607
1608
			$transfer->BackupDataTransferID = $backupTransfer->ID;
1609
			$transfer->write();
1610
		}
1611
1612
		$backupTransfer->start();
0 ignored issues
show
Bug introduced by
The variable $backupTransfer does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

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