Completed
Pull Request — master (#526)
by Stig
03:13
created

DNRoot::CreateEnvironmentList()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 11
rs 9.4285
cc 2
eloc 8
nc 2
nop 0
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 PROJECT_OVERVIEW = 'overview';
23
24
	/**
25
	 * @var string
26
	 */
27
	private $actionType = self::ACTION_DEPLOY;
28
29
	/**
30
	 * Bypass pipeline permission code
31
	 */
32
	const DEPLOYNAUT_BYPASS_PIPELINE = 'DEPLOYNAUT_BYPASS_PIPELINE';
33
34
	/**
35
	 * Allow dryrun of pipelines
36
	 */
37
	const DEPLOYNAUT_DRYRUN_PIPELINE = 'DEPLOYNAUT_DRYRUN_PIPELINE';
38
39
	/**
40
	 * Allow advanced options on deployments
41
	 */
42
	const DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS = 'DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS';
43
44
	const ALLOW_PROD_DEPLOYMENT = 'ALLOW_PROD_DEPLOYMENT';
45
	const ALLOW_NON_PROD_DEPLOYMENT = 'ALLOW_NON_PROD_DEPLOYMENT';
46
	const ALLOW_PROD_SNAPSHOT = 'ALLOW_PROD_SNAPSHOT';
47
	const ALLOW_NON_PROD_SNAPSHOT = 'ALLOW_NON_PROD_SNAPSHOT';
48
	const ALLOW_CREATE_ENVIRONMENT = 'ALLOW_CREATE_ENVIRONMENT';
49
50
	/**
51
	 * @var array
52
	 */
53
	private static $allowed_actions = array(
54
		'projects',
55
		'nav',
56
		'update',
57
		'project',
58
		'toggleprojectstar',
59
		'branch',
60
		'environment',
61
		'abortpipeline',
62
		'pipeline',
63
		'pipelinelog',
64
		'metrics',
65
		'getDeployForm',
66
		'doDeploy',
67
		'deploy',
68
		'deploylog',
69
		'getDataTransferForm',
70
		'transfer',
71
		'transferlog',
72
		'snapshots',
73
		'createsnapshot',
74
		'snapshotslog',
75
		'uploadsnapshot',
76
		'getUploadSnapshotForm',
77
		'getPostSnapshotForm',
78
		'getDataTransferRestoreForm',
79
		'getDeleteForm',
80
		'getMoveForm',
81
		'restoresnapshot',
82
		'deletesnapshot',
83
		'movesnapshot',
84
		'postsnapshotsuccess',
85
		'gitRevisions',
86
		'deploySummary',
87
		'startDeploy'
88
	);
89
90
	/**
91
	 * URL handlers pretending that we have a deep URL structure.
92
	 */
93
	private static $url_handlers = array(
94
		'project/$Project/environment/$Environment/DeployForm' => 'getDeployForm',
95
		'project/$Project/createsnapshot/DataTransferForm' => 'getDataTransferForm',
96
		'project/$Project/DataTransferForm' => 'getDataTransferForm',
97
		'project/$Project/DataTransferRestoreForm' => 'getDataTransferRestoreForm',
98
		'project/$Project/DeleteForm' => 'getDeleteForm',
99
		'project/$Project/MoveForm' => 'getMoveForm',
100
		'project/$Project/UploadSnapshotForm' => 'getUploadSnapshotForm',
101
		'project/$Project/PostSnapshotForm' => 'getPostSnapshotForm',
102
		'project/$Project/environment/$Environment/metrics' => 'metrics',
103
		'project/$Project/environment/$Environment/pipeline/$Identifier//$Action/$ID/$OtherID' => 'pipeline',
104
		'project/$Project/environment/$Environment/deploy_summary' => 'deploySummary',
105
		'project/$Project/environment/$Environment/git_revisions' => 'gitRevisions',
106
		'project/$Project/environment/$Environment/start-deploy' => 'startDeploy',
107
		'project/$Project/environment/$Environment/deploy/$Identifier/log' => 'deploylog',
108
		'project/$Project/environment/$Environment/deploy/$Identifier' => 'deploy',
109
		'project/$Project/transfer/$Identifier/log' => 'transferlog',
110
		'project/$Project/transfer/$Identifier' => 'transfer',
111
		'project/$Project/environment/$Environment' => 'environment',
112
		'project/$Project/branch' => 'branch',
113
		'project/$Project/build/$Build' => 'build',
114
		'project/$Project/restoresnapshot/$DataArchiveID' => 'restoresnapshot',
115
		'project/$Project/deletesnapshot/$DataArchiveID' => 'deletesnapshot',
116
		'project/$Project/movesnapshot/$DataArchiveID' => 'movesnapshot',
117
		'project/$Project/update' => 'update',
118
		'project/$Project/snapshots' => 'snapshots',
119
		'project/$Project/createsnapshot' => 'createsnapshot',
120
		'project/$Project/uploadsnapshot' => 'uploadsnapshot',
121
		'project/$Project/snapshotslog' => 'snapshotslog',
122
		'project/$Project/postsnapshotsuccess/$DataArchiveID' => 'postsnapshotsuccess',
123
		'project/$Project/star' => 'toggleprojectstar',
124
		'project/$Project' => 'project',
125
		'nav/$Project' => 'nav',
126
		'projects' => 'projects',
127
	);
128
129
	/**
130
	 * @var array
131
	 */
132
	protected static $_project_cache = array();
133
134
	/**
135
	 * @var array
136
	 */
137
	private static $support_links = array();
138
139
	/**
140
	 * @var array
141
	 */
142
	private static $platform_specific_strings = array();
143
144
	/**
145
	 * @var array
146
	 */
147
	private static $action_types = array(
148
		self::ACTION_DEPLOY,
149
		self::ACTION_SNAPSHOT,
150
		self::PROJECT_OVERVIEW
151
	);
152
153
	/**
154
	 * @var DNData
155
	 */
156
	protected $data;
157
158
	/**
159
	 * Include requirements that deploynaut needs, such as javascript.
160
	 */
161
	public static function include_requirements() {
162
163
		// JS should always go to the bottom, otherwise there's the risk that Requirements
164
		// puts them halfway through the page to the nearest <script> tag. We don't want that.
165
		Requirements::set_force_js_to_bottom(true);
166
167
		// todo these should be bundled into the same JS as the others in "static" below.
168
		// We've deliberately not used combined_files as it can mess with some of the JS used
169
		// here and cause sporadic errors.
170
		Requirements::javascript('deploynaut/javascript/jquery.js');
171
		Requirements::javascript('deploynaut/javascript/bootstrap.js');
172
		Requirements::javascript('deploynaut/javascript/q.js');
173
		Requirements::javascript('deploynaut/javascript/tablefilter.js');
174
		Requirements::javascript('deploynaut/javascript/deploynaut.js');
175
		Requirements::javascript('deploynaut/javascript/react-with-addons.js');
176
		Requirements::javascript('deploynaut/javascript/bootstrap.file-input.js');
177
		Requirements::javascript('deploynaut/thirdparty/select2/dist/js/select2.min.js');
178
		Requirements::javascript('deploynaut/javascript/material.js');
179
180
		// Load the buildable dependencies only if not loaded centrally.
181
		if (!is_dir(BASE_PATH . DIRECTORY_SEPARATOR . 'static')) {
182
			if (\Director::isDev()) {
183
				\Requirements::javascript('deploynaut/static/bundle-debug.js');
184
			} else {
185
				\Requirements::javascript('deploynaut/static/bundle.js');
186
			}
187
		}
188
189
		Requirements::css('deploynaut/static/style.css');
190
	}
191
192
	/**
193
	 * Check for feature flags:
194
	 * - FLAG_SNAPSHOTS_ENABLED: set to true to enable globally
195
	 * - FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS: set to semicolon-separated list of email addresses of allowed users.
196
	 *
197
	 * @return boolean
198
	 */
199
	public static function FlagSnapshotsEnabled() {
200
		if(defined('FLAG_SNAPSHOTS_ENABLED') && FLAG_SNAPSHOTS_ENABLED) {
201
			return true;
202
		}
203
		if(defined('FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS') && FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS) {
204
			$allowedMembers = explode(';', FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS);
205
			$member = Member::currentUser();
206
			if($allowedMembers && $member && in_array($member->Email, $allowedMembers)) {
207
				return true;
208
			}
209
		}
210
		return false;
211
	}
212
213
	/**
214
	 * @return ArrayList
215
	 */
216
	public static function get_support_links() {
217
		$supportLinks = self::config()->support_links;
218
		if($supportLinks) {
219
			return new ArrayList($supportLinks);
220
		}
221
	}
222
223
	/**
224
	 * @return array
225
	 */
226
	public static function get_template_global_variables() {
227
		return array(
228
			'RedisUnavailable' => 'RedisUnavailable',
229
			'RedisWorkersCount' => 'RedisWorkersCount',
230
			'SidebarLinks' => 'SidebarLinks',
231
			"SupportLinks" => 'get_support_links'
232
		);
233
	}
234
235
	/**
236
	 */
237
	public function init() {
238
		parent::init();
239
240
		if(!Member::currentUser() && !Session::get('AutoLoginHash')) {
241
			return Security::permissionFailure();
242
		}
243
244
		// Block framework jquery
245
		Requirements::block(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
246
247
		self::include_requirements();
248
	}
249
250
	/**
251
	 * @return string
252
	 */
253
	public function Link() {
254
		return "naut/";
255
	}
256
257
	/**
258
	 * Actions
259
	 *
260
	 * @param SS_HTTPRequest $request
261
	 * @return \SS_HTTPResponse
262
	 */
263
	public function index(SS_HTTPRequest $request) {
264
		return $this->redirect($this->Link() . 'projects/');
265
	}
266
267
	/**
268
	 * Action
269
	 *
270
	 * @param SS_HTTPRequest $request
271
	 * @return string - HTML
272
	 */
273
	public function projects(SS_HTTPRequest $request) {
274
		// Performs canView permission check by limiting visible projects in DNProjectsList() call.
275
		return $this->customise(array(
276
			'Title' => 'Projects',
277
		))->render();
278
	}
279
280
	/**
281
	 * @param SS_HTTPRequest $request
282
	 * @return HTMLText
283
	 */
284
	public function nav(SS_HTTPRequest $request) {
285
		return $this->renderWith('Nav');
286
	}
287
288
	/**
289
	 * Return a link to the navigation template used for AJAX requests.
290
	 * @return string
291
	 */
292
	public function NavLink() {
293
		$currentProject = $this->getCurrentProject();
294
		$projectName = $currentProject ? $currentProject->Name : null;
295
		return Controller::join_links(Director::absoluteBaseURL(), 'naut', 'nav', $projectName);
296
	}
297
298
	/**
299
	 * Action
300
	 *
301
	 * @param SS_HTTPRequest $request
302
	 * @return SS_HTTPResponse - HTML
303
	 */
304
	public function snapshots(SS_HTTPRequest $request) {
305
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
306
		return $this->getCustomisedViewSection('SnapshotsSection', 'Data Snapshots');
307
	}
308
309
	/**
310
	 * Action
311
	 *
312
	 * @param SS_HTTPRequest $request
313
	 * @return string - HTML
314
	 */
315 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...
316
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
317
318
		// Performs canView permission check by limiting visible projects
319
		$project = $this->getCurrentProject();
320
		if(!$project) {
321
			return $this->project404Response();
322
		}
323
324
		if(!$project->canBackup()) {
325
			return new SS_HTTPResponse("Not allowed to create snapshots on any environments", 401);
326
		}
327
328
		return $this->customise(array(
329
			'Title' => 'Create Data Snapshot',
330
			'SnapshotsSection' => 1,
331
			'DataTransferForm' => $this->getDataTransferForm($request)
332
		))->render();
333
	}
334
335
	/**
336
	 * Action
337
	 *
338
	 * @param SS_HTTPRequest $request
339
	 * @return string - HTML
340
	 */
341 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...
342
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
343
344
		// Performs canView permission check by limiting visible projects
345
		$project = $this->getCurrentProject();
346
		if(!$project) {
347
			return $this->project404Response();
348
		}
349
350
		if(!$project->canUploadArchive()) {
351
			return new SS_HTTPResponse("Not allowed to upload", 401);
352
		}
353
354
		return $this->customise(array(
355
			'SnapshotsSection' => 1,
356
			'UploadSnapshotForm' => $this->getUploadSnapshotForm($request),
357
			'PostSnapshotForm' => $this->getPostSnapshotForm($request)
358
		))->render();
359
	}
360
361
	/**
362
	 * Return the upload limit for snapshot uploads
363
	 * @return string
364
	 */
365
	public function UploadLimit() {
366
		return File::format_size(min(
367
			File::ini2bytes(ini_get('upload_max_filesize')),
368
			File::ini2bytes(ini_get('post_max_size'))
369
		));
370
	}
371
372
	/**
373
	 * Construct the upload form.
374
	 *
375
	 * @param SS_HTTPRequest $request
376
	 * @return Form
377
	 */
378
	public function getUploadSnapshotForm(SS_HTTPRequest $request) {
379
		// Performs canView permission check by limiting visible projects
380
		$project = $this->getCurrentProject();
381
		if(!$project) {
382
			return $this->project404Response();
383
		}
384
385
		if(!$project->canUploadArchive()) {
386
			return new SS_HTTPResponse("Not allowed to upload", 401);
387
		}
388
389
		// Framing an environment as a "group of people with download access"
390
		// makes more sense to the user here, while still allowing us to enforce
391
		// environment specific restrictions on downloading the file later on.
392
		$envs = $project->DNEnvironmentList()->filterByCallback(function($item) {
393
			return $item->canUploadArchive();
394
		});
395
		$envsMap = array();
396
		foreach($envs as $env) {
397
			$envsMap[$env->ID] = $env->Name;
398
		}
399
400
		$maxSize = min(File::ini2bytes(ini_get('upload_max_filesize')), File::ini2bytes(ini_get('post_max_size')));
401
		$fileField = DataArchiveFileField::create('ArchiveFile', 'File');
402
		$fileField->getValidator()->setAllowedExtensions(array('sspak'));
403
		$fileField->getValidator()->setAllowedMaxFileSize(array('*' => $maxSize));
404
405
		$form = Form::create(
406
			$this,
407
			'UploadSnapshotForm',
408
			FieldList::create(
409
				$fileField,
410
				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
411
				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
412
					->setEmptyString('Select an environment')
413
			),
414
			FieldList::create(
415
				FormAction::create('doUploadSnapshot', 'Upload File')
416
					->addExtraClass('btn')
417
			),
418
			RequiredFields::create('ArchiveFile')
419
		);
420
421
		$form->disableSecurityToken();
422
		$form->addExtraClass('fields-wide');
423
		// Tweak the action so it plays well with our fake URL structure.
424
		$form->setFormAction($project->Link() . '/UploadSnapshotForm');
425
426
		return $form;
427
	}
428
429
	/**
430
	 * @param array $data
431
	 * @param Form $form
432
	 *
433
	 * @return bool|HTMLText|SS_HTTPResponse
434
	 */
435
	public function doUploadSnapshot($data, Form $form) {
436
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
437
438
		// Performs canView permission check by limiting visible projects
439
		$project = $this->getCurrentProject();
440
		if(!$project) {
441
			return $this->project404Response();
442
		}
443
444
		$validEnvs = $project->DNEnvironmentList()
445
			->filterByCallback(function($item) {
446
				return $item->canUploadArchive();
447
			});
448
449
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
450
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
451
		if(!$environment) {
452
			throw new LogicException('Invalid environment');
453
		}
454
455
		$this->validateSnapshotMode($data['Mode']);
456
457
		$dataArchive = DNDataArchive::create(array(
458
			'AuthorID' => Member::currentUserID(),
459
			'EnvironmentID' => $data['EnvironmentID'],
460
			'IsManualUpload' => true,
461
		));
462
		// needs an ID and transfer to determine upload path
463
		$dataArchive->write();
464
		$dataTransfer = DNDataTransfer::create(array(
465
			'AuthorID' => Member::currentUserID(),
466
			'Mode' => $data['Mode'],
467
			'Origin' => 'ManualUpload',
468
			'EnvironmentID' => $data['EnvironmentID']
469
		));
470
		$dataTransfer->write();
471
		$dataArchive->DataTransfers()->add($dataTransfer);
472
		$form->saveInto($dataArchive);
473
		$dataArchive->write();
474
		$workingDir = TEMP_FOLDER . DIRECTORY_SEPARATOR . 'deploynaut-transfer-' . $dataTransfer->ID;
475
476
		$cleanupFn = function() use($workingDir, $dataTransfer, $dataArchive) {
477
			$process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir)));
478
			$process->run();
479
			$dataTransfer->delete();
480
			$dataArchive->delete();
481
		};
482
483
		// extract the sspak contents so we can inspect them
484
		try {
485
			$dataArchive->extractArchive($workingDir);
486
		} catch(Exception $e) {
487
			$cleanupFn();
488
			$form->sessionMessage(
489
				'There was a problem trying to open your snapshot for processing. Please try uploading again',
490
				'bad'
491
			);
492
			return $this->redirectBack();
493
		}
494
495
		// validate that the sspak contents match the declared contents
496
		$result = $dataArchive->validateArchiveContents();
497
		if(!$result->valid()) {
498
			$cleanupFn();
499
			$form->sessionMessage($result->message(), 'bad');
500
			return $this->redirectBack();
501
		}
502
503
		// fix file permissions of extracted sspak files then re-build the sspak
504
		try {
505
			$dataArchive->fixArchivePermissions($workingDir);
506
			$dataArchive->setArchiveFromFiles($workingDir);
507
		} catch(Exception $e) {
508
			$cleanupFn();
509
			$form->sessionMessage(
510
				'There was a problem processing your snapshot. Please try uploading again',
511
				'bad'
512
			);
513
			return $this->redirectBack();
514
		}
515
516
		// cleanup any extracted sspak contents lying around
517
		$process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir)));
518
		$process->run();
519
520
		return $this->customise(array(
521
			'Project' => $project,
522
			'CurrentProject' => $project,
523
			'SnapshotsSection' => 1,
524
			'DataArchive' => $dataArchive,
525
			'DataTransferRestoreForm' => $this->getDataTransferRestoreForm($this->request, $dataArchive),
526
			'BackURL' => $project->Link('snapshots')
527
		))->renderWith(array('DNRoot_uploadsnapshot', 'DNRoot'));
528
	}
529
530
	/**
531
	 * @param SS_HTTPRequest $request
532
	 * @return Form
533
	 */
534
	public function getPostSnapshotForm(SS_HTTPRequest $request) {
535
		// Performs canView permission check by limiting visible projects
536
		$project = $this->getCurrentProject();
537
		if(!$project) {
538
			return $this->project404Response();
539
		}
540
541
		if(!$project->canUploadArchive()) {
542
			return new SS_HTTPResponse("Not allowed to upload", 401);
543
		}
544
545
		// Framing an environment as a "group of people with download access"
546
		// makes more sense to the user here, while still allowing us to enforce
547
		// environment specific restrictions on downloading the file later on.
548
		$envs = $project->DNEnvironmentList()->filterByCallback(function($item) {
549
			return $item->canUploadArchive();
550
		});
551
		$envsMap = array();
552
		foreach($envs as $env) {
553
			$envsMap[$env->ID] = $env->Name;
554
		}
555
556
		$form = Form::create(
557
			$this,
558
			'PostSnapshotForm',
559
			FieldList::create(
560
				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
561
				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
562
					->setEmptyString('Select an environment')
563
			),
564
			FieldList::create(
565
				FormAction::create('doPostSnapshot', 'Submit request')
566
					->addExtraClass('btn')
567
			),
568
			RequiredFields::create('File')
569
		);
570
571
		$form->disableSecurityToken();
572
		$form->addExtraClass('fields-wide');
573
		// Tweak the action so it plays well with our fake URL structure.
574
		$form->setFormAction($project->Link() . '/PostSnapshotForm');
575
576
		return $form;
577
	}
578
579
	/**
580
	 * @param array $data
581
	 * @param Form $form
582
	 *
583
	 * @return SS_HTTPResponse
584
	 */
585
	public function doPostSnapshot($data, $form) {
586
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
587
588
		$project = $this->getCurrentProject();
589
		if(!$project) {
590
			return $this->project404Response();
591
		}
592
593
		$validEnvs = $project->DNEnvironmentList()->filterByCallback(function($item) {
594
				return $item->canUploadArchive();
595
		});
596
597
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
598
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
599
		if(!$environment) {
600
			throw new LogicException('Invalid environment');
601
		}
602
603
		$dataArchive = DNDataArchive::create(array(
604
			'UploadToken' => DNDataArchive::generate_upload_token(),
605
		));
606
		$form->saveInto($dataArchive);
607
		$dataArchive->write();
608
609
		return $this->redirect(Controller::join_links(
610
			$project->Link(),
611
			'postsnapshotsuccess',
612
			$dataArchive->ID
613
		));
614
	}
615
616
	/**
617
	 * Action
618
	 *
619
	 * @param SS_HTTPRequest $request
620
	 * @return SS_HTTPResponse - HTML
621
	 */
622
	public function snapshotslog(SS_HTTPRequest $request) {
623
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
624
		return $this->getCustomisedViewSection('SnapshotsSection', 'Snapshots log');
625
	}
626
627
	/**
628
	 * @param SS_HTTPRequest $request
629
	 * @return SS_HTTPResponse|string
630
	 * @throws SS_HTTPResponse_Exception
631
	 */
632
	public function postsnapshotsuccess(SS_HTTPRequest $request) {
633
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
634
635
		// Performs canView permission check by limiting visible projects
636
		$project = $this->getCurrentProject();
637
		if(!$project) {
638
			return $this->project404Response();
639
		}
640
641
		if(!$project->canUploadArchive()) {
642
			return new SS_HTTPResponse("Not allowed to upload", 401);
643
		}
644
645
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
646
		if(!$dataArchive) {
647
			return new SS_HTTPResponse("Archive not found.", 404);
648
		}
649
650
		if(!$dataArchive->canRestore()) {
651
			throw new SS_HTTPResponse_Exception('Not allowed to restore archive', 403);
652
		}
653
654
		return $this->render(array(
655
				'Title' => 'How to send us your Data Snapshot by post',
656
				'DataArchive' => $dataArchive,
657
				'Address' => Config::inst()->get('Deploynaut', 'snapshot_post_address'),
658
				'BackURL' => $project->Link(),
659
			));
660
	}
661
662
	/**
663
	 * @param SS_HTTPRequest $request
664
	 * @return \SS_HTTPResponse
665
	 */
666
	public function project(SS_HTTPRequest $request) {
667
		$this->setCurrentActionType(self::PROJECT_OVERVIEW);
668
		return $this->getCustomisedViewSection('ProjectOverview', '', array('IsAdmin' => Permission::check('ADMIN')));
669
	}
670
671
	/**
672
	 * This action will star / unstar a project for the current member
673
	 *
674
	 * @param SS_HTTPRequest $request
675
	 *
676
	 * @return SS_HTTPResponse
677
	 */
678
	public function toggleprojectstar(SS_HTTPRequest $request) {
679
		$project = $this->getCurrentProject();
680
		if(!$project) {
681
			return $this->project404Response();
682
		}
683
684
		$member = Member::currentUser();
685
		if($member === null) {
686
			return $this->project404Response();
687
		}
688
		$favProject = $member->StarredProjects()
689
			->filter('DNProjectID', $project->ID)
690
			->first();
691
692
		if($favProject) {
693
			$member->StarredProjects()->remove($favProject);
694
		} else {
695
			$member->StarredProjects()->add($project);
696
		}
697
		return $this->redirectBack();
698
	}
699
700
	/**
701
	 * @param SS_HTTPRequest $request
702
	 * @return \SS_HTTPResponse
703
	 */
704
	public function branch(SS_HTTPRequest $request) {
705
		$project = $this->getCurrentProject();
706
		if(!$project) {
707
			return $this->project404Response();
708
		}
709
710
		$branchName = $request->getVar('name');
711
		$branch = $project->DNBranchList()->byName($branchName);
712
		if(!$branch) {
713
			return new SS_HTTPResponse("Branch '" . Convert::raw2xml($branchName) . "' not found.", 404);
714
		}
715
716
		return $this->render(array(
717
			'CurrentBranch' => $branch,
718
		));
719
	}
720
721
	/**
722
	 * @param SS_HTTPRequest $request
723
	 * @return \SS_HTTPResponse
724
	 */
725
	public function environment(SS_HTTPRequest $request) {
726
		// Performs canView permission check by limiting visible projects
727
		$project = $this->getCurrentProject();
728
		if(!$project) {
729
			return $this->project404Response();
730
		}
731
732
		// Performs canView permission check by limiting visible projects
733
		$env = $this->getCurrentEnvironment($project);
734
		if(!$env) {
735
			return $this->environment404Response();
736
		}
737
738
		return $this->render(array(
739
			'DNEnvironmentList' => $this->getCurrentProject()->DNEnvironmentList(),
740
			'FlagSnapshotsEnabled' => $this->FlagSnapshotsEnabled(),
741
		));
742
	}
743
744
745
	/**
746
	 * Initiate a pipeline dry run
747
	 *
748
	 * @param array $data
749
	 * @param DeployForm $form
750
	 *
751
	 * @return SS_HTTPResponse
752
	 */
753
	public function doDryRun($data, DeployForm $form) {
754
		return $this->beginPipeline($data, $form, true);
755
	}
756
757
	/**
758
	 * Initiate a pipeline
759
	 *
760
	 * @param array $data
761
	 * @param DeployForm $form
762
	 * @return \SS_HTTPResponse
763
	 */
764
	public function startPipeline($data, $form) {
765
		return $this->beginPipeline($data, $form);
766
	}
767
768
	/**
769
	 * Start a pipeline
770
	 *
771
	 * @param array $data
772
	 * @param DeployForm $form
773
	 * @param bool $isDryRun
774
	 * @return \SS_HTTPResponse
775
	 */
776
	protected function beginPipeline($data, DeployForm $form, $isDryRun = false) {
777
		$buildName = $form->getSelectedBuild($data);
778
779
		// Performs canView permission check by limiting visible projects
780
		$project = $this->getCurrentProject();
781
		if(!$project) {
782
			return $this->project404Response();
783
		}
784
785
		// Performs canView permission check by limiting visible projects
786
		$environment = $this->getCurrentEnvironment($project);
787
		if(!$environment) {
788
			return $this->environment404Response();
789
		}
790
791
		if(!$environment->DryRunEnabled && $isDryRun) {
792
			return new SS_HTTPResponse("Dry-run for pipelines is not enabled for this environment", 404);
793
		}
794
795
		// Initiate the pipeline
796
		$sha = $project->DNBuildList()->byName($buildName);
797
		$pipeline = Pipeline::create();
798
		$pipeline->DryRun = $isDryRun;
799
		$pipeline->EnvironmentID = $environment->ID;
800
		$pipeline->AuthorID = Member::currentUserID();
801
		$pipeline->SHA = $sha->FullName();
802
		// Record build at time of execution
803
		if($currentBuild = $environment->CurrentBuild()) {
804
			$pipeline->PreviousDeploymentID = $currentBuild->ID;
805
		}
806
		$pipeline->start(); // start() will call write(), so no need to do it here as well.
807
		return $this->redirect($environment->Link());
808
	}
809
810
	/**
811
	 * @param SS_HTTPRequest $request
812
	 *
813
	 * @return SS_HTTPResponse
814
	 * @throws SS_HTTPResponse_Exception
815
	 */
816
	public function pipeline(SS_HTTPRequest $request) {
817
		$params = $request->params();
818
		$pipeline = Pipeline::get()->byID($params['Identifier']);
819
820
		if(!$pipeline || !$pipeline->ID || !$pipeline->Environment()) {
821
			throw new SS_HTTPResponse_Exception('Pipeline not found', 404);
822
		}
823
		if(!$pipeline->Environment()->canView()) {
824
			return Security::permissionFailure();
825
		}
826
827
		$environment = $pipeline->Environment();
828
		$project = $pipeline->Environment()->Project();
829
830
		if($environment->Name != $params['Environment']) {
831
			throw new LogicException("Environment in URL doesn't match this pipeline");
832
		}
833
		if($project->Name != $params['Project']) {
834
			throw new LogicException("Project in URL doesn't match this pipeline");
835
		}
836
837
		// Delegate to sub-requesthandler
838
		return PipelineController::create($this, $pipeline);
839
	}
840
841
	/**
842
	 *
843
	 * @param SS_HTTPRequest $request
844
	 * @return \SS_HTTPResponse
845
	 */
846
	public function metrics(SS_HTTPRequest $request) {
847
		// Performs canView permission check by limiting visible projects
848
		$project = $this->getCurrentProject();
849
		if(!$project) {
850
			return $this->project404Response();
851
		}
852
853
		// Performs canView permission check by limiting visible projects
854
		$env = $this->getCurrentEnvironment($project);
855
		if(!$env) {
856
			return $this->environment404Response();
857
		}
858
859
		return $this->render();
860
	}
861
862
	/**
863
	 * Get the DNData object.
864
	 *
865
	 * @return DNData
866
	 */
867
	public function DNData() {
868
		return DNData::inst();
869
	}
870
871
	/**
872
	 * Provide a list of all projects.
873
	 *
874
	 * @return SS_List
875
	 */
876
	public function DNProjectList() {
877
		$memberId = Member::currentUserID();
878
		if(!$memberId) {
879
			return new ArrayList();
880
		}
881
882
		if(Permission::check('ADMIN')) {
883
			return DNProject::get();
884
		}
885
886
		$projects = Member::get()->filter('ID', $memberId)
887
			->relation('Groups')
888
			->relation('Projects');
889
890
		$this->extend('updateDNProjectList', $projects);
891
		return $projects;
892
	}
893
894
	/**
895
	 * @return ArrayList
896
	 */
897
	public function getPlatformSpecificStrings() {
898
		$strings = $this->config()->platform_specific_strings;
899
		if ($strings) {
900
			return new ArrayList($strings);
901
		}
902
	}
903
904
	/**
905
	 * Provide a list of all starred projects for the currently logged in member
906
	 *
907
	 * @return SS_List
908
	 */
909
	public function getStarredProjects() {
910
		$member = Member::currentUser();
911
		if($member === null) {
912
			return new ArrayList();
913
		}
914
915
		$favProjects = $member->StarredProjects();
916
917
		$list = new ArrayList();
918
		foreach($favProjects as $project) {
919
			if($project->canView($member)) {
920
				$list->add($project);
921
			}
922
		}
923
		return $list;
924
	}
925
926
	/**
927
	 * Returns top level navigation of projects.
928
	 *
929
	 * @param int $limit
930
	 *
931
	 * @return ArrayList
932
	 */
933
	public function Navigation($limit = 5) {
934
		$navigation = new ArrayList();
935
936
		$currentProject = $this->getCurrentProject();
937
		$currentEnvironment = $this->getCurrentEnvironment();
938
		$actionType = $this->getCurrentActionType();
939
940
		$projects = $this->getStarredProjects();
941
		if($projects->count() < 1) {
942
			$projects = $this->DNProjectList();
943
		} else {
944
			$limit = -1;
945
		}
946
947
		if($projects->count() > 0) {
948
			$activeProject = false;
949
950
			if($limit > 0) {
951
				$limitedProjects = $projects->limit($limit);
952
			} else {
953
				$limitedProjects = $projects;
954
			}
955
956
			foreach($limitedProjects as $project) {
957
				$isActive = $currentProject && $currentProject->ID == $project->ID;
958
				if($isActive) {
959
					$activeProject = true;
960
				}
961
962
				$isCurrentEnvironment = false;
963
				if($project && $currentEnvironment) {
964
					$isCurrentEnvironment = (bool) $project->DNEnvironmentList()->find('ID', $currentEnvironment->ID);
965
				}
966
967
				$navigation->push(array(
968
					'Project' => $project,
969
					'IsCurrentEnvironment' => $isCurrentEnvironment,
970
					'IsActive' => $currentProject && $currentProject->ID == $project->ID,
971
					'IsOverview' => $actionType == self::PROJECT_OVERVIEW && $currentProject->ID == $project->ID
972
				));
973
			}
974
975
			// Ensure the current project is in the list
976
			if(!$activeProject && $currentProject) {
977
				$navigation->unshift(array(
978
					'Project' => $currentProject,
979
					'IsActive' => true,
980
					'IsCurrentEnvironment' => $currentEnvironment,
981
					'IsOverview' => $actionType == self::PROJECT_OVERVIEW
982
				));
983
				if($limit > 0 && $navigation->count() > $limit) {
984
					$navigation->pop();
985
				}
986
			}
987
		}
988
989
		return $navigation;
990
	}
991
992
	/**
993
	 * Construct the deployment form
994
	 *
995
	 * @return Form
996
	 */
997
	public function getDeployForm($request = null) {
998
999
		// Performs canView permission check by limiting visible projects
1000
		$project = $this->getCurrentProject();
1001
		if(!$project) {
1002
			return $this->project404Response();
1003
		}
1004
1005
		// Performs canView permission check by limiting visible projects
1006
		$environment = $this->getCurrentEnvironment($project);
1007
		if(!$environment) {
1008
			return $this->environment404Response();
1009
		}
1010
1011
		if(!$environment->canDeploy()) {
1012
			return new SS_HTTPResponse("Not allowed to deploy", 401);
1013
		}
1014
1015
		// Generate the form
1016
		$form = new DeployForm($this, 'DeployForm', $environment, $project);
1017
1018
		// If this is an ajax request we don't want to submit the form - we just want to retrieve the markup.
1019
		if(
1020
			$request &&
1021
			!$request->requestVar('action_showDeploySummary') &&
1022
			$this->getRequest()->isAjax() &&
1023
			$this->getRequest()->isGET()
1024
		) {
1025
			// We can just use the URL we're accessing
1026
			$form->setFormAction($this->getRequest()->getURL());
1027
1028
			$body = json_encode(array('Content' => $form->forAjaxTemplate()->forTemplate()));
1029
			$this->getResponse()->addHeader('Content-Type', 'application/json');
1030
			$this->getResponse()->setBody($body);
1031
			return $body;
1032
		}
1033
1034
		$form->setFormAction($this->getRequest()->getURL() . '/DeployForm');
1035
		return $form;
1036
	}
1037
1038
	/**
1039
	 * @param SS_HTTPRequest $request
1040
	 *
1041
	 * @return SS_HTTPResponse|string
1042
	 */
1043
	public function gitRevisions(SS_HTTPRequest $request) {
1044
1045
		// Performs canView permission check by limiting visible projects
1046
		$project = $this->getCurrentProject();
1047
		if(!$project) {
1048
			return $this->project404Response();
1049
		}
1050
1051
		// Performs canView permission check by limiting visible projects
1052
		$env = $this->getCurrentEnvironment($project);
1053
		if(!$env) {
1054
			return $this->environment404Response();
1055
		}
1056
1057
		// For now only permit advanced options on one environment type, because we hacked the "full-deploy"
1058
		// checkbox in. Other environments such as the fast or capistrano one wouldn't know what to do with it.
1059
		if(get_class($env) === 'RainforestEnvironment') {
1060
			$advanced = Permission::check('DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS') ? 'true' : 'false';
1061
		} else {
1062
			$advanced = 'false';
1063
		}
1064
1065
		$tabs = array();
1066
		$id = 0;
1067
		$data = array(
1068
			'id' => ++$id,
1069
			'name' => 'Deploy the latest version of a branch',
1070
			'field_type' => 'dropdown',
1071
			'field_label' => 'Choose a branch',
1072
			'field_id' => 'branch',
1073
			'field_data' => array(),
1074
			'advanced_opts' => $advanced
1075
		);
1076
		foreach($project->DNBranchList() as $branch) {
1077
			$sha = $branch->SHA();
1078
			$name = $branch->Name();
1079
			$branchValue = sprintf("%s (%s, %s old)",
1080
				$name,
1081
				substr($sha, 0, 8),
1082
				$branch->LastUpdated()->TimeDiff()
1083
			);
1084
			$data['field_data'][] = array(
1085
				'id' => $sha,
1086
				'text' => $branchValue
1087
			);
1088
		}
1089
		$tabs[] = $data;
1090
1091
		$data = array(
1092
			'id' => ++$id,
1093
			'name' => 'Deploy a tagged release',
1094
			'field_type' => 'dropdown',
1095
			'field_label' => 'Choose a tag',
1096
			'field_id' => 'tag',
1097
			'field_data' => array(),
1098
			'advanced_opts' => $advanced
1099
		);
1100
1101
		foreach($project->DNTagList()->setLimit(null) as $tag) {
1102
			$name = $tag->Name();
1103
			$data['field_data'][] = array(
1104
				'id' => $tag->SHA(),
1105
				'text' => sprintf("%s", $name)
1106
			);
1107
		}
1108
1109
		// show newest tags first.
1110
		$data['field_data'] = array_reverse($data['field_data']);
1111
1112
		$tabs[] = $data;
1113
1114
		// Past deployments
1115
		$data = array(
1116
			'id' => ++$id,
1117
			'name' => 'Redeploy a release that was previously deployed (to any environment)',
1118
			'field_type' => 'dropdown',
1119
			'field_label' => 'Choose a previously deployed release',
1120
			'field_id' => 'release',
1121
			'field_data' => array(),
1122
			'advanced_opts' => $advanced
1123
		);
1124
		// We are aiming at the format:
1125
		// [{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...
1126
		$redeploy = array();
1127
		foreach($project->DNEnvironmentList() as $dnEnvironment) {
1128
			$envName = $dnEnvironment->Name;
1129
			$perEnvDeploys = array();
1130
1131
			foreach($dnEnvironment->DeployHistory() as $deploy) {
1132
				$sha = $deploy->SHA;
1133
1134
				// Check if exists to make sure the newest deployment date is used.
1135
				if(!isset($perEnvDeploys[$sha])) {
1136
					$pastValue = sprintf("%s (deployed %s)",
1137
						substr($sha, 0, 8),
1138
						$deploy->obj('LastEdited')->Ago()
1139
					);
1140
					$perEnvDeploys[$sha] = array(
1141
						'id' => $sha,
1142
						'text' => $pastValue
1143
					);
1144
				}
1145
			}
1146
1147
			if(!empty($perEnvDeploys)) {
1148
				$redeploy[$envName] = array_values($perEnvDeploys);
1149
			}
1150
		}
1151
		// Convert the array to the frontend format (i.e. keyed to regular array)
1152
		foreach($redeploy as $env => $descr) {
1153
			$data['field_data'][] = array('text'=>$env, 'children'=>$descr);
1154
		}
1155
		$tabs[] = $data;
1156
1157
		$data = array(
1158
			'id' => ++$id,
1159
			'name' => 'Deploy a specific SHA',
1160
			'field_type' => 'textfield',
1161
			'field_label' => 'Choose a SHA',
1162
			'field_id' => 'SHA',
1163
			'field_data' => array(),
1164
			'advanced_opts' => $advanced
1165
		);
1166
		$tabs[] = $data;
1167
1168
		// get the last time git fetch was run
1169
		$lastFetched = 'never';
1170
		$fetch = DNGitFetch::get()
1171
			->filter('ProjectID', $project->ID)
1172
			->sort('LastEdited', 'DESC')
1173
			->first();
1174
		if($fetch) {
1175
			$lastFetched = $fetch->dbObject('LastEdited')->Ago();
1176
		}
1177
1178
		$data = array(
1179
			'Tabs' => $tabs,
1180
			'last_fetched' => $lastFetched
1181
		);
1182
1183
		return json_encode($data, JSON_PRETTY_PRINT);
1184
	}
1185
1186
	/**
1187
	 * Check and regenerate a global CSRF token
1188
	 *
1189
	 * @param SS_HTTPRequest $request
1190
	 * @param bool $resetToken
1191
	 *
1192
	 * @return bool
1193
	 */
1194
	protected function checkCsrfToken(SS_HTTPRequest $request, $resetToken = true) {
1195
		$token = SecurityToken::inst();
1196
1197
		// Ensure the submitted token has a value
1198
		$submittedToken = $request->postVar('SecurityID');
1199
		if(!$submittedToken) {
1200
			return false;
1201
		}
1202
1203
		// Do the actual check.
1204
		$check = $token->check($submittedToken);
1205
1206
		// Reset the token after we've checked the existing token
1207
		if($resetToken) {
1208
			$token->reset();
1209
		}
1210
1211
		// Return whether the token was correct or not
1212
		return $check;
1213
	}
1214
1215
	/**
1216
	 * @param SS_HTTPRequest $request
1217
	 *
1218
	 * @return string
1219
	 */
1220
	public function deploySummary(SS_HTTPRequest $request) {
1221
1222
		// Performs canView permission check by limiting visible projects
1223
		$project = $this->getCurrentProject();
1224
		if(!$project) {
1225
			return $this->project404Response();
1226
		}
1227
1228
		// Performs canView permission check by limiting visible projects
1229
		$environment = $this->getCurrentEnvironment($project);
1230
		if(!$environment) {
1231
			return $this->environment404Response();
1232
		}
1233
1234
		// Plan the deployment.
1235
		$strategy = $environment->Backend()->planDeploy(
1236
			$environment,
1237
			$request->requestVars()
0 ignored issues
show
Bug introduced by
It seems like $request->requestVars() targeting SS_HTTPRequest::requestVars() can also be of type null; however, DeploymentBackend::planDeploy() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1238
		);
1239
		$data = $strategy->toArray();
1240
1241
		// Add in a URL for comparing from->to code changes. Ensure that we have
1242
		// two proper 40 character SHAs, otherwise we can't show the compare link.
1243
		$interface = $project->getRepositoryInterface();
1244
		if(
1245
			!empty($interface) && !empty($interface->URL)
1246
			&& !empty($data['changes']['Code version']['from'])
1247
			&& strlen($data['changes']['Code version']['from']) == '40'
1248
			&& !empty($data['changes']['Code version']['to'])
1249
			&& strlen($data['changes']['Code version']['to']) == '40'
1250
		) {
1251
			$compareurl = sprintf(
1252
				'%s/compare/%s...%s',
1253
				$interface->URL,
1254
				$data['changes']['Code version']['from'],
1255
				$data['changes']['Code version']['to']
1256
			);
1257
			$data['changes']['Code version']['compareUrl'] = $compareurl;
1258
		}
1259
1260
		// Append json to response
1261
		$token = SecurityToken::inst();
1262
		$data['SecurityID'] = $token->getValue();
1263
1264
		return json_encode($data);
1265
	}
1266
1267
	/**
1268
	 * Deployment form submission handler.
1269
	 *
1270
	 * Initiate a DNDeployment record and redirect to it for status polling
1271
	 *
1272
	 * @param SS_HTTPRequest $request
1273
	 *
1274
	 * @return SS_HTTPResponse
1275
	 * @throws ValidationException
1276
	 * @throws null
1277
	 */
1278
	public function startDeploy(SS_HTTPRequest $request) {
1279
1280
		// Ensure the CSRF Token is correct
1281
		if(!$this->checkCsrfToken($request)) {
1282
			// CSRF token didn't match
1283
			return $this->httpError(400, 'Bad Request');
1284
		}
1285
1286
		// Performs canView permission check by limiting visible projects
1287
		$project = $this->getCurrentProject();
1288
		if(!$project) {
1289
			return $this->project404Response();
1290
		}
1291
1292
		// Performs canView permission check by limiting visible projects
1293
		$environment = $this->getCurrentEnvironment($project);
1294
		if(!$environment) {
1295
			return $this->environment404Response();
1296
		}
1297
1298
		// Initiate the deployment
1299
		// The extension point should pass in: Project, Environment, SelectRelease, buildName
1300
		$this->extend('doDeploy', $project, $environment, $buildName, $data);
1301
1302
		// Start the deployment based on the approved strategy.
1303
		$strategy = new DeploymentStrategy($environment);
1304
		$strategy->fromArray($request->requestVar('strategy'));
1305
		$deployment = $strategy->createDeployment();
1306
		$deployment->start();
1307
1308
		return json_encode(array(
1309
			'url' => Director::absoluteBaseURL() . $deployment->Link()
1310
		), JSON_PRETTY_PRINT);
1311
	}
1312
1313
	/**
1314
	 * Action - Do the actual deploy
1315
	 *
1316
	 * @param SS_HTTPRequest $request
1317
	 *
1318
	 * @return SS_HTTPResponse|string
1319
	 * @throws SS_HTTPResponse_Exception
1320
	 */
1321
	public function deploy(SS_HTTPRequest $request) {
1322
		$params = $request->params();
1323
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1324
1325
		if(!$deployment || !$deployment->ID) {
1326
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1327
		}
1328
		if(!$deployment->canView()) {
1329
			return Security::permissionFailure();
1330
		}
1331
1332
		$environment = $deployment->Environment();
1333
		$project = $environment->Project();
1334
1335
		if($environment->Name != $params['Environment']) {
1336
			throw new LogicException("Environment in URL doesn't match this deploy");
1337
		}
1338
		if($project->Name != $params['Project']) {
1339
			throw new LogicException("Project in URL doesn't match this deploy");
1340
		}
1341
1342
		return $this->render(array(
1343
			'Deployment' => $deployment,
1344
		));
1345
	}
1346
1347
1348
	/**
1349
	 * Action - Get the latest deploy log
1350
	 *
1351
	 * @param SS_HTTPRequest $request
1352
	 *
1353
	 * @return string
1354
	 * @throws SS_HTTPResponse_Exception
1355
	 */
1356
	public function deploylog(SS_HTTPRequest $request) {
1357
		$params = $request->params();
1358
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1359
1360
		if(!$deployment || !$deployment->ID) {
1361
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1362
		}
1363
		if(!$deployment->canView()) {
1364
			return Security::permissionFailure();
1365
		}
1366
1367
		$environment = $deployment->Environment();
1368
		$project = $environment->Project();
1369
1370
		if($environment->Name != $params['Environment']) {
1371
			throw new LogicException("Environment in URL doesn't match this deploy");
1372
		}
1373
		if($project->Name != $params['Project']) {
1374
			throw new LogicException("Project in URL doesn't match this deploy");
1375
		}
1376
1377
		$log = $deployment->log();
1378
		if($log->exists()) {
1379
			$content = $log->content();
1380
		} else {
1381
			$content = 'Waiting for action to start';
1382
		}
1383
1384
		return $this->sendResponse($deployment->ResqueStatus(), $content);
1385
	}
1386
1387
	/**
1388
	 * @param SS_HTTPRequest|null $request
1389
	 *
1390
	 * @return Form
1391
	 */
1392
	public function getDataTransferForm(SS_HTTPRequest $request = null) {
1393
		// Performs canView permission check by limiting visible projects
1394
		$envs = $this->getCurrentProject()->DNEnvironmentList()->filterByCallback(function($item) {
1395
			return $item->canBackup();
1396
		});
1397
1398
		if(!$envs) {
1399
			return $this->environment404Response();
1400
		}
1401
1402
		$form = Form::create(
1403
			$this,
1404
			'DataTransferForm',
1405
			FieldList::create(
1406
				HiddenField::create('Direction', null, 'get'),
1407
				DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1408
					->setEmptyString('Select an environment'),
1409
				DropdownField::create('Mode', 'Transfer', DNDataArchive::get_mode_map())
1410
			),
1411
			FieldList::create(
1412
				FormAction::create('doDataTransfer', 'Create')
1413
					->addExtraClass('btn')
1414
			)
1415
		);
1416
		$form->setFormAction($this->getRequest()->getURL() . '/DataTransferForm');
1417
1418
		return $form;
1419
	}
1420
1421
	/**
1422
	 * @param array $data
1423
	 * @param Form $form
1424
	 *
1425
	 * @return SS_HTTPResponse
1426
	 * @throws SS_HTTPResponse_Exception
1427
	 */
1428
	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...
1429
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1430
1431
		// Performs canView permission check by limiting visible projects
1432
		$project = $this->getCurrentProject();
1433
		if(!$project) {
1434
			return $this->project404Response();
1435
		}
1436
1437
		$dataArchive = null;
1438
1439
		// Validate direction.
1440
		if($data['Direction'] == 'get') {
1441
			$validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1442
				->filterByCallback(function($item) {
1443
					return $item->canBackup();
1444
				});
1445
		} else if($data['Direction'] == 'push') {
1446
			$validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1447
				->filterByCallback(function($item) {
1448
					return $item->canRestore();
1449
				});
1450
		} else {
1451
			throw new LogicException('Invalid direction');
1452
		}
1453
1454
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
1455
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
1456
		if(!$environment) {
1457
			throw new LogicException('Invalid environment');
1458
		}
1459
1460
		$this->validateSnapshotMode($data['Mode']);
1461
1462
1463
		// Only 'push' direction is allowed an association with an existing archive.
1464
		if(
1465
			$data['Direction'] == 'push'
1466
			&& isset($data['DataArchiveID'])
1467
			&& is_numeric($data['DataArchiveID'])
1468
		) {
1469
			$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1470
			if(!$dataArchive) {
1471
				throw new LogicException('Invalid data archive');
1472
			}
1473
1474
			if(!$dataArchive->canDownload()) {
1475
				throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1476
			}
1477
		}
1478
1479
		$transfer = DNDataTransfer::create();
1480
		$transfer->EnvironmentID = $environment->ID;
1481
		$transfer->Direction = $data['Direction'];
1482
		$transfer->Mode = $data['Mode'];
1483
		$transfer->DataArchiveID = $dataArchive ? $dataArchive->ID : null;
1484
		if($data['Direction'] == 'push') {
1485
			$transfer->setBackupBeforePush(!empty($data['BackupBeforePush']));
1486
		}
1487
		$transfer->write();
1488
		$transfer->start();
1489
1490
		return $this->redirect($transfer->Link());
1491
	}
1492
1493
	/**
1494
	 * View into the log for a {@link DNDataTransfer}.
1495
	 *
1496
	 * @param SS_HTTPRequest $request
1497
	 *
1498
	 * @return SS_HTTPResponse|string
1499
	 * @throws SS_HTTPResponse_Exception
1500
	 */
1501
	public function transfer(SS_HTTPRequest $request) {
1502
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1503
1504
		$params = $request->params();
1505
		$transfer = DNDataTransfer::get()->byId($params['Identifier']);
1506
1507
		if(!$transfer || !$transfer->ID) {
1508
			throw new SS_HTTPResponse_Exception('Transfer not found', 404);
1509
		}
1510
		if(!$transfer->canView()) {
1511
			return Security::permissionFailure();
1512
		}
1513
1514
		$environment = $transfer->Environment();
1515
		$project = $environment->Project();
1516
1517
		if($project->Name != $params['Project']) {
1518
			throw new LogicException("Project in URL doesn't match this deploy");
1519
		}
1520
1521
		return $this->render(array(
1522
			'CurrentTransfer' => $transfer,
1523
			'SnapshotsSection' => 1,
1524
		));
1525
	}
1526
1527
	/**
1528
	 * Action - Get the latest deploy log
1529
	 *
1530
	 * @param SS_HTTPRequest $request
1531
	 *
1532
	 * @return string
1533
	 * @throws SS_HTTPResponse_Exception
1534
	 */
1535
	public function transferlog(SS_HTTPRequest $request) {
1536
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1537
1538
		$params = $request->params();
1539
		$transfer = DNDataTransfer::get()->byId($params['Identifier']);
1540
1541
		if(!$transfer || !$transfer->ID) {
1542
			throw new SS_HTTPResponse_Exception('Transfer not found', 404);
1543
		}
1544
		if(!$transfer->canView()) {
1545
			return Security::permissionFailure();
1546
		}
1547
1548
		$environment = $transfer->Environment();
1549
		$project = $environment->Project();
1550
1551
		if($project->Name != $params['Project']) {
1552
			throw new LogicException("Project in URL doesn't match this deploy");
1553
		}
1554
1555
		$log = $transfer->log();
1556
		if($log->exists()) {
1557
			$content = $log->content();
1558
		} else {
1559
			$content = 'Waiting for action to start';
1560
		}
1561
1562
		return $this->sendResponse($transfer->ResqueStatus(), $content);
1563
	}
1564
1565
	/**
1566
	 * Note: Submits to the same action as {@link getDataTransferForm()},
1567
	 * but with a Direction=push and an archive reference.
1568
	 *
1569
	 * @param SS_HTTPRequest $request
1570
	 * @param DNDataArchive|null $dataArchive Only set when method is called manually in {@link restore()},
1571
	 *                            otherwise the state is inferred from the request data.
1572
	 * @return Form
1573
	 */
1574
	public function getDataTransferRestoreForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) {
1575
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1576
1577
		// Performs canView permission check by limiting visible projects
1578
		$project = $this->getCurrentProject();
1579
		$envs = $project->DNEnvironmentList()->filterByCallback(function($item) {
1580
			return $item->canRestore();
1581
		});
1582
1583
		if(!$envs) {
1584
			return $this->environment404Response();
1585
		}
1586
1587
		$modesMap = array();
1588
		if(in_array($dataArchive->Mode, array('all'))) {
1589
			$modesMap['all'] = 'Database and Assets';
1590
		};
1591
		if(in_array($dataArchive->Mode, array('all', 'db'))) {
1592
			$modesMap['db'] = 'Database only';
1593
		};
1594
		if(in_array($dataArchive->Mode, array('all', 'assets'))) {
1595
			$modesMap['assets'] = 'Assets only';
1596
		};
1597
1598
		$alertMessage = '<div class="alert alert-warning"><strong>Warning:</strong> '
1599
			. 'This restore will overwrite the data on the chosen environment below</div>';
1600
1601
		$form = Form::create(
1602
			$this,
1603
			'DataTransferRestoreForm',
1604
			FieldList::create(
1605
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1606
				HiddenField::create('Direction', null, 'push'),
1607
				LiteralField::create('Warning', $alertMessage),
1608
				DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1609
					->setEmptyString('Select an environment'),
1610
				DropdownField::create('Mode', 'Transfer', $modesMap),
1611
				CheckboxField::create('BackupBeforePush', 'Backup existing data', '1')
1612
			),
1613
			FieldList::create(
1614
				FormAction::create('doDataTransfer', 'Restore Data')
1615
					->addExtraClass('btn')
1616
			)
1617
		);
1618
		$form->setFormAction($project->Link() . '/DataTransferRestoreForm');
1619
1620
		return $form;
1621
	}
1622
1623
	/**
1624
	 * View a form to restore a specific {@link DataArchive}.
1625
	 * Permission checks are handled in {@link DataArchives()}.
1626
	 * Submissions are handled through {@link doDataTransfer()}, same as backup operations.
1627
	 *
1628
	 * @param SS_HTTPRequest $request
1629
	 *
1630
	 * @return HTMLText
1631
	 * @throws SS_HTTPResponse_Exception
1632
	 */
1633 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...
1634
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1635
1636
		/** @var DNDataArchive $dataArchive */
1637
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1638
1639
		if(!$dataArchive) {
1640
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1641
		}
1642
1643
		// We check for canDownload because that implies access to the data.
1644
		// canRestore is later checked on the actual restore action per environment.
1645
		if(!$dataArchive->canDownload()) {
1646
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1647
		}
1648
1649
		$form = $this->getDataTransferRestoreForm($this->request, $dataArchive);
1650
1651
		// View currently only available via ajax
1652
		return $form->forTemplate();
1653
	}
1654
1655
	/**
1656
	 * View a form to delete a specific {@link DataArchive}.
1657
	 * Permission checks are handled in {@link DataArchives()}.
1658
	 * Submissions are handled through {@link doDelete()}.
1659
	 *
1660
	 * @param SS_HTTPRequest $request
1661
	 *
1662
	 * @return HTMLText
1663
	 * @throws SS_HTTPResponse_Exception
1664
	 */
1665 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...
1666
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1667
1668
		/** @var DNDataArchive $dataArchive */
1669
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1670
1671
		if(!$dataArchive) {
1672
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1673
		}
1674
1675
		if(!$dataArchive->canDelete()) {
1676
			throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403);
1677
		}
1678
1679
		$form = $this->getDeleteForm($this->request, $dataArchive);
1680
1681
		// View currently only available via ajax
1682
		return $form->forTemplate();
1683
	}
1684
1685
	/**
1686
	 * @param SS_HTTPRequest $request
1687
	 * @param DNDataArchive|null $dataArchive Only set when method is called manually, otherwise the state is inferred
1688
	 *        from the request data.
1689
	 * @return Form
1690
	 */
1691
	public function getDeleteForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) {
1692
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1693
1694
		// Performs canView permission check by limiting visible projects
1695
		$project = $this->getCurrentProject();
1696
		if(!$project) {
1697
			return $this->project404Response();
1698
		}
1699
1700
		$snapshotDeleteWarning = '<div class="alert alert-warning">'
1701
			. 'Are you sure you want to permanently delete this snapshot from this archive area?'
1702
			. '</div>';
1703
1704
		$form = Form::create(
1705
			$this,
1706
			'DeleteForm',
1707
			FieldList::create(
1708
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1709
				LiteralField::create('Warning', $snapshotDeleteWarning)
1710
			),
1711
			FieldList::create(
1712
				FormAction::create('doDelete', 'Delete')
1713
					->addExtraClass('btn')
1714
			)
1715
		);
1716
		$form->setFormAction($project->Link() . '/DeleteForm');
1717
1718
		return $form;
1719
	}
1720
1721
	/**
1722
	 * @param array $data
1723
	 * @param Form $form
1724
	 *
1725
	 * @return bool|SS_HTTPResponse
1726
	 * @throws SS_HTTPResponse_Exception
1727
	 */
1728
	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...
1729
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1730
1731
		// Performs canView permission check by limiting visible projects
1732
		$project = $this->getCurrentProject();
1733
		if(!$project) {
1734
			return $this->project404Response();
1735
		}
1736
1737
		$dataArchive = null;
1738
1739
		if(
1740
			isset($data['DataArchiveID'])
1741
			&& is_numeric($data['DataArchiveID'])
1742
		) {
1743
			$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1744
		}
1745
1746
		if(!$dataArchive) {
1747
			throw new LogicException('Invalid data archive');
1748
		}
1749
1750
		if(!$dataArchive->canDelete()) {
1751
			throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403);
1752
		}
1753
1754
		$dataArchive->delete();
1755
1756
		return $this->redirectBack();
1757
	}
1758
1759
	/**
1760
	 * View a form to move a specific {@link DataArchive}.
1761
	 *
1762
	 * @param SS_HTTPRequest $request
1763
	 *
1764
	 * @return HTMLText
1765
	 * @throws SS_HTTPResponse_Exception
1766
	 */
1767 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...
1768
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1769
1770
		/** @var DNDataArchive $dataArchive */
1771
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1772
1773
		if(!$dataArchive) {
1774
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1775
		}
1776
1777
		// We check for canDownload because that implies access to the data.
1778
		if(!$dataArchive->canDownload()) {
1779
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1780
		}
1781
1782
		$form = $this->getMoveForm($this->request, $dataArchive);
1783
1784
		// View currently only available via ajax
1785
		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...
1786
	}
1787
1788
	/**
1789
	 * Build snapshot move form.
1790
	 *
1791
	 * @param SS_HTTPRequest $request
1792
	 * @param DNDataArchive|null $dataArchive
1793
	 *
1794
	 * @return Form|SS_HTTPResponse
1795
	 */
1796
	public function getMoveForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) {
1797
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1798
1799
		$envs = $dataArchive->validTargetEnvironments();
1800
		if(!$envs) {
1801
			return $this->environment404Response();
1802
		}
1803
1804
		$warningMessage = '<div class="alert alert-warning"><strong>Warning:</strong> This will make the snapshot '
1805
			. 'available to people with access to the target environment.<br>By pressing "Change ownership" you '
1806
			. 'confirm that you have considered data confidentiality regulations.</div>';
1807
1808
		$form = Form::create(
1809
			$this,
1810
			'MoveForm',
1811
			FieldList::create(
1812
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1813
				LiteralField::create('Warning', $warningMessage),
1814
				DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1815
					->setEmptyString('Select an environment')
1816
			),
1817
			FieldList::create(
1818
				FormAction::create('doMove', 'Change ownership')
1819
					->addExtraClass('btn')
1820
			)
1821
		);
1822
		$form->setFormAction($this->getCurrentProject()->Link() . '/MoveForm');
1823
1824
		return $form;
1825
	}
1826
1827
	/**
1828
	 * @param array $data
1829
	 * @param Form $form
1830
	 *
1831
	 * @return bool|SS_HTTPResponse
1832
	 * @throws SS_HTTPResponse_Exception
1833
	 * @throws ValidationException
1834
	 * @throws null
1835
	 */
1836
	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...
1837
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1838
1839
		// Performs canView permission check by limiting visible projects
1840
		$project = $this->getCurrentProject();
1841
		if(!$project) {
1842
			return $this->project404Response();
1843
		}
1844
1845
		/** @var DNDataArchive $dataArchive */
1846
		$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1847
		if(!$dataArchive) {
1848
			throw new LogicException('Invalid data archive');
1849
		}
1850
1851
		// We check for canDownload because that implies access to the data.
1852
		if(!$dataArchive->canDownload()) {
1853
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1854
		}
1855
1856
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
1857
		$validEnvs = $dataArchive->validTargetEnvironments();
1858
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
1859
		if(!$environment) {
1860
			throw new LogicException('Invalid environment');
1861
		}
1862
1863
		$dataArchive->EnvironmentID = $environment->ID;
1864
		$dataArchive->write();
1865
1866
		return $this->redirectBack();
1867
	}
1868
1869
	/**
1870
	 * Returns an error message if redis is unavailable
1871
	 *
1872
	 * @return string
1873
	 */
1874
	public static function RedisUnavailable() {
1875
		try {
1876
			Resque::queues();
1877
		} catch(Exception $e) {
1878
			return $e->getMessage();
1879
		}
1880
		return '';
1881
	}
1882
1883
	/**
1884
	 * Returns the number of connected Redis workers
1885
	 *
1886
	 * @return int
1887
	 */
1888
	public static function RedisWorkersCount() {
1889
		return count(Resque_Worker::all());
1890
	}
1891
1892
	/**
1893
	 * @return array
1894
	 */
1895
	public function providePermissions() {
1896
		return array(
1897
			self::DEPLOYNAUT_BYPASS_PIPELINE => array(
1898
				'name' => "Bypass Pipeline",
1899
				'category' => "Deploynaut",
1900
				'help' => "Enables users to directly initiate deployments, bypassing any pipeline",
1901
			),
1902
			self::DEPLOYNAUT_DRYRUN_PIPELINE => array(
1903
				'name' => 'Dry-run Pipeline',
1904
				'category' => 'Deploynaut',
1905
				'help' => 'Enable dry-run execution of pipelines for testing'
1906
			),
1907
			self::DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS => array(
1908
				'name' => "Access to advanced deploy options",
1909
				'category' => "Deploynaut",
1910
			),
1911
1912
			// Permissions that are intended to be added to the roles.
1913
			self::ALLOW_PROD_DEPLOYMENT => array(
1914
				'name' => "Ability to deploy to production environments",
1915
				'category' => "Deploynaut",
1916
			),
1917
			self::ALLOW_NON_PROD_DEPLOYMENT => array(
1918
				'name' => "Ability to deploy to non-production environments",
1919
				'category' => "Deploynaut",
1920
			),
1921
			self::ALLOW_PROD_SNAPSHOT => array(
1922
				'name' => "Ability to make production snapshots",
1923
				'category' => "Deploynaut",
1924
			),
1925
			self::ALLOW_NON_PROD_SNAPSHOT => array(
1926
				'name' => "Ability to make non-production snapshots",
1927
				'category' => "Deploynaut",
1928
			),
1929
			self::ALLOW_CREATE_ENVIRONMENT => array(
1930
				'name' => "Ability to create environments",
1931
				'category' => "Deploynaut",
1932
			),
1933
		);
1934
	}
1935
1936
	/**
1937
	 * @return DNProject|null
1938
	 */
1939
	public function getCurrentProject() {
1940
		$projectName = trim($this->getRequest()->param('Project'));
1941
		if(!$projectName) {
1942
			return null;
1943
		}
1944
		if(empty(self::$_project_cache[$projectName])) {
1945
			self::$_project_cache[$projectName] = $this->DNProjectList()->filter('Name', $projectName)->First();
1946
		}
1947
		return self::$_project_cache[$projectName];
1948
	}
1949
1950
	/**
1951
	 * @param DNProject|null $project
1952
	 * @return DNEnvironment|null
1953
	 */
1954
	public function getCurrentEnvironment(DNProject $project = null) {
1955
		if($this->getRequest()->param('Environment') === null) {
1956
			return null;
1957
		}
1958
		if($project === null) {
1959
			$project = $this->getCurrentProject();
1960
		}
1961
		// project can still be null
1962
		if($project === null) {
1963
			return null;
1964
		}
1965
		return $project->DNEnvironmentList()->filter('Name', $this->getRequest()->param('Environment'))->First();
1966
	}
1967
1968
	/**
1969
	 * This will return a const that indicates the class of action currently being performed
1970
	 *
1971
	 * Until DNRoot is de-godded, it does a bunch of different actions all in the same class.
1972
	 * So we just have each action handler calll setCurrentActionType to define what sort of
1973
	 * action it is.
1974
	 *
1975
	 * @return string - one of the consts from self::$action_types
1976
	 */
1977
	public function getCurrentActionType() {
1978
		return $this->actionType;
1979
	}
1980
1981
	/**
1982
	 * Sets the current action type
1983
	 *
1984
	 * @param string $actionType string - one of the consts from self::$action_types
1985
	 */
1986
	public function setCurrentActionType($actionType) {
1987
		$this->actionType = $actionType;
1988
	}
1989
1990
	/**
1991
	 * Helper method to allow templates to know whether they should show the 'Archive List' include or not.
1992
	 * The actual permissions are set on a per-environment level, so we need to find out if this $member can upload to
1993
	 * or download from *any* {@link DNEnvironment} that (s)he has access to.
1994
	 *
1995
	 * TODO To be replaced with a method that just returns the list of archives this {@link Member} has access to.
1996
	 *
1997
	 * @param Member|null $member The {@link Member} to check (or null to check the currently logged in Member)
1998
	 * @return boolean|null true if $member has access to upload or download to at least one {@link DNEnvironment}.
1999
	 */
2000
	public function CanViewArchives(Member $member = null) {
2001
		if($member === null) {
2002
			$member = Member::currentUser();
2003
		}
2004
2005
		if(Permission::checkMember($member, 'ADMIN')) {
2006
			return true;
2007
		}
2008
2009
		$allProjects = $this->DNProjectList();
2010
		if(!$allProjects) {
2011
			return false;
2012
		}
2013
2014
		foreach($allProjects as $project) {
2015
			if($project->Environments()) {
2016
				foreach($project->Environments() as $environment) {
2017
					if(
2018
						$environment->canRestore($member) ||
2019
						$environment->canBackup($member) ||
2020
						$environment->canUploadArchive($member) ||
2021
						$environment->canDownloadArchive($member)
2022
					) {
2023
						// We can return early as we only need to know that we can access one environment
2024
						return true;
2025
					}
2026
				}
2027
			}
2028
		}
2029
	}
2030
2031
	/**
2032
	 * Returns a list of all archive files that can be accessed by the currently logged-in {@link Member}
2033
	 *
2034
	 * @return PaginatedList
2035
	 */
2036
	public function CompleteDataArchives() {
2037
		$project = $this->getCurrentProject();
2038
		$archives = new ArrayList();
2039
2040
		$archiveList = $project->Environments()->relation("DataArchives");
2041
		if($archiveList->count() > 0) {
2042
			foreach($archiveList as $archive) {
2043
				if($archive->canView() && !$archive->isPending()) {
2044
					$archives->push($archive);
2045
				}
2046
			}
2047
		}
2048
		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...
2049
	}
2050
2051
	/**
2052
	 * @return PaginatedList The list of "pending" data archives which are waiting for a file
2053
	 * to be delivered offline by post, and manually uploaded into the system.
2054
	 */
2055
	public function PendingDataArchives() {
2056
		$project = $this->getCurrentProject();
2057
		$archives = new ArrayList();
2058
		foreach($project->DNEnvironmentList() as $env) {
2059
			foreach($env->DataArchives() as $archive) {
2060
				if($archive->canView() && $archive->isPending()) {
2061
					$archives->push($archive);
2062
				}
2063
			}
2064
		}
2065
		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...
2066
	}
2067
2068
	/**
2069
	 * @return PaginatedList
2070
	 */
2071
	public function DataTransferLogs() {
2072
		$project = $this->getCurrentProject();
2073
2074
		$transfers = DNDataTransfer::get()->filterByCallback(function($record) use($project) {
2075
			return
2076
				$record->Environment()->Project()->ID == $project->ID && // Ensure only the current Project is shown
2077
				(
2078
					$record->Environment()->canRestore() || // Ensure member can perform an action on the transfers env
2079
					$record->Environment()->canBackup() ||
2080
					$record->Environment()->canUploadArchive() ||
2081
					$record->Environment()->canDownloadArchive()
2082
				);
2083
		});
2084
2085
		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...
2086
	}
2087
2088
	/**
2089
	 * @return null|PaginatedList
2090
	 */
2091
	public function DeployHistory() {
2092
		if($env = $this->getCurrentEnvironment()) {
2093
			$history = $env->DeployHistory();
2094
			if($history->count() > 0) {
2095
				$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...
2096
				$pagination->setPageLength(8);
2097
				return $pagination;
2098
			}
2099
		}
2100
		return null;
2101
	}
2102
2103
	/**
2104
	 * @return SS_HTTPResponse
2105
	 */
2106
	protected function project404Response() {
2107
		return new SS_HTTPResponse(
2108
			"Project '" . Convert::raw2xml($this->getRequest()->param('Project')) . "' not found.",
2109
			404
2110
		);
2111
	}
2112
2113
	/**
2114
	 * @return SS_HTTPResponse
2115
	 */
2116
	protected function environment404Response() {
2117
		$envName = Convert::raw2xml($this->getRequest()->param('Environment'));
2118
		return new SS_HTTPResponse("Environment '" . $envName . "' not found.", 404);
2119
	}
2120
2121
	/**
2122
	 * @param string $status
2123
	 * @param string $content
2124
	 *
2125
	 * @return string
2126
	 */
2127
	public function sendResponse($status, $content) {
2128
		// strip excessive newlines
2129
		$content = preg_replace('/(?:(?:\r\n|\r|\n)\s*){2}/s', "\n", $content);
2130
2131
		$sendJSON = (strpos($this->getRequest()->getHeader('Accept'), 'application/json') !== false)
2132
			|| $this->getRequest()->getExtension() == 'json';
2133
2134
		if(!$sendJSON) {
2135
			$this->response->addHeader("Content-type", "text/plain");
2136
			return $content;
2137
		}
2138
		$this->response->addHeader("Content-type", "application/json");
2139
		return json_encode(array(
2140
			'status' => $status,
2141
			'content' => $content,
2142
		));
2143
	}
2144
2145
	/**
2146
	 * Validate the snapshot mode
2147
	 *
2148
	 * @param string $mode
2149
	 */
2150
	protected function validateSnapshotMode($mode) {
2151
		if(!in_array($mode, array('all', 'assets', 'db'))) {
2152
			throw new LogicException('Invalid mode');
2153
		}
2154
	}
2155
2156
	/**
2157
	 * @param string $sectionName
2158
	 * @param string $title
2159
	 *
2160
	 * @return SS_HTTPResponse
2161
	 */
2162
	protected function getCustomisedViewSection($sectionName, $title = '', $data = array()) {
2163
		// Performs canView permission check by limiting visible projects
2164
		$project = $this->getCurrentProject();
2165
		if(!$project) {
2166
			return $this->project404Response();
2167
		}
2168
		$data[$sectionName] = 1;
2169
2170
		if($this !== '') {
2171
			$data['Title'] = $title;
2172
		}
2173
2174
		return $this->render($data);
2175
	}
2176
2177
	/**
2178
	 * Get items for the ambient menu that should be accessible from all pages.
2179
	 *
2180
	 * @return ArrayList
2181
	 */
2182
	public function AmbientMenu() {
2183
		$list = new ArrayList();
2184
2185
		if (Member::currentUserID()) {
2186
			$list->push(new ArrayData(array(
2187
				'Classes' => 'logout',
2188
				'FaIcon' => 'sign-out',
2189
				'Link' => 'Security/logout',
2190
				'Title' => 'Log out',
2191
				'IsCurrent' => false,
2192
				'IsSection' => false
2193
			)));
2194
		}
2195
2196
		$this->extend('updateAmbientMenu', $list);
2197
		return $list;
2198
	}
2199
2200
	/**
2201
	 * Checks whether the user can create a project.
2202
	 *
2203
	 * @return bool
2204
	 */
2205
	public function canCreateProjects($member = null) {
2206
		if(!$member) $member = Member::currentUser();
2207
		if(!$member) return false;
2208
2209
		return singleton('DNProject')->canCreate($member);
2210
	}
2211
2212
}
2213
2214