Completed
Push — master ( 163f02...ecf5fa )
by Sean
1360:37 queued 1357:19
created

DNEnvironment   F

Complexity

Total Complexity 199

Size/Duplication

Total Lines 1340
Duplicated Lines 10.97 %

Coupling/Cohesion

Components 4
Dependencies 40

Importance

Changes 6
Bugs 0 Features 2
Metric Value
wmc 199
c 6
b 0
f 2
lcom 4
cbo 40
dl 147
loc 1340
rs 0.5217

63 Methods

Rating   Name   Duplication   Size   Complexity  
A create_from_path() 0 10 1
B Backend() 0 23 4
A getDeployStrategy() 0 3 1
A Menu() 0 17 2
A CurrentMenu() 0 3 1
A getFullName() 0 3 1
A getDefaultURL() 0 3 1
A getBareURL() 0 6 2
A getBareDefaultURL() 0 6 2
A HasPipelineSupport() 0 4 2
A GenericPipeline() 0 5 1
A GenericPipelineConfig() 0 6 2
A loadPipelineConfig() 0 8 2
A DependsOnEnvironment() 0 8 2
A HasCurrentPipeline() 0 3 2
A CurrentPipeline() 0 3 1
A CanCancelPipeline() 0 7 2
B canView() 0 21 7
B canDeploy() 18 18 8
B canRestore() 18 18 8
C canBackup() 23 23 10
C canUploadArchive() 23 23 10
B canDownloadArchive() 18 18 8
B canAbort() 15 15 5
B canApprove() 14 14 5
B canDeleteArchive() 18 18 8
A getDeployersList() 0 9 1
A getCanRestoreMembersList() 0 9 1
A getCanBackupMembersList() 0 9 1
A getArchiveUploadersList() 0 9 1
A getArchiveDownloadersList() 0 9 1
A getArchiveDeletersList() 0 9 1
A getPipelineApproversList() 0 9 1
A getPipelineCancellersList() 0 9 1
A DNData() 0 3 1
B CurrentBuild() 0 32 6
A DeployHistory() 0 5 1
A Link() 0 3 1
A isCurrent() 0 3 2
A isSection() 0 5 2
A buildPermissionField() 0 15 1
C getCMSFields() 0 175 12
A setDeployConfigurationFields() 0 20 3
B setPipelineConfigurationFields() 0 25 4
A onBeforeWrite() 0 9 3
A onAfterWrite() 0 15 4
A checkEnvironmentPath() 0 7 3
B writeConfigFile() 0 16 8
B writePipelineFile() 0 13 5
A getEnvironmentConfig() 0 6 2
A envFileExists() 0 6 2
A getConfigFilename() 0 9 3
A getPipelineFilename() 0 10 3
A pipelineFileExists() 0 7 2
B array_to_viewabledata() 0 25 5
B getDependentFilteredCommits() 0 25 5
A enableMaintenace() 0 4 1
A disableMaintenance() 0 4 1
A validate() 0 10 3
A getCannotDeployMessage() 0 3 1
B getCommitData() 0 24 3
B onAfterDelete() 0 12 5
A runningDeployments() 0 8 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like DNEnvironment often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DNEnvironment, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * DNEnvironment
5
 *
6
 * This dataobject represents a target environment that source code can be deployed to.
7
 * Permissions are controlled by environment, see the various many-many relationships.
8
 *
9
 * @property string $Filename
10
 * @property string $Name
11
 * @property string $URL
12
 * @property string $BackendIdentifier
13
 * @property bool $DryRunEnabled
14
 * @property bool $Usage
15
 *
16
 * @method DNProject Project()
17
 * @property int $ProjectID
18
 *
19
 * @method HasManyList Deployments()
20
 * @method HasManyList DataArchives()
21
 * @method HasManyList Pipelines()
22
 *
23
 * @method ManyManyList Viewers()
24
 * @method ManyManyList ViewerGroups()
25
 * @method ManyManyList Deployers()
26
 * @method ManyManyList DeployerGroups()
27
 * @method ManyManyList CanRestoreMembers()
28
 * @method ManyManyList CanRestoreGroups()
29
 * @method ManyManyList CanBackupMembers()
30
 * @method ManyManyList CanBackupGroups()
31
 * @method ManyManyList ArchiveUploaders()
32
 * @method ManyManyList ArchiveUploaderGroups()
33
 * @method ManyManyList ArchiveDownloaders()
34
 * @method ManyManyList ArchiveDownloaderGroups()
35
 * @method ManyManyList ArchiveDeleters()
36
 * @method ManyManyList ArchiveDeleterGroups()
37
 * @method ManyManyList PipelineApprovers()
38
 * @method ManyManyList PipelineApproverGroups()
39
 * @method ManyManyList PipelineCancellers()
40
 * @method ManyManyList PipelineCancellerGroups()
41
 */
42
class DNEnvironment extends DataObject {
43
44
	/**
45
	 * If this is set to a full pathfile, it will be used as template
46
	 * file when creating a new capistrano environment config file.
47
	 *
48
	 * If not set, the default 'environment.template' from the module
49
	 * root is used
50
	 *
51
	 * @config
52
	 * @var string
53
	 */
54
	private static $template_file = '';
55
56
	/**
57
	 * Set this to true to allow editing of the environment files via the web admin
58
	 *
59
	 * @var bool
60
	 */
61
	private static $allow_web_editing = false;
62
63
	/**
64
	 * @var array
65
	 */
66
	private static $casting = array(
67
		'DeployHistory' => 'Text'
68
	);
69
70
	/**
71
	 * Allowed backends. A map of Injector identifier to human-readable label.
72
	 *
73
	 * @config
74
	 * @var array
75
	 */
76
	private static $allowed_backends = array();
77
78
	/**
79
	 * @var array
80
	 */
81
	public static $db = array(
82
		"Filename" => "Varchar(255)",
83
		"Name" => "Varchar(255)",
84
		"URL" => "Varchar(255)",
85
		"BackendIdentifier" => "Varchar(255)", // Injector identifier of the DeploymentBackend
86
		"DryRunEnabled" => "Boolean", // True if the dry run button should be enabled on the frontend
87
		"Usage" => "Enum('Production, UAT, Test, Unspecified', 'Unspecified')"
88
	);
89
90
	/**
91
	 * @var array
92
	 */
93
	public static $has_one = array(
94
		"Project" => "DNProject",
95
		"CreateEnvironment" => "DNCreateEnvironment"
96
	);
97
98
	/**
99
	 * @var array
100
	 */
101
	public static $has_many = array(
102
		"Deployments" => "DNDeployment",
103
		"DataArchives" => "DNDataArchive",
104
		"Pipelines" => "Pipeline" // Only one Pipeline can be 'Running' at any one time. @see self::CurrentPipeline().
105
	);
106
107
	/**
108
	 * @var array
109
	 */
110
	public static $many_many = array(
111
		"Viewers"            => "Member", // Who can view this environment
112
		"ViewerGroups"       => "Group",
113
		"Deployers"          => "Member", // Who can deploy to this environment
114
		"DeployerGroups" => "Group",
115
		"CanRestoreMembers"  => "Member", // Who can restore archive files to this environment
116
		"CanRestoreGroups"  => "Group",
117
		"CanBackupMembers"   => "Member", // Who can backup archive files from this environment
118
		"CanBackupGroups"   => "Group",
119
		"ArchiveUploaders"   => "Member", // Who can upload archive files linked to this environment
120
		"ArchiveUploaderGroups" => "Group",
121
		"ArchiveDownloaders" => "Member", // Who can download archive files from this environment
122
		"ArchiveDownloaderGroups" => "Group",
123
		"ArchiveDeleters"    => "Member", // Who can delete archive files from this environment,
124
		"ArchiveDeleterGroups" => "Group",
125
		"PipelineApprovers"  => "Member", // Who can approve / reject pipelines from this environment
126
		"PipelineApproverGroups" => "Group",
127
		"PipelineCancellers"   => "Member", // Who can abort pipelines
128
		"PipelineCancellerGroups" => "Group"
129
	);
130
131
	/**
132
	 * @var array
133
	 */
134
	public static $summary_fields = array(
135
		"Name" => "Environment Name",
136
		"Usage" => "Usage",
137
		"URL" => "URL",
138
		"DeployersList" => "Can Deploy List",
139
		"CanRestoreMembersList" => "Can Restore List",
140
		"CanBackupMembersList" => "Can Backup List",
141
		"ArchiveUploadersList" => "Can Upload List",
142
		"ArchiveDownloadersList" => "Can Download List",
143
		"ArchiveDeletersList"  => "Can Delete List",
144
		"PipelineApproversList" => "Can Approve List",
145
		"PipelineCancellersList" => "Can Cancel List"
146
	);
147
148
	private static $singular_name = 'Capistrano Environment';
149
150
	private static $plural_name = 'Capistrano Environments';
151
152
	/**
153
	 * @var array
154
	 */
155
	public static $searchable_fields = array(
156
		"Name",
157
	);
158
159
	/**
160
	 * @var string
161
	 */
162
	private static $default_sort = 'Name';
163
164
	/**
165
	 * Used by the sync task
166
	 *
167
	 * @param string $path
168
	 * @return \DNEnvironment
169
	 */
170
	public static function create_from_path($path) {
171
		$e = DNEnvironment::create();
172
		$e->Filename = $path;
173
		$e->Name = basename($e->Filename, '.rb');
174
175
		// add each administrator member as a deployer of the new environment
176
		$adminGroup = Group::get()->filter('Code', 'administrators')->first();
177
		$e->DeployerGroups()->add($adminGroup);
178
		return $e;
179
	}
180
181
	/**
182
	 * Get the deployment backend used for this environment.
183
	 *
184
	 * Enforces compliance with the allowed_backends setting; if the DNEnvironment.BackendIdentifier value is
185
	 * illegal then that value is ignored.
186
	 *
187
	 * @return DeploymentBackend
188
	 */
189
	public function Backend() {
190
		$backends = array_keys($this->config()->get('allowed_backends', Config::FIRST_SET));
191
		switch(sizeof($backends)) {
192
		// Nothing allowed, use the default value "DeploymentBackend"
193
			case 0:
194
				$backend = "DeploymentBackend";
195
				break;
196
197
			// Only 1 thing allowed, use that
198
			case 1:
199
				$backend = $backends[0];
200
				break;
201
202
			// Multiple choices, use our choice if it's legal, otherwise default to the first item on the list
203
			default:
204
				$backend = $this->BackendIdentifier;
205
				if(!in_array($backend, $backends)) {
206
					$backend = $backends[0];
207
				}
208
		}
209
210
		return Injector::inst()->get($backend);
211
	}
212
213
	/**
214
	 * @param SS_HTTPRequest $request
215
	 *
216
	 * @return DeploymentStrategy
217
	 */
218
	public function getDeployStrategy(\SS_HTTPRequest $request) {
219
		return $this->Backend()->planDeploy($this, $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...
220
	}
221
222
	public function Menu() {
223
		$list = new ArrayList();
224
225
		$controller = Controller::curr();
226
		$actionType = $controller->getField('CurrentActionType');
227
228
		$list->push(new ArrayData(array(
229
			'Link' => sprintf('naut/project/%s/environment/%s', $this->Project()->Name, $this->Name),
230
			'Title' => 'Deployments',
231
			'IsCurrent' => $this->isCurrent(),
232
			'IsSection' => $this->isSection() && $actionType == DNRoot::ACTION_DEPLOY
233
		)));
234
235
		$this->extend('updateMenu', $list);
236
237
		return $list;
238
	}
239
240
	/**
241
	 * Return the current object from $this->Menu()
242
	 * Good for making titles and things
243
	 */
244
	public function CurrentMenu() {
245
		return $this->Menu()->filter('IsSection', true)->First();
246
	}
247
248
	/**
249
	 * Return a name for this environment.
250
	 *
251
	 * @param string $separator The string used when concatenating project with env name
252
	 * @return string
253
	 */
254
	public function getFullName($separator = ':') {
255
		return sprintf('%s%s%s', $this->Project()->Name, $separator, $this->Name);
256
	}
257
258
	/**
259
	 * URL for the environment that can be used if no explicit URL is set.
260
	 */
261
	public function getDefaultURL() {
262
		return null;
263
	}
264
265
	public function getBareURL() {
266
		$url = parse_url($this->URL);
267
		if(isset($url['host'])) {
268
			return strtolower($url['host']);
269
		}
270
	}
271
272
	public function getBareDefaultURL() {
273
		$url = parse_url($this->getDefaultURL());
274
		if(isset($url['host'])) {
275
			return strtolower($url['host']);
276
		}
277
	}
278
279
	/**
280
	 * @return boolean true if there is a pipeline for the current environment.
281
	 */
282
	public function HasPipelineSupport() {
283
		$config = $this->GenericPipelineConfig();
284
		return $config instanceof ArrayData && isset($config->Steps);
285
	}
286
287
	/**
288
	 * Returns a {@link Pipeline} object that is linked to this environment, but isn't saved into the database. This
289
	 * shouldn't be saved into the database unless you plan on starting an actual pipeline.
290
	 *
291
	 * @return Pipeline
292
	 */
293
	public function GenericPipeline() {
294
		$pipeline = Pipeline::create();
295
		$pipeline->EnvironmentID = $this->ID;
296
		return $pipeline;
297
	}
298
299
	/**
300
	 * Returns the parsed config, based on a {@link Pipeline} being created for this {@link DNEnvironment}.
301
	 *
302
	 * @return ArrayData
303
	 */
304
	public function GenericPipelineConfig() {
305
		$config = $this->loadPipelineConfig();
306
		if($config) {
307
			return self::array_to_viewabledata($config);
308
		}
309
	}
310
311
	/**
312
	 * Extract pipeline configuration data from the source yml file
313
	 *
314
	 * @return array
315
	 */
316
	public function loadPipelineConfig() {
317
		require_once 'thirdparty/spyc/spyc.php';
318
319
		$path = $this->getPipelineFilename();
320
		if(file_exists($path)) {
321
			return Spyc::YAMLLoad($path);
322
		}
323
	}
324
325
	/**
326
	 * Returns the {@link DNEnvironment} object relating to the pipeline config for this environment. The environment
327
	 * YAML file (e.g. project1-uat.yml; see docs/en/pipelines.md) contains two variable called `DependsOnProject` and
328
	 * `DependsOnEnvironment` - these are used together to find the {@link DNEnvironment} that this environment should
329
	 * rely on.
330
	 */
331
	public function DependsOnEnvironment() {
332
		if($this->HasPipelineSupport()) {
333
			$pipeline = $this->GenericPipeline();
334
			return $pipeline->getDependentEnvironment();
335
		}
336
337
		return null;
338
	}
339
340
	/**
341
	 * @return bool true if there is a currently running Pipeline, and false if there isn't
342
	 */
343
	public function HasCurrentPipeline() {
344
		return $this->CurrentPipeline() && $this->CurrentPipeline()->isInDB();
345
	}
346
347
	/**
348
	 * This can be used to determine if there is a currently running pipeline (there can only be one running per
349
	 * {@link DNEnvironment} at once), as well as getting the current pipeline to be shown in templates.
350
	 *
351
	 * @return DataObject|null The currently running pipeline, or null if there isn't any.
352
	 */
353
	public function CurrentPipeline() {
354
		return $this->Pipelines()->filter('Status', array('Running', 'Rollback'))->first();
355
	}
356
357
	/**
358
	 * @return bool true if the current user can cancel a running pipeline
359
	 */
360
	public function CanCancelPipeline() {
361
		// do we have a current pipeline
362
		if($this->HasCurrentPipeline()) {
363
			return $this->CurrentPipeline()->canAbort();
364
		}
365
		return false;
366
	}
367
368
	/**
369
	 * Environments are only viewable by people that can view the environment.
370
	 *
371
	 * @param Member|null $member
372
	 * @return boolean
373
	 */
374
	public function canView($member = null) {
375
		if(!$member) {
376
			$member = Member::currentUser();
377
		}
378
		if(!$member) {
379
			return false;
380
		}
381
		// Must be logged in to check permissions
382
383
		if(Permission::checkMember($member, 'ADMIN')) {
384
			return true;
385
		}
386
387
		// if no Viewers or ViewerGroups defined, fallback to DNProject::canView permissions
388
		if($this->Viewers()->exists() || $this->ViewerGroups()->exists()) {
389
			return $this->Viewers()->byID($member->ID)
390
				|| $member->inGroups($this->ViewerGroups());
391
		}
392
393
		return $this->Project()->canView($member);
394
	}
395
396
	/**
397
	 * Allow deploy only to some people.
398
	 *
399
	 * @param Member|null $member
400
	 * @return boolean
401
	 */
402 View Code Duplication
	public function canDeploy($member = null) {
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...
403
		if(!$member) {
404
			$member = Member::currentUser();
405
		}
406
		if(!$member) {
407
			return false;
408
		}
409
		// Must be logged in to check permissions
410
411
		if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
412
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_DEPLOYMENT, $member)) return true;
413
		} else {
414
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_DEPLOYMENT, $member)) return true;
415
		}
416
417
		return $this->Deployers()->byID($member->ID)
418
			|| $member->inGroups($this->DeployerGroups());
419
	}
420
421
	/**
422
	 * Provide reason why the user cannot deploy.
423
	 *
424
	 * @return string
425
	 */
426
	public function getCannotDeployMessage() {
427
		return 'You cannot deploy to this environment.';
428
	}
429
430
	/**
431
	 * Allows only selected {@link Member} objects to restore {@link DNDataArchive} objects into this
432
	 * {@link DNEnvironment}.
433
	 *
434
	 * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
435
	 * @return boolean true if $member can restore, and false if they can't.
436
	 */
437 View Code Duplication
	public function canRestore($member = null) {
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...
438
		if(!$member) {
439
			$member = Member::currentUser();
440
		}
441
		if(!$member) {
442
			return false;
443
		}
444
		// Must be logged in to check permissions
445
446
		if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
447
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) return true;
448
		} else {
449
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) return true;
450
		}
451
452
		return $this->CanRestoreMembers()->byID($member->ID)
453
			|| $member->inGroups($this->CanRestoreGroups());
454
	}
455
456
	/**
457
	 * Allows only selected {@link Member} objects to backup this {@link DNEnvironment} to a {@link DNDataArchive}
458
	 * file.
459
	 *
460
	 * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
461
	 * @return boolean true if $member can backup, and false if they can't.
462
	 */
463 View Code Duplication
	public function canBackup($member = null) {
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...
464
		$project = $this->Project();
465
		if($project->HasDiskQuota() && $project->HasExceededDiskQuota()) {
466
			return false;
467
		}
468
469
		if(!$member) {
470
			$member = Member::currentUser();
471
		}
472
		// Must be logged in to check permissions
473
		if(!$member) {
474
			return false;
475
		}
476
477
		if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
478
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) return true;
479
		} else {
480
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) return true;
481
		}
482
483
		return $this->CanBackupMembers()->byID($member->ID)
484
			|| $member->inGroups($this->CanBackupGroups());
485
	}
486
487
	/**
488
	 * Allows only selected {@link Member} objects to upload {@link DNDataArchive} objects linked to this
489
	 * {@link DNEnvironment}.
490
	 *
491
	 * Note: This is not uploading them to the actual environment itself (e.g. uploading to the live site) - it is the
492
	 * process of uploading a *.sspak file into Deploynaut for later 'restoring' to an environment. See
493
	 * {@link self::canRestore()}.
494
	 *
495
	 * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
496
	 * @return boolean true if $member can upload archives linked to this environment, false if they can't.
497
	 */
498 View Code Duplication
	public function canUploadArchive($member = null) {
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...
499
		$project = $this->Project();
500
		if($project->HasDiskQuota() && $project->HasExceededDiskQuota()) {
501
			return false;
502
		}
503
504
		if(!$member) {
505
			$member = Member::currentUser();
506
		}
507
		if(!$member) {
508
			return false;
509
		}
510
		// Must be logged in to check permissions
511
512
		if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
513
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) return true;
514
		} else {
515
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) return true;
516
		}
517
518
		return $this->ArchiveUploaders()->byID($member->ID)
519
			|| $member->inGroups($this->ArchiveUploaderGroups());
520
	}
521
522
	/**
523
	 * Allows only selected {@link Member} objects to download {@link DNDataArchive} objects from this
524
	 * {@link DNEnvironment}.
525
	 *
526
	 * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
527
	 * @return boolean true if $member can download archives from this environment, false if they can't.
528
	 */
529 View Code Duplication
	public function canDownloadArchive($member = null) {
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...
530
		if(!$member) {
531
			$member = Member::currentUser();
532
		}
533
		if(!$member) {
534
			return false;
535
		}
536
		// Must be logged in to check permissions
537
538
		if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
539
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) return true;
540
		} else {
541
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) return true;
542
		}
543
544
		return $this->ArchiveDownloaders()->byID($member->ID)
545
			|| $member->inGroups($this->ArchiveDownloaderGroups());
546
	}
547
548
	/**
549
	 * Determine if the specified user can abort any pipelines
550
	 *
551
	 * @param Member|null $member
552
	 * @return boolean
553
	 */
554 View Code Duplication
	public function canAbort($member = null) {
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...
555
		if(!$member) {
556
			$member = Member::currentUser();
557
		}
558
		if(!$member) {
559
			return false;
560
		}
561
562
		if(Permission::checkMember($member, 'ADMIN')) {
563
			return true;
564
		}
565
566
		return $this->PipelineCancellers()->byID($member->ID)
567
			|| $member->inGroups($this->PipelineCancellerGroups());
568
	}
569
570
	/**
571
	 * Determine if the specified user can approve any pipelines
572
	 *
573
	 * @param Member|null $member
574
	 * @return boolean
575
	 */
576 View Code Duplication
	public function canApprove($member = null) {
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...
577
		if(!$member) {
578
			$member = Member::currentUser();
579
		}
580
		if(!$member) {
581
			return false;
582
		}
583
584
		if(Permission::checkMember($member, 'ADMIN')) {
585
			return true;
586
		}
587
		return $this->PipelineApprovers()->byID($member->ID)
588
			|| $member->inGroups($this->PipelineApproverGroups());
589
	}
590
591
	/**
592
	 * Allows only selected {@link Member} objects to delete {@link DNDataArchive} objects from this
593
	 * {@link DNEnvironment}.
594
	 *
595
	 * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
596
	 * @return boolean true if $member can delete archives from this environment, false if they can't.
597
	 */
598 View Code Duplication
	public function canDeleteArchive($member = null) {
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...
599
		if(!$member) {
600
			$member = Member::currentUser();
601
		}
602
		if(!$member) {
603
			return false;
604
		}
605
		// Must be logged in to check permissions
606
607
		if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
608
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) return true;
609
		} else {
610
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) return true;
611
		}
612
613
		return $this->ArchiveDeleters()->byID($member->ID)
614
			|| $member->inGroups($this->ArchiveDeleterGroups());
615
	}
616
	/**
617
	 * Get a string of groups/people that are allowed to deploy to this environment.
618
	 * Used in DNRoot_project.ss to list {@link Member}s who have permission to perform this action.
619
	 *
620
	 * @return string
621
	 */
622
	public function getDeployersList() {
623
		return implode(
624
			", ",
625
			array_merge(
626
				$this->DeployerGroups()->column("Title"),
627
				$this->Deployers()->column("FirstName")
628
			)
629
		);
630
	}
631
632
	/**
633
	 * Get a string of groups/people that are allowed to restore {@link DNDataArchive} objects into this environment.
634
	 *
635
	 * @return string
636
	 */
637
	public function getCanRestoreMembersList() {
638
		return implode(
639
			", ",
640
			array_merge(
641
				$this->CanRestoreGroups()->column("Title"),
642
				$this->CanRestoreMembers()->column("FirstName")
643
			)
644
		);
645
	}
646
647
	/**
648
	 * Get a string of groups/people that are allowed to backup {@link DNDataArchive} objects from this environment.
649
	 *
650
	 * @return string
651
	 */
652
	public function getCanBackupMembersList() {
653
		return implode(
654
			", ",
655
			array_merge(
656
				$this->CanBackupGroups()->column("Title"),
657
				$this->CanBackupMembers()->column("FirstName")
658
			)
659
		);
660
	}
661
662
	/**
663
	 * Get a string of groups/people that are allowed to upload {@link DNDataArchive}
664
	 *  objects linked to this environment.
665
	 *
666
	 * @return string
667
	 */
668
	public function getArchiveUploadersList() {
669
		return implode(
670
			", ",
671
			array_merge(
672
				$this->ArchiveUploaderGroups()->column("Title"),
673
				$this->ArchiveUploaders()->column("FirstName")
674
			)
675
		);
676
	}
677
678
	/**
679
	 * Get a string of groups/people that are allowed to download {@link DNDataArchive} objects from this environment.
680
	 *
681
	 * @return string
682
	 */
683
	public function getArchiveDownloadersList() {
684
		return implode(
685
			", ",
686
			array_merge(
687
				$this->ArchiveDownloaderGroups()->column("Title"),
688
				$this->ArchiveDownloaders()->column("FirstName")
689
			)
690
		);
691
	}
692
693
	/**
694
	 * Get a string of groups/people that are allowed to delete {@link DNDataArchive} objects from this environment.
695
	 *
696
	 * @return string
697
	 */
698
	public function getArchiveDeletersList() {
699
		return implode(
700
			", ",
701
			array_merge(
702
				$this->ArchiveDeleterGroups()->column("Title"),
703
				$this->ArchiveDeleters()->column("FirstName")
704
			)
705
		);
706
	}
707
708
	/**
709
	 * Get a string of groups/people that are allowed to approve pipelines
710
	 *
711
	 * @return string
712
	 */
713
	public function getPipelineApproversList() {
714
		return implode(
715
			", ",
716
			array_merge(
717
				$this->PipelineApproverGroups()->column("Title"),
718
				$this->PipelineApprovers()->column("FirstName")
719
			)
720
		);
721
	}
722
723
	/**
724
	 * Get a string of groups/people that are allowed to cancel pipelines
725
	 *
726
	 * @return string
727
	 */
728
	public function getPipelineCancellersList() {
729
		return implode(
730
			", ",
731
			array_merge(
732
				$this->PipelineCancellerGroups()->column("Title"),
733
				$this->PipelineCancellers()->column("FirstName")
734
			)
735
		);
736
	}
737
738
	/**
739
	 * @return DNData
740
	 */
741
	public function DNData() {
742
		return DNData::inst();
743
	}
744
745
	/**
746
	 * Get the current deployed build for this environment
747
	 *
748
	 * Dear people of the future: If you are looking to optimize this, simply create a CurrentBuildSHA(), which can be
749
	 * a lot faster. I presume you came here because of the Project display template, which only needs a SHA.
750
	 *
751
	 * @return false|DNDeployment
752
	 */
753
	public function CurrentBuild() {
754
		// The DeployHistory function is far too slow to use for this
755
756
		/** @var DNDeployment $deploy */
757
		$deploy = DNDeployment::get()->filter(array(
758
			'EnvironmentID' => $this->ID,
759
			'Status' => 'Finished'
760
		))->sort('LastEdited DESC')->first();
761
762
		if(!$deploy || (!$deploy->SHA)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $deploy->SHA of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
763
			return false;
764
		}
765
766
		$repo = $this->Project()->getRepository();
767
		if(!$repo) {
768
			return $deploy;
769
		}
770
771
		try {
772
			$commit = $repo->getCommit($deploy->SHA);
773
			if($commit) {
774
				$deploy->Message = Convert::raw2xml($commit->getMessage());
0 ignored issues
show
Documentation introduced by
The property Message does not exist on object<DNDeployment>. 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...
775
				$deploy->Committer = Convert::raw2xml($commit->getCommitterName());
0 ignored issues
show
Documentation introduced by
The property Committer does not exist on object<DNDeployment>. 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...
776
				$deploy->CommitDate = $commit->getCommitterDate()->Format('d/m/Y g:ia');
0 ignored issues
show
Documentation introduced by
The property CommitDate does not exist on object<DNDeployment>. 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...
777
				$deploy->Author = Convert::raw2xml($commit->getAuthorName());
0 ignored issues
show
Documentation introduced by
The property Author does not exist on object<DNDeployment>. 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...
778
				$deploy->AuthorDate = $commit->getAuthorDate()->Format('d/m/Y g:ia');
0 ignored issues
show
Documentation introduced by
The property AuthorDate does not exist on object<DNDeployment>. 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...
779
			}
780
			// We can't find this SHA, so we ignore adding a commit message to the deployment
781
		} catch(Exception $ex) { }
782
783
		return $deploy;
784
	}
785
786
	/**
787
	 * A history of all builds deployed to this environment
788
	 *
789
	 * @return ArrayList
790
	 */
791
	public function DeployHistory() {
792
		return $this->Deployments()
793
			->where('SHA IS NOT NULL')
794
			->sort('LastEdited DESC');
795
	}
796
797
	/**
798
	 * @param string $sha
799
	 * @return array
800
	 */
801
	protected function getCommitData($sha) {
802
		try {
803
			$repo = $this->Project()->getRepository();
804
			if($repo !== false) {
805
				$commit = new \Gitonomy\Git\Commit($repo, $sha);
806
				return [
807
					'AuthorName' => (string)Convert::raw2xml($commit->getAuthorName()),
808
					'AuthorEmail' => (string)Convert::raw2xml($commit->getAuthorEmail()),
809
					'Message' => (string)Convert::raw2xml($commit->getMessage()),
810
					'ShortHash' => Convert::raw2xml($commit->getFixedShortHash(8)),
811
					'Hash' => Convert::raw2xml($commit->getHash())
812
				];
813
			}
814
		} catch(\Gitonomy\Git\Exception\ReferenceNotFoundException $exc) {
815
			SS_Log::log($exc, SS_Log::WARN);
816
		}
817
		return array(
818
			'AuthorName' => '(unknown)',
819
			'AuthorEmail' => '(unknown)',
820
			'Message' => '(unknown)',
821
			'ShortHash' => $sha,
822
			'Hash' => '(unknown)',
823
		);
824
	}
825
826
	/**
827
	 * @return string
828
	 */
829
	public function Link() {
830
		return $this->Project()->Link() . "/environment/" . $this->Name;
831
	}
832
833
	/**
834
	 * Is this environment currently at the root level of the controller that handles it?
835
	 * @return bool
836
	 */
837
	public function isCurrent() {
838
		return $this->isSection() && Controller::curr()->getAction() == 'environment';
839
	}
840
841
	/**
842
	 * Is this environment currently in a controller that is handling it or performing a sub-task?
843
	 * @return bool
844
	 */
845
	public function isSection() {
846
		$controller = Controller::curr();
847
		$environment = $controller->getField('CurrentEnvironment');
848
		return $environment && $environment->ID == $this->ID;
849
	}
850
851
852
	/**
853
	 * Build a set of multi-select fields for assigning permissions to a pair of group and member many_many relations
854
	 *
855
	 * @param string $groupField Group field name
856
	 * @param string $memberField Member field name
857
	 * @param array $groups List of groups
858
	 * @param array $members List of members
859
	 * @return FieldGroup
860
	 */
861
	protected function buildPermissionField($groupField, $memberField, $groups, $members) {
862
		return FieldGroup::create(
863
			ListboxField::create($groupField, false, $groups)
864
				->setMultiple(true)
865
				->setAttribute('data-placeholder', 'Groups')
866
				->setAttribute('placeholder', 'Groups')
867
				->setAttribute('style', 'width: 400px;'),
868
869
			ListboxField::create($memberField, false, $members)
870
				->setMultiple(true)
871
				->setAttribute('data-placeholder', 'Members')
872
				->setAttribute('placeholder', 'Members')
873
				->setAttribute('style', 'width: 400px;')
874
		);
875
	}
876
877
	/**
878
	 * @return FieldList
879
	 */
880
	public function getCMSFields() {
881
		$fields = new FieldList(new TabSet('Root'));
882
883
		$project = $this->Project();
884
		if($project && $project->exists()) {
885
			$viewerGroups = $project->Viewers();
886
			$groups = $viewerGroups->sort('Title')->map()->toArray();
887
			$members = array();
888
			foreach($viewerGroups as $group) {
889
				foreach($group->Members()->map() as $k => $v) {
890
					$members[$k] = $v;
891
				}
892
			}
893
			asort($members);
894
		} else {
895
			$groups = array();
896
			$members = array();
897
		}
898
899
		// Main tab
900
		$fields->addFieldsToTab('Root.Main', array(
901
			// The Main.ProjectID
902
			TextField::create('ProjectName', 'Project')
903
				->setValue(($project = $this->Project()) ? $project->Name : null)
904
				->performReadonlyTransformation(),
905
906
			// The Main.Name
907
			TextField::create('Name', 'Environment name')
908
				->setDescription('A descriptive name for this environment, e.g. staging, uat, production'),
909
910
911
			$this->obj('Usage')->scaffoldFormField('Environment usage'),
912
913
			// The Main.URL field
914
			TextField::create('URL', 'Server URL')
915
				->setDescription('This url will be used to provide the front-end with a link to this environment'),
916
917
			// The Main.Filename
918
			TextField::create('Filename')
919
				->setDescription('The capistrano environment file name')
920
				->performReadonlyTransformation(),
921
		));
922
923
		// Backend identifier - pick from a named list of configurations specified in YML config
924
		$backends = $this->config()->get('allowed_backends', Config::FIRST_SET);
925
		// If there's only 1 backend, then user selection isn't needed
926
		if(sizeof($backends) > 1) {
927
			$fields->addFieldToTab('Root.Main', DropdownField::create('BackendIdentifier', 'Deployment backend')
928
				->setSource($backends)
929
				->setDescription('What kind of deployment system should be used to deploy to this environment'));
930
		}
931
932
		$fields->addFieldsToTab('Root.UserPermissions', array(
933
			// The viewers of the environment
934
			$this
935
				->buildPermissionField('ViewerGroups', 'Viewers', $groups, $members)
936
				->setTitle('Who can view this environment?')
937
				->setDescription('Groups or Users who can view this environment'),
938
939
			// The Main.Deployers
940
			$this
941
				->buildPermissionField('DeployerGroups', 'Deployers', $groups, $members)
942
				->setTitle('Who can deploy?')
943
				->setDescription('Groups or Users who can deploy to this environment'),
944
945
			// A box to select all snapshot options.
946
			$this
947
				->buildPermissionField('TickAllSnapshotGroups', 'TickAllSnapshot', $groups, $members)
948
				->setTitle("<em>All snapshot permissions</em>")
949
				->addExtraClass('tickall')
950
				->setDescription('UI shortcut to select all snapshot-related options - not written to the database.'),
951
952
			// The Main.CanRestoreMembers
953
			$this
954
				->buildPermissionField('CanRestoreGroups', 'CanRestoreMembers', $groups, $members)
955
				->setTitle('Who can restore?')
956
				->setDescription('Groups or Users who can restore archives from Deploynaut into this environment'),
957
958
			// The Main.CanBackupMembers
959
			$this
960
				->buildPermissionField('CanBackupGroups', 'CanBackupMembers', $groups, $members)
961
				->setTitle('Who can backup?')
962
				->setDescription('Groups or Users who can backup archives from this environment into Deploynaut'),
963
964
			// The Main.ArchiveDeleters
965
			$this
966
				->buildPermissionField('ArchiveDeleterGroups', 'ArchiveDeleters', $groups, $members)
967
				->setTitle('Who can delete?')
968
				->setDescription("Groups or Users who can delete archives from this environment's staging area."),
969
970
			// The Main.ArchiveUploaders
971
			$this
972
				->buildPermissionField('ArchiveUploaderGroups', 'ArchiveUploaders', $groups, $members)
973
				->setTitle('Who can upload?')
974
				->setDescription(
975
					'Users who can upload archives linked to this environment into Deploynaut.<br />' .
976
					'Linking them to an environment allows limiting download permissions (see below).'
977
				),
978
979
			// The Main.ArchiveDownloaders
980
			$this
981
				->buildPermissionField('ArchiveDownloaderGroups', 'ArchiveDownloaders', $groups, $members)
982
				->setTitle('Who can download?')
983
				->setDescription(<<<PHP
984
Users who can download archives from this environment to their computer.<br />
985
Since this implies access to the snapshot, it is also a prerequisite for restores
986
to other environments, alongside the "Who can restore" permission.<br>
987
Should include all users with upload permissions, otherwise they can't download
988
their own uploads.
989
PHP
990
				),
991
992
			// The Main.PipelineApprovers
993
			$this
994
				->buildPermissionField('PipelineApproverGroups', 'PipelineApprovers', $groups, $members)
995
				->setTitle('Who can approve pipelines?')
996
				->setDescription('Users who can approve waiting deployment pipelines.'),
997
998
			// The Main.PipelineCancellers
999
			$this
1000
				->buildPermissionField('PipelineCancellerGroups', 'PipelineCancellers', $groups, $members)
1001
				->setTitle('Who can cancel pipelines?')
1002
				->setDescription('Users who can cancel in-progess deployment pipelines.')
1003
		));
1004
1005
		// The Main.DeployConfig
1006
		if($this->Project()->exists()) {
1007
			$this->setDeployConfigurationFields($fields);
1008
		}
1009
1010
		// The DataArchives
1011
		$dataArchiveConfig = GridFieldConfig_RecordViewer::create();
1012
		$dataArchiveConfig->removeComponentsByType('GridFieldAddNewButton');
1013
		if(class_exists('GridFieldBulkManager')) {
1014
			$dataArchiveConfig->addComponent(new GridFieldBulkManager());
1015
		}
1016
		$dataArchive = GridField::create('DataArchives', 'Data Archives', $this->DataArchives(), $dataArchiveConfig);
1017
		$fields->addFieldToTab('Root.DataArchive', $dataArchive);
1018
1019
		// Pipeline templates
1020
		$this->setPipelineConfigurationFields($fields);
1021
1022
		// Pipelines
1023
		if($this->Pipelines()->Count()) {
1024
			$pipelinesConfig = GridFieldConfig_RecordEditor::create();
1025
			$pipelinesConfig->removeComponentsByType('GridFieldAddNewButton');
1026
			if(class_exists('GridFieldBulkManager')) {
1027
				$pipelinesConfig->addComponent(new GridFieldBulkManager());
1028
			}
1029
			$pipelines = GridField::create('Pipelines', 'Pipelines', $this->Pipelines(), $pipelinesConfig);
1030
			$fields->addFieldToTab('Root.Pipelines', $pipelines);
1031
		}
1032
1033
		// Deployments
1034
		$deploymentsConfig = GridFieldConfig_RecordEditor::create();
1035
		$deploymentsConfig->removeComponentsByType('GridFieldAddNewButton');
1036
		if(class_exists('GridFieldBulkManager')) {
1037
			$deploymentsConfig->addComponent(new GridFieldBulkManager());
1038
		}
1039
		$deployments = GridField::create('Deployments', 'Deployments', $this->Deployments(), $deploymentsConfig);
1040
		$fields->addFieldToTab('Root.Deployments', $deployments);
1041
1042
		Requirements::javascript('deploynaut/javascript/environment.js');
1043
1044
		// Add actions
1045
		$action = new FormAction('check', 'Check Connection');
1046
		$action->setUseButtonTag(true);
1047
		$dataURL = Director::absoluteBaseURL() . 'naut/api/' . $this->Project()->Name . '/' . $this->Name . '/ping';
1048
		$action->setAttribute('data-url', $dataURL);
1049
		$fields->insertBefore($action, 'Name');
0 ignored issues
show
Documentation introduced by
'Name' is of type string, but the function expects a object<FormField>.

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...
1050
1051
		// Allow extensions
1052
		$this->extend('updateCMSFields', $fields);
1053
		return $fields;
1054
	}
1055
1056
	/**
1057
	 * @param FieldList $fields
1058
	 */
1059
	protected function setDeployConfigurationFields(&$fields) {
1060
		if(!$this->config()->get('allow_web_editing')) {
1061
			return;
1062
		}
1063
1064
		if($this->envFileExists()) {
1065
			$deployConfig = new TextareaField('DeployConfig', 'Deploy config', $this->getEnvironmentConfig());
1066
			$deployConfig->setRows(40);
1067
			$fields->insertAfter($deployConfig, 'Filename');
0 ignored issues
show
Documentation introduced by
'Filename' is of type string, but the function expects a object<FormField>.

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...
1068
			return;
1069
		}
1070
1071
		$warning = 'Warning: This environment doesn\'t have deployment configuration.';
1072
		$noDeployConfig = new LabelField('noDeployConfig', $warning);
1073
		$noDeployConfig->addExtraClass('message warning');
1074
		$fields->insertAfter($noDeployConfig, 'Filename');
0 ignored issues
show
Documentation introduced by
'Filename' is of type string, but the function expects a object<FormField>.

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...
1075
		$createConfigField = new CheckboxField('CreateEnvConfig', 'Create Config');
1076
		$createConfigField->setDescription('Would you like to create the capistrano deploy configuration?');
1077
		$fields->insertAfter($createConfigField, 'noDeployConfig');
0 ignored issues
show
Documentation introduced by
'noDeployConfig' is of type string, but the function expects a object<FormField>.

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...
1078
	}
1079
1080
	/**
1081
	 * @param FieldList $fields
1082
	 */
1083
	protected function setPipelineConfigurationFields($fields) {
1084
		if(!$this->config()->get('allow_web_editing')) {
1085
			return;
1086
		}
1087
		$config = $this->pipelineFileExists()
1088
			? file_get_contents($this->getPipelineFilename())
1089
			: '';
1090
		$deployConfig = new TextareaField('PipelineConfig', 'Pipeline config', $config);
1091
		$deployConfig->setRows(40);
1092
		if(!$this->pipelineFileExists()) {
1093
			$deployConfig->setDescription(
1094
				"No pipeline is configured for this environment. Saving content here will generate a new template."
1095
			);
1096
		}
1097
		$fields->addFieldsToTab('Root.PipelineSettings', array(
1098
			FieldGroup::create(
1099
				CheckboxField::create('DryRunEnabled', 'Enable dry-run?')
1100
			)
1101
				->setTitle('Pipeline Options')
1102
				->setDescription(
1103
					"Allows admins to run simulated pipelines without triggering deployments or notifications."
1104
				),
1105
			$deployConfig
1106
		));
1107
	}
1108
1109
	/**
1110
	 */
1111
	public function onBeforeWrite() {
1112
		parent::onBeforeWrite();
1113
		if($this->Name && $this->Name . '.rb' != $this->Filename) {
1114
			$this->Filename = $this->Name . '.rb';
1115
		}
1116
		$this->checkEnvironmentPath();
1117
		$this->writeConfigFile();
1118
		$this->writePipelineFile();
1119
	}
1120
1121
	public function onAfterWrite() {
1122
		parent::onAfterWrite();
1123
1124
		if($this->Usage == 'Production' || $this->Usage == 'UAT') {
1125
			$conflicting = DNEnvironment::get()
1126
				->filter('ProjectID', $this->ProjectID)
1127
				->filter('Usage', $this->Usage)
1128
				->exclude('ID', $this->ID);
1129
1130
			foreach($conflicting as $otherEnvironment) {
1131
				$otherEnvironment->Usage = 'Unspecified';
1132
				$otherEnvironment->write();
1133
			}
1134
		}
1135
	}
1136
1137
1138
	/**
1139
	 * Ensure that environment paths are setup on the local filesystem
1140
	 */
1141
	protected function checkEnvironmentPath() {
1142
		// Create folder if it doesn't exist
1143
		$configDir = dirname($this->getConfigFilename());
1144
		if(!file_exists($configDir) && $configDir) {
1145
			mkdir($configDir, 0777, true);
1146
		}
1147
	}
1148
1149
	/**
1150
	 * Write the deployment config file to filesystem
1151
	 */
1152
	protected function writeConfigFile() {
1153
		if(!$this->config()->get('allow_web_editing')) {
1154
			return;
1155
		}
1156
1157
		// Create a basic new environment config from a template
1158
		if(!$this->envFileExists()
1159
			&& $this->Filename
1160
			&& $this->CreateEnvConfig
0 ignored issues
show
Documentation introduced by
The property CreateEnvConfig does not exist on object<DNEnvironment>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
1161
		) {
1162
			$templateFile = $this->config()->template_file ?: BASE_PATH . '/deploynaut/environment.template';
1163
			file_put_contents($this->getConfigFilename(), file_get_contents($templateFile));
1164
		} else if($this->envFileExists() && $this->DeployConfig) {
0 ignored issues
show
Documentation introduced by
The property DeployConfig does not exist on object<DNEnvironment>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
1165
			file_put_contents($this->getConfigFilename(), $this->DeployConfig);
0 ignored issues
show
Documentation introduced by
The property DeployConfig does not exist on object<DNEnvironment>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
1166
		}
1167
	}
1168
1169
	/**
1170
	 * Write the pipeline config file to filesystem
1171
	 */
1172
	protected function writePipelineFile() {
1173
		if(!$this->config()->get('allow_web_editing')) {
1174
			return;
1175
		}
1176
		$path = $this->getPipelineFilename();
1177
		if($this->PipelineConfig) {
0 ignored issues
show
Documentation introduced by
The property PipelineConfig does not exist on object<DNEnvironment>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
1178
			// Update file
1179
			file_put_contents($path, $this->PipelineConfig);
0 ignored issues
show
Documentation introduced by
The property PipelineConfig does not exist on object<DNEnvironment>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
1180
		} elseif($this->isChanged('PipelineConfig') && file_exists($path)) {
1181
			// Remove file if deleted
1182
			unlink($path);
1183
		}
1184
	}
1185
1186
	/**
1187
	 * Delete any related config files
1188
	 */
1189
	public function onAfterDelete() {
1190
		parent::onAfterDelete();
1191
		// Create a basic new environment config from a template
1192
		if($this->config()->get('allow_web_editing') && $this->envFileExists()) {
1193
			unlink($this->getConfigFilename());
1194
		}
1195
1196
		$create = $this->CreateEnvironment();
0 ignored issues
show
Documentation Bug introduced by
The method CreateEnvironment does not exist on object<DNEnvironment>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1197
		if($create && $create->exists()) {
1198
			$create->delete();
1199
		}
1200
	}
1201
1202
	/**
1203
	 * @return string
1204
	 */
1205
	protected function getEnvironmentConfig() {
1206
		if(!$this->envFileExists()) {
1207
			return '';
1208
		}
1209
		return file_get_contents($this->getConfigFilename());
1210
	}
1211
1212
	/**
1213
	 * @return boolean
1214
	 */
1215
	protected function envFileExists() {
1216
		if(!$this->getConfigFilename()) {
1217
			return false;
1218
		}
1219
		return file_exists($this->getConfigFilename());
1220
	}
1221
1222
	/**
1223
	 * Returns the path to the ruby config file
1224
	 *
1225
	 * @return string
1226
	 */
1227
	public function getConfigFilename() {
1228
		if(!$this->Project()->exists()) {
1229
			return '';
1230
		}
1231
		if(!$this->Filename) {
1232
			return '';
1233
		}
1234
		return $this->DNData()->getEnvironmentDir() . '/' . $this->Project()->Name . '/' . $this->Filename;
1235
	}
1236
1237
	/**
1238
	 * Returns the path to the {@link Pipeline} configuration for this environment.
1239
	 * Uses the same path and filename as the capistrano config, but with .yml extension.
1240
	 *
1241
	 * @return string
1242
	 */
1243
	public function getPipelineFilename() {
1244
		$name = $this->getConfigFilename();
1245
		if(!$name) {
1246
			return null;
1247
		}
1248
		$path = pathinfo($name);
1249
		if($path) {
1250
			return $path['dirname'] . '/' . $path['filename'] . '.yml';
1251
		}
1252
	}
1253
1254
	/**
1255
	 * Does this environment have a pipeline config file
1256
	 *
1257
	 * @return boolean
1258
	 */
1259
	protected function pipelineFileExists() {
1260
		$filename = $this->getPipelineFilename();
1261
		if(empty($filename)) {
1262
			return false;
1263
		}
1264
		return file_exists($filename);
1265
	}
1266
1267
	/**
1268
	 * Helper function to convert a multi-dimensional array (associative or indexed) to an {@link ArrayList} or
1269
	 * {@link ArrayData} object structure, so that values can be used in templates.
1270
	 *
1271
	 * @param array $array The (single- or multi-dimensional) array to convert
1272
	 * @return object Either an {@link ArrayList} or {@link ArrayData} object, or the original item ($array) if $array
1273
	 * isn't an array.
1274
	 */
1275
	public static function array_to_viewabledata($array) {
1276
		// Don't transform non-arrays
1277
		if(!is_array($array)) {
1278
			return $array;
1279
		}
1280
1281
		// Figure out whether this is indexed or associative
1282
		$keys = array_keys($array);
1283
		$assoc = ($keys != array_keys($keys));
1284
		if($assoc) {
1285
			// Treat as viewable data
1286
			$data = new ArrayData(array());
1287
			foreach($array as $key => $value) {
1288
				$data->setField($key, self::array_to_viewabledata($value));
1289
			}
1290
			return $data;
1291
		} else {
1292
			// Treat this as basic non-associative list
1293
			$list = new ArrayList();
1294
			foreach($array as $value) {
1295
				$list->push(self::array_to_viewabledata($value));
1296
			}
1297
			return $list;
1298
		}
1299
	}
1300
1301
1302
1303
	/**
1304
	 * Helper function to retrieve filtered commits from an environment
1305
	 * this environment depends on
1306
	 *
1307
	 * @return DataList
1308
	 */
1309
	public function getDependentFilteredCommits() {
1310
		// check if this environment depends on another environemnt
1311
		$dependsOnEnv = $this->DependsOnEnvironment();
1312
		if(empty($dependsOnEnv)) {
1313
			return null;
1314
		}
1315
1316
		// Check if there is a filter
1317
		$config = $this->GenericPipelineConfig();
1318
		$filter = isset($config->PipelineConfig->FilteredCommits)
1319
			? $config->PipelineConfig->FilteredCommits
1320
			: null;
1321
		if(empty($filter)) {
1322
			return null;
1323
		}
1324
1325
		// Create and execute filter
1326
		if(!class_exists($filter)) {
1327
			throw new Exception(sprintf("Class %s does not exist", $filter));
1328
		}
1329
		$commitClass = $filter::create();
1330
		// setup the environment to check for commits
1331
		$commitClass->env = $dependsOnEnv;
1332
		return $commitClass->getCommits();
1333
	}
1334
1335
	/**
1336
	 * Enable the maintenance page
1337
	 *
1338
	 * @param DeploynautLogFile $log
1339
	 */
1340
	public function enableMaintenace($log) {
1341
		$this->Backend()
1342
			->enableMaintenance($this, $log, $this->Project());
1343
	}
1344
1345
	/**
1346
	 * Disable maintenance page
1347
	 *
1348
	 * @param DeploynautLogFile $log
1349
	 */
1350
	public function disableMaintenance($log) {
1351
		$this->Backend()
1352
			->disableMaintenance($this, $log, $this->Project());
1353
	}
1354
1355
	protected function validate() {
1356
		$result = parent::validate();
1357
		$backend = $this->Backend();
1358
1359
		if(strcasecmp('test', $this->Name) === 0 && get_class($backend) == 'CapistranoDeploymentBackend') {
1360
			$result->error('"test" is not a valid environment name when using Capistrano backend.');
1361
		}
1362
1363
		return $result;
1364
	}
1365
1366
	/**
1367
	 * Fetchs all deployments in progress. Limits to 1 hour to prevent deployments
1368
	 * if an old deployment is stuck.
1369
	 *
1370
	 * @return DataList
1371
	 */
1372
	public function runningDeployments() {
1373
		return DNDeployment::get()
1374
			->filter([
1375
				'EnvironmentID' => $this->ID,
1376
				'Status' => ['Queued', 'Started'],
1377
				'Created:GreaterThan' => strtotime('-1 hour')
1378
			]);
1379
	}
1380
1381
}
1382
1383