Completed
Pull Request — master (#602)
by Sean
04:48 queued 01:00
created

DNRoot::doCreateEnvironment()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 24
rs 8.9713
c 0
b 0
f 0
cc 3
eloc 14
nc 3
nop 2
1
<?php
2
3
/**
4
 * God controller for the deploynaut interface
5
 *
6
 * @package deploynaut
7
 * @subpackage control
8
 */
9
class DNRoot extends Controller implements PermissionProvider, TemplateGlobalProvider {
10
11
	/**
12
	 * @const string - action type for actions that perform deployments
13
	 */
14
	const ACTION_DEPLOY = 'deploy';
15
16
	/**
17
	 * @const string - action type for actions that manipulate snapshots
18
	 */
19
	const ACTION_SNAPSHOT = 'snapshot';
20
21
	const ACTION_ENVIRONMENTS = 'createenv';
22
23
	const PROJECT_OVERVIEW = 'overview';
24
25
	/**
26
	 * @var string
27
	 */
28
	private $actionType = self::ACTION_DEPLOY;
29
30
	/**
31
	 * Allow advanced options on deployments
32
	 */
33
	const DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS = 'DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS';
34
35
	const ALLOW_PROD_DEPLOYMENT = 'ALLOW_PROD_DEPLOYMENT';
36
	const ALLOW_NON_PROD_DEPLOYMENT = 'ALLOW_NON_PROD_DEPLOYMENT';
37
	const ALLOW_PROD_SNAPSHOT = 'ALLOW_PROD_SNAPSHOT';
38
	const ALLOW_NON_PROD_SNAPSHOT = 'ALLOW_NON_PROD_SNAPSHOT';
39
	const ALLOW_CREATE_ENVIRONMENT = 'ALLOW_CREATE_ENVIRONMENT';
40
41
	/**
42
	 * @var array
43
	 */
44
	private static $allowed_actions = array(
45
		'projects',
46
		'nav',
47
		'update',
48
		'project',
49
		'toggleprojectstar',
50
		'branch',
51
		'environment',
52
		'metrics',
53
		'createenvlog',
54
		'createenv',
55
		'getDeployForm',
56
		'doDeploy',
57
		'deploy',
58
		'deploylog',
59
		'abortDeploy',
60
		'getDataTransferForm',
61
		'transfer',
62
		'transferlog',
63
		'snapshots',
64
		'createsnapshot',
65
		'snapshotslog',
66
		'uploadsnapshot',
67
		'getCreateEnvironmentForm',
68
		'getUploadSnapshotForm',
69
		'getPostSnapshotForm',
70
		'getDataTransferRestoreForm',
71
		'getDeleteForm',
72
		'getMoveForm',
73
		'restoresnapshot',
74
		'deletesnapshot',
75
		'movesnapshot',
76
		'postsnapshotsuccess',
77
		'gitRevisions',
78
		'deploySummary',
79
		'startDeploy'
80
	);
81
82
	/**
83
	 * URL handlers pretending that we have a deep URL structure.
84
	 */
85
	private static $url_handlers = array(
86
		'project/$Project/environment/$Environment/DeployForm' => 'getDeployForm',
87
		'project/$Project/createsnapshot/DataTransferForm' => 'getDataTransferForm',
88
		'project/$Project/DataTransferForm' => 'getDataTransferForm',
89
		'project/$Project/DataTransferRestoreForm' => 'getDataTransferRestoreForm',
90
		'project/$Project/DeleteForm' => 'getDeleteForm',
91
		'project/$Project/MoveForm' => 'getMoveForm',
92
		'project/$Project/UploadSnapshotForm' => 'getUploadSnapshotForm',
93
		'project/$Project/PostSnapshotForm' => 'getPostSnapshotForm',
94
		'project/$Project/environment/$Environment/metrics' => 'metrics',
95
		'project/$Project/environment/$Environment/deploy_summary' => 'deploySummary',
96
		'project/$Project/environment/$Environment/git_revisions' => 'gitRevisions',
97
		'project/$Project/environment/$Environment/start-deploy' => 'startDeploy',
98
		'project/$Project/environment/$Environment/deploy/$Identifier/log' => 'deploylog',
99
		'project/$Project/environment/$Environment/deploy/$Identifier/abort-deploy' => 'abortDeploy',
100
		'project/$Project/environment/$Environment/deploy/$Identifier' => 'deploy',
101
		'project/$Project/transfer/$Identifier/log' => 'transferlog',
102
		'project/$Project/transfer/$Identifier' => 'transfer',
103
		'project/$Project/environment/$Environment' => 'environment',
104
		'project/$Project/createenv/$Identifier/log' => 'createenvlog',
105
		'project/$Project/createenv/$Identifier' => 'createenv',
106
		'project/$Project/CreateEnvironmentForm' => 'getCreateEnvironmentForm',
107
		'project/$Project/branch' => 'branch',
108
		'project/$Project/build/$Build' => 'build',
109
		'project/$Project/restoresnapshot/$DataArchiveID' => 'restoresnapshot',
110
		'project/$Project/deletesnapshot/$DataArchiveID' => 'deletesnapshot',
111
		'project/$Project/movesnapshot/$DataArchiveID' => 'movesnapshot',
112
		'project/$Project/update' => 'update',
113
		'project/$Project/snapshots' => 'snapshots',
114
		'project/$Project/createsnapshot' => 'createsnapshot',
115
		'project/$Project/uploadsnapshot' => 'uploadsnapshot',
116
		'project/$Project/snapshotslog' => 'snapshotslog',
117
		'project/$Project/postsnapshotsuccess/$DataArchiveID' => 'postsnapshotsuccess',
118
		'project/$Project/star' => 'toggleprojectstar',
119
		'project/$Project' => 'project',
120
		'nav/$Project' => 'nav',
121
		'projects' => 'projects',
122
	);
123
124
	/**
125
	 * @var array
126
	 */
127
	protected static $_project_cache = array();
128
129
	/**
130
	 * @var array
131
	 */
132
	private static $support_links = array();
133
134
	/**
135
	 * @var array
136
	 */
137
	private static $platform_specific_strings = array();
138
139
	/**
140
	 * @var array
141
	 */
142
	private static $action_types = array(
143
		self::ACTION_DEPLOY,
144
		self::ACTION_SNAPSHOT,
145
		self::PROJECT_OVERVIEW
146
	);
147
148
	/**
149
	 * @var DNData
150
	 */
151
	protected $data;
152
153
	/**
154
	 * Include requirements that deploynaut needs, such as javascript.
155
	 */
156
	public static function include_requirements() {
157
158
		// JS should always go to the bottom, otherwise there's the risk that Requirements
159
		// puts them halfway through the page to the nearest <script> tag. We don't want that.
160
		Requirements::set_force_js_to_bottom(true);
161
162
		// todo these should be bundled into the same JS as the others in "static" below.
163
		// We've deliberately not used combined_files as it can mess with some of the JS used
164
		// here and cause sporadic errors.
165
		Requirements::javascript('deploynaut/javascript/jquery.js');
166
		Requirements::javascript('deploynaut/javascript/bootstrap.js');
167
		Requirements::javascript('deploynaut/javascript/q.js');
168
		Requirements::javascript('deploynaut/javascript/tablefilter.js');
169
		Requirements::javascript('deploynaut/javascript/deploynaut.js');
170
171
		Requirements::javascript('deploynaut/javascript/bootstrap.file-input.js');
172
		Requirements::javascript('deploynaut/thirdparty/select2/dist/js/select2.min.js');
173
		Requirements::javascript('deploynaut/thirdparty/bootstrap-switch/dist/js/bootstrap-switch.min.js');
174
		Requirements::javascript('deploynaut/javascript/material.js');
175
176
		// Load the buildable dependencies only if not loaded centrally.
177
		if (!is_dir(BASE_PATH . DIRECTORY_SEPARATOR . 'static')) {
178
			if (\Director::isDev()) {
179
				\Requirements::javascript('deploynaut/static/bundle-debug.js');
180
			} else {
181
				\Requirements::javascript('deploynaut/static/bundle.js');
182
			}
183
		}
184
185
		Requirements::css('deploynaut/static/style.css');
186
	}
187
188
	/**
189
	 * Check for feature flags:
190
	 * - FLAG_SNAPSHOTS_ENABLED: set to true to enable globally
191
	 * - FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS: set to semicolon-separated list of email addresses of allowed users.
192
	 *
193
	 * @return boolean
194
	 */
195
	public static function FlagSnapshotsEnabled() {
196
		if(defined('FLAG_SNAPSHOTS_ENABLED') && FLAG_SNAPSHOTS_ENABLED) {
197
			return true;
198
		}
199
		if(defined('FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS') && FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS) {
200
			$allowedMembers = explode(';', FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS);
201
			$member = Member::currentUser();
202
			if($allowedMembers && $member && in_array($member->Email, $allowedMembers)) {
203
				return true;
204
			}
205
		}
206
		return false;
207
	}
208
209
	/**
210
	 * @return ArrayList
211
	 */
212
	public static function get_support_links() {
213
		$supportLinks = self::config()->support_links;
214
		if($supportLinks) {
215
			return new ArrayList($supportLinks);
216
		}
217
	}
218
219
	/**
220
	 * @return array
221
	 */
222
	public static function get_template_global_variables() {
223
		return array(
224
			'RedisUnavailable' => 'RedisUnavailable',
225
			'RedisWorkersCount' => 'RedisWorkersCount',
226
			'SidebarLinks' => 'SidebarLinks',
227
			"SupportLinks" => 'get_support_links'
228
		);
229
	}
230
231
	/**
232
	 */
233
	public function init() {
234
		parent::init();
235
236
		if(!Member::currentUser() && !Session::get('AutoLoginHash')) {
237
			return Security::permissionFailure();
238
		}
239
240
		// Block framework jquery
241
		Requirements::block(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
242
243
		self::include_requirements();
244
	}
245
246
	/**
247
	 * @return string
248
	 */
249
	public function Link() {
250
		return "naut/";
251
	}
252
253
	/**
254
	 * Actions
255
	 *
256
	 * @param SS_HTTPRequest $request
257
	 * @return \SS_HTTPResponse
258
	 */
259
	public function index(SS_HTTPRequest $request) {
260
		return $this->redirect($this->Link() . 'projects/');
261
	}
262
263
	/**
264
	 * Action
265
	 *
266
	 * @param SS_HTTPRequest $request
267
	 * @return string - HTML
268
	 */
269
	public function projects(SS_HTTPRequest $request) {
270
		// Performs canView permission check by limiting visible projects in DNProjectsList() call.
271
		return $this->customise(array(
272
			'Title' => 'Projects',
273
		))->render();
274
	}
275
276
	/**
277
	 * @param SS_HTTPRequest $request
278
	 * @return HTMLText
279
	 */
280
	public function nav(SS_HTTPRequest $request) {
281
		return $this->renderWith('Nav');
282
	}
283
284
	/**
285
	 * Return a link to the navigation template used for AJAX requests.
286
	 * @return string
287
	 */
288
	public function NavLink() {
289
		$currentProject = $this->getCurrentProject();
290
		$projectName = $currentProject ? $currentProject->Name : null;
291
		return Controller::join_links(Director::absoluteBaseURL(), 'naut', 'nav', $projectName);
292
	}
293
294
	/**
295
	 * Action
296
	 *
297
	 * @param SS_HTTPRequest $request
298
	 * @return SS_HTTPResponse - HTML
299
	 */
300
	public function snapshots(SS_HTTPRequest $request) {
301
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
302
		return $this->getCustomisedViewSection('SnapshotsSection', 'Data Snapshots');
303
	}
304
305
	/**
306
	 * Action
307
	 *
308
	 * @param SS_HTTPRequest $request
309
	 * @return string - HTML
310
	 */
311 View Code Duplication
	public function createsnapshot(SS_HTTPRequest $request) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

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

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

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

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

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

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

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
872
873
		$job = DNCreateEnvironment::create();
874
875
		$job->Data = serialize($data);
876
		$job->ProjectID = $project->ID;
877
		$job->write();
878
		$job->start();
879
880
		return $this->redirect($project->Link('createenv') . '/' . $job->ID);
881
	}
882
883
	/**
884
	 *
885
	 * @param SS_HTTPRequest $request
886
	 * @return \SS_HTTPResponse
887
	 */
888
	public function metrics(SS_HTTPRequest $request) {
889
		// Performs canView permission check by limiting visible projects
890
		$project = $this->getCurrentProject();
891
		if(!$project) {
892
			return $this->project404Response();
893
		}
894
895
		// Performs canView permission check by limiting visible projects
896
		$env = $this->getCurrentEnvironment($project);
897
		if(!$env) {
898
			return $this->environment404Response();
899
		}
900
901
		return $this->render();
902
	}
903
904
	/**
905
	 * Get the DNData object.
906
	 *
907
	 * @return DNData
908
	 */
909
	public function DNData() {
910
		return DNData::inst();
911
	}
912
913
	/**
914
	 * Provide a list of all projects.
915
	 *
916
	 * @return SS_List
917
	 */
918
	public function DNProjectList() {
919
		$memberId = Member::currentUserID();
920
		if(!$memberId) {
921
			return new ArrayList();
922
		}
923
924
		if(Permission::check('ADMIN')) {
925
			return DNProject::get();
926
		}
927
928
		$projects = Member::get()->filter('ID', $memberId)
929
			->relation('Groups')
930
			->relation('Projects');
931
932
		$this->extend('updateDNProjectList', $projects);
933
		return $projects;
934
	}
935
936
	/**
937
	 * @return ArrayList
938
	 */
939
	public function getPlatformSpecificStrings() {
940
		$strings = $this->config()->platform_specific_strings;
941
		if ($strings) {
942
			return new ArrayList($strings);
943
		}
944
	}
945
946
	/**
947
	 * Provide a list of all starred projects for the currently logged in member
948
	 *
949
	 * @return SS_List
950
	 */
951
	public function getStarredProjects() {
952
		$member = Member::currentUser();
953
		if($member === null) {
954
			return new ArrayList();
955
		}
956
957
		$favProjects = $member->StarredProjects();
958
959
		$list = new ArrayList();
960
		foreach($favProjects as $project) {
961
			if($project->canView($member)) {
962
				$list->add($project);
963
			}
964
		}
965
		return $list;
966
	}
967
968
	/**
969
	 * Returns top level navigation of projects.
970
	 *
971
	 * @param int $limit
972
	 *
973
	 * @return ArrayList
974
	 */
975
	public function Navigation($limit = 5) {
976
		$navigation = new ArrayList();
977
978
		$currentProject = $this->getCurrentProject();
979
		$currentEnvironment = $this->getCurrentEnvironment();
980
		$actionType = $this->getCurrentActionType();
981
982
		$projects = $this->getStarredProjects();
983
		if($projects->count() < 1) {
984
			$projects = $this->DNProjectList();
985
		} else {
986
			$limit = -1;
987
		}
988
989
		if($projects->count() > 0) {
990
			$activeProject = false;
991
992
			if($limit > 0) {
993
				$limitedProjects = $projects->limit($limit);
994
			} else {
995
				$limitedProjects = $projects;
996
			}
997
998
			foreach($limitedProjects as $project) {
999
				$isActive = $currentProject && $currentProject->ID == $project->ID;
1000
				if($isActive) {
1001
					$activeProject = true;
1002
				}
1003
1004
				$isCurrentEnvironment = false;
1005
				if($project && $currentEnvironment) {
1006
					$isCurrentEnvironment = (bool) $project->DNEnvironmentList()->find('ID', $currentEnvironment->ID);
1007
				}
1008
1009
				$navigation->push(array(
1010
					'Project' => $project,
1011
					'IsCurrentEnvironment' => $isCurrentEnvironment,
1012
					'IsActive' => $currentProject && $currentProject->ID == $project->ID,
1013
					'IsOverview' => $actionType == self::PROJECT_OVERVIEW && $currentProject->ID == $project->ID
1014
				));
1015
			}
1016
1017
			// Ensure the current project is in the list
1018
			if(!$activeProject && $currentProject) {
1019
				$navigation->unshift(array(
1020
					'Project' => $currentProject,
1021
					'IsActive' => true,
1022
					'IsCurrentEnvironment' => $currentEnvironment,
1023
					'IsOverview' => $actionType == self::PROJECT_OVERVIEW
1024
				));
1025
				if($limit > 0 && $navigation->count() > $limit) {
1026
					$navigation->pop();
1027
				}
1028
			}
1029
		}
1030
1031
		return $navigation;
1032
	}
1033
1034
	/**
1035
	 * Construct the deployment form
1036
	 *
1037
	 * @return Form
1038
	 */
1039
	public function getDeployForm($request = null) {
1040
1041
		// Performs canView permission check by limiting visible projects
1042
		$project = $this->getCurrentProject();
1043
		if(!$project) {
1044
			return $this->project404Response();
1045
		}
1046
1047
		// Performs canView permission check by limiting visible projects
1048
		$environment = $this->getCurrentEnvironment($project);
1049
		if(!$environment) {
1050
			return $this->environment404Response();
1051
		}
1052
1053
		if(!$environment->canDeploy()) {
1054
			return new SS_HTTPResponse("Not allowed to deploy", 401);
1055
		}
1056
1057
		// Generate the form
1058
		$form = new DeployForm($this, 'DeployForm', $environment, $project);
1059
1060
		// If this is an ajax request we don't want to submit the form - we just want to retrieve the markup.
1061
		if(
1062
			$request &&
1063
			!$request->requestVar('action_showDeploySummary') &&
1064
			$this->getRequest()->isAjax() &&
1065
			$this->getRequest()->isGET()
1066
		) {
1067
			// We can just use the URL we're accessing
1068
			$form->setFormAction($this->getRequest()->getURL());
1069
1070
			$body = json_encode(array('Content' => $form->forAjaxTemplate()->forTemplate()));
1071
			$this->getResponse()->addHeader('Content-Type', 'application/json');
1072
			$this->getResponse()->setBody($body);
1073
			return $body;
1074
		}
1075
1076
		$form->setFormAction($this->getRequest()->getURL() . '/DeployForm');
1077
		return $form;
1078
	}
1079
1080
	/**
1081
	 * @param SS_HTTPRequest $request
1082
	 *
1083
	 * @return SS_HTTPResponse|string
1084
	 */
1085
	public function gitRevisions(SS_HTTPRequest $request) {
1086
1087
		// Performs canView permission check by limiting visible projects
1088
		$project = $this->getCurrentProject();
1089
		if(!$project) {
1090
			return $this->project404Response();
1091
		}
1092
1093
		// Performs canView permission check by limiting visible projects
1094
		$env = $this->getCurrentEnvironment($project);
1095
		if(!$env) {
1096
			return $this->environment404Response();
1097
		}
1098
1099
		$options = [];
1100
		foreach ($env->Backend()->getDeployOptions() as $option) {
1101
			$options[] = [
1102
				'name' => $option->getName(),
1103
				'title' => $option->getTitle(),
1104
				'defaultValue' => $option->getDefaultValue()
1105
			];
1106
		}
1107
1108
		$tabs = array();
1109
		$id = 0;
1110
		$data = array(
1111
			'id' => ++$id,
1112
			'name' => 'Deploy the latest version of a branch',
1113
			'field_type' => 'dropdown',
1114
			'field_label' => 'Choose a branch',
1115
			'field_id' => 'branch',
1116
			'field_data' => [],
1117
			'options' => $options
1118
		);
1119
		foreach($project->DNBranchList() as $branch) {
1120
			$sha = $branch->SHA();
1121
			$name = $branch->Name();
1122
			$branchValue = sprintf("%s (%s, %s old)",
1123
				$name,
1124
				substr($sha, 0, 8),
1125
				$branch->LastUpdated()->TimeDiff()
1126
			);
1127
			$data['field_data'][] = array(
1128
				'id' => $sha,
1129
				'text' => $branchValue,
1130
				'branch_name' => $name // the raw branch name, not including the time etc
1131
			);
1132
		}
1133
		$tabs[] = $data;
1134
1135
		$data = array(
1136
			'id' => ++$id,
1137
			'name' => 'Deploy a tagged release',
1138
			'field_type' => 'dropdown',
1139
			'field_label' => 'Choose a tag',
1140
			'field_id' => 'tag',
1141
			'field_data' => [],
1142
			'options' => $options
1143
		);
1144
1145
		foreach($project->DNTagList()->setLimit(null) as $tag) {
1146
			$name = $tag->Name();
1147
			$data['field_data'][] = array(
1148
				'id' => $tag->SHA(),
1149
				'text' => sprintf("%s", $name)
1150
			);
1151
		}
1152
1153
		// show newest tags first.
1154
		$data['field_data'] = array_reverse($data['field_data']);
1155
1156
		$tabs[] = $data;
1157
1158
		// Past deployments
1159
		$data = array(
1160
			'id' => ++$id,
1161
			'name' => 'Redeploy a release that was previously deployed (to any environment)',
1162
			'field_type' => 'dropdown',
1163
			'field_label' => 'Choose a previously deployed release',
1164
			'field_id' => 'release',
1165
			'field_data' => [],
1166
			'options' => $options
1167
		);
1168
		// We are aiming at the format:
1169
		// [{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...
1170
		$redeploy = array();
1171 View Code Duplication
		foreach($project->DNEnvironmentList() as $dnEnvironment) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1172
			$envName = $dnEnvironment->Name;
1173
			$perEnvDeploys = array();
1174
1175
			foreach($dnEnvironment->DeployHistory() as $deploy) {
1176
				$sha = $deploy->SHA;
1177
1178
				// Check if exists to make sure the newest deployment date is used.
1179
				if(!isset($perEnvDeploys[$sha])) {
1180
					$pastValue = sprintf("%s (deployed %s)",
1181
						substr($sha, 0, 8),
1182
						$deploy->obj('LastEdited')->Ago()
1183
					);
1184
					$perEnvDeploys[$sha] = array(
1185
						'id' => $sha,
1186
						'text' => $pastValue
1187
					);
1188
				}
1189
			}
1190
1191
			if(!empty($perEnvDeploys)) {
1192
				$redeploy[$envName] = array_values($perEnvDeploys);
1193
			}
1194
		}
1195
		// Convert the array to the frontend format (i.e. keyed to regular array)
1196
		foreach($redeploy as $name => $descr) {
1197
			$data['field_data'][] = array('text'=>$name, 'children'=>$descr);
1198
		}
1199
		$tabs[] = $data;
1200
1201
		$data = array(
1202
			'id' => ++$id,
1203
			'name' => 'Deploy a specific SHA',
1204
			'field_type' => 'textfield',
1205
			'field_label' => 'Choose a SHA',
1206
			'field_id' => 'SHA',
1207
			'field_data' => [],
1208
			'options' => $options
1209
		);
1210
		$tabs[] = $data;
1211
1212
		// get the last time git fetch was run
1213
		$lastFetched = 'never';
1214
		$fetch = DNGitFetch::get()
1215
			->filter('ProjectID', $project->ID)
1216
			->sort('LastEdited', 'DESC')
1217
			->first();
1218
		if($fetch) {
1219
			$lastFetched = $fetch->dbObject('LastEdited')->Ago();
1220
		}
1221
1222
		$data = array(
1223
			'Tabs' => $tabs,
1224
			'last_fetched' => $lastFetched
1225
		);
1226
1227
		$this->applyRedeploy($request, $data);
1228
1229
		return json_encode($data, JSON_PRETTY_PRINT);
1230
	}
1231
1232
	protected function applyRedeploy(SS_HTTPRequest $request, &$data) {
1233
		if (!$request->getVar('redeploy')) return;
1234
1235
		$project = $this->getCurrentProject();
1236
		if(!$project) {
1237
			return $this->project404Response();
1238
		}
1239
1240
		// Performs canView permission check by limiting visible projects
1241
		$env = $this->getCurrentEnvironment($project);
1242
		if(!$env) {
1243
			return $this->environment404Response();
1244
		}
1245
1246
		$current = $env->CurrentBuild();
1247
		if ($current && $current->exists()) {
1248
			$data['preselect_tab'] = 3;
1249
			$data['preselect_sha'] = $current->SHA;
1250
		} else {
1251
			$master = $project->DNBranchList()->byName('master');
1252
			if ($master) {
1253
				$data['preselect_tab'] = 1;
1254
				$data['preselect_sha'] = $master->SHA();
0 ignored issues
show
Bug introduced by
The method SHA cannot be called on $master (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
1255
			}
1256
		}
1257
	}
1258
1259
	/**
1260
	 * Check and regenerate a global CSRF token
1261
	 *
1262
	 * @param SS_HTTPRequest $request
1263
	 * @param bool $resetToken
1264
	 *
1265
	 * @return bool
1266
	 */
1267
	protected function checkCsrfToken(SS_HTTPRequest $request, $resetToken = true) {
1268
		$token = SecurityToken::inst();
1269
1270
		// Ensure the submitted token has a value
1271
		$submittedToken = $request->postVar('SecurityID');
1272
		if(!$submittedToken) {
1273
			return false;
1274
		}
1275
1276
		// Do the actual check.
1277
		$check = $token->check($submittedToken);
1278
1279
		// Reset the token after we've checked the existing token
1280
		if($resetToken) {
1281
			$token->reset();
1282
		}
1283
1284
		// Return whether the token was correct or not
1285
		return $check;
1286
	}
1287
1288
	/**
1289
	 * @param SS_HTTPRequest $request
1290
	 *
1291
	 * @return string
1292
	 */
1293
	public function deploySummary(SS_HTTPRequest $request) {
1294
1295
		// Performs canView permission check by limiting visible projects
1296
		$project = $this->getCurrentProject();
1297
		if(!$project) {
1298
			return $this->project404Response();
1299
		}
1300
1301
		// Performs canView permission check by limiting visible projects
1302
		$environment = $this->getCurrentEnvironment($project);
1303
		if(!$environment) {
1304
			return $this->environment404Response();
1305
		}
1306
1307
		// Plan the deployment.
1308
		$strategy = $environment->getDeployStrategy($request);
1309
		$data = $strategy->toArray();
1310
1311
		// Add in a URL for comparing from->to code changes. Ensure that we have
1312
		// two proper 40 character SHAs, otherwise we can't show the compare link.
1313
		$interface = $project->getRepositoryInterface();
1314
		if(
1315
			!empty($interface) && !empty($interface->URL)
1316
			&& !empty($data['changes']['Code version']['from'])
1317
			&& strlen($data['changes']['Code version']['from']) == '40'
1318
			&& !empty($data['changes']['Code version']['to'])
1319
			&& strlen($data['changes']['Code version']['to']) == '40'
1320
		) {
1321
			$compareurl = sprintf(
1322
				'%s/compare/%s...%s',
1323
				$interface->URL,
1324
				$data['changes']['Code version']['from'],
1325
				$data['changes']['Code version']['to']
1326
			);
1327
			$data['changes']['Code version']['compareUrl'] = $compareurl;
1328
		}
1329
1330
		// Append json to response
1331
		$token = SecurityToken::inst();
1332
		$data['SecurityID'] = $token->getValue();
1333
1334
		$this->extend('updateDeploySummary', $data);
1335
1336
		return json_encode($data);
1337
	}
1338
1339
	/**
1340
	 * Deployment form submission handler.
1341
	 *
1342
	 * Initiate a DNDeployment record and redirect to it for status polling
1343
	 *
1344
	 * @param SS_HTTPRequest $request
1345
	 *
1346
	 * @return SS_HTTPResponse
1347
	 * @throws ValidationException
1348
	 * @throws null
1349
	 */
1350
	public function startDeploy(SS_HTTPRequest $request) {
1351
1352
		// Ensure the CSRF Token is correct
1353
		if(!$this->checkCsrfToken($request)) {
1354
			// CSRF token didn't match
1355
			return $this->httpError(400, 'Bad Request');
1356
		}
1357
1358
		// Performs canView permission check by limiting visible projects
1359
		$project = $this->getCurrentProject();
1360
		if(!$project) {
1361
			return $this->project404Response();
1362
		}
1363
1364
		// Performs canView permission check by limiting visible projects
1365
		$environment = $this->getCurrentEnvironment($project);
1366
		if(!$environment) {
1367
			return $this->environment404Response();
1368
		}
1369
1370
		// Initiate the deployment
1371
		// The extension point should pass in: Project, Environment, SelectRelease, buildName
1372
		$this->extend('doDeploy', $project, $environment, $buildName, $data);
1373
1374
		// Start the deployment based on the approved strategy.
1375
		$strategy = new DeploymentStrategy($environment);
1376
		$strategy->fromArray($request->requestVar('strategy'));
1377
		$deployment = $strategy->createDeployment();
1378
		// Skip through the approval state for now.
1379
		$deployment->getMachine()->apply(DNDeployment::TR_SUBMIT);
1380
		$deployment->getMachine()->apply(DNDeployment::TR_QUEUE);
1381
1382
		return json_encode(array(
1383
			'url' => Director::absoluteBaseURL() . $deployment->Link()
1384
		), JSON_PRETTY_PRINT);
1385
	}
1386
1387
	/**
1388
	 * Action - Do the actual deploy
1389
	 *
1390
	 * @param SS_HTTPRequest $request
1391
	 *
1392
	 * @return SS_HTTPResponse|string
1393
	 * @throws SS_HTTPResponse_Exception
1394
	 */
1395
	public function deploy(SS_HTTPRequest $request) {
1396
		$params = $request->params();
1397
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1398
1399
		if(!$deployment || !$deployment->ID) {
1400
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1401
		}
1402
		if(!$deployment->canView()) {
1403
			return Security::permissionFailure();
1404
		}
1405
1406
		$environment = $deployment->Environment();
1407
		$project = $environment->Project();
1408
1409
		if($environment->Name != $params['Environment']) {
1410
			throw new LogicException("Environment in URL doesn't match this deploy");
1411
		}
1412
		if($project->Name != $params['Project']) {
1413
			throw new LogicException("Project in URL doesn't match this deploy");
1414
		}
1415
1416
		return $this->render(array(
1417
			'Deployment' => $deployment,
1418
		));
1419
	}
1420
1421
1422
	/**
1423
	 * Action - Get the latest deploy log
1424
	 *
1425
	 * @param SS_HTTPRequest $request
1426
	 *
1427
	 * @return string
1428
	 * @throws SS_HTTPResponse_Exception
1429
	 */
1430
	public function deploylog(SS_HTTPRequest $request) {
1431
		$params = $request->params();
1432
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1433
1434
		if(!$deployment || !$deployment->ID) {
1435
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1436
		}
1437
		if(!$deployment->canView()) {
1438
			return Security::permissionFailure();
1439
		}
1440
1441
		$environment = $deployment->Environment();
1442
		$project = $environment->Project();
1443
1444
		if($environment->Name != $params['Environment']) {
1445
			throw new LogicException("Environment in URL doesn't match this deploy");
1446
		}
1447
		if($project->Name != $params['Project']) {
1448
			throw new LogicException("Project in URL doesn't match this deploy");
1449
		}
1450
1451
		$log = $deployment->log();
1452
		if($log->exists()) {
1453
			$content = $log->content();
1454
		} else {
1455
			$content = 'Waiting for action to start';
1456
		}
1457
1458
		return $this->sendResponse($deployment->ResqueStatus(), $content);
1459
	}
1460
1461
	public function abortDeploy(SS_HTTPRequest $request) {
1462
		$params = $request->params();
1463
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1464
1465
		if(!$deployment || !$deployment->ID) {
1466
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1467
		}
1468
		if(!$deployment->canView()) {
1469
			return Security::permissionFailure();
1470
		}
1471
1472
		// For now restrict to ADMINs only.
1473
		if(!Permission::check('ADMIN')) {
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
		if (!in_array($deployment->Status, ['Queued', 'Deploying', 'Aborting'])) {
1488
			throw new LogicException(sprintf("Cannot abort from %s state.", $deployment->Status));
1489
		}
1490
1491
		$deployment->getMachine()->apply(DNDeployment::TR_ABORT);
1492
1493
		return $this->sendResponse($deployment->ResqueStatus(), []);
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2211
				$pagination->setPageLength(8);
2212
				return $pagination;
2213
			}
2214
		}
2215
		return null;
2216
	}
2217
2218
	/**
2219
	 * @return SS_HTTPResponse
2220
	 */
2221
	protected function project404Response() {
2222
		return new SS_HTTPResponse(
2223
			"Project '" . Convert::raw2xml($this->getRequest()->param('Project')) . "' not found.",
2224
			404
2225
		);
2226
	}
2227
2228
	/**
2229
	 * @return SS_HTTPResponse
2230
	 */
2231
	protected function environment404Response() {
2232
		$envName = Convert::raw2xml($this->getRequest()->param('Environment'));
2233
		return new SS_HTTPResponse("Environment '" . $envName . "' not found.", 404);
2234
	}
2235
2236
	/**
2237
	 * @param string $status
2238
	 * @param string $content
2239
	 *
2240
	 * @return string
2241
	 */
2242
	public function sendResponse($status, $content) {
2243
		// strip excessive newlines
2244
		$content = preg_replace('/(?:(?:\r\n|\r|\n)\s*){2}/s', "\n", $content);
2245
2246
		$sendJSON = (strpos($this->getRequest()->getHeader('Accept'), 'application/json') !== false)
2247
			|| $this->getRequest()->getExtension() == 'json';
2248
2249
		if(!$sendJSON) {
2250
			$this->response->addHeader("Content-type", "text/plain");
2251
			return $content;
2252
		}
2253
		$this->response->addHeader("Content-type", "application/json");
2254
		return json_encode(array(
2255
			'status' => $status,
2256
			'content' => $content,
2257
		));
2258
	}
2259
2260
	/**
2261
	 * Validate the snapshot mode
2262
	 *
2263
	 * @param string $mode
2264
	 */
2265
	protected function validateSnapshotMode($mode) {
2266
		if(!in_array($mode, array('all', 'assets', 'db'))) {
2267
			throw new LogicException('Invalid mode');
2268
		}
2269
	}
2270
2271
	/**
2272
	 * @param string $sectionName
2273
	 * @param string $title
2274
	 *
2275
	 * @return SS_HTTPResponse
2276
	 */
2277
	protected function getCustomisedViewSection($sectionName, $title = '', $data = array()) {
2278
		// Performs canView permission check by limiting visible projects
2279
		$project = $this->getCurrentProject();
2280
		if(!$project) {
2281
			return $this->project404Response();
2282
		}
2283
		$data[$sectionName] = 1;
2284
2285
		if($this !== '') {
2286
			$data['Title'] = $title;
2287
		}
2288
2289
		return $this->render($data);
2290
	}
2291
2292
	/**
2293
	 * Get items for the ambient menu that should be accessible from all pages.
2294
	 *
2295
	 * @return ArrayList
2296
	 */
2297
	public function AmbientMenu() {
2298
		$list = new ArrayList();
2299
2300
		if (Member::currentUserID()) {
2301
			$list->push(new ArrayData(array(
2302
				'Classes' => 'logout',
2303
				'FaIcon' => 'sign-out',
2304
				'Link' => 'Security/logout',
2305
				'Title' => 'Log out',
2306
				'IsCurrent' => false,
2307
				'IsSection' => false
2308
			)));
2309
		}
2310
2311
		$this->extend('updateAmbientMenu', $list);
2312
		return $list;
2313
	}
2314
2315
	/**
2316
	 * Checks whether the user can create a project.
2317
	 *
2318
	 * @return bool
2319
	 */
2320
	public function canCreateProjects($member = null) {
2321
		if(!$member) $member = Member::currentUser();
2322
		if(!$member) return false;
2323
2324
		return singleton('DNProject')->canCreate($member);
2325
	}
2326
2327
}
2328
2329