Completed
Pull Request — master (#602)
by Sean
05:04 queued 01:53
created

DNRoot::abortDeploy()   C

Complexity

Conditions 8
Paths 7

Size

Total Lines 34
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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

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

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

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

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

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