Completed
Pull Request — master (#575)
by Mateusz
03:07
created

DNRoot::abortDeploy()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 30
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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