Completed
Pull Request — master (#529)
by Sean
129:17 queued 126:02
created

DNRoot::getCurrentEnvironment()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 13
Code Lines 8

Duplication

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