Completed
Push — master ( c8af5e...053de4 )
by Sean
03:21
created

DNRoot::checkCsrfToken()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 20
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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

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

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

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

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

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

Loading history...
1170
		$redeploy = array();
1171 View Code Duplication
		foreach($project->DNEnvironmentList() as $dnEnvironment) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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

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

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