Completed
Pull Request — master (#576)
by Sean
03:21
created

DNRoot::pipeline()   C

Complexity

Conditions 7
Paths 5

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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