Completed
Pull Request — master (#542)
by Mateusz
238:22 queued 235:03
created

DNRoot::doDelete()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 30
Code Lines 16

Duplication

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