Completed
Pull Request — master (#488)
by Helpful
1295:51 queued 1292:33
created

DNRoot::postsnapshotsuccess()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 29
Code Lines 17

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 29
rs 8.439
cc 5
eloc 17
nc 5
nop 1
1
<?php
2
use \Symfony\Component\Process\Process;
3
4
/**
5
 * God controller for the deploynaut interface
6
 *
7
 * @package deploynaut
8
 * @subpackage control
9
 */
10
class DNRoot extends Controller implements PermissionProvider, TemplateGlobalProvider {
11
12
	/**
13
	 * @const string - action type for actions that perform deployments
14
	 */
15
	const ACTION_DEPLOY = 'deploy';
16
17
	/**
18
	 * @const string - action type for actions that manipulate snapshots
19
	 */
20
	const ACTION_SNAPSHOT = 'snapshot';
21
22
	const ACTION_ENVIRONMENTS = 'createenv';
23
24
	/**
25
	 * @var string
26
	 */
27
	private $actionType = self::ACTION_DEPLOY;
28
29
	/**
30
	 * Bypass pipeline permission code
31
	 */
32
	const DEPLOYNAUT_BYPASS_PIPELINE = 'DEPLOYNAUT_BYPASS_PIPELINE';
33
34
	/**
35
	 * Allow dryrun of pipelines
36
	 */
37
	const DEPLOYNAUT_DRYRUN_PIPELINE = 'DEPLOYNAUT_DRYRUN_PIPELINE';
38
39
	/**
40
	 * Allow advanced options on deployments
41
	 */
42
	const DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS = 'DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS';
43
44
	const ALLOW_PROD_DEPLOYMENT = 'ALLOW_PROD_DEPLOYMENT';
45
	const ALLOW_NON_PROD_DEPLOYMENT = 'ALLOW_NON_PROD_DEPLOYMENT';
46
	const ALLOW_PROD_SNAPSHOT = 'ALLOW_PROD_SNAPSHOT';
47
	const ALLOW_NON_PROD_SNAPSHOT = 'ALLOW_NON_PROD_SNAPSHOT';
48
	const ALLOW_CREATE_ENVIRONMENT = 'ALLOW_CREATE_ENVIRONMENT';
49
50
	/**
51
	 * @var array
52
	 */
53
	private static $allowed_actions = array(
54
		'projects',
55
		'nav',
56
		'update',
57
		'project',
58
		'toggleprojectstar',
59
		'branch',
60
		'environment',
61
		'abortpipeline',
62
		'pipeline',
63
		'pipelinelog',
64
		'metrics',
65
		'createenvlog',
66
		'createenv',
67
		'getDeployForm',
68
		'doDeploy',
69
		'deploy',
70
		'deploylog',
71
		'getDataTransferForm',
72
		'transfer',
73
		'transferlog',
74
		'snapshots',
75
		'createsnapshot',
76
		'snapshotslog',
77
		'uploadsnapshot',
78
		'getCreateEnvironmentForm',
79
		'getUploadSnapshotForm',
80
		'getPostSnapshotForm',
81
		'getDataTransferRestoreForm',
82
		'getDeleteForm',
83
		'getMoveForm',
84
		'restoresnapshot',
85
		'deletesnapshot',
86
		'movesnapshot',
87
		'postsnapshotsuccess',
88
		'gitRevisions',
89
		'deploySummary',
90
		'startDeploy',
91
		'createproject',
92
		'CreateProjectForm',
93
		'createprojectprogress',
94
		'checkrepoaccess',
95
	);
96
97
	/**
98
	 * URL handlers pretending that we have a deep URL structure.
99
	 */
100
	private static $url_handlers = array(
101
		'project/$Project/environment/$Environment/DeployForm' => 'getDeployForm',
102
		'project/$Project/createsnapshot/DataTransferForm' => 'getDataTransferForm',
103
		'project/$Project/DataTransferForm' => 'getDataTransferForm',
104
		'project/$Project/DataTransferRestoreForm' => 'getDataTransferRestoreForm',
105
		'project/$Project/DeleteForm' => 'getDeleteForm',
106
		'project/$Project/MoveForm' => 'getMoveForm',
107
		'project/$Project/UploadSnapshotForm' => 'getUploadSnapshotForm',
108
		'project/$Project/PostSnapshotForm' => 'getPostSnapshotForm',
109
		'project/$Project/environment/$Environment/metrics' => 'metrics',
110
		'project/$Project/environment/$Environment/pipeline/$Identifier//$Action/$ID/$OtherID' => 'pipeline',
111
		'project/$Project/environment/$Environment/deploy_summary' => 'deploySummary',
112
		'project/$Project/environment/$Environment/git_revisions' => 'gitRevisions',
113
		'project/$Project/environment/$Environment/start-deploy' => 'startDeploy',
114
		'project/$Project/environment/$Environment/deploy/$Identifier/log' => 'deploylog',
115
		'project/$Project/environment/$Environment/deploy/$Identifier' => 'deploy',
116
		'project/$Project/transfer/$Identifier/log' => 'transferlog',
117
		'project/$Project/transfer/$Identifier' => 'transfer',
118
		'project/$Project/environment/$Environment' => 'environment',
119
		'project/$Project/createenv/$Identifier/log' => 'createenvlog',
120
		'project/$Project/createenv/$Identifier' => 'createenv',
121
		'project/$Project/CreateEnvironmentForm' => 'getCreateEnvironmentForm',
122
		'project/$Project/branch' => 'branch',
123
		'project/$Project/build/$Build' => 'build',
124
		'project/$Project/restoresnapshot/$DataArchiveID' => 'restoresnapshot',
125
		'project/$Project/deletesnapshot/$DataArchiveID' => 'deletesnapshot',
126
		'project/$Project/movesnapshot/$DataArchiveID' => 'movesnapshot',
127
		'project/$Project/update' => 'update',
128
		'project/$Project/snapshots' => 'snapshots',
129
		'project/$Project/createsnapshot' => 'createsnapshot',
130
		'project/$Project/uploadsnapshot' => 'uploadsnapshot',
131
		'project/$Project/snapshotslog' => 'snapshotslog',
132
		'project/$Project/postsnapshotsuccess/$DataArchiveID' => 'postsnapshotsuccess',
133
		'project/$Project/star' => 'toggleprojectstar',
134
		'project/$Project/createprojectprogress' => 'createprojectprogress',
135
		'project/$Project/checkrepoaccess' => 'checkrepoaccess',
136
		'project/$Project' => 'project',
137
		'nav/$Project' => 'nav',
138
		'projects' => 'projects',
139
	);
140
141
	/**
142
	 * @var array
143
	 */
144
	protected static $_project_cache = array();
145
146
	/**
147
	 * @var array
148
	 */
149
	private static $support_links = array();
150
151
	/**
152
	 * @var array
153
	 */
154
	private static $platform_specific_strings = array();
155
156
	/**
157
	 * @var array
158
	 */
159
	private static $action_types = array(
160
		self::ACTION_DEPLOY,
161
		self::ACTION_SNAPSHOT
162
	);
163
164
	/**
165
	 * @var DNData
166
	 */
167
	protected $data;
168
169
	/**
170
	 * Include requirements that deploynaut needs, such as javascript.
171
	 */
172
	public static function include_requirements() {
173
174
		// JS should always go to the bottom, otherwise there's the risk that Requirements
175
		// puts them halfway through the page to the nearest <script> tag. We don't want that.
176
		Requirements::set_force_js_to_bottom(true);
177
178
		// todo these should be bundled into the same JS as the others in "static" below.
179
		// We've deliberately not used combined_files as it can mess with some of the JS used
180
		// here and cause sporadic errors.
181
		Requirements::javascript('deploynaut/javascript/jquery.js');
182
		Requirements::javascript('deploynaut/javascript/bootstrap.js');
183
		Requirements::javascript('deploynaut/javascript/q.js');
184
		Requirements::javascript('deploynaut/javascript/tablefilter.js');
185
		Requirements::javascript('deploynaut/javascript/deploynaut.js');
186
		Requirements::javascript('deploynaut/javascript/react-with-addons.js');
187
		Requirements::javascript('deploynaut/javascript/bootstrap.file-input.js');
188
		Requirements::javascript('deploynaut/thirdparty/select2/dist/js/select2.min.js');
189
		Requirements::javascript('deploynaut/javascript/material.js');
190
191
		// Load the buildable dependencies only if not loaded centrally.
192
		if (!is_dir(BASE_PATH . DIRECTORY_SEPARATOR . 'static')) {
193
			if (\Director::isDev()) {
194
				\Requirements::javascript('deploynaut/static/bundle-debug.js');
195
			} else {
196
				\Requirements::javascript('deploynaut/static/bundle.js');
197
			}
198
		}
199
200
		Requirements::css('deploynaut/static/style.css');
201
	}
202
203
	/**
204
	 * Check for feature flags:
205
	 * - FLAG_SNAPSHOTS_ENABLED: set to true to enable globally
206
	 * - FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS: set to semicolon-separated list of email addresses of allowed users.
207
	 *
208
	 * @return boolean
209
	 */
210
	public static function FlagSnapshotsEnabled() {
211
		if(defined('FLAG_SNAPSHOTS_ENABLED') && FLAG_SNAPSHOTS_ENABLED) {
212
			return true;
213
		}
214
		if(defined('FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS') && FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS) {
215
			$allowedMembers = explode(';', FLAG_SNAPSHOTS_ENABLED_FOR_MEMBERS);
216
			$member = Member::currentUser();
217
			if($allowedMembers && $member && in_array($member->Email, $allowedMembers)) {
218
				return true;
219
			}
220
		}
221
		return false;
222
	}
223
224
	/**
225
	 * @return ArrayList
226
	 */
227
	public static function get_support_links() {
228
		$supportLinks = self::config()->support_links;
229
		if($supportLinks) {
230
			return new ArrayList($supportLinks);
231
		}
232
	}
233
234
	/**
235
	 * @return array
236
	 */
237
	public static function get_template_global_variables() {
238
		return array(
239
			'RedisUnavailable' => 'RedisUnavailable',
240
			'RedisWorkersCount' => 'RedisWorkersCount',
241
			'SidebarLinks' => 'SidebarLinks',
242
			"SupportLinks" => 'get_support_links'
243
		);
244
	}
245
246
	/**
247
	 */
248
	public function init() {
249
		parent::init();
250
251
		if(!Member::currentUser() && !Session::get('AutoLoginHash')) {
252
			return Security::permissionFailure();
253
		}
254
255
		// Block framework jquery
256
		Requirements::block(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
257
258
		self::include_requirements();
259
	}
260
261
	/**
262
	 * @return string
263
	 */
264
	public function Link() {
265
		return "naut/";
266
	}
267
268
	/**
269
	 * Actions
270
	 *
271
	 * @param SS_HTTPRequest $request
272
	 * @return \SS_HTTPResponse
273
	 */
274
	public function index(SS_HTTPRequest $request) {
275
		return $this->redirect($this->Link() . 'projects/');
276
	}
277
278
	/**
279
	 * Action
280
	 *
281
	 * @param SS_HTTPRequest $request
282
	 * @return string - HTML
283
	 */
284
	public function projects(SS_HTTPRequest $request) {
285
		// Performs canView permission check by limiting visible projects in DNProjectsList() call.
286
		return $this->customise(array(
287
			'Title' => 'Projects',
288
		))->render();
289
	}
290
291
	/**
292
	 * @param SS_HTTPRequest $request
293
	 * @return HTMLText
294
	 */
295
	public function nav(SS_HTTPRequest $request) {
296
		return $this->renderWith('Nav');
297
	}
298
299
	/**
300
	 * Return a link to the navigation template used for AJAX requests.
301
	 * @return string
302
	 */
303
	public function NavLink() {
304
		$currentProject = $this->getCurrentProject();
305
		$projectName = $currentProject ? $currentProject->Name : null;
306
		return Controller::join_links(Director::absoluteBaseURL(), 'naut', 'nav', $projectName);
307
	}
308
309
	/**
310
	 * Action
311
	 *
312
	 * @param SS_HTTPRequest $request
313
	 * @return SS_HTTPResponse - HTML
314
	 */
315
	public function snapshots(SS_HTTPRequest $request) {
316
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
317
		return $this->getCustomisedViewSection('SnapshotsSection', 'Data Snapshots');
318
	}
319
320
	/**
321
	 * Action
322
	 *
323
	 * @param SS_HTTPRequest $request
324
	 * @return string - HTML
325
	 */
326 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...
327
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
328
329
		// Performs canView permission check by limiting visible projects
330
		$project = $this->getCurrentProject();
331
		if(!$project) {
332
			return $this->project404Response();
333
		}
334
335
		if(!$project->canBackup()) {
336
			return new SS_HTTPResponse("Not allowed to create snapshots on any environments", 401);
337
		}
338
339
		return $this->customise(array(
340
			'Title' => 'Create Data Snapshot',
341
			'SnapshotsSection' => 1,
342
			'DataTransferForm' => $this->getDataTransferForm($request)
343
		))->render();
344
	}
345
346
	/**
347
	 * Action
348
	 *
349
	 * @param SS_HTTPRequest $request
350
	 * @return string - HTML
351
	 */
352 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...
353
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
354
355
		// Performs canView permission check by limiting visible projects
356
		$project = $this->getCurrentProject();
357
		if(!$project) {
358
			return $this->project404Response();
359
		}
360
361
		if(!$project->canUploadArchive()) {
362
			return new SS_HTTPResponse("Not allowed to upload", 401);
363
		}
364
365
		return $this->customise(array(
366
			'SnapshotsSection' => 1,
367
			'UploadSnapshotForm' => $this->getUploadSnapshotForm($request),
368
			'PostSnapshotForm' => $this->getPostSnapshotForm($request)
369
		))->render();
370
	}
371
372
	/**
373
	 * Return the upload limit for snapshot uploads
374
	 * @return string
375
	 */
376
	public function UploadLimit() {
377
		return File::format_size(min(
378
			File::ini2bytes(ini_get('upload_max_filesize')),
379
			File::ini2bytes(ini_get('post_max_size'))
380
		));
381
	}
382
383
	/**
384
	 * Construct the upload form.
385
	 *
386
	 * @param SS_HTTPRequest $request
387
	 * @return Form
388
	 */
389
	public function getUploadSnapshotForm(SS_HTTPRequest $request) {
390
		// Performs canView permission check by limiting visible projects
391
		$project = $this->getCurrentProject();
392
		if(!$project) {
393
			return $this->project404Response();
394
		}
395
396
		if(!$project->canUploadArchive()) {
397
			return new SS_HTTPResponse("Not allowed to upload", 401);
398
		}
399
400
		// Framing an environment as a "group of people with download access"
401
		// makes more sense to the user here, while still allowing us to enforce
402
		// environment specific restrictions on downloading the file later on.
403
		$envs = $project->DNEnvironmentList()->filterByCallback(function($item) {
404
			return $item->canUploadArchive();
405
		});
406
		$envsMap = array();
407
		foreach($envs as $env) {
408
			$envsMap[$env->ID] = $env->Name;
409
		}
410
411
		$maxSize = min(File::ini2bytes(ini_get('upload_max_filesize')), File::ini2bytes(ini_get('post_max_size')));
412
		$fileField = DataArchiveFileField::create('ArchiveFile', 'File');
413
		$fileField->getValidator()->setAllowedExtensions(array('sspak'));
414
		$fileField->getValidator()->setAllowedMaxFileSize(array('*' => $maxSize));
415
416
		$form = Form::create(
417
			$this,
418
			'UploadSnapshotForm',
419
			FieldList::create(
420
				$fileField,
421
				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
422
				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
423
					->setEmptyString('Select an environment')
424
			),
425
			FieldList::create(
426
				FormAction::create('doUploadSnapshot', 'Upload File')
427
					->addExtraClass('btn')
428
			),
429
			RequiredFields::create('ArchiveFile')
430
		);
431
432
		$form->disableSecurityToken();
433
		$form->addExtraClass('fields-wide');
434
		// Tweak the action so it plays well with our fake URL structure.
435
		$form->setFormAction($project->Link() . '/UploadSnapshotForm');
436
437
		return $form;
438
	}
439
440
	/**
441
	 * @param array $data
442
	 * @param Form $form
443
	 *
444
	 * @return bool|HTMLText|SS_HTTPResponse
445
	 */
446
	public function doUploadSnapshot($data, Form $form) {
447
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
448
449
		// Performs canView permission check by limiting visible projects
450
		$project = $this->getCurrentProject();
451
		if(!$project) {
452
			return $this->project404Response();
453
		}
454
455
		$validEnvs = $project->DNEnvironmentList()
456
			->filterByCallback(function($item) {
457
				return $item->canUploadArchive();
458
			});
459
460
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
461
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
462
		if(!$environment) {
463
			throw new LogicException('Invalid environment');
464
		}
465
466
		$this->validateSnapshotMode($data['Mode']);
467
468
		$dataArchive = DNDataArchive::create(array(
469
			'AuthorID' => Member::currentUserID(),
470
			'EnvironmentID' => $data['EnvironmentID'],
471
			'IsManualUpload' => true,
472
		));
473
		// needs an ID and transfer to determine upload path
474
		$dataArchive->write();
475
		$dataTransfer = DNDataTransfer::create(array(
476
			'AuthorID' => Member::currentUserID(),
477
			'Mode' => $data['Mode'],
478
			'Origin' => 'ManualUpload',
479
			'EnvironmentID' => $data['EnvironmentID']
480
		));
481
		$dataTransfer->write();
482
		$dataArchive->DataTransfers()->add($dataTransfer);
483
		$form->saveInto($dataArchive);
484
		$dataArchive->write();
485
		$workingDir = TEMP_FOLDER . DIRECTORY_SEPARATOR . 'deploynaut-transfer-' . $dataTransfer->ID;
486
487
		$cleanupFn = function() use($workingDir, $dataTransfer, $dataArchive) {
488
			$process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir)));
489
			$process->run();
490
			$dataTransfer->delete();
491
			$dataArchive->delete();
492
		};
493
494
		// extract the sspak contents so we can inspect them
495
		try {
496
			$dataArchive->extractArchive($workingDir);
497
		} catch(Exception $e) {
498
			$cleanupFn();
499
			$form->sessionMessage(
500
				'There was a problem trying to open your snapshot for processing. Please try uploading again',
501
				'bad'
502
			);
503
			return $this->redirectBack();
504
		}
505
506
		// validate that the sspak contents match the declared contents
507
		$result = $dataArchive->validateArchiveContents();
508
		if(!$result->valid()) {
509
			$cleanupFn();
510
			$form->sessionMessage($result->message(), 'bad');
511
			return $this->redirectBack();
512
		}
513
514
		// fix file permissions of extracted sspak files then re-build the sspak
515
		try {
516
			$dataArchive->fixArchivePermissions($workingDir);
517
			$dataArchive->setArchiveFromFiles($workingDir);
518
		} catch(Exception $e) {
519
			$cleanupFn();
520
			$form->sessionMessage(
521
				'There was a problem processing your snapshot. Please try uploading again',
522
				'bad'
523
			);
524
			return $this->redirectBack();
525
		}
526
527
		// cleanup any extracted sspak contents lying around
528
		$process = new Process(sprintf('rm -rf %s', escapeshellarg($workingDir)));
529
		$process->run();
530
531
		return $this->customise(array(
532
			'Project' => $project,
533
			'CurrentProject' => $project,
534
			'SnapshotsSection' => 1,
535
			'DataArchive' => $dataArchive,
536
			'DataTransferRestoreForm' => $this->getDataTransferRestoreForm($this->request, $dataArchive),
537
			'BackURL' => $project->Link('snapshots')
538
		))->renderWith(array('DNRoot_uploadsnapshot', 'DNRoot'));
539
	}
540
541
	/**
542
	 * @param SS_HTTPRequest $request
543
	 * @return Form
544
	 */
545
	public function getPostSnapshotForm(SS_HTTPRequest $request) {
546
		// Performs canView permission check by limiting visible projects
547
		$project = $this->getCurrentProject();
548
		if(!$project) {
549
			return $this->project404Response();
550
		}
551
552
		if(!$project->canUploadArchive()) {
553
			return new SS_HTTPResponse("Not allowed to upload", 401);
554
		}
555
556
		// Framing an environment as a "group of people with download access"
557
		// makes more sense to the user here, while still allowing us to enforce
558
		// environment specific restrictions on downloading the file later on.
559
		$envs = $project->DNEnvironmentList()->filterByCallback(function($item) {
560
			return $item->canUploadArchive();
561
		});
562
		$envsMap = array();
563
		foreach($envs as $env) {
564
			$envsMap[$env->ID] = $env->Name;
565
		}
566
567
		$form = Form::create(
568
			$this,
569
			'PostSnapshotForm',
570
			FieldList::create(
571
				DropdownField::create('Mode', 'What does this file contain?', DNDataArchive::get_mode_map()),
572
				DropdownField::create('EnvironmentID', 'Initial ownership of the file', $envsMap)
573
					->setEmptyString('Select an environment')
574
			),
575
			FieldList::create(
576
				FormAction::create('doPostSnapshot', 'Submit request')
577
					->addExtraClass('btn')
578
			),
579
			RequiredFields::create('File')
580
		);
581
582
		$form->disableSecurityToken();
583
		$form->addExtraClass('fields-wide');
584
		// Tweak the action so it plays well with our fake URL structure.
585
		$form->setFormAction($project->Link() . '/PostSnapshotForm');
586
587
		return $form;
588
	}
589
590
	/**
591
	 * @param array $data
592
	 * @param Form $form
593
	 *
594
	 * @return SS_HTTPResponse
595
	 */
596
	public function doPostSnapshot($data, $form) {
597
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
598
599
		$project = $this->getCurrentProject();
600
		if(!$project) {
601
			return $this->project404Response();
602
		}
603
604
		$validEnvs = $project->DNEnvironmentList()->filterByCallback(function($item) {
605
				return $item->canUploadArchive();
606
		});
607
608
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
609
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
610
		if(!$environment) {
611
			throw new LogicException('Invalid environment');
612
		}
613
614
		$dataArchive = DNDataArchive::create(array(
615
			'UploadToken' => DNDataArchive::generate_upload_token(),
616
		));
617
		$form->saveInto($dataArchive);
618
		$dataArchive->write();
619
620
		return $this->redirect(Controller::join_links(
621
			$project->Link(),
622
			'postsnapshotsuccess',
623
			$dataArchive->ID
624
		));
625
	}
626
627
	/**
628
	 * Action
629
	 *
630
	 * @param SS_HTTPRequest $request
631
	 * @return SS_HTTPResponse - HTML
632
	 */
633
	public function snapshotslog(SS_HTTPRequest $request) {
634
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
635
		return $this->getCustomisedViewSection('SnapshotsSection', 'Data Snapshots Log');
636
	}
637
638
	/**
639
	 * @param SS_HTTPRequest $request
640
	 * @return SS_HTTPResponse|string
641
	 * @throws SS_HTTPResponse_Exception
642
	 */
643
	public function postsnapshotsuccess(SS_HTTPRequest $request) {
644
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
645
646
		// Performs canView permission check by limiting visible projects
647
		$project = $this->getCurrentProject();
648
		if(!$project) {
649
			return $this->project404Response();
650
		}
651
652
		if(!$project->canUploadArchive()) {
653
			return new SS_HTTPResponse("Not allowed to upload", 401);
654
		}
655
656
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
657
		if(!$dataArchive) {
658
			return new SS_HTTPResponse("Archive not found.", 404);
659
		}
660
661
		if(!$dataArchive->canRestore()) {
662
			throw new SS_HTTPResponse_Exception('Not allowed to restore archive', 403);
663
		}
664
665
		return $this->render(array(
666
				'Title' => 'How to send us your Data Snapshot by post',
667
				'DataArchive' => $dataArchive,
668
				'Address' => Config::inst()->get('Deploynaut', 'snapshot_post_address'),
669
				'BackURL' => $project->Link(),
670
			));
671
	}
672
673
	/**
674
	 * @param SS_HTTPRequest $request
675
	 * @return \SS_HTTPResponse
676
	 */
677
	public function project(SS_HTTPRequest $request) {
678
		return $this->getCustomisedViewSection('ProjectOverview', '', array('IsAdmin' => Permission::check('ADMIN')));
679
	}
680
681
	/**
682
	 * This action will star / unstar a project for the current member
683
	 *
684
	 * @param SS_HTTPRequest $request
685
	 *
686
	 * @return SS_HTTPResponse
687
	 */
688
	public function toggleprojectstar(SS_HTTPRequest $request) {
689
		$project = $this->getCurrentProject();
690
		if(!$project) {
691
			return $this->project404Response();
692
		}
693
694
		$member = Member::currentUser();
695
		if($member === null) {
696
			return $this->project404Response();
697
		}
698
		$favProject = $member->StarredProjects()
699
			->filter('DNProjectID', $project->ID)
700
			->first();
701
702
		if($favProject) {
703
			$member->StarredProjects()->remove($favProject);
704
		} else {
705
			$member->StarredProjects()->add($project);
706
		}
707
		return $this->redirectBack();
708
	}
709
710
	/**
711
	 * @param SS_HTTPRequest $request
712
	 * @return \SS_HTTPResponse
713
	 */
714
	public function branch(SS_HTTPRequest $request) {
715
		$project = $this->getCurrentProject();
716
		if(!$project) {
717
			return $this->project404Response();
718
		}
719
720
		$branchName = $request->getVar('name');
721
		$branch = $project->DNBranchList()->byName($branchName);
722
		if(!$branch) {
723
			return new SS_HTTPResponse("Branch '" . Convert::raw2xml($branchName) . "' not found.", 404);
724
		}
725
726
		return $this->render(array(
727
			'CurrentBranch' => $branch,
728
		));
729
	}
730
731
	/**
732
	 * @param SS_HTTPRequest $request
733
	 * @return \SS_HTTPResponse
734
	 */
735
	public function environment(SS_HTTPRequest $request) {
736
		// Performs canView permission check by limiting visible projects
737
		$project = $this->getCurrentProject();
738
		if(!$project) {
739
			return $this->project404Response();
740
		}
741
742
		// Performs canView permission check by limiting visible projects
743
		$env = $this->getCurrentEnvironment($project);
744
		if(!$env) {
745
			return $this->environment404Response();
746
		}
747
748
		return $this->render(array(
749
			'DNEnvironmentList' => $this->getCurrentProject()->DNEnvironmentList(),
750
			'FlagSnapshotsEnabled' => $this->FlagSnapshotsEnabled(),
751
		));
752
	}
753
754
755
	/**
756
	 * Initiate a pipeline dry run
757
	 *
758
	 * @param array $data
759
	 * @param DeployForm $form
760
	 *
761
	 * @return SS_HTTPResponse
762
	 */
763
	public function doDryRun($data, DeployForm $form) {
764
		return $this->beginPipeline($data, $form, true);
765
	}
766
767
	/**
768
	 * Initiate a pipeline
769
	 *
770
	 * @param array $data
771
	 * @param DeployForm $form
772
	 * @return \SS_HTTPResponse
773
	 */
774
	public function startPipeline($data, $form) {
775
		return $this->beginPipeline($data, $form);
776
	}
777
778
	/**
779
	 * Start a pipeline
780
	 *
781
	 * @param array $data
782
	 * @param DeployForm $form
783
	 * @param bool $isDryRun
784
	 * @return \SS_HTTPResponse
785
	 */
786
	protected function beginPipeline($data, DeployForm $form, $isDryRun = false) {
787
		$buildName = $form->getSelectedBuild($data);
788
789
		// Performs canView permission check by limiting visible projects
790
		$project = $this->getCurrentProject();
791
		if(!$project) {
792
			return $this->project404Response();
793
		}
794
795
		// Performs canView permission check by limiting visible projects
796
		$environment = $this->getCurrentEnvironment($project);
797
		if(!$environment) {
798
			return $this->environment404Response();
799
		}
800
801
		if(!$environment->DryRunEnabled && $isDryRun) {
802
			return new SS_HTTPResponse("Dry-run for pipelines is not enabled for this environment", 404);
803
		}
804
805
		// Initiate the pipeline
806
		$sha = $project->DNBuildList()->byName($buildName);
807
		$pipeline = Pipeline::create();
808
		$pipeline->DryRun = $isDryRun;
809
		$pipeline->EnvironmentID = $environment->ID;
810
		$pipeline->AuthorID = Member::currentUserID();
811
		$pipeline->SHA = $sha->FullName();
812
		// Record build at time of execution
813
		if($currentBuild = $environment->CurrentBuild()) {
814
			$pipeline->PreviousDeploymentID = $currentBuild->ID;
815
		}
816
		$pipeline->start(); // start() will call write(), so no need to do it here as well.
817
		return $this->redirect($environment->Link());
818
	}
819
820
	/**
821
	 * @param SS_HTTPRequest $request
822
	 *
823
	 * @return SS_HTTPResponse
824
	 * @throws SS_HTTPResponse_Exception
825
	 */
826
	public function pipeline(SS_HTTPRequest $request) {
827
		$params = $request->params();
828
		$pipeline = Pipeline::get()->byID($params['Identifier']);
829
830
		if(!$pipeline || !$pipeline->ID || !$pipeline->Environment()) {
831
			throw new SS_HTTPResponse_Exception('Pipeline not found', 404);
832
		}
833
		if(!$pipeline->Environment()->canView()) {
834
			return Security::permissionFailure();
835
		}
836
837
		$environment = $pipeline->Environment();
838
		$project = $pipeline->Environment()->Project();
839
840
		if($environment->Name != $params['Environment']) {
841
			throw new LogicException("Environment in URL doesn't match this pipeline");
842
		}
843
		if($project->Name != $params['Project']) {
844
			throw new LogicException("Project in URL doesn't match this pipeline");
845
		}
846
847
		// Delegate to sub-requesthandler
848
		return PipelineController::create($this, $pipeline);
849
	}
850
851
	/**
852
	 * Shows the creation log.
853
	 *
854
	 * @param SS_HTTPRequest $request
855
	 * @return string
856
	 */
857
	public function createenv(SS_HTTPRequest $request) {
858
		$params = $request->params();
859
		if($params['Identifier']) {
860
			$record = DNCreateEnvironment::get()->byId($params['Identifier']);
861
862
			if(!$record || !$record->ID) {
863
				throw new SS_HTTPResponse_Exception('Create environment not found', 404);
864
			}
865
			if(!$record->canView()) {
866
				return Security::permissionFailure();
867
			}
868
869
			$project = $this->getCurrentProject();
870
			if(!$project) {
871
				return $this->project404Response();
872
			}
873
874
			if($project->Name != $params['Project']) {
875
				throw new LogicException("Project in URL doesn't match this creation");
876
			}
877
878
			return $this->render(array(
879
				'CreateEnvironment' => $record,
880
			));
881
		}
882
		return $this->render(array('CurrentTitle' => 'Create an environment'));
883
	}
884
885
886 View Code Duplication
	public function createenvlog(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...
887
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
888
889
		$params = $request->params();
890
		$env = DNCreateEnvironment::get()->byId($params['Identifier']);
891
892
		if(!$env || !$env->ID) {
893
			throw new SS_HTTPResponse_Exception('Log not found', 404);
894
		}
895
		if(!$env->canView()) {
896
			return Security::permissionFailure();
897
		}
898
899
		$project = $env->Project();
900
901
		if($project->Name != $params['Project']) {
902
			throw new LogicException("Project in URL doesn't match this deploy");
903
		}
904
905
		$log = $env->log();
906
		if($log->exists()) {
907
			$content = $log->content();
908
		} else {
909
			$content = 'Waiting for action to start';
910
		}
911
912
		return $this->sendResponse($env->ResqueStatus(), $content);
913
	}
914
915
	/**
916
	 * @param SS_HTTPRequest $request
917
	 * @return Form
918
	 */
919
	public function getCreateEnvironmentForm(SS_HTTPRequest $request) {
920
		$this->setCurrentActionType(self::ACTION_ENVIRONMENTS);
921
922
		$project = $this->getCurrentProject();
923
		if(!$project) {
924
			return $this->project404Response();
925
		}
926
927
		$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...
928
		if(!$envType || !class_exists($envType)) {
929
			return null;
930
		}
931
932
		$backend = Injector::inst()->get($envType);
933
		if(!($backend instanceof EnvironmentCreateBackend)) {
934
			// Only allow this for supported backends.
935
			return null;
936
		}
937
938
		$fields = $backend->getCreateEnvironmentFields($project);
939
		if(!$fields) return null;
940
941
		if(!$project->canCreateEnvironments()) {
942
			return new SS_HTTPResponse('Not allowed to create environments for this project', 401);
943
		}
944
945
		$form = Form::create(
946
			$this,
947
			'CreateEnvironmentForm',
948
			$fields,
949
			FieldList::create(
950
				FormAction::create('doCreateEnvironment', 'Create')
951
					->addExtraClass('btn')
952
			),
953
			$backend->getCreateEnvironmentValidator()
954
		);
955
956
		// Tweak the action so it plays well with our fake URL structure.
957
		$form->setFormAction($project->Link() . '/CreateEnvironmentForm');
958
959
		return $form;
960
	}
961
962
	/**
963
	 * @param array $data
964
	 * @param Form $form
965
	 *
966
	 * @return bool|HTMLText|SS_HTTPResponse
967
	 */
968
	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...
969
		$this->setCurrentActionType(self::ACTION_ENVIRONMENTS);
970
971
		$project = $this->getCurrentProject();
972
		if(!$project) {
973
			return $this->project404Response();
974
		}
975
976
		if(!$project->canCreateEnvironments()) {
977
			return new SS_HTTPResponse('Not allowed to create environments for this project', 401);
978
		}
979
980
		// Set the environment type so we know what we're creating.
981
		$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...
982
983
		$job = DNCreateEnvironment::create();
984
985
		$job->Data = serialize($data);
986
		$job->ProjectID = $project->ID;
987
		$job->write();
988
		$job->start();
989
990
		return $this->redirect($project->Link('createenv') . '/' . $job->ID);
991
	}
992
993
	/**
994
	 *
995
	 * @param SS_HTTPRequest $request
996
	 * @return \SS_HTTPResponse
997
	 */
998
	public function metrics(SS_HTTPRequest $request) {
999
		// Performs canView permission check by limiting visible projects
1000
		$project = $this->getCurrentProject();
1001
		if(!$project) {
1002
			return $this->project404Response();
1003
		}
1004
1005
		// Performs canView permission check by limiting visible projects
1006
		$env = $this->getCurrentEnvironment($project);
1007
		if(!$env) {
1008
			return $this->environment404Response();
1009
		}
1010
1011
		return $this->render();
1012
	}
1013
1014
	/**
1015
	 * Get the DNData object.
1016
	 *
1017
	 * @return DNData
1018
	 */
1019
	public function DNData() {
1020
		return DNData::inst();
1021
	}
1022
1023
	/**
1024
	 * Provide a list of all projects.
1025
	 *
1026
	 * @return SS_List
1027
	 */
1028
	public function DNProjectList() {
1029
		$memberId = Member::currentUserID();
1030
		if(!$memberId) {
1031
			return new ArrayList();
1032
		}
1033
1034
		if(Permission::check('ADMIN')) {
1035
			return DNProject::get();
1036
		}
1037
1038
		return Member::get()->filter('ID', $memberId)
1039
			->relation('Groups')
1040
			->relation('Projects');
1041
	}
1042
1043
	/**
1044
	 * @return ArrayList
1045
	 */
1046
	public function getPlatformSpecificStrings() {
1047
		$strings = $this->config()->platform_specific_strings;
1048
		if ($strings) {
1049
			return new ArrayList($strings);
1050
		}
1051
	}
1052
1053
	/**
1054
	 * Provide a list of all starred projects for the currently logged in member
1055
	 *
1056
	 * @return SS_List
1057
	 */
1058
	public function getStarredProjects() {
1059
		$member = Member::currentUser();
1060
		if($member === null) {
1061
			return new ArrayList();
1062
		}
1063
1064
		$favProjects = $member->StarredProjects();
1065
1066
		$list = new ArrayList();
1067
		foreach($favProjects as $project) {
1068
			if($project->canView($member)) {
1069
				$list->add($project);
1070
			}
1071
		}
1072
		return $list;
1073
	}
1074
1075
	/**
1076
	 * Returns top level navigation of projects.
1077
	 *
1078
	 * @param int $limit
1079
	 *
1080
	 * @return ArrayList
1081
	 */
1082
	public function Navigation($limit = 5) {
1083
		$navigation = new ArrayList();
1084
1085
		$currentProject = $this->getCurrentProject();
1086
1087
		$projects = $this->getStarredProjects();
1088
		if($projects->count() < 1) {
1089
			$projects = $this->DNProjectList();
1090
		} else {
1091
			$limit = -1;
1092
		}
1093
1094
		if($projects->count() > 0) {
1095
			$activeProject = false;
1096
1097
			if($limit > 0) {
1098
				$limitedProjects = $projects->limit($limit);
1099
			} else {
1100
				$limitedProjects = $projects;
1101
			}
1102
1103
			foreach($limitedProjects as $project) {
1104
				$isActive = $currentProject && $currentProject->ID == $project->ID;
1105
				if($isActive) {
1106
					$activeProject = true;
1107
				}
1108
1109
				$navigation->push(array(
1110
					'Project' => $project,
1111
					'IsActive' => $currentProject && $currentProject->ID == $project->ID,
1112
				));
1113
			}
1114
1115
			// Ensure the current project is in the list
1116
			if(!$activeProject && $currentProject) {
1117
				$navigation->unshift(array(
1118
					'Project' => $currentProject,
1119
					'IsActive' => true,
1120
				));
1121
				if($limit > 0 && $navigation->count() > $limit) {
1122
					$navigation->pop();
1123
				}
1124
			}
1125
		}
1126
1127
		return $navigation;
1128
	}
1129
1130
	/**
1131
	 * Construct the deployment form
1132
	 *
1133
	 * @return Form
1134
	 */
1135
	public function getDeployForm($request = null) {
1136
1137
		// Performs canView permission check by limiting visible projects
1138
		$project = $this->getCurrentProject();
1139
		if(!$project) {
1140
			return $this->project404Response();
1141
		}
1142
1143
		// Performs canView permission check by limiting visible projects
1144
		$environment = $this->getCurrentEnvironment($project);
1145
		if(!$environment) {
1146
			return $this->environment404Response();
1147
		}
1148
1149
		if(!$environment->canDeploy()) {
1150
			return new SS_HTTPResponse("Not allowed to deploy", 401);
1151
		}
1152
1153
		// Generate the form
1154
		$form = new DeployForm($this, 'DeployForm', $environment, $project);
1155
1156
		// If this is an ajax request we don't want to submit the form - we just want to retrieve the markup.
1157
		if(
1158
			$request &&
1159
			!$request->requestVar('action_showDeploySummary') &&
1160
			$this->getRequest()->isAjax() &&
1161
			$this->getRequest()->isGET()
1162
		) {
1163
			// We can just use the URL we're accessing
1164
			$form->setFormAction($this->getRequest()->getURL());
1165
1166
			$body = json_encode(array('Content' => $form->forAjaxTemplate()->forTemplate()));
1167
			$this->getResponse()->addHeader('Content-Type', 'application/json');
1168
			$this->getResponse()->setBody($body);
1169
			return $body;
1170
		}
1171
1172
		$form->setFormAction($this->getRequest()->getURL() . '/DeployForm');
1173
		return $form;
1174
	}
1175
1176
	/**
1177
	 * @param SS_HTTPRequest $request
1178
	 *
1179
	 * @return SS_HTTPResponse|string
1180
	 */
1181
	public function gitRevisions(SS_HTTPRequest $request) {
1182
1183
		// Performs canView permission check by limiting visible projects
1184
		$project = $this->getCurrentProject();
1185
		if(!$project) {
1186
			return $this->project404Response();
1187
		}
1188
1189
		// Performs canView permission check by limiting visible projects
1190
		$env = $this->getCurrentEnvironment($project);
1191
		if(!$env) {
1192
			return $this->environment404Response();
1193
		}
1194
1195
		// For now only permit advanced options on one environment type, because we hacked the "full-deploy"
1196
		// checkbox in. Other environments such as the fast or capistrano one wouldn't know what to do with it.
1197
		if(get_class($env) === 'RainforestEnvironment') {
1198
			$advanced = Permission::check('DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS') ? 'true' : 'false';
1199
		} else {
1200
			$advanced = 'false';
1201
		}
1202
1203
		$tabs = array();
1204
		$id = 0;
1205
		$data = array(
1206
			'id' => ++$id,
1207
			'name' => 'Deploy the latest version of a branch',
1208
			'field_type' => 'dropdown',
1209
			'field_label' => 'Choose a branch',
1210
			'field_id' => 'branch',
1211
			'field_data' => array(),
1212
			'advanced_opts' => $advanced
1213
		);
1214
		foreach($project->DNBranchList() as $branch) {
1215
			$sha = $branch->SHA();
1216
			$name = $branch->Name();
1217
			$branchValue = sprintf("%s (%s, %s old)",
1218
				$name,
1219
				substr($sha, 0, 8),
1220
				$branch->LastUpdated()->TimeDiff()
1221
			);
1222
			$data['field_data'][] = array(
1223
				'id' => $sha,
1224
				'text' => $branchValue
1225
			);
1226
		}
1227
		$tabs[] = $data;
1228
1229
		$data = array(
1230
			'id' => ++$id,
1231
			'name' => 'Deploy a tagged release',
1232
			'field_type' => 'dropdown',
1233
			'field_label' => 'Choose a tag',
1234
			'field_id' => 'tag',
1235
			'field_data' => array(),
1236
			'advanced_opts' => $advanced
1237
		);
1238
1239
		foreach($project->DNTagList()->setLimit(null) as $tag) {
1240
			$name = $tag->Name();
1241
			$data['field_data'][] = array(
1242
				'id' => $tag->SHA(),
1243
				'text' => sprintf("%s", $name)
1244
			);
1245
		}
1246
1247
		// show newest tags first.
1248
		$data['field_data'] = array_reverse($data['field_data']);
1249
1250
		$tabs[] = $data;
1251
1252
		// Past deployments
1253
		$data = array(
1254
			'id' => ++$id,
1255
			'name' => 'Redeploy a release that was previously deployed (to any environment)',
1256
			'field_type' => 'dropdown',
1257
			'field_label' => 'Choose a previously deployed release',
1258
			'field_id' => 'release',
1259
			'field_data' => array(),
1260
			'advanced_opts' => $advanced
1261
		);
1262
		// We are aiming at the format:
1263
		// [{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...
1264
		$redeploy = array();
1265
		foreach($project->DNEnvironmentList() as $dnEnvironment) {
1266
			$envName = $dnEnvironment->Name;
1267
			$perEnvDeploys = array();
1268
1269
			foreach($dnEnvironment->DeployHistory() as $deploy) {
1270
				$sha = $deploy->SHA;
1271
1272
				// Check if exists to make sure the newest deployment date is used.
1273
				if(!isset($perEnvDeploys[$sha])) {
1274
					$pastValue = sprintf("%s (deployed %s)",
1275
						substr($sha, 0, 8),
1276
						$deploy->obj('LastEdited')->Ago()
1277
					);
1278
					$perEnvDeploys[$sha] = array(
1279
						'id' => $sha,
1280
						'text' => $pastValue
1281
					);
1282
				}
1283
			}
1284
1285
			if(!empty($perEnvDeploys)) {
1286
				$redeploy[$envName] = array_values($perEnvDeploys);
1287
			}
1288
		}
1289
		// Convert the array to the frontend format (i.e. keyed to regular array)
1290
		foreach($redeploy as $env => $descr) {
1291
			$data['field_data'][] = array('text'=>$env, 'children'=>$descr);
1292
		}
1293
		$tabs[] = $data;
1294
1295
		$data = array(
1296
			'id' => ++$id,
1297
			'name' => 'Deploy a specific SHA',
1298
			'field_type' => 'textfield',
1299
			'field_label' => 'Choose a SHA',
1300
			'field_id' => 'SHA',
1301
			'field_data' => array(),
1302
			'advanced_opts' => $advanced
1303
		);
1304
		$tabs[] = $data;
1305
1306
		// get the last time git fetch was run
1307
		$lastFetched = 'never';
1308
		$fetch = DNGitFetch::get()
1309
			->filter('ProjectID', $project->ID)
1310
			->sort('LastEdited', 'DESC')
1311
			->first();
1312
		if($fetch) {
1313
			$lastFetched = $fetch->dbObject('LastEdited')->Ago();
1314
		}
1315
1316
		$data = array(
1317
			'Tabs' => $tabs,
1318
			'last_fetched' => $lastFetched
1319
		);
1320
1321
		return json_encode($data, JSON_PRETTY_PRINT);
1322
	}
1323
1324
	/**
1325
	 * Check and regenerate a global CSRF token
1326
	 *
1327
	 * @param SS_HTTPRequest $request
1328
	 * @param bool $resetToken
1329
	 *
1330
	 * @return bool
1331
	 */
1332
	protected function checkCsrfToken(SS_HTTPRequest $request, $resetToken = true) {
1333
		$token = SecurityToken::inst();
1334
1335
		// Ensure the submitted token has a value
1336
		$submittedToken = $request->postVar('SecurityID');
1337
		if(!$submittedToken) {
1338
			return false;
1339
		}
1340
1341
		// Do the actual check.
1342
		$check = $token->check($submittedToken);
1343
1344
		// Reset the token after we've checked the existing token
1345
		if($resetToken) {
1346
			$token->reset();
1347
		}
1348
1349
		// Return whether the token was correct or not
1350
		return $check;
1351
	}
1352
1353
	/**
1354
	 * @param SS_HTTPRequest $request
1355
	 *
1356
	 * @return string
1357
	 */
1358
	public function deploySummary(SS_HTTPRequest $request) {
1359
1360
		// Performs canView permission check by limiting visible projects
1361
		$project = $this->getCurrentProject();
1362
		if(!$project) {
1363
			return $this->project404Response();
1364
		}
1365
1366
		// Performs canView permission check by limiting visible projects
1367
		$environment = $this->getCurrentEnvironment($project);
1368
		if(!$environment) {
1369
			return $this->environment404Response();
1370
		}
1371
1372
		// Plan the deployment.
1373
		$strategy = $environment->Backend()->planDeploy(
1374
			$environment,
1375
			$request->requestVars()
0 ignored issues
show
Bug introduced by
It seems like $request->requestVars() targeting SS_HTTPRequest::requestVars() can also be of type null; however, DeploymentBackend::planDeploy() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1376
		);
1377
		$data = $strategy->toArray();
1378
1379
		// Add in a URL for comparing from->to code changes. Ensure that we have
1380
		// two proper 40 character SHAs, otherwise we can't show the compare link.
1381
		$interface = $project->getRepositoryInterface();
1382
		if(
1383
			!empty($interface) && !empty($interface->URL)
1384
			&& !empty($data['changes']['Code version']['from'])
1385
			&& strlen($data['changes']['Code version']['from']) == '40'
1386
			&& !empty($data['changes']['Code version']['to'])
1387
			&& strlen($data['changes']['Code version']['to']) == '40'
1388
		) {
1389
			$compareurl = sprintf(
1390
				'%s/compare/%s...%s',
1391
				$interface->URL,
1392
				$data['changes']['Code version']['from'],
1393
				$data['changes']['Code version']['to']
1394
			);
1395
			$data['changes']['Code version']['compareUrl'] = $compareurl;
1396
		}
1397
1398
		// Append json to response
1399
		$token = SecurityToken::inst();
1400
		$data['SecurityID'] = $token->getValue();
1401
1402
		return json_encode($data);
1403
	}
1404
1405
	/**
1406
	 * Deployment form submission handler.
1407
	 *
1408
	 * Initiate a DNDeployment record and redirect to it for status polling
1409
	 *
1410
	 * @param SS_HTTPRequest $request
1411
	 *
1412
	 * @return SS_HTTPResponse
1413
	 * @throws ValidationException
1414
	 * @throws null
1415
	 */
1416
	public function startDeploy(SS_HTTPRequest $request) {
1417
1418
		// Ensure the CSRF Token is correct
1419
		if(!$this->checkCsrfToken($request)) {
1420
			// CSRF token didn't match
1421
			return $this->httpError(400, 'Bad Request');
1422
		}
1423
1424
		// Performs canView permission check by limiting visible projects
1425
		$project = $this->getCurrentProject();
1426
		if(!$project) {
1427
			return $this->project404Response();
1428
		}
1429
1430
		// Performs canView permission check by limiting visible projects
1431
		$environment = $this->getCurrentEnvironment($project);
1432
		if(!$environment) {
1433
			return $this->environment404Response();
1434
		}
1435
1436
		// Initiate the deployment
1437
		// The extension point should pass in: Project, Environment, SelectRelease, buildName
1438
		$this->extend('doDeploy', $project, $environment, $buildName, $data);
1439
1440
		// Start the deployment based on the approved strategy.
1441
		$strategy = new DeploymentStrategy($environment);
1442
		$strategy->fromArray($request->requestVar('strategy'));
1443
		$deployment = $strategy->createDeployment();
1444
		$deployment->start();
1445
1446
		return json_encode(array(
1447
			'url' => Director::absoluteBaseURL() . $deployment->Link()
1448
		), JSON_PRETTY_PRINT);
1449
	}
1450
1451
	/**
1452
	 * Action - Do the actual deploy
1453
	 *
1454
	 * @param SS_HTTPRequest $request
1455
	 *
1456
	 * @return SS_HTTPResponse|string
1457
	 * @throws SS_HTTPResponse_Exception
1458
	 */
1459
	public function deploy(SS_HTTPRequest $request) {
1460
		$params = $request->params();
1461
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1462
1463
		if(!$deployment || !$deployment->ID) {
1464
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1465
		}
1466
		if(!$deployment->canView()) {
1467
			return Security::permissionFailure();
1468
		}
1469
1470
		$environment = $deployment->Environment();
1471
		$project = $environment->Project();
1472
1473
		if($environment->Name != $params['Environment']) {
1474
			throw new LogicException("Environment in URL doesn't match this deploy");
1475
		}
1476
		if($project->Name != $params['Project']) {
1477
			throw new LogicException("Project in URL doesn't match this deploy");
1478
		}
1479
1480
		return $this->render(array(
1481
			'Deployment' => $deployment,
1482
		));
1483
	}
1484
1485
1486
	/**
1487
	 * Action - Get the latest deploy log
1488
	 *
1489
	 * @param SS_HTTPRequest $request
1490
	 *
1491
	 * @return string
1492
	 * @throws SS_HTTPResponse_Exception
1493
	 */
1494
	public function deploylog(SS_HTTPRequest $request) {
1495
		$params = $request->params();
1496
		$deployment = DNDeployment::get()->byId($params['Identifier']);
1497
1498
		if(!$deployment || !$deployment->ID) {
1499
			throw new SS_HTTPResponse_Exception('Deployment not found', 404);
1500
		}
1501
		if(!$deployment->canView()) {
1502
			return Security::permissionFailure();
1503
		}
1504
1505
		$environment = $deployment->Environment();
1506
		$project = $environment->Project();
1507
1508
		if($environment->Name != $params['Environment']) {
1509
			throw new LogicException("Environment in URL doesn't match this deploy");
1510
		}
1511
		if($project->Name != $params['Project']) {
1512
			throw new LogicException("Project in URL doesn't match this deploy");
1513
		}
1514
1515
		$log = $deployment->log();
1516
		if($log->exists()) {
1517
			$content = $log->content();
1518
		} else {
1519
			$content = 'Waiting for action to start';
1520
		}
1521
1522
		return $this->sendResponse($deployment->ResqueStatus(), $content);
1523
	}
1524
1525
	/**
1526
	 * @param SS_HTTPRequest|null $request
1527
	 *
1528
	 * @return Form
1529
	 */
1530
	public function getDataTransferForm(SS_HTTPRequest $request = null) {
1531
		// Performs canView permission check by limiting visible projects
1532
		$envs = $this->getCurrentProject()->DNEnvironmentList()->filterByCallback(function($item) {
1533
			return $item->canBackup();
1534
		});
1535
1536
		if(!$envs) {
1537
			return $this->environment404Response();
1538
		}
1539
1540
		$form = Form::create(
1541
			$this,
1542
			'DataTransferForm',
1543
			FieldList::create(
1544
				HiddenField::create('Direction', null, 'get'),
1545
				DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1546
					->setEmptyString('Select an environment'),
1547
				DropdownField::create('Mode', 'Transfer', DNDataArchive::get_mode_map())
1548
			),
1549
			FieldList::create(
1550
				FormAction::create('doDataTransfer', 'Create')
1551
					->addExtraClass('btn')
1552
			)
1553
		);
1554
		$form->setFormAction($this->getRequest()->getURL() . '/DataTransferForm');
1555
1556
		return $form;
1557
	}
1558
1559
	/**
1560
	 * @param array $data
1561
	 * @param Form $form
1562
	 *
1563
	 * @return SS_HTTPResponse
1564
	 * @throws SS_HTTPResponse_Exception
1565
	 */
1566
	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...
1567
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1568
1569
		// Performs canView permission check by limiting visible projects
1570
		$project = $this->getCurrentProject();
1571
		if(!$project) {
1572
			return $this->project404Response();
1573
		}
1574
1575
		$dataArchive = null;
1576
1577
		// Validate direction.
1578
		if($data['Direction'] == 'get') {
1579
			$validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1580
				->filterByCallback(function($item) {
1581
					return $item->canBackup();
1582
				});
1583
		} else if($data['Direction'] == 'push') {
1584
			$validEnvs = $this->getCurrentProject()->DNEnvironmentList()
1585
				->filterByCallback(function($item) {
1586
					return $item->canRestore();
1587
				});
1588
		} else {
1589
			throw new LogicException('Invalid direction');
1590
		}
1591
1592
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
1593
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
1594
		if(!$environment) {
1595
			throw new LogicException('Invalid environment');
1596
		}
1597
1598
		$this->validateSnapshotMode($data['Mode']);
1599
1600
1601
		// Only 'push' direction is allowed an association with an existing archive.
1602
		if(
1603
			$data['Direction'] == 'push'
1604
			&& isset($data['DataArchiveID'])
1605
			&& is_numeric($data['DataArchiveID'])
1606
		) {
1607
			$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1608
			if(!$dataArchive) {
1609
				throw new LogicException('Invalid data archive');
1610
			}
1611
1612
			if(!$dataArchive->canDownload()) {
1613
				throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1614
			}
1615
		}
1616
1617
		$transfer = DNDataTransfer::create();
1618
		$transfer->EnvironmentID = $environment->ID;
1619
		$transfer->Direction = $data['Direction'];
1620
		$transfer->Mode = $data['Mode'];
1621
		$transfer->DataArchiveID = $dataArchive ? $dataArchive->ID : null;
1622
		if($data['Direction'] == 'push') {
1623
			$transfer->setBackupBeforePush(!empty($data['BackupBeforePush']));
1624
		}
1625
		$transfer->write();
1626
		$transfer->start();
1627
1628
		return $this->redirect($transfer->Link());
1629
	}
1630
1631
	/**
1632
	 * View into the log for a {@link DNDataTransfer}.
1633
	 *
1634
	 * @param SS_HTTPRequest $request
1635
	 *
1636
	 * @return SS_HTTPResponse|string
1637
	 * @throws SS_HTTPResponse_Exception
1638
	 */
1639
	public function transfer(SS_HTTPRequest $request) {
1640
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1641
1642
		$params = $request->params();
1643
		$transfer = DNDataTransfer::get()->byId($params['Identifier']);
1644
1645
		if(!$transfer || !$transfer->ID) {
1646
			throw new SS_HTTPResponse_Exception('Transfer not found', 404);
1647
		}
1648
		if(!$transfer->canView()) {
1649
			return Security::permissionFailure();
1650
		}
1651
1652
		$environment = $transfer->Environment();
1653
		$project = $environment->Project();
1654
1655
		if($project->Name != $params['Project']) {
1656
			throw new LogicException("Project in URL doesn't match this deploy");
1657
		}
1658
1659
		return $this->render(array(
1660
			'CurrentTransfer' => $transfer,
1661
			'SnapshotsSection' => 1,
1662
		));
1663
	}
1664
1665
	/**
1666
	 * Action - Get the latest deploy log
1667
	 *
1668
	 * @param SS_HTTPRequest $request
1669
	 *
1670
	 * @return string
1671
	 * @throws SS_HTTPResponse_Exception
1672
	 */
1673 View Code Duplication
	public function transferlog(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...
1674
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1675
1676
		$params = $request->params();
1677
		$transfer = DNDataTransfer::get()->byId($params['Identifier']);
1678
1679
		if(!$transfer || !$transfer->ID) {
1680
			throw new SS_HTTPResponse_Exception('Transfer not found', 404);
1681
		}
1682
		if(!$transfer->canView()) {
1683
			return Security::permissionFailure();
1684
		}
1685
1686
		$environment = $transfer->Environment();
1687
		$project = $environment->Project();
1688
1689
		if($project->Name != $params['Project']) {
1690
			throw new LogicException("Project in URL doesn't match this deploy");
1691
		}
1692
1693
		$log = $transfer->log();
1694
		if($log->exists()) {
1695
			$content = $log->content();
1696
		} else {
1697
			$content = 'Waiting for action to start';
1698
		}
1699
1700
		return $this->sendResponse($transfer->ResqueStatus(), $content);
1701
	}
1702
1703
	/**
1704
	 * Note: Submits to the same action as {@link getDataTransferForm()},
1705
	 * but with a Direction=push and an archive reference.
1706
	 *
1707
	 * @param SS_HTTPRequest $request
1708
	 * @param DNDataArchive|null $dataArchive Only set when method is called manually in {@link restore()},
1709
	 *                            otherwise the state is inferred from the request data.
1710
	 * @return Form
1711
	 */
1712
	public function getDataTransferRestoreForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) {
1713
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1714
1715
		// Performs canView permission check by limiting visible projects
1716
		$project = $this->getCurrentProject();
1717
		$envs = $project->DNEnvironmentList()->filterByCallback(function($item) {
1718
			return $item->canRestore();
1719
		});
1720
1721
		if(!$envs) {
1722
			return $this->environment404Response();
1723
		}
1724
1725
		$modesMap = array();
1726
		if(in_array($dataArchive->Mode, array('all'))) {
1727
			$modesMap['all'] = 'Database and Assets';
1728
		};
1729
		if(in_array($dataArchive->Mode, array('all', 'db'))) {
1730
			$modesMap['db'] = 'Database only';
1731
		};
1732
		if(in_array($dataArchive->Mode, array('all', 'assets'))) {
1733
			$modesMap['assets'] = 'Assets only';
1734
		};
1735
1736
		$alertMessage = '<div class="alert alert-warning"><strong>Warning:</strong> '
1737
			. 'This restore will overwrite the data on the chosen environment below</div>';
1738
1739
		$form = Form::create(
1740
			$this,
1741
			'DataTransferRestoreForm',
1742
			FieldList::create(
1743
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1744
				HiddenField::create('Direction', null, 'push'),
1745
				LiteralField::create('Warning', $alertMessage),
1746
				DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1747
					->setEmptyString('Select an environment'),
1748
				DropdownField::create('Mode', 'Transfer', $modesMap),
1749
				CheckboxField::create('BackupBeforePush', 'Backup existing data', '1')
1750
			),
1751
			FieldList::create(
1752
				FormAction::create('doDataTransfer', 'Restore Data')
1753
					->addExtraClass('btn')
1754
			)
1755
		);
1756
		$form->setFormAction($project->Link() . '/DataTransferRestoreForm');
1757
1758
		return $form;
1759
	}
1760
1761
	/**
1762
	 * View a form to restore a specific {@link DataArchive}.
1763
	 * Permission checks are handled in {@link DataArchives()}.
1764
	 * Submissions are handled through {@link doDataTransfer()}, same as backup operations.
1765
	 *
1766
	 * @param SS_HTTPRequest $request
1767
	 *
1768
	 * @return HTMLText
1769
	 * @throws SS_HTTPResponse_Exception
1770
	 */
1771 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...
1772
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1773
1774
		/** @var DNDataArchive $dataArchive */
1775
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1776
1777
		if(!$dataArchive) {
1778
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1779
		}
1780
1781
		// We check for canDownload because that implies access to the data.
1782
		// canRestore is later checked on the actual restore action per environment.
1783
		if(!$dataArchive->canDownload()) {
1784
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1785
		}
1786
1787
		$form = $this->getDataTransferRestoreForm($this->request, $dataArchive);
1788
1789
		// View currently only available via ajax
1790
		return $form->forTemplate();
1791
	}
1792
1793
	/**
1794
	 * View a form to delete a specific {@link DataArchive}.
1795
	 * Permission checks are handled in {@link DataArchives()}.
1796
	 * Submissions are handled through {@link doDelete()}.
1797
	 *
1798
	 * @param SS_HTTPRequest $request
1799
	 *
1800
	 * @return HTMLText
1801
	 * @throws SS_HTTPResponse_Exception
1802
	 */
1803 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...
1804
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1805
1806
		/** @var DNDataArchive $dataArchive */
1807
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1808
1809
		if(!$dataArchive) {
1810
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1811
		}
1812
1813
		if(!$dataArchive->canDelete()) {
1814
			throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403);
1815
		}
1816
1817
		$form = $this->getDeleteForm($this->request, $dataArchive);
1818
1819
		// View currently only available via ajax
1820
		return $form->forTemplate();
1821
	}
1822
1823
	/**
1824
	 * @param SS_HTTPRequest $request
1825
	 * @param DNDataArchive|null $dataArchive Only set when method is called manually, otherwise the state is inferred
1826
	 *        from the request data.
1827
	 * @return Form
1828
	 */
1829
	public function getDeleteForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) {
1830
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1831
1832
		// Performs canView permission check by limiting visible projects
1833
		$project = $this->getCurrentProject();
1834
		if(!$project) {
1835
			return $this->project404Response();
1836
		}
1837
1838
		$snapshotDeleteWarning = '<div class="alert alert-warning">'
1839
			. 'Are you sure you want to permanently delete this snapshot from this archive area?'
1840
			. '</div>';
1841
1842
		$form = Form::create(
1843
			$this,
1844
			'DeleteForm',
1845
			FieldList::create(
1846
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1847
				LiteralField::create('Warning', $snapshotDeleteWarning)
1848
			),
1849
			FieldList::create(
1850
				FormAction::create('doDelete', 'Delete')
1851
					->addExtraClass('btn')
1852
			)
1853
		);
1854
		$form->setFormAction($project->Link() . '/DeleteForm');
1855
1856
		return $form;
1857
	}
1858
1859
	/**
1860
	 * @param array $data
1861
	 * @param Form $form
1862
	 *
1863
	 * @return bool|SS_HTTPResponse
1864
	 * @throws SS_HTTPResponse_Exception
1865
	 */
1866
	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...
1867
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1868
1869
		// Performs canView permission check by limiting visible projects
1870
		$project = $this->getCurrentProject();
1871
		if(!$project) {
1872
			return $this->project404Response();
1873
		}
1874
1875
		$dataArchive = null;
1876
1877
		if(
1878
			isset($data['DataArchiveID'])
1879
			&& is_numeric($data['DataArchiveID'])
1880
		) {
1881
			$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1882
		}
1883
1884
		if(!$dataArchive) {
1885
			throw new LogicException('Invalid data archive');
1886
		}
1887
1888
		if(!$dataArchive->canDelete()) {
1889
			throw new SS_HTTPResponse_Exception('Not allowed to delete archive', 403);
1890
		}
1891
1892
		$dataArchive->delete();
1893
1894
		return $this->redirectBack();
1895
	}
1896
1897
	/**
1898
	 * View a form to move a specific {@link DataArchive}.
1899
	 *
1900
	 * @param SS_HTTPRequest $request
1901
	 *
1902
	 * @return HTMLText
1903
	 * @throws SS_HTTPResponse_Exception
1904
	 */
1905 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...
1906
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1907
1908
		/** @var DNDataArchive $dataArchive */
1909
		$dataArchive = DNDataArchive::get()->byId($request->param('DataArchiveID'));
1910
1911
		if(!$dataArchive) {
1912
			throw new SS_HTTPResponse_Exception('Archive not found', 404);
1913
		}
1914
1915
		// We check for canDownload because that implies access to the data.
1916
		if(!$dataArchive->canDownload()) {
1917
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1918
		}
1919
1920
		$form = $this->getMoveForm($this->request, $dataArchive);
1921
1922
		// View currently only available via ajax
1923
		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...
1924
	}
1925
1926
	/**
1927
	 * Build snapshot move form.
1928
	 *
1929
	 * @param SS_HTTPRequest $request
1930
	 * @param DNDataArchive|null $dataArchive
1931
	 *
1932
	 * @return Form|SS_HTTPResponse
1933
	 */
1934
	public function getMoveForm(SS_HTTPRequest $request, DNDataArchive $dataArchive = null) {
1935
		$dataArchive = $dataArchive ? $dataArchive : DNDataArchive::get()->byId($request->requestVar('DataArchiveID'));
1936
1937
		$envs = $dataArchive->validTargetEnvironments();
1938
		if(!$envs) {
1939
			return $this->environment404Response();
1940
		}
1941
1942
		$warningMessage = '<div class="alert alert-warning"><strong>Warning:</strong> This will make the snapshot '
1943
			. 'available to people with access to the target environment.<br>By pressing "Change ownership" you '
1944
			. 'confirm that you have considered data confidentiality regulations.</div>';
1945
1946
		$form = Form::create(
1947
			$this,
1948
			'MoveForm',
1949
			FieldList::create(
1950
				HiddenField::create('DataArchiveID', null, $dataArchive->ID),
1951
				LiteralField::create('Warning', $warningMessage),
1952
				DropdownField::create('EnvironmentID', 'Environment', $envs->map())
1953
					->setEmptyString('Select an environment')
1954
			),
1955
			FieldList::create(
1956
				FormAction::create('doMove', 'Change ownership')
1957
					->addExtraClass('btn')
1958
			)
1959
		);
1960
		$form->setFormAction($this->getCurrentProject()->Link() . '/MoveForm');
1961
1962
		return $form;
1963
	}
1964
1965
	/**
1966
	 * @param array $data
1967
	 * @param Form $form
1968
	 *
1969
	 * @return bool|SS_HTTPResponse
1970
	 * @throws SS_HTTPResponse_Exception
1971
	 * @throws ValidationException
1972
	 * @throws null
1973
	 */
1974
	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...
1975
		$this->setCurrentActionType(self::ACTION_SNAPSHOT);
1976
1977
		// Performs canView permission check by limiting visible projects
1978
		$project = $this->getCurrentProject();
1979
		if(!$project) {
1980
			return $this->project404Response();
1981
		}
1982
1983
		/** @var DNDataArchive $dataArchive */
1984
		$dataArchive = DNDataArchive::get()->byId($data['DataArchiveID']);
1985
		if(!$dataArchive) {
1986
			throw new LogicException('Invalid data archive');
1987
		}
1988
1989
		// We check for canDownload because that implies access to the data.
1990
		if(!$dataArchive->canDownload()) {
1991
			throw new SS_HTTPResponse_Exception('Not allowed to access archive', 403);
1992
		}
1993
1994
		// Validate $data['EnvironmentID'] by checking against $validEnvs.
1995
		$validEnvs = $dataArchive->validTargetEnvironments();
1996
		$environment = $validEnvs->find('ID', $data['EnvironmentID']);
1997
		if(!$environment) {
1998
			throw new LogicException('Invalid environment');
1999
		}
2000
2001
		$dataArchive->EnvironmentID = $environment->ID;
2002
		$dataArchive->write();
2003
2004
		return $this->redirectBack();
2005
	}
2006
2007
	/**
2008
	 * Returns an error message if redis is unavailable
2009
	 *
2010
	 * @return string
2011
	 */
2012
	public static function RedisUnavailable() {
2013
		try {
2014
			Resque::queues();
2015
		} catch(Exception $e) {
2016
			return $e->getMessage();
2017
		}
2018
		return '';
2019
	}
2020
2021
	/**
2022
	 * Returns the number of connected Redis workers
2023
	 *
2024
	 * @return int
2025
	 */
2026
	public static function RedisWorkersCount() {
2027
		return count(Resque_Worker::all());
2028
	}
2029
2030
	/**
2031
	 * @return array
2032
	 */
2033
	public function providePermissions() {
2034
		return array(
2035
			self::DEPLOYNAUT_BYPASS_PIPELINE => array(
2036
				'name' => "Bypass Pipeline",
2037
				'category' => "Deploynaut",
2038
				'help' => "Enables users to directly initiate deployments, bypassing any pipeline",
2039
			),
2040
			self::DEPLOYNAUT_DRYRUN_PIPELINE => array(
2041
				'name' => 'Dry-run Pipeline',
2042
				'category' => 'Deploynaut',
2043
				'help' => 'Enable dry-run execution of pipelines for testing'
2044
			),
2045
			self::DEPLOYNAUT_ADVANCED_DEPLOY_OPTIONS => array(
2046
				'name' => "Access to advanced deploy options",
2047
				'category' => "Deploynaut",
2048
			),
2049
2050
			// Permissions that are intended to be added to the roles.
2051
			self::ALLOW_PROD_DEPLOYMENT => array(
2052
				'name' => "Ability to deploy to production environments",
2053
				'category' => "Deploynaut",
2054
			),
2055
			self::ALLOW_NON_PROD_DEPLOYMENT => array(
2056
				'name' => "Ability to deploy to non-production environments",
2057
				'category' => "Deploynaut",
2058
			),
2059
			self::ALLOW_PROD_SNAPSHOT => array(
2060
				'name' => "Ability to make production snapshots",
2061
				'category' => "Deploynaut",
2062
			),
2063
			self::ALLOW_NON_PROD_SNAPSHOT => array(
2064
				'name' => "Ability to make non-production snapshots",
2065
				'category' => "Deploynaut",
2066
			),
2067
			self::ALLOW_CREATE_ENVIRONMENT => array(
2068
				'name' => "Ability to create environments",
2069
				'category' => "Deploynaut",
2070
			),
2071
		);
2072
	}
2073
2074
	/**
2075
	 * @return DNProject|null
2076
	 */
2077
	public function getCurrentProject() {
2078
		$projectName = trim($this->getRequest()->param('Project'));
2079
		if(!$projectName) {
2080
			return null;
2081
		}
2082
		if(empty(self::$_project_cache[$projectName])) {
2083
			self::$_project_cache[$projectName] = $this->DNProjectList()->filter('Name', $projectName)->First();
2084
		}
2085
		return self::$_project_cache[$projectName];
2086
	}
2087
2088
	/**
2089
	 * @param DNProject|null $project
2090
	 * @return DNEnvironment|null
2091
	 */
2092
	public function getCurrentEnvironment(DNProject $project = null) {
2093
		if($this->getRequest()->param('Environment') === null) {
2094
			return null;
2095
		}
2096
		if($project === null) {
2097
			$project = $this->getCurrentProject();
2098
		}
2099
		// project can still be null
2100
		if($project === null) {
2101
			return null;
2102
		}
2103
		return $project->DNEnvironmentList()->filter('Name', $this->getRequest()->param('Environment'))->First();
2104
	}
2105
2106
	/**
2107
	 * This will return a const that indicates the class of action currently being performed
2108
	 *
2109
	 * Until DNRoot is de-godded, it does a bunch of different actions all in the same class.
2110
	 * So we just have each action handler calll setCurrentActionType to define what sort of
2111
	 * action it is.
2112
	 *
2113
	 * @return string - one of the consts from self::$action_types
2114
	 */
2115
	public function getCurrentActionType() {
2116
		return $this->actionType;
2117
	}
2118
2119
	/**
2120
	 * Sets the current action type
2121
	 *
2122
	 * @param string $actionType string - one of the consts from self::$action_types
2123
	 */
2124
	public function setCurrentActionType($actionType) {
2125
		$this->actionType = $actionType;
2126
	}
2127
2128
	/**
2129
	 * Helper method to allow templates to know whether they should show the 'Archive List' include or not.
2130
	 * The actual permissions are set on a per-environment level, so we need to find out if this $member can upload to
2131
	 * or download from *any* {@link DNEnvironment} that (s)he has access to.
2132
	 *
2133
	 * TODO To be replaced with a method that just returns the list of archives this {@link Member} has access to.
2134
	 *
2135
	 * @param Member|null $member The {@link Member} to check (or null to check the currently logged in Member)
2136
	 * @return boolean|null true if $member has access to upload or download to at least one {@link DNEnvironment}.
2137
	 */
2138
	public function CanViewArchives(Member $member = null) {
2139
		if($member === null) {
2140
			$member = Member::currentUser();
2141
		}
2142
2143
		if(Permission::checkMember($member, 'ADMIN')) {
2144
			return true;
2145
		}
2146
2147
		$allProjects = $this->DNProjectList();
2148
		if(!$allProjects) {
2149
			return false;
2150
		}
2151
2152
		foreach($allProjects as $project) {
2153
			if($project->Environments()) {
2154
				foreach($project->Environments() as $environment) {
2155
					if(
2156
						$environment->canRestore($member) ||
2157
						$environment->canBackup($member) ||
2158
						$environment->canUploadArchive($member) ||
2159
						$environment->canDownloadArchive($member)
2160
					) {
2161
						// We can return early as we only need to know that we can access one environment
2162
						return true;
2163
					}
2164
				}
2165
			}
2166
		}
2167
	}
2168
2169
	/**
2170
	 * Returns a list of attempted environment creations.
2171
	 *
2172
	 * @return PaginatedList
2173
	 */
2174
	public function CreateEnvironmentList() {
2175
		$project = $this->getCurrentProject();
2176
		if($project) {
2177
			return new PaginatedList($project->CreateEnvironments()->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...
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...
2178
		}
2179
		return new PaginatedList(new ArrayList(), $this->request);
0 ignored issues
show
Documentation introduced by
$this->request is of type object<SS_HTTPRequest>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2180
	}
2181
2182
	/**
2183
	 * Returns a list of all archive files that can be accessed by the currently logged-in {@link Member}
2184
	 *
2185
	 * @return PaginatedList
2186
	 */
2187
	public function CompleteDataArchives() {
2188
		$project = $this->getCurrentProject();
2189
		$archives = new ArrayList();
2190
2191
		$archiveList = $project->Environments()->relation("DataArchives");
2192
		if($archiveList->count() > 0) {
2193
			foreach($archiveList as $archive) {
2194
				if($archive->canView() && !$archive->isPending()) {
2195
					$archives->push($archive);
2196
				}
2197
			}
2198
		}
2199
		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...
2200
	}
2201
2202
	/**
2203
	 * @return PaginatedList The list of "pending" data archives which are waiting for a file
2204
	 * to be delivered offline by post, and manually uploaded into the system.
2205
	 */
2206
	public function PendingDataArchives() {
2207
		$project = $this->getCurrentProject();
2208
		$archives = new ArrayList();
2209
		foreach($project->DNEnvironmentList() as $env) {
2210
			foreach($env->DataArchives() as $archive) {
2211
				if($archive->canView() && $archive->isPending()) {
2212
					$archives->push($archive);
2213
				}
2214
			}
2215
		}
2216
		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...
2217
	}
2218
2219
	/**
2220
	 * @return PaginatedList
2221
	 */
2222
	public function DataTransferLogs() {
2223
		$project = $this->getCurrentProject();
2224
2225
		$transfers = DNDataTransfer::get()->filterByCallback(function($record) use($project) {
2226
			return
2227
				$record->Environment()->Project()->ID == $project->ID && // Ensure only the current Project is shown
2228
				(
2229
					$record->Environment()->canRestore() || // Ensure member can perform an action on the transfers env
2230
					$record->Environment()->canBackup() ||
2231
					$record->Environment()->canUploadArchive() ||
2232
					$record->Environment()->canDownloadArchive()
2233
				);
2234
		});
2235
2236
		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...
2237
	}
2238
2239
	/**
2240
	 * @return null|PaginatedList
2241
	 */
2242
	public function DeployHistory() {
2243
		if($env = $this->getCurrentEnvironment()) {
2244
			$history = $env->DeployHistory();
2245
			if($history->count() > 0) {
2246
				$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...
2247
				$pagination->setPageLength(8);
2248
				return $pagination;
2249
			}
2250
		}
2251
		return null;
2252
	}
2253
2254
	/**
2255
	 * @return SS_HTTPResponse
2256
	 */
2257
	protected function project404Response() {
2258
		return new SS_HTTPResponse(
2259
			"Project '" . Convert::raw2xml($this->getRequest()->param('Project')) . "' not found.",
2260
			404
2261
		);
2262
	}
2263
2264
	/**
2265
	 * @return SS_HTTPResponse
2266
	 */
2267
	protected function environment404Response() {
2268
		$envName = Convert::raw2xml($this->getRequest()->param('Environment'));
2269
		return new SS_HTTPResponse("Environment '" . $envName . "' not found.", 404);
2270
	}
2271
2272
	/**
2273
	 * @param string $status
2274
	 * @param string $content
2275
	 *
2276
	 * @return string
2277
	 */
2278
	protected function sendResponse($status, $content) {
2279
		// strip excessive newlines
2280
		$content = preg_replace('/(?:(?:\r\n|\r|\n)\s*){2}/s', "\n", $content);
2281
2282
		$sendJSON = (strpos($this->getRequest()->getHeader('Accept'), 'application/json') !== false)
2283
			|| $this->getRequest()->getExtension() == 'json';
2284
2285
		if(!$sendJSON) {
2286
			$this->response->addHeader("Content-type", "text/plain");
2287
			return $content;
2288
		}
2289
		$this->response->addHeader("Content-type", "application/json");
2290
		return json_encode(array(
2291
			'status' => $status,
2292
			'content' => $content,
2293
		));
2294
	}
2295
2296
	/**
2297
	 * Validate the snapshot mode
2298
	 *
2299
	 * @param string $mode
2300
	 */
2301
	protected function validateSnapshotMode($mode) {
2302
		if(!in_array($mode, array('all', 'assets', 'db'))) {
2303
			throw new LogicException('Invalid mode');
2304
		}
2305
	}
2306
2307
	/**
2308
	 * @param string $sectionName
2309
	 * @param string $title
2310
	 *
2311
	 * @return SS_HTTPResponse
2312
	 */
2313
	protected function getCustomisedViewSection($sectionName, $title = '', $data = array()) {
2314
		// Performs canView permission check by limiting visible projects
2315
		$project = $this->getCurrentProject();
2316
		if(!$project) {
2317
			return $this->project404Response();
2318
		}
2319
		$data[$sectionName] = 1;
2320
2321
		if($this !== '') {
2322
			$data['Title'] = $title;
2323
		}
2324
2325
		return $this->render($data);
2326
	}
2327
2328
	/**
2329
	 * Get items for the ambient menu that should be accessible from all pages.
2330
	 *
2331
	 * @return ArrayList
2332
	 */
2333
	public function AmbientMenu() {
2334
		$list = new ArrayList();
2335
2336
		if (Member::currentUserID()) {
2337
			$list->push(new ArrayData(array(
2338
				'Classes' => 'logout',
2339
				'FaIcon' => 'sign-out',
2340
				'Link' => 'Security/logout',
2341
				'Title' => 'Log out',
2342
				'IsCurrent' => false,
2343
				'IsSection' => false
2344
			)));
2345
		}
2346
2347
		$this->extend('updateAmbientMenu', $list);
2348
		return $list;
2349
	}
2350
2351
	/**
2352
	 * Create project action.
2353
	 *
2354
	 * @return SS_HTTPResponse
2355
	 */
2356
	public function createproject(SS_HTTPRequest $request) {
2357
		if($this->canCreateProjects()) {
2358
			return $this->render(['CurrentTitle' => 'Create Stack']);
2359
		}
2360
		return $this->httpError(403);
2361
	}
2362
2363
	/**
2364
	 * Checks whether the user can create a project.
2365
	 *
2366
	 * @return bool
2367
	 */
2368
	public function canCreateProjects($member = null) {
2369
		if(!$member) $member = Member::currentUser();
2370
		if(!$member) return false;
2371
2372
		return singleton('DNProject')->canCreate($member);
2373
	}
2374
2375
	/**
2376
	 * @return Form
2377
	 */
2378
	public function CreateProjectForm() {
2379
		$form = Form::create(
2380
			$this,
2381
			__FUNCTION__,
2382
			$this->getCreateProjectFormFields(),
2383
			$this->getCreateProjectFormActions(),
2384
			new RequiredFields('Name', 'CVSPath')
2385
		);
2386
		$this->extend('updateCreateProjectForm', $form);
2387
		return $form;
2388
	}
2389
2390
	/**
2391
	 * @return FieldList
2392
	 */
2393
	protected function getCreateProjectFormFields() {
2394
		$fields = FieldList::create();
2395
		$fields->merge([
2396
			TextField::create('Name', 'Title')->setDescription('Limited to alphanumeric characters, underscores and hyphens.'),
2397
			TextField::create('CVSPath', 'Git URL')->setDescription('Your repository URL so we can clone your code (eg. [email protected]:silverstripe/silverstripe-installer.git)')
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 168 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
2398
		]);
2399
		$this->extend('updateCreateProjectFormFields', $fields);
2400
		return $fields;
2401
	}
2402
2403
	/**
2404
	 * @return FieldList
2405
	 */
2406
	protected function getCreateProjectFormActions() {
2407
		$fields = FieldList::create(
2408
			FormAction::create('doCreateProject', 'Create Stack')
2409
		);
2410
		$this->extend('updateCreateProjectFormActions', $fields);
2411
		return $fields;
2412
	}
2413
2414
	/**
2415
	 * Does the actual project creation.
2416
	 *
2417
	 * @param $data array
2418
	 * @param $form Form
2419
	 *
2420
	 * @return SS_HTTPResponse
2421
	 */
2422
	public function doCreateProject($data, $form) {
2423
		$form->loadDataFrom($data);
2424
		$project = DNProject::create();
2425
2426
		$form->saveInto($project);
2427
		$this->extend('onBeforeCreateProject', $project, $data, $form);
2428
		try {
2429
			if($project->write() > 0) {
2430
				$this->extend('onAfterCreateProject', $project, $data, $form);
2431
2432
				// If an extension hasn't redirected us, we'll redirect to the project.
2433
				if(!$this->redirectedTo()) {
2434
					return $this->redirect($project->Link());
2435
				} else {
2436
					return $this->response;
2437
				}
2438
			} else {
2439
				$form->sessionMessage('Unable to write the stack to the database.', 'bad');
2440
			}
2441
		} catch (ValidationException $e) {
2442
			$form->sessionMessage($e->getMessage(), 'bad');
2443
		}
2444
		return $this->redirectBack();
2445
	}
2446
2447
	/**
2448
	 * Returns the state of a current project build.
2449
	 *
2450
	 * @param SS_HTTPRequest $request
2451
	 *
2452
	 * @return SS_HTTPResponse
2453
	 */
2454
	public function createprojectprogress(SS_HTTPRequest $request) {
2455
		$project = $this->getCurrentProject();
2456
		if(!$project) {
2457
			return $this->httpError(404);
2458
		}
2459
2460
		$envCreations = $project->getInitialEnvironmentCreations();
2461
		$complete = array();
2462
		$inProgress = array();
2463
		$failed = array();
2464
		if($envCreations->count() > 0) {
2465
			foreach($envCreations as $env) {
2466
				$data = unserialize($env->Data);
2467
				if(!isset($data['Name'])) {
2468
					$data['Name'] = 'Unknown';
2469
				}
2470
				switch($env->ResqueStatus()) {
2471
					case "Queued":
2472
					case "Running":
2473
						$inProgress[$env->ID] = Convert::raw2xml($env->ResqueStatus());
2474
						break;
2475
					case "Complete":
2476
						$complete[$env->ID] = Convert::raw2xml($data['Name']);
2477
						break;
2478
					case "Failed":
2479
					case "Invalid":
2480
					default:
2481
						$failed[$env->ID] = Convert::raw2xml($data['Name']);
2482
				}
2483
			}
2484
		}
2485
2486
		$data = [
2487
			'complete' => $project->isProjectReady(),
2488
			'progress' => [
2489
				'environments' => [
2490
					'complete' => $complete,
2491
					'inProgress' => $inProgress,
2492
					'failed' => $failed,
2493
				]
2494
			]
2495
		];
2496
		$this->extend('updateCreateProjectProgressData', $data);
2497
2498
		$response = $this->getResponse();
2499
		$response->addHeader('Content-Type', 'application/json');
2500
		$response->setBody(json_encode($data));
2501
		return $response;
2502
	}
2503
2504
	public function checkrepoaccess(SS_HTTPRequest $request) {
2505
		$project = $this->getCurrentProject();
2506
		if(!$project) {
2507
			return $this->httpError(404);
2508
		}
2509
2510
		if($project->CVSPath) {
2511
			$fetch = new FetchJob();
2512
			$fetch->args = array('projectID' => $project->ID);
2513
			try {
2514
				$fetch->perform();
2515
				$canAccessRepo = true;
2516
			} catch (RuntimeException $e) {
2517
				$canAccessRepo = false;
2518
			}
2519
			$data = ['canAccessRepo' => $canAccessRepo];
2520
		} else {
2521
			$data = ['canAccessRepo' => false];
2522
		}
2523
2524
		$response = $this->getResponse();
2525
		$response->addHeader("Content-Type", "application/json");
2526
		$response->setBody(json_encode($data));
2527
		return $response;
2528
	}
2529
2530
}
2531
2532