Completed
Push — master ( 753b30...d7e370 )
by Mateusz
07:29 queued 04:18
created

DNRoot::DataTransferLogs()   A

Complexity

Conditions 4
Paths 1

Size

Total Lines 15
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 15
rs 9.2
cc 4
eloc 12
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
	 * Allow advanced options on deployments
33
	 */
34
	const DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS = 'DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS';
35
36
	const ALLOW_PROD_DEPLOYMENT = 'ALLOW_PROD_DEPLOYMENT';
37
	const ALLOW_NON_PROD_DEPLOYMENT = 'ALLOW_NON_PROD_DEPLOYMENT';
38
	const ALLOW_PROD_SNAPSHOT = 'ALLOW_PROD_SNAPSHOT';
39
	const ALLOW_NON_PROD_SNAPSHOT = 'ALLOW_NON_PROD_SNAPSHOT';
40
	const ALLOW_CREATE_ENVIRONMENT = 'ALLOW_CREATE_ENVIRONMENT';
41
42
	/**
43
	 * @var array
44
	 */
45
	private static $allowed_actions = array(
46
		'projects',
47
		'nav',
48
		'update',
49
		'project',
50
		'toggleprojectstar',
51
		'branch',
52
		'environment',
53
		'metrics',
54
		'createenvlog',
55
		'createenv',
56
		'getDeployForm',
57
		'doDeploy',
58
		'deploy',
59
		'deploylog',
60
		'abortDeploy',
61
		'getDataTransferForm',
62
		'transfer',
63
		'transferlog',
64
		'snapshots',
65
		'createsnapshot',
66
		'snapshotslog',
67
		'uploadsnapshot',
68
		'getCreateEnvironmentForm',
69
		'getUploadSnapshotForm',
70
		'getPostSnapshotForm',
71
		'getDataTransferRestoreForm',
72
		'getDeleteForm',
73
		'getMoveForm',
74
		'restoresnapshot',
75
		'deletesnapshot',
76
		'movesnapshot',
77
		'postsnapshotsuccess',
78
		'gitRevisions',
79
		'deploySummary',
80
		'startDeploy'
81
	);
82
83
	/**
84
	 * URL handlers pretending that we have a deep URL structure.
85
	 */
86
	private static $url_handlers = array(
87
		'project/$Project/environment/$Environment/DeployForm' => 'getDeployForm',
88
		'project/$Project/createsnapshot/DataTransferForm' => 'getDataTransferForm',
89
		'project/$Project/DataTransferForm' => 'getDataTransferForm',
90
		'project/$Project/DataTransferRestoreForm' => 'getDataTransferRestoreForm',
91
		'project/$Project/DeleteForm' => 'getDeleteForm',
92
		'project/$Project/MoveForm' => 'getMoveForm',
93
		'project/$Project/UploadSnapshotForm' => 'getUploadSnapshotForm',
94
		'project/$Project/PostSnapshotForm' => 'getPostSnapshotForm',
95
		'project/$Project/environment/$Environment/metrics' => 'metrics',
96
		'project/$Project/environment/$Environment/deploy_summary' => 'deploySummary',
97
		'project/$Project/environment/$Environment/git_revisions' => 'gitRevisions',
98
		'project/$Project/environment/$Environment/start-deploy' => 'startDeploy',
99
		'project/$Project/environment/$Environment/deploy/$Identifier/log' => 'deploylog',
100
		'project/$Project/environment/$Environment/deploy/$Identifier/abort-deploy' => 'abortDeploy',
101
		'project/$Project/environment/$Environment/deploy/$Identifier' => 'deploy',
102
		'project/$Project/transfer/$Identifier/log' => 'transferlog',
103
		'project/$Project/transfer/$Identifier' => 'transfer',
104
		'project/$Project/environment/$Environment' => 'environment',
105
		'project/$Project/createenv/$Identifier/log' => 'createenvlog',
106
		'project/$Project/createenv/$Identifier' => 'createenv',
107
		'project/$Project/CreateEnvironmentForm' => 'getCreateEnvironmentForm',
108
		'project/$Project/branch' => 'branch',
109
		'project/$Project/build/$Build' => 'build',
110
		'project/$Project/restoresnapshot/$DataArchiveID' => 'restoresnapshot',
111
		'project/$Project/deletesnapshot/$DataArchiveID' => 'deletesnapshot',
112
		'project/$Project/movesnapshot/$DataArchiveID' => 'movesnapshot',
113
		'project/$Project/update' => 'update',
114
		'project/$Project/snapshots' => 'snapshots',
115
		'project/$Project/createsnapshot' => 'createsnapshot',
116
		'project/$Project/uploadsnapshot' => 'uploadsnapshot',
117
		'project/$Project/snapshotslog' => 'snapshotslog',
118
		'project/$Project/postsnapshotsuccess/$DataArchiveID' => 'postsnapshotsuccess',
119
		'project/$Project/star' => 'toggleprojectstar',
120
		'project/$Project' => 'project',
121
		'nav/$Project' => 'nav',
122
		'projects' => 'projects',
123
	);
124
125
	/**
126
	 * @var array
127
	 */
128
	protected static $_project_cache = array();
129
130
	/**
131
	 * @var array
132
	 */
133
	private static $support_links = array();
134
135
	/**
136
	 * @var array
137
	 */
138
	private static $platform_specific_strings = array();
139
140
	/**
141
	 * @var array
142
	 */
143
	private static $action_types = array(
144
		self::ACTION_DEPLOY,
145
		self::ACTION_SNAPSHOT,
146
		self::PROJECT_OVERVIEW
147
	);
148
149
	/**
150
	 * @var DNData
151
	 */
152
	protected $data;
153
154
	/**
155
	 * Include requirements that deploynaut needs, such as javascript.
156
	 */
157
	public static function include_requirements() {
158
159
		// JS should always go to the bottom, otherwise there's the risk that Requirements
160
		// puts them halfway through the page to the nearest <script> tag. We don't want that.
161
		Requirements::set_force_js_to_bottom(true);
162
163
		// todo these should be bundled into the same JS as the others in "static" below.
164
		// We've deliberately not used combined_files as it can mess with some of the JS used
165
		// here and cause sporadic errors.
166
		Requirements::javascript('deploynaut/javascript/jquery.js');
167
		Requirements::javascript('deploynaut/javascript/bootstrap.js');
168
		Requirements::javascript('deploynaut/javascript/q.js');
169
		Requirements::javascript('deploynaut/javascript/tablefilter.js');
170
		Requirements::javascript('deploynaut/javascript/deploynaut.js');
171
172
		Requirements::javascript('deploynaut/javascript/bootstrap.file-input.js');
173
		Requirements::javascript('deploynaut/thirdparty/select2/dist/js/select2.min.js');
174
		Requirements::javascript('deploynaut/thirdparty/bootstrap-switch/dist/js/bootstrap-switch.min.js');
175
		Requirements::javascript('deploynaut/javascript/material.js');
176
177
		// Load the buildable dependencies only if not loaded centrally.
178
		if (!is_dir(BASE_PATH . DIRECTORY_SEPARATOR . 'static')) {
179
			if (\Director::isDev()) {
180
				\Requirements::javascript('deploynaut/static/bundle-debug.js');
181
			} else {
182
				\Requirements::javascript('deploynaut/static/bundle.js');
183
			}
184
		}
185
186
		Requirements::css('deploynaut/static/style.css');
187
	}
188
189
	/**
190
	 * Check for feature flags:
191
	 * - FLAG_SNAPSHOTS_ENABLED: set to true to enable globally
192
	 * - FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS: set to semicolon-separated list of email addresses of allowed users.
193
	 *
194
	 * @return boolean
195
	 */
196
	public static function FlagSnapshotsEnabled() {
197
		if(defined('FLAG_SNAPSHOTS_ENABLED') && FLAG_SNAPSHOTS_ENABLED) {
198
			return true;
199
		}
200
		if(defined('FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS') && FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS) {
201
			$allowedMembers = explode(';', FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS);
202
			$member = Member::currentUser();
203
			if($allowedMembers && $member && in_array($member->Email, $allowedMembers)) {
204
				return true;
205
			}
206
		}
207
		return false;
208
	}
209
210
	/**
211
	 * @return ArrayList
212
	 */
213
	public static function get_support_links() {
214
		$supportLinks = self::config()->support_links;
215
		if($supportLinks) {
216
			return new ArrayList($supportLinks);
217
		}
218
	}
219
220
	/**
221
	 * @return array
222
	 */
223
	public static function get_template_global_variables() {
224
		return array(
225
			'RedisUnavailable' => 'RedisUnavailable',
226
			'RedisWorkersCount' => 'RedisWorkersCount',
227
			'SidebarLinks' => 'SidebarLinks',
228
			"SupportLinks" => 'get_support_links'
229
		);
230
	}
231
232
	/**
233
	 */
234
	public function init() {
235
		parent::init();
236
237
		if(!Member::currentUser() && !Session::get('AutoLoginHash')) {
238
			return Security::permissionFailure();
239
		}
240
241
		// Block framework jquery
242
		Requirements::block(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
243
244
		self::include_requirements();
245
	}
246
247
	/**
248
	 * @return string
249
	 */
250
	public function Link() {
251
		return "naut/";
252
	}
253
254
	/**
255
	 * Actions
256
	 *
257
	 * @param SS_HTTPRequest $request
258
	 * @return \SS_HTTPResponse
259
	 */
260
	public function index(SS_HTTPRequest $request) {
261
		return $this->redirect($this->Link() . 'projects/');
262
	}
263
264
	/**
265
	 * Action
266
	 *
267
	 * @param SS_HTTPRequest $request
268
	 * @return string - HTML
269
	 */
270
	public function projects(SS_HTTPRequest $request) {
271
		// Performs canView permission check by limiting visible projects in DNProjectsList() call.
272
		return $this->customise(array(
273
			'Title' => 'Projects',
274
		))->render();
275
	}
276
277
	/**
278
	 * @param SS_HTTPRequest $request
279
	 * @return HTMLText
280
	 */
281
	public function nav(SS_HTTPRequest $request) {
282
		return $this->renderWith('Nav');
283
	}
284
285
	/**
286
	 * Return a link to the navigation template used for AJAX requests.
287
	 * @return string
288
	 */
289
	public function NavLink() {
290
		$currentProject = $this->getCurrentProject();
291
		$projectName = $currentProject ? $currentProject->Name : null;
292
		return Controller::join_links(Director::absoluteBaseURL(), 'naut', 'nav', $projectName);
293
	}
294
295
	/**
296
	 * Action
297
	 *
298
	 * @param SS_HTTPRequest $request
299
	 * @return SS_HTTPResponse - HTML
300
	 */
301
	public function snapshots(SS_HTTPRequest $request) {
302
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
303
		return $this->getCustomisedViewSection('SnapshotsSection', 'Data Snapshots');
304
	}
305
306
	/**
307
	 * Action
308
	 *
309
	 * @param SS_HTTPRequest $request
310
	 * @return string - HTML
311
	 */
312 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...
313
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
314
315
		// Performs canView permission check by limiting visible projects
316
		$project = $this->getCurrentProject();
317
		if(!$project) {
318
			return $this->project404Response();
319
		}
320
321
		if(!$project->canBackup()) {
322
			return new SS_HTTPResponse("Not allowed to create snapshots on any environments", 401);
323
		}
324
325
		return $this->customise(array(
326
			'Title' => 'Create Data Snapshot',
327
			'SnapshotsSection' => 1,
328
			'DataTransferForm' => $this->getDataTransferForm($request)
329
		))->render();
330
	}
331
332
	/**
333
	 * Action
334
	 *
335
	 * @param SS_HTTPRequest $request
336
	 * @return string - HTML
337
	 */
338 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...
339
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
340
341
		// Performs canView permission check by limiting visible projects
342
		$project = $this->getCurrentProject();
343
		if(!$project) {
344
			return $this->project404Response();
345
		}
346
347
		if(!$project->canUploadArchive()) {
348
			return new SS_HTTPResponse("Not allowed to upload", 401);
349
		}
350
351
		return $this->customise(array(
352
			'SnapshotsSection' => 1,
353
			'UploadSnapshotForm' => $this->getUploadSnapshotForm($request),
354
			'PostSnapshotForm' => $this->getPostSnapshotForm($request)
355
		))->render();
356
	}
357
358
	/**
359
	 * Return the upload limit for snapshot uploads
360
	 * @return string
361
	 */
362
	public function UploadLimit() {
363
		return File::format_size(min(
364
			File::ini2bytes(ini_get('upload_max_filesize')),
365
			File::ini2bytes(ini_get('post_max_size'))
366
		));
367
	}
368
369
	/**
370
	 * Construct the upload form.
371
	 *
372
	 * @param SS_HTTPRequest $request
373
	 * @return Form
374
	 */
375
	public function getUploadSnapshotForm(SS_HTTPRequest $request) {
376
		// Performs canView permission check by limiting visible projects
377
		$project = $this->getCurrentProject();
378
		if(!$project) {
379
			return $this->project404Response();
380
		}
381
382
		if(!$project->canUploadArchive()) {
383
			return new SS_HTTPResponse("Not allowed to upload", 401);
384
		}
385
386
		// Framing an environment as a "group of people with download access"
387
		// makes more sense to the user here, while still allowing us to enforce
388
		// environment specific restrictions on downloading the file later on.
389
		$envs = $project->DNEnvironmentList()->filterByCallback(function($item) {
390
			return $item->canUploadArchive();
391
		});
392
		$envsMap = array();
393
		foreach($envs as $env) {
394
			$envsMap[$env->ID] = $env->Name;
395
		}
396
397
		$maxSize = min(File::ini2bytes(ini_get('upload_max_filesize')), File::ini2bytes(ini_get('post_max_size')));
398
		$fileField = DataArchiveFileField::create('ArchiveFile', 'File');
399
		$fileField->getValidator()->setAllowedExtensions(array('sspak'));
400
		$fileField->getValidator()->setAllowedMaxFileSize(array('*' => $maxSize));
401
402
		$form = Form::create(
403
			$this,
404
			'UploadSnapshotForm',
405
			FieldList::create(
406
				$fileField,
407
				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
408
				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
409
					->setEmptyString('Select an environment')
410
			),
411
			FieldList::create(
412
				FormAction::create('doUploadSnapshot', 'Upload File')
413
					->addExtraClass('btn')
414
			),
415
			RequiredFields::create('ArchiveFile')
416
		);
417
418
		$form->disableSecurityToken();
419
		$form->addExtraClass('fields-wide');
420
		// Tweak the action so it plays well with our fake URL structure.
421
		$form->setFormAction($project->Link() . '/UploadSnapshotForm');
422
423
		return $form;
424
	}
425
426
	/**
427
	 * @param array $data
428
	 * @param Form $form
429
	 *
430
	 * @return bool|HTMLText|SS_HTTPResponse
431
	 */
432
	public function doUploadSnapshot($data, Form $form) {
433
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
434
435
		// Performs canView permission check by limiting visible projects
436
		$project = $this->getCurrentProject();
437
		if(!$project) {
438
			return $this->project404Response();
439
		}
440
441
		$validEnvs = $project->DNEnvironmentList()
442
			->filterByCallback(function($item) {
443
				return $item->canUploadArchive();
444
			});
445
446
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
447
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
448
		if(!$environment) {
449
			throw new LogicException('Invalid environment');
450
		}
451
452
		$this->validateSnapshotMode($data['Mode']);
453
454
		$dataArchive = DNDataArchive::create(array(
455
			'AuthorID' => Member::currentUserID(),
456
			'EnvironmentID' => $data['EnvironmentID'],
457
			'IsManualUpload' => true,
458
		));
459
		// needs an ID and transfer to determine upload path
460
		$dataArchive->write();
461
		$dataTransfer = DNDataTransfer::create(array(
462
			'AuthorID' => Member::currentUserID(),
463
			'Mode' => $data['Mode'],
464
			'Origin' => 'ManualUpload',
465
			'EnvironmentID' => $data['EnvironmentID']
466
		));
467
		$dataTransfer->write();
468
		$dataArchive->DataTransfers()->add($dataTransfer);
469
		$form->saveInto($dataArchive);
470
		$dataArchive->write();
471
		$workingDir = TEMP_FOLDER . DIRECTORY_SEPARATOR . 'deploynaut-transfer-' . $dataTransfer->ID;
472
473 View Code Duplication
		$cleanupFn = function() use($workingDir, $dataTransfer, $dataArchive) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
474
			$process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir)));
475
			$process->setTimeout(120);
476
			$process->run();
477
			$dataTransfer->delete();
478
			$dataArchive->delete();
479
		};
480
481
		// extract the sspak contents so we can inspect them
482
		try {
483
			$dataArchive->extractArchive($workingDir);
484
		} catch(Exception $e) {
485
			$cleanupFn();
486
			$form->sessionMessage(
487
				'There was a problem trying to open your snapshot for processing. Please try uploading again',
488
				'bad'
489
			);
490
			return $this->redirectBack();
491
		}
492
493
		// validate that the sspak contents match the declared contents
494
		$result = $dataArchive->validateArchiveContents();
495
		if(!$result->valid()) {
496
			$cleanupFn();
497
			$form->sessionMessage($result->message(), 'bad');
498
			return $this->redirectBack();
499
		}
500
501
		// fix file permissions of extracted sspak files then re-build the sspak
502
		try {
503
			$dataArchive->fixArchivePermissions($workingDir);
504
			$dataArchive->setArchiveFromFiles($workingDir);
505
		} catch(Exception $e) {
506
			$cleanupFn();
507
			$form->sessionMessage(
508
				'There was a problem processing your snapshot. Please try uploading again',
509
				'bad'
510
			);
511
			return $this->redirectBack();
512
		}
513
514
		// cleanup any extracted sspak contents lying around
515
		$process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir)));
516
		$process->setTimeout(120);
517
		$process->run();
518
519
		return $this->customise(array(
520
			'Project' => $project,
521
			'CurrentProject' => $project,
522
			'SnapshotsSection' => 1,
523
			'DataArchive' => $dataArchive,
524
			'DataTransferRestoreForm' => $this->getDataTransferRestoreForm($this->request, $dataArchive),
525
			'BackURL' => $project->Link('snapshots')
526
		))->renderWith(array('DNRoot_uploadsnapshot', 'DNRoot'));
527
	}
528
529
	/**
530
	 * @param SS_HTTPRequest $request
531
	 * @return Form
532
	 */
533
	public function getPostSnapshotForm(SS_HTTPRequest $request) {
534
		// Performs canView permission check by limiting visible projects
535
		$project = $this->getCurrentProject();
536
		if(!$project) {
537
			return $this->project404Response();
538
		}
539
540
		if(!$project->canUploadArchive()) {
541
			return new SS_HTTPResponse("Not allowed to upload", 401);
542
		}
543
544
		// Framing an environment as a "group of people with download access"
545
		// makes more sense to the user here, while still allowing us to enforce
546
		// environment specific restrictions on downloading the file later on.
547
		$envs = $project->DNEnvironmentList()->filterByCallback(function($item) {
548
			return $item->canUploadArchive();
549
		});
550
		$envsMap = array();
551
		foreach($envs as $env) {
552
			$envsMap[$env->ID] = $env->Name;
553
		}
554
555
		$form = Form::create(
556
			$this,
557
			'PostSnapshotForm',
558
			FieldList::create(
559
				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
560
				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
561
					->setEmptyString('Select an environment')
562
			),
563
			FieldList::create(
564
				FormAction::create('doPostSnapshot', 'Submit request')
565
					->addExtraClass('btn')
566
			),
567
			RequiredFields::create('File')
568
		);
569
570
		$form->disableSecurityToken();
571
		$form->addExtraClass('fields-wide');
572
		// Tweak the action so it plays well with our fake URL structure.
573
		$form->setFormAction($project->Link() . '/PostSnapshotForm');
574
575
		return $form;
576
	}
577
578
	/**
579
	 * @param array $data
580
	 * @param Form $form
581
	 *
582
	 * @return SS_HTTPResponse
583
	 */
584
	public function doPostSnapshot($data, $form) {
585
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
586
587
		$project = $this->getCurrentProject();
588
		if(!$project) {
589
			return $this->project404Response();
590
		}
591
592
		$validEnvs = $project->DNEnvironmentList()->filterByCallback(function($item) {
593
				return $item->canUploadArchive();
594
		});
595
596
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
597
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
598
		if(!$environment) {
599
			throw new LogicException('Invalid environment');
600
		}
601
602
		$dataArchive = DNDataArchive::create(array(
603
			'UploadToken' => DNDataArchive::generate_upload_token(),
604
		));
605
		$form->saveInto($dataArchive);
606
		$dataArchive->write();
607
608
		return $this->redirect(Controller::join_links(
609
			$project->Link(),
610
			'postsnapshotsuccess',
611
			$dataArchive->ID
612
		));
613
	}
614
615
	/**
616
	 * Action
617
	 *
618
	 * @param SS_HTTPRequest $request
619
	 * @return SS_HTTPResponse - HTML
620
	 */
621
	public function snapshotslog(SS_HTTPRequest $request) {
622
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
623
		return $this->getCustomisedViewSection('SnapshotsSection', 'Snapshots log');
624
	}
625
626
	/**
627
	 * @param SS_HTTPRequest $request
628
	 * @return SS_HTTPResponse|string
629
	 * @throws SS_HTTPResponse_Exception
630
	 */
631
	public function postsnapshotsuccess(SS_HTTPRequest $request) {
632
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
633
634
		// Performs canView permission check by limiting visible projects
635
		$project = $this->getCurrentProject();
636
		if(!$project) {
637
			return $this->project404Response();
638
		}
639
640
		if(!$project->canUploadArchive()) {
641
			return new SS_HTTPResponse("Not allowed to upload", 401);
642
		}
643
644
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
645
		if(!$dataArchive) {
646
			return new SS_HTTPResponse("Archive not found.", 404);
647
		}
648
649
		if(!$dataArchive->canRestore()) {
650
			throw new SS_HTTPResponse_Exception('Not allowed to restore archive', 403);
651
		}
652
653
		return $this->render(array(
654
				'Title' => 'How to send us your Data Snapshot by post',
655
				'DataArchive' => $dataArchive,
656
				'Address' => Config::inst()->get('Deploynaut', 'snapshot_post_address'),
657
				'BackURL' => $project->Link(),
658
			));
659
	}
660
661
	/**
662
	 * @param SS_HTTPRequest $request
663
	 * @return \SS_HTTPResponse
664
	 */
665
	public function project(SS_HTTPRequest $request) {
666
		$this->setCurrentActionType(self::PROJECT_OVERVIEW);
667
		return $this->getCustomisedViewSection('ProjectOverview', '', array('IsAdmin' => Permission::check('ADMIN')));
668
	}
669
670
	/**
671
	 * This action will star / unstar a project for the current member
672
	 *
673
	 * @param SS_HTTPRequest $request
674
	 *
675
	 * @return SS_HTTPResponse
676
	 */
677
	public function toggleprojectstar(SS_HTTPRequest $request) {
678
		$project = $this->getCurrentProject();
679
		if(!$project) {
680
			return $this->project404Response();
681
		}
682
683
		$member = Member::currentUser();
684
		if($member === null) {
685
			return $this->project404Response();
686
		}
687
		$favProject = $member->StarredProjects()
688
			->filter('DNProjectID', $project->ID)
689
			->first();
690
691
		if($favProject) {
692
			$member->StarredProjects()->remove($favProject);
693
		} else {
694
			$member->StarredProjects()->add($project);
695
		}
696
		return $this->redirectBack();
697
	}
698
699
	/**
700
	 * @param SS_HTTPRequest $request
701
	 * @return \SS_HTTPResponse
702
	 */
703
	public function branch(SS_HTTPRequest $request) {
704
		$project = $this->getCurrentProject();
705
		if(!$project) {
706
			return $this->project404Response();
707
		}
708
709
		$branchName = $request->getVar('name');
710
		$branch = $project->DNBranchList()->byName($branchName);
711
		if(!$branch) {
712
			return new SS_HTTPResponse("Branch '" . Convert::raw2xml($branchName) . "' not found.", 404);
713
		}
714
715
		return $this->render(array(
716
			'CurrentBranch' => $branch,
717
		));
718
	}
719
720
	/**
721
	 * @param SS_HTTPRequest $request
722
	 * @return \SS_HTTPResponse
723
	 */
724
	public function environment(SS_HTTPRequest $request) {
725
		// Performs canView permission check by limiting visible projects
726
		$project = $this->getCurrentProject();
727
		if(!$project) {
728
			return $this->project404Response();
729
		}
730
731
		// Performs canView permission check by limiting visible projects
732
		$env = $this->getCurrentEnvironment($project);
733
		if(!$env) {
734
			return $this->environment404Response();
735
		}
736
737
		return $this->render(array(
738
			'DNEnvironmentList' => $this->getCurrentProject()->DNEnvironmentList(),
739
			'FlagSnapshotsEnabled' => $this->FlagSnapshotsEnabled(),
740
			'Redeploy' => (bool)$request->getVar('redeploy')
741
		));
742
	}
743
744
	/**
745
	 * Shows the creation log.
746
	 *
747
	 * @param SS_HTTPRequest $request
748
	 * @return string
749
	 */
750
	public function createenv(SS_HTTPRequest $request) {
751
		$params = $request->params();
752
		if($params['Identifier']) {
753
			$record = DNCreateEnvironment::get()->byId($params['Identifier']);
754
755
			if(!$record || !$record->ID) {
756
				throw new SS_HTTPResponse_Exception('Create environment not found', 404);
757
			}
758
			if(!$record->canView()) {
759
				return Security::permissionFailure();
760
			}
761
762
			$project = $this->getCurrentProject();
763
			if(!$project) {
764
				return $this->project404Response();
765
			}
766
767
			if($project->Name != $params['Project']) {
768
				throw new LogicException("Project in URL doesn't match this creation");
769
			}
770
771
			return $this->render(array(
772
				'CreateEnvironment' => $record,
773
			));
774
		}
775
		return $this->render(array('CurrentTitle' => 'Create an environment'));
776
	}
777
778
779
	public function createenvlog(SS_HTTPRequest $request) {
780
		$params = $request->params();
781
		$env = DNCreateEnvironment::get()->byId($params['Identifier']);
782
783
		if(!$env || !$env->ID) {
784
			throw new SS_HTTPResponse_Exception('Log not found', 404);
785
		}
786
		if(!$env->canView()) {
787
			return Security::permissionFailure();
788
		}
789
790
		$project = $env->Project();
791
792
		if($project->Name != $params['Project']) {
793
			throw new LogicException("Project in URL doesn't match this deploy");
794
		}
795
796
		$log = $env->log();
797
		if($log->exists()) {
798
			$content = $log->content();
799
		} else {
800
			$content = 'Waiting for action to start';
801
		}
802
803
		return $this->sendResponse($env->ResqueStatus(), $content);
804
	}
805
806
	/**
807
	 * @param SS_HTTPRequest $request
808
	 * @return Form
809
	 */
810
	public function getCreateEnvironmentForm(SS_HTTPRequest $request) {
811
		$this->setCurrentActionType(self::ACTION_ENVIRONMENTS);
812
813
		$project = $this->getCurrentProject();
814
		if(!$project) {
815
			return $this->project404Response();
816
		}
817
818
		$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...
819
		if(!$envType || !class_exists($envType)) {
820
			return null;
821
		}
822
823
		$backend = Injector::inst()->get($envType);
824
		if(!($backend instanceof EnvironmentCreateBackend)) {
825
			// Only allow this for supported backends.
826
			return null;
827
		}
828
829
		$fields = $backend->getCreateEnvironmentFields($project);
830
		if(!$fields) return null;
831
832
		if(!$project->canCreateEnvironments()) {
833
			return new SS_HTTPResponse('Not allowed to create environments for this project', 401);
834
		}
835
836
		$form = Form::create(
837
			$this,
838
			'CreateEnvironmentForm',
839
			$fields,
840
			FieldList::create(
841
				FormAction::create('doCreateEnvironment', 'Create')
842
					->addExtraClass('btn')
843
			),
844
			$backend->getCreateEnvironmentValidator()
845
		);
846
847
		// Tweak the action so it plays well with our fake URL structure.
848
		$form->setFormAction($project->Link() . '/CreateEnvironmentForm');
849
850
		return $form;
851
	}
852
853
	/**
854
	 * @param array $data
855
	 * @param Form $form
856
	 *
857
	 * @return bool|HTMLText|SS_HTTPResponse
858
	 */
859
	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...
860
		$this->setCurrentActionType(self::ACTION_ENVIRONMENTS);
861
862
		$project = $this->getCurrentProject();
863
		if(!$project) {
864
			return $this->project404Response();
865
		}
866
867
		if(!$project->canCreateEnvironments()) {
868
			return new SS_HTTPResponse('Not allowed to create environments for this project', 401);
869
		}
870
871
		// Set the environment type so we know what we're creating.
872
		$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...
873
874
		$job = DNCreateEnvironment::create();
875
876
		$job->Data = serialize($data);
877
		$job->ProjectID = $project->ID;
878
		$job->write();
879
		$job->start();
880
881
		return $this->redirect($project->Link('createenv') . '/' . $job->ID);
882
	}
883
884
	/**
885
	 *
886
	 * @param SS_HTTPRequest $request
887
	 * @return \SS_HTTPResponse
888
	 */
889
	public function metrics(SS_HTTPRequest $request) {
890
		// Performs canView permission check by limiting visible projects
891
		$project = $this->getCurrentProject();
892
		if(!$project) {
893
			return $this->project404Response();
894
		}
895
896
		// Performs canView permission check by limiting visible projects
897
		$env = $this->getCurrentEnvironment($project);
898
		if(!$env) {
899
			return $this->environment404Response();
900
		}
901
902
		return $this->render();
903
	}
904
905
	/**
906
	 * Get the DNData object.
907
	 *
908
	 * @return DNData
909
	 */
910
	public function DNData() {
911
		return DNData::inst();
912
	}
913
914
	/**
915
	 * Provide a list of all projects.
916
	 *
917
	 * @return SS_List
918
	 */
919
	public function DNProjectList() {
920
		$memberId = Member::currentUserID();
921
		if(!$memberId) {
922
			return new ArrayList();
923
		}
924
925
		if(Permission::check('ADMIN')) {
926
			return DNProject::get();
927
		}
928
929
		$projects = Member::get()->filter('ID', $memberId)
930
			->relation('Groups')
931
			->relation('Projects');
932
933
		$this->extend('updateDNProjectList', $projects);
934
		return $projects;
935
	}
936
937
	/**
938
	 * @return ArrayList
939
	 */
940
	public function getPlatformSpecificStrings() {
941
		$strings = $this->config()->platform_specific_strings;
942
		if ($strings) {
943
			return new ArrayList($strings);
944
		}
945
	}
946
947
	/**
948
	 * Provide a list of all starred projects for the currently logged in member
949
	 *
950
	 * @return SS_List
951
	 */
952
	public function getStarredProjects() {
953
		$member = Member::currentUser();
954
		if($member === null) {
955
			return new ArrayList();
956
		}
957
958
		$favProjects = $member->StarredProjects();
959
960
		$list = new ArrayList();
961
		foreach($favProjects as $project) {
962
			if($project->canView($member)) {
963
				$list->add($project);
964
			}
965
		}
966
		return $list;
967
	}
968
969
	/**
970
	 * Returns top level navigation of projects.
971
	 *
972
	 * @param int $limit
973
	 *
974
	 * @return ArrayList
975
	 */
976
	public function Navigation($limit = 5) {
977
		$navigation = new ArrayList();
978
979
		$currentProject = $this->getCurrentProject();
980
		$currentEnvironment = $this->getCurrentEnvironment();
981
		$actionType = $this->getCurrentActionType();
982
983
		$projects = $this->getStarredProjects();
984
		if($projects->count() < 1) {
985
			$projects = $this->DNProjectList();
986
		} else {
987
			$limit = -1;
988
		}
989
990
		if($projects->count() > 0) {
991
			$activeProject = false;
992
993
			if($limit > 0) {
994
				$limitedProjects = $projects->limit($limit);
995
			} else {
996
				$limitedProjects = $projects;
997
			}
998
999
			foreach($limitedProjects as $project) {
1000
				$isActive = $currentProject && $currentProject->ID == $project->ID;
1001
				if($isActive) {
1002
					$activeProject = true;
1003
				}
1004
1005
				$isCurrentEnvironment = false;
1006
				if($project && $currentEnvironment) {
1007
					$isCurrentEnvironment = (bool) $project->DNEnvironmentList()->find('ID', $currentEnvironment->ID);
1008
				}
1009
1010
				$navigation->push(array(
1011
					'Project' => $project,
1012
					'IsCurrentEnvironment' => $isCurrentEnvironment,
1013
					'IsActive' => $currentProject && $currentProject->ID == $project->ID,
1014
					'IsOverview' => $actionType == self::PROJECT_OVERVIEW && $currentProject->ID == $project->ID
1015
				));
1016
			}
1017
1018
			// Ensure the current project is in the list
1019
			if(!$activeProject && $currentProject) {
1020
				$navigation->unshift(array(
1021
					'Project' => $currentProject,
1022
					'IsActive' => true,
1023
					'IsCurrentEnvironment' => $currentEnvironment,
1024
					'IsOverview' => $actionType == self::PROJECT_OVERVIEW
1025
				));
1026
				if($limit > 0 && $navigation->count() > $limit) {
1027
					$navigation->pop();
1028
				}
1029
			}
1030
		}
1031
1032
		return $navigation;
1033
	}
1034
1035
	/**
1036
	 * Construct the deployment form
1037
	 *
1038
	 * @return Form
1039
	 */
1040
	public function getDeployForm($request = null) {
1041
1042
		// Performs canView permission check by limiting visible projects
1043
		$project = $this->getCurrentProject();
1044
		if(!$project) {
1045
			return $this->project404Response();
1046
		}
1047
1048
		// Performs canView permission check by limiting visible projects
1049
		$environment = $this->getCurrentEnvironment($project);
1050
		if(!$environment) {
1051
			return $this->environment404Response();
1052
		}
1053
1054
		if(!$environment->canDeploy()) {
1055
			return new SS_HTTPResponse("Not allowed to deploy", 401);
1056
		}
1057
1058
		// Generate the form
1059
		$form = new DeployForm($this, 'DeployForm', $environment, $project);
1060
1061
		// If this is an ajax request we don't want to submit the form - we just want to retrieve the markup.
1062
		if(
1063
			$request &&
1064
			!$request->requestVar('action_showDeploySummary') &&
1065
			$this->getRequest()->isAjax() &&
1066
			$this->getRequest()->isGET()
1067
		) {
1068
			// We can just use the URL we're accessing
1069
			$form->setFormAction($this->getRequest()->getURL());
1070
1071
			$body = json_encode(array('Content' => $form->forAjaxTemplate()->forTemplate()));
1072
			$this->getResponse()->addHeader('Content-Type', 'application/json');
1073
			$this->getResponse()->setBody($body);
1074
			return $body;
1075
		}
1076
1077
		$form->setFormAction($this->getRequest()->getURL() . '/DeployForm');
1078
		return $form;
1079
	}
1080
1081
	/**
1082
	 * @param SS_HTTPRequest $request
1083
	 *
1084
	 * @return SS_HTTPResponse|string
1085
	 */
1086
	public function gitRevisions(SS_HTTPRequest $request) {
1087
1088
		// Performs canView permission check by limiting visible projects
1089
		$project = $this->getCurrentProject();
1090
		if(!$project) {
1091
			return $this->project404Response();
1092
		}
1093
1094
		// Performs canView permission check by limiting visible projects
1095
		$env = $this->getCurrentEnvironment($project);
1096
		if(!$env) {
1097
			return $this->environment404Response();
1098
		}
1099
1100
		// For now only permit advanced options on one environment type, because we hacked the "full-deploy"
1101
		// checkbox in. Other environments such as the fast or capistrano one wouldn't know what to do with it.
1102
		if(get_class($env) === 'RainforestEnvironment') {
1103
			$advanced = Permission::check('DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS') ? 'true' : 'false';
1104
		} else {
1105
			$advanced = 'false';
1106
		}
1107
1108
		$tabs = array();
1109
		$id = 0;
1110
		$data = array(
1111
			'id' => ++$id,
1112
			'name' => 'Deploy the latest version of a branch',
1113
			'field_type' => 'dropdown',
1114
			'field_label' => 'Choose a branch',
1115
			'field_id' => 'branch',
1116
			'field_data' => array(),
1117
			'advanced_opts' => $advanced
1118
		);
1119
		foreach($project->DNBranchList() as $branch) {
1120
			$sha = $branch->SHA();
1121
			$name = $branch->Name();
1122
			$branchValue = sprintf("%s (%s, %s old)",
1123
				$name,
1124
				substr($sha, 0, 8),
1125
				$branch->LastUpdated()->TimeDiff()
1126
			);
1127
			$data['field_data'][] = array(
1128
				'id' => $sha,
1129
				'text' => $branchValue,
1130
				'branch_name' => $name // the raw branch name, not including the time etc
1131
			);
1132
		}
1133
		$tabs[] = $data;
1134
1135
		$data = array(
1136
			'id' => ++$id,
1137
			'name' => 'Deploy a tagged release',
1138
			'field_type' => 'dropdown',
1139
			'field_label' => 'Choose a tag',
1140
			'field_id' => 'tag',
1141
			'field_data' => array(),
1142
			'advanced_opts' => $advanced
1143
		);
1144
1145
		foreach($project->DNTagList()->setLimit(null) as $tag) {
1146
			$name = $tag->Name();
1147
			$data['field_data'][] = array(
1148
				'id' => $tag->SHA(),
1149
				'text' => sprintf("%s", $name)
1150
			);
1151
		}
1152
1153
		// show newest tags first.
1154
		$data['field_data'] = array_reverse($data['field_data']);
1155
1156
		$tabs[] = $data;
1157
1158
		// Past deployments
1159
		$data = array(
1160
			'id' => ++$id,
1161
			'name' => 'Redeploy a release that was previously deployed (to any environment)',
1162
			'field_type' => 'dropdown',
1163
			'field_label' => 'Choose a previously deployed release',
1164
			'field_id' => 'release',
1165
			'field_data' => array(),
1166
			'advanced_opts' => $advanced
1167
		);
1168
		// We are aiming at the format:
1169
		// [{text: 'optgroup text', children: [{id: '<sha>', text: '<inner text>'}]}]
0 ignored issues
show
Unused Code Comprehensibility introduced by
55% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

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

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

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1494
	}
1495
1496
	/**
1497
	 * @param SS_HTTPRequest|null $request
1498
	 *
1499
	 * @return Form
1500
	 */
1501
	public function getDataTransferForm(SS_HTTPRequest $request = null) {
1502
		// Performs canView permission check by limiting visible projects
1503
		$envs = $this->getCurrentProject()->DNEnvironmentList()->filterByCallback(function($item) {
1504
			return $item->canBackup();
1505
		});
1506
1507
		if(!$envs) {
1508
			return $this->environment404Response();
1509
		}
1510
1511
		$form = Form::create(
1512
			$this,
1513
			'DataTransferForm',
1514
			FieldList::create(
1515
				HiddenField::create('Direction', null, 'get'),
1516
				DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1517
					->setEmptyString('Select an environment'),
1518
				DropdownField::create('Mode', 'Transfer', DNDataArchive::get_mode_map())
1519
			),
1520
			FieldList::create(
1521
				FormAction::create('doDataTransfer', 'Create')
1522
					->addExtraClass('btn')
1523
			)
1524
		);
1525
		$form->setFormAction($this->getRequest()->getURL() . '/DataTransferForm');
1526
1527
		return $form;
1528
	}
1529
1530
	/**
1531
	 * @param array $data
1532
	 * @param Form $form
1533
	 *
1534
	 * @return SS_HTTPResponse
1535
	 * @throws SS_HTTPResponse_Exception
1536
	 */
1537
	public function doDataTransfer($data, Form $form) {
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

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

Loading history...
1538
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1539
1540
		// Performs canView permission check by limiting visible projects
1541
		$project = $this->getCurrentProject();
1542
		if(!$project) {
1543
			return $this->project404Response();
1544
		}
1545
1546
		$dataArchive = null;
1547
1548
		// Validate direction.
1549
		if($data['Direction'] == 'get') {
1550
			$validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1551
				->filterByCallback(function($item) {
1552
					return $item->canBackup();
1553
				});
1554
		} else if($data['Direction'] == 'push') {
1555
			$validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1556
				->filterByCallback(function($item) {
1557
					return $item->canRestore();
1558
				});
1559
		} else {
1560
			throw new LogicException('Invalid direction');
1561
		}
1562
1563
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
1564
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
1565
		if(!$environment) {
1566
			throw new LogicException('Invalid environment');
1567
		}
1568
1569
		$this->validateSnapshotMode($data['Mode']);
1570
1571
1572
		// Only 'push' direction is allowed an association with an existing archive.
1573
		if(
1574
			$data['Direction'] == 'push'
1575
			&& isset($data['DataArchiveID'])
1576
			&& is_numeric($data['DataArchiveID'])
1577
		) {
1578
			$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1579
			if(!$dataArchive) {
1580
				throw new LogicException('Invalid data archive');
1581
			}
1582
1583
			if(!$dataArchive->canDownload()) {
1584
				throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1585
			}
1586
		}
1587
1588
		$transfer = DNDataTransfer::create();
1589
		$transfer->EnvironmentID = $environment->ID;
1590
		$transfer->Direction = $data['Direction'];
1591
		$transfer->Mode = $data['Mode'];
1592
		$transfer->DataArchiveID = $dataArchive ? $dataArchive->ID : null;
1593
		if($data['Direction'] == 'push') {
1594
			$transfer->setBackupBeforePush(!empty($data['BackupBeforePush']));
1595
		}
1596
		$transfer->write();
1597
		$transfer->start();
1598
1599
		return $this->redirect($transfer->Link());
1600
	}
1601
1602
	/**
1603
	 * View into the log for a {@link DNDataTransfer}.
1604
	 *
1605
	 * @param SS_HTTPRequest $request
1606
	 *
1607
	 * @return SS_HTTPResponse|string
1608
	 * @throws SS_HTTPResponse_Exception
1609
	 */
1610
	public function transfer(SS_HTTPRequest $request) {
1611
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1612
1613
		$params = $request->params();
1614
		$transfer = DNDataTransfer::get()->byId($params['Identifier']);
1615
1616
		if(!$transfer || !$transfer->ID) {
1617
			throw new SS_HTTPResponse_Exception('Transfer not found', 404);
1618
		}
1619
		if(!$transfer->canView()) {
1620
			return Security::permissionFailure();
1621
		}
1622
1623
		$environment = $transfer->Environment();
1624
		$project = $environment->Project();
1625
1626
		if($project->Name != $params['Project']) {
1627
			throw new LogicException("Project in URL doesn't match this deploy");
1628
		}
1629
1630
		return $this->render(array(
1631
			'CurrentTransfer' => $transfer,
1632
			'SnapshotsSection' => 1,
1633
		));
1634
	}
1635
1636
	/**
1637
	 * Action - Get the latest deploy log
1638
	 *
1639
	 * @param SS_HTTPRequest $request
1640
	 *
1641
	 * @return string
1642
	 * @throws SS_HTTPResponse_Exception
1643
	 */
1644
	public function transferlog(SS_HTTPRequest $request) {
1645
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1646
1647
		$params = $request->params();
1648
		$transfer = DNDataTransfer::get()->byId($params['Identifier']);
1649
1650
		if(!$transfer || !$transfer->ID) {
1651
			throw new SS_HTTPResponse_Exception('Transfer not found', 404);
1652
		}
1653
		if(!$transfer->canView()) {
1654
			return Security::permissionFailure();
1655
		}
1656
1657
		$environment = $transfer->Environment();
1658
		$project = $environment->Project();
1659
1660
		if($project->Name != $params['Project']) {
1661
			throw new LogicException("Project in URL doesn't match this deploy");
1662
		}
1663
1664
		$log = $transfer->log();
1665
		if($log->exists()) {
1666
			$content = $log->content();
1667
		} else {
1668
			$content = 'Waiting for action to start';
1669
		}
1670
1671
		return $this->sendResponse($transfer->ResqueStatus(), $content);
1672
	}
1673
1674
	/**
1675
	 * Note: Submits to the same action as {@link getDataTransferForm()},
1676
	 * but with a Direction=push and an archive reference.
1677
	 *
1678
	 * @param SS_HTTPRequest $request
1679
	 * @param DNDataArchive|null $dataArchive Only set when method is called manually in {@link restore()},
1680
	 *                            otherwise the state is inferred from the request data.
1681
	 * @return Form
1682
	 */
1683
	public function getDataTransferRestoreForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) {
1684
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1685
1686
		// Performs canView permission check by limiting visible projects
1687
		$project = $this->getCurrentProject();
1688
		$envs = $project->DNEnvironmentList()->filterByCallback(function($item) {
1689
			return $item->canRestore();
1690
		});
1691
1692
		if(!$envs) {
1693
			return $this->environment404Response();
1694
		}
1695
1696
		$modesMap = array();
1697
		if(in_array($dataArchive->Mode, array('all'))) {
1698
			$modesMap['all'] = 'Database and Assets';
1699
		};
1700
		if(in_array($dataArchive->Mode, array('all', 'db'))) {
1701
			$modesMap['db'] = 'Database only';
1702
		};
1703
		if(in_array($dataArchive->Mode, array('all', 'assets'))) {
1704
			$modesMap['assets'] = 'Assets only';
1705
		};
1706
1707
		$alertMessage = '<div class="alert alert-warning"><strong>Warning:</strong> '
1708
			. 'This restore will overwrite the data on the chosen environment below</div>';
1709
1710
		$form = Form::create(
1711
			$this,
1712
			'DataTransferRestoreForm',
1713
			FieldList::create(
1714
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1715
				HiddenField::create('Direction', null, 'push'),
1716
				LiteralField::create('Warning', $alertMessage),
1717
				DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1718
					->setEmptyString('Select an environment'),
1719
				DropdownField::create('Mode', 'Transfer', $modesMap),
1720
				CheckboxField::create('BackupBeforePush', 'Backup existing data', '1')
1721
			),
1722
			FieldList::create(
1723
				FormAction::create('doDataTransfer', 'Restore Data')
1724
					->addExtraClass('btn')
1725
			)
1726
		);
1727
		$form->setFormAction($project->Link() . '/DataTransferRestoreForm');
1728
1729
		return $form;
1730
	}
1731
1732
	/**
1733
	 * View a form to restore a specific {@link DataArchive}.
1734
	 * Permission checks are handled in {@link DataArchives()}.
1735
	 * Submissions are handled through {@link doDataTransfer()}, same as backup operations.
1736
	 *
1737
	 * @param SS_HTTPRequest $request
1738
	 *
1739
	 * @return HTMLText
1740
	 * @throws SS_HTTPResponse_Exception
1741
	 */
1742 View Code Duplication
	public function restoresnapshot(SS_HTTPRequest $request) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1743
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1744
1745
		/** @var DNDataArchive $dataArchive */
1746
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1747
1748
		if(!$dataArchive) {
1749
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1750
		}
1751
1752
		// We check for canDownload because that implies access to the data.
1753
		// canRestore is later checked on the actual restore action per environment.
1754
		if(!$dataArchive->canDownload()) {
1755
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1756
		}
1757
1758
		$form = $this->getDataTransferRestoreForm($this->request, $dataArchive);
1759
1760
		// View currently only available via ajax
1761
		return $form->forTemplate();
1762
	}
1763
1764
	/**
1765
	 * View a form to delete a specific {@link DataArchive}.
1766
	 * Permission checks are handled in {@link DataArchives()}.
1767
	 * Submissions are handled through {@link doDelete()}.
1768
	 *
1769
	 * @param SS_HTTPRequest $request
1770
	 *
1771
	 * @return HTMLText
1772
	 * @throws SS_HTTPResponse_Exception
1773
	 */
1774 View Code Duplication
	public function deletesnapshot(SS_HTTPRequest $request) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1775
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1776
1777
		/** @var DNDataArchive $dataArchive */
1778
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1779
1780
		if(!$dataArchive) {
1781
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1782
		}
1783
1784
		if(!$dataArchive->canDelete()) {
1785
			throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403);
1786
		}
1787
1788
		$form = $this->getDeleteForm($this->request, $dataArchive);
1789
1790
		// View currently only available via ajax
1791
		return $form->forTemplate();
1792
	}
1793
1794
	/**
1795
	 * @param SS_HTTPRequest $request
1796
	 * @param DNDataArchive|null $dataArchive Only set when method is called manually, otherwise the state is inferred
1797
	 *        from the request data.
1798
	 * @return Form
1799
	 */
1800
	public function getDeleteForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) {
1801
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1802
1803
		// Performs canView permission check by limiting visible projects
1804
		$project = $this->getCurrentProject();
1805
		if(!$project) {
1806
			return $this->project404Response();
1807
		}
1808
1809
		$snapshotDeleteWarning = '<div class="alert alert-warning">'
1810
			. 'Are you sure you want to permanently delete this snapshot from this archive area?'
1811
			. '</div>';
1812
1813
		$form = Form::create(
1814
			$this,
1815
			'DeleteForm',
1816
			FieldList::create(
1817
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1818
				LiteralField::create('Warning', $snapshotDeleteWarning)
1819
			),
1820
			FieldList::create(
1821
				FormAction::create('doDelete', 'Delete')
1822
					->addExtraClass('btn')
1823
			)
1824
		);
1825
		$form->setFormAction($project->Link() . '/DeleteForm');
1826
1827
		return $form;
1828
	}
1829
1830
	/**
1831
	 * @param array $data
1832
	 * @param Form $form
1833
	 *
1834
	 * @return bool|SS_HTTPResponse
1835
	 * @throws SS_HTTPResponse_Exception
1836
	 */
1837
	public function doDelete($data, Form $form) {
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

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

Loading history...
1838
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1839
1840
		// Performs canView permission check by limiting visible projects
1841
		$project = $this->getCurrentProject();
1842
		if(!$project) {
1843
			return $this->project404Response();
1844
		}
1845
1846
		$dataArchive = null;
1847
1848
		if(
1849
			isset($data['DataArchiveID'])
1850
			&& is_numeric($data['DataArchiveID'])
1851
		) {
1852
			$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1853
		}
1854
1855
		if(!$dataArchive) {
1856
			throw new LogicException('Invalid data archive');
1857
		}
1858
1859
		if(!$dataArchive->canDelete()) {
1860
			throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403);
1861
		}
1862
1863
		$dataArchive->delete();
1864
1865
		return $this->redirectBack();
1866
	}
1867
1868
	/**
1869
	 * View a form to move a specific {@link DataArchive}.
1870
	 *
1871
	 * @param SS_HTTPRequest $request
1872
	 *
1873
	 * @return HTMLText
1874
	 * @throws SS_HTTPResponse_Exception
1875
	 */
1876 View Code Duplication
	public function movesnapshot(SS_HTTPRequest $request) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1877
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1878
1879
		/** @var DNDataArchive $dataArchive */
1880
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1881
1882
		if(!$dataArchive) {
1883
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1884
		}
1885
1886
		// We check for canDownload because that implies access to the data.
1887
		if(!$dataArchive->canDownload()) {
1888
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1889
		}
1890
1891
		$form = $this->getMoveForm($this->request, $dataArchive);
1892
1893
		// View currently only available via ajax
1894
		return $form->forTemplate();
0 ignored issues
show
Bug introduced by
The method forTemplate does only exist in Form, but not in SS_HTTPResponse.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1895
	}
1896
1897
	/**
1898
	 * Build snapshot move form.
1899
	 *
1900
	 * @param SS_HTTPRequest $request
1901
	 * @param DNDataArchive|null $dataArchive
1902
	 *
1903
	 * @return Form|SS_HTTPResponse
1904
	 */
1905
	public function getMoveForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) {
1906
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1907
1908
		$envs = $dataArchive->validTargetEnvironments();
1909
		if(!$envs) {
1910
			return $this->environment404Response();
1911
		}
1912
1913
		$warningMessage = '<div class="alert alert-warning"><strong>Warning:</strong> This will make the snapshot '
1914
			. 'available to people with access to the target environment.<br>By pressing "Change ownership" you '
1915
			. 'confirm that you have considered data confidentiality regulations.</div>';
1916
1917
		$form = Form::create(
1918
			$this,
1919
			'MoveForm',
1920
			FieldList::create(
1921
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1922
				LiteralField::create('Warning', $warningMessage),
1923
				DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1924
					->setEmptyString('Select an environment')
1925
			),
1926
			FieldList::create(
1927
				FormAction::create('doMove', 'Change ownership')
1928
					->addExtraClass('btn')
1929
			)
1930
		);
1931
		$form->setFormAction($this->getCurrentProject()->Link() . '/MoveForm');
1932
1933
		return $form;
1934
	}
1935
1936
	/**
1937
	 * @param array $data
1938
	 * @param Form $form
1939
	 *
1940
	 * @return bool|SS_HTTPResponse
1941
	 * @throws SS_HTTPResponse_Exception
1942
	 * @throws ValidationException
1943
	 * @throws null
1944
	 */
1945
	public function doMove($data, Form $form) {
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

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

Loading history...
1946
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1947
1948
		// Performs canView permission check by limiting visible projects
1949
		$project = $this->getCurrentProject();
1950
		if(!$project) {
1951
			return $this->project404Response();
1952
		}
1953
1954
		/** @var DNDataArchive $dataArchive */
1955
		$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1956
		if(!$dataArchive) {
1957
			throw new LogicException('Invalid data archive');
1958
		}
1959
1960
		// We check for canDownload because that implies access to the data.
1961
		if(!$dataArchive->canDownload()) {
1962
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1963
		}
1964
1965
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
1966
		$validEnvs = $dataArchive->validTargetEnvironments();
1967
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
1968
		if(!$environment) {
1969
			throw new LogicException('Invalid environment');
1970
		}
1971
1972
		$dataArchive->EnvironmentID = $environment->ID;
1973
		$dataArchive->write();
1974
1975
		return $this->redirectBack();
1976
	}
1977
1978
	/**
1979
	 * Returns an error message if redis is unavailable
1980
	 *
1981
	 * @return string
1982
	 */
1983
	public static function RedisUnavailable() {
1984
		try {
1985
			Resque::queues();
1986
		} catch(Exception $e) {
1987
			return $e->getMessage();
1988
		}
1989
		return '';
1990
	}
1991
1992
	/**
1993
	 * Returns the number of connected Redis workers
1994
	 *
1995
	 * @return int
1996
	 */
1997
	public static function RedisWorkersCount() {
1998
		return count(Resque_Worker::all());
1999
	}
2000
2001
	/**
2002
	 * @return array
2003
	 */
2004
	public function providePermissions() {
2005
		return array(
2006
			self::DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS => array(
2007
				'name' => "Access to advanced deploy options",
2008
				'category' => "Deploynaut",
2009
			),
2010
2011
			// Permissions that are intended to be added to the roles.
2012
			self::ALLOW_PROD_DEPLOYMENT => array(
2013
				'name' => "Ability to deploy to production environments",
2014
				'category' => "Deploynaut",
2015
			),
2016
			self::ALLOW_NON_PROD_DEPLOYMENT => array(
2017
				'name' => "Ability to deploy to non-production environments",
2018
				'category' => "Deploynaut",
2019
			),
2020
			self::ALLOW_PROD_SNAPSHOT => array(
2021
				'name' => "Ability to make production snapshots",
2022
				'category' => "Deploynaut",
2023
			),
2024
			self::ALLOW_NON_PROD_SNAPSHOT => array(
2025
				'name' => "Ability to make non-production snapshots",
2026
				'category' => "Deploynaut",
2027
			),
2028
			self::ALLOW_CREATE_ENVIRONMENT => array(
2029
				'name' => "Ability to create environments",
2030
				'category' => "Deploynaut",
2031
			),
2032
		);
2033
	}
2034
2035
	/**
2036
	 * @return DNProject|null
2037
	 */
2038
	public function getCurrentProject() {
2039
		$projectName = trim($this->getRequest()->param('Project'));
2040
		if(!$projectName) {
2041
			return null;
2042
		}
2043
		if(empty(self::$_project_cache[$projectName])) {
2044
			self::$_project_cache[$projectName] = $this->DNProjectList()->filter('Name', $projectName)->First();
2045
		}
2046
		return self::$_project_cache[$projectName];
2047
	}
2048
2049
	/**
2050
	 * @param DNProject|null $project
2051
	 * @return DNEnvironment|null
2052
	 */
2053
	public function getCurrentEnvironment(DNProject $project = null) {
2054
		if($this->getRequest()->param('Environment') === null) {
2055
			return null;
2056
		}
2057
		if($project === null) {
2058
			$project = $this->getCurrentProject();
2059
		}
2060
		// project can still be null
2061
		if($project === null) {
2062
			return null;
2063
		}
2064
		return $project->DNEnvironmentList()->filter('Name', $this->getRequest()->param('Environment'))->First();
2065
	}
2066
2067
	/**
2068
	 * This will return a const that indicates the class of action currently being performed
2069
	 *
2070
	 * Until DNRoot is de-godded, it does a bunch of different actions all in the same class.
2071
	 * So we just have each action handler calll setCurrentActionType to define what sort of
2072
	 * action it is.
2073
	 *
2074
	 * @return string - one of the consts from self::$action_types
2075
	 */
2076
	public function getCurrentActionType() {
2077
		return $this->actionType;
2078
	}
2079
2080
	/**
2081
	 * Sets the current action type
2082
	 *
2083
	 * @param string $actionType string - one of the consts from self::$action_types
2084
	 */
2085
	public function setCurrentActionType($actionType) {
2086
		$this->actionType = $actionType;
2087
	}
2088
2089
	/**
2090
	 * Helper method to allow templates to know whether they should show the 'Archive List' include or not.
2091
	 * The actual permissions are set on a per-environment level, so we need to find out if this $member can upload to
2092
	 * or download from *any* {@link DNEnvironment} that (s)he has access to.
2093
	 *
2094
	 * TODO To be replaced with a method that just returns the list of archives this {@link Member} has access to.
2095
	 *
2096
	 * @param Member|null $member The {@link Member} to check (or null to check the currently logged in Member)
2097
	 * @return boolean|null true if $member has access to upload or download to at least one {@link DNEnvironment}.
2098
	 */
2099
	public function CanViewArchives(Member $member = null) {
2100
		if($member === null) {
2101
			$member = Member::currentUser();
2102
		}
2103
2104
		if(Permission::checkMember($member, 'ADMIN')) {
2105
			return true;
2106
		}
2107
2108
		$allProjects = $this->DNProjectList();
2109
		if(!$allProjects) {
2110
			return false;
2111
		}
2112
2113
		foreach($allProjects as $project) {
2114
			if($project->Environments()) {
2115
				foreach($project->Environments() as $environment) {
2116
					if(
2117
						$environment->canRestore($member) ||
2118
						$environment->canBackup($member) ||
2119
						$environment->canUploadArchive($member) ||
2120
						$environment->canDownloadArchive($member)
2121
					) {
2122
						// We can return early as we only need to know that we can access one environment
2123
						return true;
2124
					}
2125
				}
2126
			}
2127
		}
2128
	}
2129
2130
	/**
2131
	 * Returns a list of attempted environment creations.
2132
	 *
2133
	 * @return PaginatedList
2134
	 */
2135
	public function CreateEnvironmentList() {
2136
		$project = $this->getCurrentProject();
2137
		if($project) {
2138
			$dataList = $project->CreateEnvironments();
0 ignored issues
show
Bug introduced by
The method CreateEnvironments() does not exist on DNProject. Did you maybe mean canCreateEnvironments()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
2139
		} else {
2140
			$dataList = new ArrayList();
2141
		}
2142
2143
		$this->extend('updateCreateEnvironmentList', $dataList);
2144
		return new PaginatedList($dataList->sort('Created DESC'), $this->request);
0 ignored issues
show
Documentation introduced by
$this->request is of type object<SS_HTTPRequest>, but the function expects a array.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2145
	}
2146
2147
	/**
2148
	 * Returns a list of all archive files that can be accessed by the currently logged-in {@link Member}
2149
	 *
2150
	 * @return PaginatedList
2151
	 */
2152
	public function CompleteDataArchives() {
2153
		$project = $this->getCurrentProject();
2154
		$archives = new ArrayList();
2155
2156
		$archiveList = $project->Environments()->relation("DataArchives");
2157
		if($archiveList->count() > 0) {
2158
			foreach($archiveList as $archive) {
2159
				if(!$archive->isPending()) {
2160
					$archives->push($archive);
2161
				}
2162
			}
2163
		}
2164
		return new PaginatedList($archives->sort("Created", "DESC"), $this->request);
0 ignored issues
show
Documentation introduced by
$this->request is of type object<SS_HTTPRequest>, but the function expects a array.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2165
	}
2166
2167
	/**
2168
	 * @return PaginatedList The list of "pending" data archives which are waiting for a file
2169
	 * to be delivered offline by post, and manually uploaded into the system.
2170
	 */
2171
	public function PendingDataArchives() {
2172
		$project = $this->getCurrentProject();
2173
		$archives = new ArrayList();
2174
		foreach($project->DNEnvironmentList() as $env) {
2175
			foreach($env->DataArchives() as $archive) {
2176
				if($archive->isPending()) {
2177
					$archives->push($archive);
2178
				}
2179
			}
2180
		}
2181
		return new PaginatedList($archives->sort("Created", "DESC"), $this->request);
0 ignored issues
show
Documentation introduced by
$this->request is of type object<SS_HTTPRequest>, but the function expects a array.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2182
	}
2183
2184
	/**
2185
	 * @return PaginatedList
2186
	 */
2187
	public function DataTransferLogs() {
2188
		$environments = $this->getCurrentProject()->Environments()->column('ID');
2189
		$transfers = DNDataTransfer::get()
2190
			->filter('EnvironmentID', $environments)
2191
			->filterByCallback(
2192
				function($record) {
2193
					return
2194
						$record->Environment()->canRestore() || // Ensure member can perform an action on the transfers env
2195
						$record->Environment()->canBackup() ||
2196
						$record->Environment()->canUploadArchive() ||
2197
						$record->Environment()->canDownloadArchive();
2198
				});
2199
2200
		return new PaginatedList($transfers->sort("Created", "DESC"), $this->request);
0 ignored issues
show
Documentation introduced by
$this->request is of type object<SS_HTTPRequest>, but the function expects a array.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2201
	}
2202
2203
	/**
2204
	 * @return null|PaginatedList
2205
	 */
2206
	public function DeployHistory() {
2207
		if($env = $this->getCurrentEnvironment()) {
2208
			$history = $env->DeployHistory();
2209
			if($history->count() > 0) {
2210
				$pagination = new PaginatedList($history, $this->getRequest());
0 ignored issues
show
Documentation introduced by
$this->getRequest() is of type object<SS_HTTPRequest>, but the function expects a array.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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