Completed
Push — master ( fac027...de03a8 )
by Sean
140:11 queued 136:59
created

DNRoot::get_template_global_variables()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

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