Completed
Pull Request — master (#592)
by Mateusz
03:00
created

DNRoot::PendingDataArchives()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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