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

DNEnvironment::getFullName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
nop 1
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
	public function Menu() {
214
		$list = new ArrayList();
215
216
		$controller = Controller::curr();
217
		$actionType = $controller->getField('CurrentActionType');
218
219
		$list->push(new ArrayData(array(
220
			'Link' => sprintf('naut/project/%s/environment/%s', $this->Project()->Name, $this->Name),
221
			'Title' => 'Deployments',
222
			'IsCurrent' => $this->isCurrent(),
223
			'IsSection' => $this->isSection() && $actionType == DNRoot::ACTION_DEPLOY
224
		)));
225
226
		$this->extend('updateMenu', $list);
227
228
		return $list;
229
	}
230
231
	/**
232
	 * Return the current object from $this->Menu()
233
	 * Good for making titles and things
234
	 */
235
	public function CurrentMenu() {
236
		return $this->Menu()->filter('IsSection', true)->First();
237
	}
238
239
	/**
240
	 * Return a name for this environment.
241
	 *
242
	 * @param string $separator The string used when concatenating project with env name
243
	 * @return string
244
	 */
245
	public function getFullName($separator = ':') {
246
		return sprintf('%s%s%s', $this->Project()->Name, $separator, $this->Name);
247
	}
248
249
	public function getBareURL() {
250
		$url = parse_url($this->URL);
251
		if(isset($url['host'])) {
252
			return strtolower($url['host']);
253
		}
254
	}
255
256
	/**
257
	 * @return boolean true if there is a pipeline for the current environment.
258
	 */
259
	public function HasPipelineSupport() {
260
		$config = $this->GenericPipelineConfig();
261
		return $config instanceof ArrayData && isset($config->Steps);
262
	}
263
264
	/**
265
	 * Returns a {@link Pipeline} object that is linked to this environment, but isn't saved into the database. This
266
	 * shouldn't be saved into the database unless you plan on starting an actual pipeline.
267
	 *
268
	 * @return Pipeline
269
	 */
270
	public function GenericPipeline() {
271
		$pipeline = Pipeline::create();
272
		$pipeline->EnvironmentID = $this->ID;
273
		return $pipeline;
274
	}
275
276
	/**
277
	 * Returns the parsed config, based on a {@link Pipeline} being created for this {@link DNEnvironment}.
278
	 *
279
	 * @return ArrayData
280
	 */
281
	public function GenericPipelineConfig() {
282
		$config = $this->loadPipelineConfig();
283
		if($config) {
284
			return self::array_to_viewabledata($config);
285
		}
286
	}
287
288
	/**
289
	 * Extract pipeline configuration data from the source yml file
290
	 *
291
	 * @return array
292
	 */
293
	public function loadPipelineConfig() {
294
		require_once 'thirdparty/spyc/spyc.php';
295
296
		$path = $this->getPipelineFilename();
297
		if(file_exists($path)) {
298
			return Spyc::YAMLLoad($path);
299
		}
300
	}
301
302
	/**
303
	 * Returns the {@link DNEnvironment} object relating to the pipeline config for this environment. The environment
304
	 * YAML file (e.g. project1-uat.yml; see docs/en/pipelines.md) contains two variable called `DependsOnProject` and
305
	 * `DependsOnEnvironment` - these are used together to find the {@link DNEnvironment} that this environment should
306
	 * rely on.
307
	 */
308
	public function DependsOnEnvironment() {
309
		if($this->HasPipelineSupport()) {
310
			$pipeline = $this->GenericPipeline();
311
			return $pipeline->getDependentEnvironment();
312
		}
313
314
		return null;
315
	}
316
317
	/**
318
	 * @return bool true if there is a currently running Pipeline, and false if there isn't
319
	 */
320
	public function HasCurrentPipeline() {
321
		return $this->CurrentPipeline() && $this->CurrentPipeline()->isInDB();
322
	}
323
324
	/**
325
	 * This can be used to determine if there is a currently running pipeline (there can only be one running per
326
	 * {@link DNEnvironment} at once), as well as getting the current pipeline to be shown in templates.
327
	 *
328
	 * @return DataObject|null The currently running pipeline, or null if there isn't any.
329
	 */
330
	public function CurrentPipeline() {
331
		return $this->Pipelines()->filter('Status', array('Running', 'Rollback'))->first();
332
	}
333
334
	/**
335
	 * @return bool true if the current user can cancel a running pipeline
336
	 */
337
	public function CanCancelPipeline() {
338
		// do we have a current pipeline
339
		if($this->HasCurrentPipeline()) {
340
			return $this->CurrentPipeline()->canAbort();
341
		}
342
		return false;
343
	}
344
345
	/**
346
	 * Environments are only viewable by people that can view the environment.
347
	 *
348
	 * @param Member|null $member
349
	 * @return boolean
350
	 */
351
	public function canView($member = null) {
352
		if(!$member) {
353
			$member = Member::currentUser();
354
		}
355
		if(!$member) {
356
			return false;
357
		}
358
		// Must be logged in to check permissions
359
360
		if(Permission::checkMember($member, 'ADMIN')) {
361
			return true;
362
		}
363
364
		// if no Viewers or ViewerGroups defined, fallback to DNProject::canView permissions
365
		if($this->Viewers()->exists() || $this->ViewerGroups()->exists()) {
366
			return $this->Viewers()->byID($member->ID)
367
				|| $member->inGroups($this->ViewerGroups());
368
		}
369
370
		return $this->Project()->canView($member);
371
	}
372
373
	/**
374
	 * Allow deploy only to some people.
375
	 *
376
	 * @param Member|null $member
377
	 * @return boolean
378
	 */
379 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...
380
		if(!$member) {
381
			$member = Member::currentUser();
382
		}
383
		if(!$member) {
384
			return false;
385
		}
386
		// Must be logged in to check permissions
387
388
		if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
389
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_DEPLOYMENT, $member)) return true;
390
		} else {
391
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_DEPLOYMENT, $member)) return true;
392
		}
393
394
		return $this->Deployers()->byID($member->ID)
395
			|| $member->inGroups($this->DeployerGroups());
396
	}
397
398
	/**
399
	 * Allows only selected {@link Member} objects to restore {@link DNDataArchive} objects into this
400
	 * {@link DNEnvironment}.
401
	 *
402
	 * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
403
	 * @return boolean true if $member can restore, and false if they can't.
404
	 */
405 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...
406
		if(!$member) {
407
			$member = Member::currentUser();
408
		}
409
		if(!$member) {
410
			return false;
411
		}
412
		// Must be logged in to check permissions
413
414
		if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
415
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) return true;
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>, but the function expects a object<Member>|null.

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...
416
		} else {
417
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) return true;
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>, but the function expects a object<Member>|null.

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...
418
		}
419
420
		return $this->CanRestoreMembers()->byID($member->ID)
421
			|| $member->inGroups($this->CanRestoreGroups());
422
	}
423
424
	/**
425
	 * Allows only selected {@link Member} objects to backup this {@link DNEnvironment} to a {@link DNDataArchive}
426
	 * file.
427
	 *
428
	 * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
429
	 * @return boolean true if $member can backup, and false if they can't.
430
	 */
431 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...
432
		$project = $this->Project();
433
		if($project->HasDiskQuota() && $project->HasExceededDiskQuota()) {
434
			return false;
435
		}
436
437
		if(!$member) {
438
			$member = Member::currentUser();
439
		}
440
		// Must be logged in to check permissions
441
		if(!$member) {
442
			return false;
443
		}
444
445
		if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
446
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) return true;
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>, but the function expects a object<Member>|null.

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...
447
		} else {
448
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) return true;
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>, but the function expects a object<Member>|null.

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...
449
		}
450
451
		return $this->CanBackupMembers()->byID($member->ID)
452
			|| $member->inGroups($this->CanBackupGroups());
453
	}
454
455
	/**
456
	 * Allows only selected {@link Member} objects to upload {@link DNDataArchive} objects linked to this
457
	 * {@link DNEnvironment}.
458
	 *
459
	 * Note: This is not uploading them to the actual environment itself (e.g. uploading to the live site) - it is the
460
	 * process of uploading a *.sspak file into Deploynaut for later 'restoring' to an environment. See
461
	 * {@link self::canRestore()}.
462
	 *
463
	 * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
464
	 * @return boolean true if $member can upload archives linked to this environment, false if they can't.
465
	 */
466 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...
467
		$project = $this->Project();
468
		if($project->HasDiskQuota() && $project->HasExceededDiskQuota()) {
469
			return false;
470
		}
471
472
		if(!$member) {
473
			$member = Member::currentUser();
474
		}
475
		if(!$member) {
476
			return false;
477
		}
478
		// Must be logged in to check permissions
479
480
		if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
481
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) return true;
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>, but the function expects a object<Member>|null.

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...
482
		} else {
483
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) return true;
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>, but the function expects a object<Member>|null.

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...
484
		}
485
486
		return $this->ArchiveUploaders()->byID($member->ID)
487
			|| $member->inGroups($this->ArchiveUploaderGroups());
488
	}
489
490
	/**
491
	 * Allows only selected {@link Member} objects to download {@link DNDataArchive} objects from this
492
	 * {@link DNEnvironment}.
493
	 *
494
	 * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
495
	 * @return boolean true if $member can download archives from this environment, false if they can't.
496
	 */
497 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...
498
		if(!$member) {
499
			$member = Member::currentUser();
500
		}
501
		if(!$member) {
502
			return false;
503
		}
504
		// Must be logged in to check permissions
505
506
		if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
507
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) return true;
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>, but the function expects a object<Member>|null.

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...
508
		} else {
509
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) return true;
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>, but the function expects a object<Member>|null.

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...
510
		}
511
512
		return $this->ArchiveDownloaders()->byID($member->ID)
513
			|| $member->inGroups($this->ArchiveDownloaderGroups());
514
	}
515
516
	/**
517
	 * Determine if the specified user can abort any pipelines
518
	 *
519
	 * @param Member|null $member
520
	 * @return boolean
521
	 */
522 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...
523
		if(!$member) {
524
			$member = Member::currentUser();
525
		}
526
		if(!$member) {
527
			return false;
528
		}
529
530
		if(Permission::checkMember($member, 'ADMIN')) {
531
			return true;
532
		}
533
534
		return $this->PipelineCancellers()->byID($member->ID)
535
			|| $member->inGroups($this->PipelineCancellerGroups());
536
	}
537
538
	/**
539
	 * Determine if the specified user can approve any pipelines
540
	 *
541
	 * @param Member|null $member
542
	 * @return boolean
543
	 */
544 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...
545
		if(!$member) {
546
			$member = Member::currentUser();
547
		}
548
		if(!$member) {
549
			return false;
550
		}
551
552
		if(Permission::checkMember($member, 'ADMIN')) {
553
			return true;
554
		}
555
		return $this->PipelineApprovers()->byID($member->ID)
556
			|| $member->inGroups($this->PipelineApproverGroups());
557
	}
558
559
	/**
560
	 * Allows only selected {@link Member} objects to delete {@link DNDataArchive} objects from this
561
	 * {@link DNEnvironment}.
562
	 *
563
	 * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
564
	 * @return boolean true if $member can delete archives from this environment, false if they can't.
565
	 */
566 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...
567
		if(!$member) {
568
			$member = Member::currentUser();
569
		}
570
		if(!$member) {
571
			return false;
572
		}
573
		// Must be logged in to check permissions
574
575
		if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
576
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) return true;
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>, but the function expects a object<Member>|null.

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...
577
		} else {
578
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) return true;
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>, but the function expects a object<Member>|null.

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...
579
		}
580
581
		return $this->ArchiveDeleters()->byID($member->ID)
582
			|| $member->inGroups($this->ArchiveDeleterGroups());
583
	}
584
	/**
585
	 * Get a string of groups/people that are allowed to deploy to this environment.
586
	 * Used in DNRoot_project.ss to list {@link Member}s who have permission to perform this action.
587
	 *
588
	 * @return string
589
	 */
590
	public function getDeployersList() {
591
		return implode(
592
			", ",
593
			array_merge(
594
				$this->DeployerGroups()->column("Title"),
595
				$this->Deployers()->column("FirstName")
596
			)
597
		);
598
	}
599
600
	/**
601
	 * Get a string of groups/people that are allowed to restore {@link DNDataArchive} objects into this environment.
602
	 *
603
	 * @return string
604
	 */
605
	public function getCanRestoreMembersList() {
606
		return implode(
607
			", ",
608
			array_merge(
609
				$this->CanRestoreGroups()->column("Title"),
610
				$this->CanRestoreMembers()->column("FirstName")
611
			)
612
		);
613
	}
614
615
	/**
616
	 * Get a string of groups/people that are allowed to backup {@link DNDataArchive} objects from this environment.
617
	 *
618
	 * @return string
619
	 */
620
	public function getCanBackupMembersList() {
621
		return implode(
622
			", ",
623
			array_merge(
624
				$this->CanBackupGroups()->column("Title"),
625
				$this->CanBackupMembers()->column("FirstName")
626
			)
627
		);
628
	}
629
630
	/**
631
	 * Get a string of groups/people that are allowed to upload {@link DNDataArchive}
632
	 *  objects linked to this environment.
633
	 *
634
	 * @return string
635
	 */
636
	public function getArchiveUploadersList() {
637
		return implode(
638
			", ",
639
			array_merge(
640
				$this->ArchiveUploaderGroups()->column("Title"),
641
				$this->ArchiveUploaders()->column("FirstName")
642
			)
643
		);
644
	}
645
646
	/**
647
	 * Get a string of groups/people that are allowed to download {@link DNDataArchive} objects from this environment.
648
	 *
649
	 * @return string
650
	 */
651
	public function getArchiveDownloadersList() {
652
		return implode(
653
			", ",
654
			array_merge(
655
				$this->ArchiveDownloaderGroups()->column("Title"),
656
				$this->ArchiveDownloaders()->column("FirstName")
657
			)
658
		);
659
	}
660
661
	/**
662
	 * Get a string of groups/people that are allowed to delete {@link DNDataArchive} objects from this environment.
663
	 *
664
	 * @return string
665
	 */
666
	public function getArchiveDeletersList() {
667
		return implode(
668
			", ",
669
			array_merge(
670
				$this->ArchiveDeleterGroups()->column("Title"),
671
				$this->ArchiveDeleters()->column("FirstName")
672
			)
673
		);
674
	}
675
676
	/**
677
	 * Get a string of groups/people that are allowed to approve pipelines
678
	 *
679
	 * @return string
680
	 */
681
	public function getPipelineApproversList() {
682
		return implode(
683
			", ",
684
			array_merge(
685
				$this->PipelineApproverGroups()->column("Title"),
686
				$this->PipelineApprovers()->column("FirstName")
687
			)
688
		);
689
	}
690
691
	/**
692
	 * Get a string of groups/people that are allowed to cancel pipelines
693
	 *
694
	 * @return string
695
	 */
696
	public function getPipelineCancellersList() {
697
		return implode(
698
			", ",
699
			array_merge(
700
				$this->PipelineCancellerGroups()->column("Title"),
701
				$this->PipelineCancellers()->column("FirstName")
702
			)
703
		);
704
	}
705
706
	/**
707
	 * @return DNData
708
	 */
709
	public function DNData() {
710
		return DNData::inst();
711
	}
712
713
	/**
714
	 * Get the current deployed build for this environment
715
	 *
716
	 * Dear people of the future: If you are looking to optimize this, simply create a CurrentBuildSHA(), which can be
717
	 * a lot faster. I presume you came here because of the Project display template, which only needs a SHA.
718
	 *
719
	 * @return false|DNDeployment
720
	 */
721
	public function CurrentBuild() {
722
		// The DeployHistory function is far too slow to use for this
723
724
		/** @var DNDeployment $deploy */
725
		$deploy = DNDeployment::get()->filter(array(
726
			'EnvironmentID' => $this->ID,
727
			'Status' => 'Finished'
728
		))->sort('LastEdited DESC')->first();
729
730
		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...
731
			return false;
732
		}
733
734
		$repo = $this->Project()->getRepository();
735
		if(!$repo) {
736
			return $deploy;
737
		}
738
739
		try {
740
			$commit = $repo->getCommit($deploy->SHA);
741
			if($commit) {
742
				$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...
743
				$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...
744
				$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...
745
				$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...
746
				$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...
747
			}
748
			// We can't find this SHA, so we ignore adding a commit message to the deployment
749
		} catch(Exception $ex) { }
750
751
		return $deploy;
752
	}
753
754
	/**
755
	 * A history of all builds deployed to this environment
756
	 *
757
	 * @return ArrayList
758
	 */
759
	public function DeployHistory() {
760
		return $this->Deployments()
761
			->where('SHA IS NOT NULL')
762
			->sort('LastEdited DESC');
763
	}
764
765
	/**
766
	 * @param string $sha
767
	 * @return array
768
	 */
769
	protected function getCommitData($sha) {
770
		try {
771
			$repo = $this->Project()->getRepository();
772
			if($repo !== false) {
773
				$commit = new \Gitonomy\Git\Commit($repo, $sha);
774
				return [
775
					'AuthorName' => (string)Convert::raw2xml($commit->getAuthorName()),
776
					'AuthorEmail' => (string)Convert::raw2xml($commit->getAuthorEmail()),
777
					'Message' => (string)Convert::raw2xml($commit->getMessage()),
778
					'ShortHash' => Convert::raw2xml($commit->getFixedShortHash(8)),
779
					'Hash' => Convert::raw2xml($commit->getHash())
780
				];
781
			}
782
		} catch(\Gitonomy\Git\Exception\ReferenceNotFoundException $exc) {
783
			SS_Log::log($exc, SS_Log::WARN);
784
		}
785
		return array(
786
			'AuthorName' => '(unknown)',
787
			'AuthorEmail' => '(unknown)',
788
			'Message' => '(unknown)',
789
			'ShortHash' => $sha,
790
			'Hash' => '(unknown)',
791
		);
792
	}
793
794
	/**
795
	 * @return string
796
	 */
797
	public function Link() {
798
		return $this->Project()->Link() . "/environment/" . $this->Name;
799
	}
800
801
	/**
802
	 * Is this environment currently at the root level of the controller that handles it?
803
	 * @return bool
804
	 */
805
	public function isCurrent() {
806
		return $this->isSection() && Controller::curr()->getAction() == 'environment';
807
	}
808
809
	/**
810
	 * Is this environment currently in a controller that is handling it or performing a sub-task?
811
	 * @return bool
812
	 */
813
	public function isSection() {
814
		$controller = Controller::curr();
815
		$environment = $controller->getField('CurrentEnvironment');
816
		return $environment && $environment->ID == $this->ID;
817
	}
818
819
820
	/**
821
	 * Build a set of multi-select fields for assigning permissions to a pair of group and member many_many relations
822
	 *
823
	 * @param string $groupField Group field name
824
	 * @param string $memberField Member field name
825
	 * @param array $groups List of groups
826
	 * @param array $members List of members
827
	 * @return FieldGroup
828
	 */
829
	protected function buildPermissionField($groupField, $memberField, $groups, $members) {
830
		return FieldGroup::create(
831
			ListboxField::create($groupField, false, $groups)
832
				->setMultiple(true)
833
				->setAttribute('data-placeholder', 'Groups')
834
				->setAttribute('placeholder', 'Groups')
835
				->setAttribute('style', 'width: 400px;'),
836
837
			ListboxField::create($memberField, false, $members)
838
				->setMultiple(true)
839
				->setAttribute('data-placeholder', 'Members')
840
				->setAttribute('placeholder', 'Members')
841
				->setAttribute('style', 'width: 400px;')
842
		);
843
	}
844
845
	/**
846
	 * @return FieldList
847
	 */
848
	public function getCMSFields() {
849
		$fields = new FieldList(new TabSet('Root'));
850
851
		$project = $this->Project();
852
		if($project && $project->exists()) {
853
			$viewerGroups = $project->Viewers();
854
			$groups = $viewerGroups->sort('Title')->map()->toArray();
855
			$members = array();
856
			foreach($viewerGroups as $group) {
857
				foreach($group->Members()->map() as $k => $v) {
858
					$members[$k] = $v;
859
				}
860
			}
861
			asort($members);
862
		} else {
863
			$groups = array();
864
			$members = array();
865
		}
866
867
		// Main tab
868
		$fields->addFieldsToTab('Root.Main', array(
869
			// The Main.ProjectID
870
			TextField::create('ProjectName', 'Project')
871
				->setValue(($project = $this->Project()) ? $project->Name : null)
872
				->performReadonlyTransformation(),
873
874
			// The Main.Name
875
			TextField::create('Name', 'Environment name')
876
				->setDescription('A descriptive name for this environment, e.g. staging, uat, production'),
877
878
879
			$this->obj('Usage')->scaffoldFormField('Environment usage'),
880
881
			// The Main.URL field
882
			TextField::create('URL', 'Server URL')
883
				->setDescription('This url will be used to provide the front-end with a link to this environment'),
884
885
			// The Main.Filename
886
			TextField::create('Filename')
887
				->setDescription('The capistrano environment file name')
888
				->performReadonlyTransformation(),
889
		));
890
891
		// Backend identifier - pick from a named list of configurations specified in YML config
892
		$backends = $this->config()->get('allowed_backends', Config::FIRST_SET);
893
		// If there's only 1 backend, then user selection isn't needed
894
		if(sizeof($backends) > 1) {
895
			$fields->addFieldToTab('Root.Main', DropdownField::create('BackendIdentifier', 'Deployment backend')
896
				->setSource($backends)
897
				->setDescription('What kind of deployment system should be used to deploy to this environment'));
898
		}
899
900
		$fields->addFieldsToTab('Root.UserPermissions', array(
901
			// The viewers of the environment
902
			$this
903
				->buildPermissionField('ViewerGroups', 'Viewers', $groups, $members)
904
				->setTitle('Who can view this environment?')
905
				->setDescription('Groups or Users who can view this environment'),
906
907
			// The Main.Deployers
908
			$this
909
				->buildPermissionField('DeployerGroups', 'Deployers', $groups, $members)
910
				->setTitle('Who can deploy?')
911
				->setDescription('Groups or Users who can deploy to this environment'),
912
913
			// A box to select all snapshot options.
914
			$this
915
				->buildPermissionField('TickAllSnapshotGroups', 'TickAllSnapshot', $groups, $members)
916
				->setTitle("<em>All snapshot permissions</em>")
917
				->addExtraClass('tickall')
918
				->setDescription('UI shortcut to select all snapshot-related options - not written to the database.'),
919
920
			// The Main.CanRestoreMembers
921
			$this
922
				->buildPermissionField('CanRestoreGroups', 'CanRestoreMembers', $groups, $members)
923
				->setTitle('Who can restore?')
924
				->setDescription('Groups or Users who can restore archives from Deploynaut into this environment'),
925
926
			// The Main.CanBackupMembers
927
			$this
928
				->buildPermissionField('CanBackupGroups', 'CanBackupMembers', $groups, $members)
929
				->setTitle('Who can backup?')
930
				->setDescription('Groups or Users who can backup archives from this environment into Deploynaut'),
931
932
			// The Main.ArchiveDeleters
933
			$this
934
				->buildPermissionField('ArchiveDeleterGroups', 'ArchiveDeleters', $groups, $members)
935
				->setTitle('Who can delete?')
936
				->setDescription("Groups or Users who can delete archives from this environment's staging area."),
937
938
			// The Main.ArchiveUploaders
939
			$this
940
				->buildPermissionField('ArchiveUploaderGroups', 'ArchiveUploaders', $groups, $members)
941
				->setTitle('Who can upload?')
942
				->setDescription(
943
					'Users who can upload archives linked to this environment into Deploynaut.<br />' .
944
					'Linking them to an environment allows limiting download permissions (see below).'
945
				),
946
947
			// The Main.ArchiveDownloaders
948
			$this
949
				->buildPermissionField('ArchiveDownloaderGroups', 'ArchiveDownloaders', $groups, $members)
950
				->setTitle('Who can download?')
951
				->setDescription(<<<PHP
952
Users who can download archives from this environment to their computer.<br />
953
Since this implies access to the snapshot, it is also a prerequisite for restores
954
to other environments, alongside the "Who can restore" permission.<br>
955
Should include all users with upload permissions, otherwise they can't download
956
their own uploads.
957
PHP
958
				),
959
960
			// The Main.PipelineApprovers
961
			$this
962
				->buildPermissionField('PipelineApproverGroups', 'PipelineApprovers', $groups, $members)
963
				->setTitle('Who can approve pipelines?')
964
				->setDescription('Users who can approve waiting deployment pipelines.'),
965
966
			// The Main.PipelineCancellers
967
			$this
968
				->buildPermissionField('PipelineCancellerGroups', 'PipelineCancellers', $groups, $members)
969
				->setTitle('Who can cancel pipelines?')
970
				->setDescription('Users who can cancel in-progess deployment pipelines.')
971
		));
972
973
		// The Main.DeployConfig
974
		if($this->Project()->exists()) {
975
			$this->setDeployConfigurationFields($fields);
976
		}
977
978
		// The DataArchives
979
		$dataArchiveConfig = GridFieldConfig_RecordViewer::create();
980
		$dataArchiveConfig->removeComponentsByType('GridFieldAddNewButton');
981
		if(class_exists('GridFieldBulkManager')) {
982
			$dataArchiveConfig->addComponent(new GridFieldBulkManager());
983
		}
984
		$dataArchive = GridField::create('DataArchives', 'Data Archives', $this->DataArchives(), $dataArchiveConfig);
985
		$fields->addFieldToTab('Root.DataArchive', $dataArchive);
986
987
		// Pipeline templates
988
		$this->setPipelineConfigurationFields($fields);
989
990
		// Pipelines
991
		if($this->Pipelines()->Count()) {
992
			$pipelinesConfig = GridFieldConfig_RecordEditor::create();
993
			$pipelinesConfig->removeComponentsByType('GridFieldAddNewButton');
994
			if(class_exists('GridFieldBulkManager')) {
995
				$pipelinesConfig->addComponent(new GridFieldBulkManager());
996
			}
997
			$pipelines = GridField::create('Pipelines', 'Pipelines', $this->Pipelines(), $pipelinesConfig);
998
			$fields->addFieldToTab('Root.Pipelines', $pipelines);
999
		}
1000
1001
		// Deployments
1002
		$deploymentsConfig = GridFieldConfig_RecordEditor::create();
1003
		$deploymentsConfig->removeComponentsByType('GridFieldAddNewButton');
1004
		if(class_exists('GridFieldBulkManager')) {
1005
			$deploymentsConfig->addComponent(new GridFieldBulkManager());
1006
		}
1007
		$deployments = GridField::create('Deployments', 'Deployments', $this->Deployments(), $deploymentsConfig);
1008
		$fields->addFieldToTab('Root.Deployments', $deployments);
1009
1010
		Requirements::javascript('deploynaut/javascript/environment.js');
1011
1012
		// Add actions
1013
		$action = new FormAction('check', 'Check Connection');
1014
		$action->setUseButtonTag(true);
1015
		$dataURL = Director::absoluteBaseURL() . 'naut/api/' . $this->Project()->Name . '/' . $this->Name . '/ping';
1016
		$action->setAttribute('data-url', $dataURL);
1017
		$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...
1018
1019
		// Allow extensions
1020
		$this->extend('updateCMSFields', $fields);
1021
		return $fields;
1022
	}
1023
1024
	/**
1025
	 * @param FieldList $fields
1026
	 */
1027
	protected function setDeployConfigurationFields(&$fields) {
1028
		if(!$this->config()->get('allow_web_editing')) {
1029
			return;
1030
		}
1031
1032
		if($this->envFileExists()) {
1033
			$deployConfig = new TextareaField('DeployConfig', 'Deploy config', $this->getEnvironmentConfig());
1034
			$deployConfig->setRows(40);
1035
			$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...
1036
			return;
1037
		}
1038
1039
		$warning = 'Warning: This environment doesn\'t have deployment configuration.';
1040
		$noDeployConfig = new LabelField('noDeployConfig', $warning);
1041
		$noDeployConfig->addExtraClass('message warning');
1042
		$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...
1043
		$createConfigField = new CheckboxField('CreateEnvConfig', 'Create Config');
1044
		$createConfigField->setDescription('Would you like to create the capistrano deploy configuration?');
1045
		$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...
1046
	}
1047
1048
	/**
1049
	 * @param FieldList $fields
1050
	 */
1051
	protected function setPipelineConfigurationFields($fields) {
1052
		if(!$this->config()->get('allow_web_editing')) {
1053
			return;
1054
		}
1055
		$config = $this->pipelineFileExists()
1056
			? file_get_contents($this->getPipelineFilename())
1057
			: '';
1058
		$deployConfig = new TextareaField('PipelineConfig', 'Pipeline config', $config);
1059
		$deployConfig->setRows(40);
1060
		if(!$this->pipelineFileExists()) {
1061
			$deployConfig->setDescription(
1062
				"No pipeline is configured for this environment. Saving content here will generate a new template."
1063
			);
1064
		}
1065
		$fields->addFieldsToTab('Root.PipelineSettings', array(
1066
			FieldGroup::create(
1067
				CheckboxField::create('DryRunEnabled', 'Enable dry-run?')
1068
			)
1069
				->setTitle('Pipeline Options')
1070
				->setDescription(
1071
					"Allows admins to run simulated pipelines without triggering deployments or notifications."
1072
				),
1073
			$deployConfig
1074
		));
1075
	}
1076
1077
	/**
1078
	 */
1079
	public function onBeforeWrite() {
1080
		parent::onBeforeWrite();
1081
		if($this->Name && $this->Name . '.rb' != $this->Filename) {
1082
			$this->Filename = $this->Name . '.rb';
1083
		}
1084
		$this->checkEnvironmentPath();
1085
		$this->writeConfigFile();
1086
		$this->writePipelineFile();
1087
	}
1088
1089
	public function onAfterWrite() {
1090
		parent::onAfterWrite();
1091
1092
		if($this->Usage == 'Production' || $this->Usage == 'UAT') {
1093
			$conflicting = DNEnvironment::get()
1094
				->filter('ProjectID', $this->ProjectID)
1095
				->filter('Usage', $this->Usage)
1096
				->exclude('ID', $this->ID);
1097
1098
			foreach($conflicting as $otherEnvironment) {
1099
				$otherEnvironment->Usage = 'Unspecified';
1100
				$otherEnvironment->write();
1101
			}
1102
		}
1103
	}
1104
1105
1106
	/**
1107
	 * Ensure that environment paths are setup on the local filesystem
1108
	 */
1109
	protected function checkEnvironmentPath() {
1110
		// Create folder if it doesn't exist
1111
		$configDir = dirname($this->getConfigFilename());
1112
		if(!file_exists($configDir) && $configDir) {
1113
			mkdir($configDir, 0777, true);
1114
		}
1115
	}
1116
1117
	/**
1118
	 * Write the deployment config file to filesystem
1119
	 */
1120
	protected function writeConfigFile() {
1121
		if(!$this->config()->get('allow_web_editing')) {
1122
			return;
1123
		}
1124
1125
		// Create a basic new environment config from a template
1126
		if(!$this->envFileExists()
1127
			&& $this->Filename
1128
			&& $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...
1129
		) {
1130
			$templateFile = $this->config()->template_file ?: BASE_PATH . '/deploynaut/environment.template';
1131
			file_put_contents($this->getConfigFilename(), file_get_contents($templateFile));
1132
		} 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...
1133
			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...
1134
		}
1135
	}
1136
1137
	/**
1138
	 * Write the pipeline config file to filesystem
1139
	 */
1140
	protected function writePipelineFile() {
1141
		if(!$this->config()->get('allow_web_editing')) {
1142
			return;
1143
		}
1144
		$path = $this->getPipelineFilename();
1145
		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...
1146
			// Update file
1147
			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...
1148
		} elseif($this->isChanged('PipelineConfig') && file_exists($path)) {
1149
			// Remove file if deleted
1150
			unlink($path);
1151
		}
1152
	}
1153
1154
	/**
1155
	 * Delete any related config files
1156
	 */
1157
	public function onAfterDelete() {
1158
		parent::onAfterDelete();
1159
		// Create a basic new environment config from a template
1160
		if($this->config()->get('allow_web_editing') && $this->envFileExists()) {
1161
			unlink($this->getConfigFilename());
1162
		}
1163
1164
		$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...
1165
		if($create && $create->exists()) {
1166
			$create->delete();
1167
		}
1168
	}
1169
1170
	/**
1171
	 * @return string
1172
	 */
1173
	protected function getEnvironmentConfig() {
1174
		if(!$this->envFileExists()) {
1175
			return '';
1176
		}
1177
		return file_get_contents($this->getConfigFilename());
1178
	}
1179
1180
	/**
1181
	 * @return boolean
1182
	 */
1183
	protected function envFileExists() {
1184
		if(!$this->getConfigFilename()) {
1185
			return false;
1186
		}
1187
		return file_exists($this->getConfigFilename());
1188
	}
1189
1190
	/**
1191
	 * Returns the path to the ruby config file
1192
	 *
1193
	 * @return string
1194
	 */
1195
	public function getConfigFilename() {
1196
		if(!$this->Project()->exists()) {
1197
			return '';
1198
		}
1199
		if(!$this->Filename) {
1200
			return '';
1201
		}
1202
		return $this->DNData()->getEnvironmentDir() . '/' . $this->Project()->Name . '/' . $this->Filename;
1203
	}
1204
1205
	/**
1206
	 * Returns the path to the {@link Pipeline} configuration for this environment.
1207
	 * Uses the same path and filename as the capistrano config, but with .yml extension.
1208
	 *
1209
	 * @return string
1210
	 */
1211
	public function getPipelineFilename() {
1212
		$name = $this->getConfigFilename();
1213
		if(!$name) {
1214
			return null;
1215
		}
1216
		$path = pathinfo($name);
1217
		if($path) {
1218
			return $path['dirname'] . '/' . $path['filename'] . '.yml';
1219
		}
1220
	}
1221
1222
	/**
1223
	 * Does this environment have a pipeline config file
1224
	 *
1225
	 * @return boolean
1226
	 */
1227
	protected function pipelineFileExists() {
1228
		$filename = $this->getPipelineFilename();
1229
		if(empty($filename)) {
1230
			return false;
1231
		}
1232
		return file_exists($filename);
1233
	}
1234
1235
	/**
1236
	 * Helper function to convert a multi-dimensional array (associative or indexed) to an {@link ArrayList} or
1237
	 * {@link ArrayData} object structure, so that values can be used in templates.
1238
	 *
1239
	 * @param array $array The (single- or multi-dimensional) array to convert
1240
	 * @return object Either an {@link ArrayList} or {@link ArrayData} object, or the original item ($array) if $array
1241
	 * isn't an array.
1242
	 */
1243
	public static function array_to_viewabledata($array) {
1244
		// Don't transform non-arrays
1245
		if(!is_array($array)) {
1246
			return $array;
1247
		}
1248
1249
		// Figure out whether this is indexed or associative
1250
		$keys = array_keys($array);
1251
		$assoc = ($keys != array_keys($keys));
1252
		if($assoc) {
1253
			// Treat as viewable data
1254
			$data = new ArrayData(array());
1255
			foreach($array as $key => $value) {
1256
				$data->setField($key, self::array_to_viewabledata($value));
1257
			}
1258
			return $data;
1259
		} else {
1260
			// Treat this as basic non-associative list
1261
			$list = new ArrayList();
1262
			foreach($array as $value) {
1263
				$list->push(self::array_to_viewabledata($value));
1264
			}
1265
			return $list;
1266
		}
1267
	}
1268
1269
1270
1271
	/**
1272
	 * Helper function to retrieve filtered commits from an environment
1273
	 * this environment depends on
1274
	 *
1275
	 * @return DataList
1276
	 */
1277
	public function getDependentFilteredCommits() {
1278
		// check if this environment depends on another environemnt
1279
		$dependsOnEnv = $this->DependsOnEnvironment();
1280
		if(empty($dependsOnEnv)) {
1281
			return null;
1282
		}
1283
1284
		// Check if there is a filter
1285
		$config = $this->GenericPipelineConfig();
1286
		$filter = isset($config->PipelineConfig->FilteredCommits)
1287
			? $config->PipelineConfig->FilteredCommits
1288
			: null;
1289
		if(empty($filter)) {
1290
			return null;
1291
		}
1292
1293
		// Create and execute filter
1294
		if(!class_exists($filter)) {
1295
			throw new Exception(sprintf("Class %s does not exist", $filter));
1296
		}
1297
		$commitClass = $filter::create();
1298
		// setup the environment to check for commits
1299
		$commitClass->env = $dependsOnEnv;
1300
		return $commitClass->getCommits();
1301
	}
1302
1303
	/**
1304
	 * Enable the maintenance page
1305
	 *
1306
	 * @param DeploynautLogFile $log
1307
	 */
1308
	public function enableMaintenace($log) {
1309
		$this->Backend()
1310
			->enableMaintenance($this, $log, $this->Project());
1311
	}
1312
1313
	/**
1314
	 * Disable maintenance page
1315
	 *
1316
	 * @param DeploynautLogFile $log
1317
	 */
1318
	public function disableMaintenance($log) {
1319
		$this->Backend()
1320
			->disableMaintenance($this, $log, $this->Project());
1321
	}
1322
1323
	protected function validate() {
1324
		$result = parent::validate();
1325
		$backend = $this->Backend();
1326
1327
		if(strcasecmp('test', $this->Name) === 0 && get_class($backend) == 'CapistranoDeploymentBackend') {
1328
			$result->error('"test" is not a valid environment name when using Capistrano backend.');
1329
		}
1330
1331
		return $result;
1332
	}
1333
1334
}
1335