Issues (524)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

code/model/DNEnvironment.php (56 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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 $Usage
14
 *
15
 * @method DNProject Project()
16
 * @property int $ProjectID
17
 *
18
 * @method HasManyList Deployments()
19
 * @method HasManyList DataArchives()
20
 *
21
 * @method ManyManyList Viewers()
22
 * @method ManyManyList ViewerGroups()
23
 * @method ManyManyList Deployers()
24
 * @method ManyManyList DeployerGroups()
25
 * @method ManyManyList CanRestoreMembers()
26
 * @method ManyManyList CanRestoreGroups()
27
 * @method ManyManyList CanBackupMembers()
28
 * @method ManyManyList CanBackupGroups()
29
 * @method ManyManyList ArchiveUploaders()
30
 * @method ManyManyList ArchiveUploaderGroups()
31
 * @method ManyManyList ArchiveDownloaders()
32
 * @method ManyManyList ArchiveDownloaderGroups()
33
 * @method ManyManyList ArchiveDeleters()
34
 * @method ManyManyList ArchiveDeleterGroups()
35
 */
36
class DNEnvironment extends DataObject {
0 ignored issues
show
The property $template_file is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
The property $allow_web_editing is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
The property $allowed_backends is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
The property $has_one is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
The property $has_many is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
The property $many_many is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
The property $summary_fields is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
The property $singular_name is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
The property $plural_name is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
The property $searchable_fields is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
The property $default_sort is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
37
38
	const UAT = 'UAT';
39
40
	const PRODUCTION = 'Production';
41
42
	const UNSPECIFIED = 'Unspecified';
43
44
	/**
45
	 * @var array
46
	 */
47
	public static $db = [
48
		"Filename" => "Varchar(255)",
49
		"Name" => "Varchar(255)",
50
		"URL" => "Varchar(255)",
51
		"BackendIdentifier" => "Varchar(255)", // Injector identifier of the DeploymentBackend
52
		"Usage" => "Enum('Production, UAT, Test, Unspecified', 'Unspecified')",
53
	];
54
55
	/**
56
	 * @var array
57
	 */
58
	public static $has_many = [
59
		"Deployments" => "DNDeployment",
60
		"DataArchives" => "DNDataArchive",
61
		"DataTransfers" => "DNDataTransfer",
62
		"Pings" => "DNPing"
63
	];
64
65
	/**
66
	 * @var array
67
	 */
68
	public static $many_many = [
69
		"Viewers" => "Member", // Who can view this environment
70
		"ViewerGroups" => "Group",
71
		"Deployers" => "Member", // Who can deploy to this environment
72
		"DeployerGroups" => "Group",
73
		"CanRestoreMembers" => "Member", // Who can restore archive files to this environment
74
		"CanRestoreGroups" => "Group",
75
		"CanBackupMembers" => "Member", // Who can backup archive files from this environment
76
		"CanBackupGroups" => "Group",
77
		"ArchiveUploaders" => "Member", // Who can upload archive files linked to this environment
78
		"ArchiveUploaderGroups" => "Group",
79
		"ArchiveDownloaders" => "Member", // Who can download archive files from this environment
80
		"ArchiveDownloaderGroups" => "Group",
81
		"ArchiveDeleters" => "Member", // Who can delete archive files from this environment,
82
		"ArchiveDeleterGroups" => "Group",
83
	];
84
85
	/**
86
	 * @var array
87
	 */
88
	public static $summary_fields = [
89
		"Name" => "Environment Name",
90
		"Usage" => "Usage",
91
		"URL" => "URL",
92
		"DeployersList" => "Can Deploy List",
93
		"CanRestoreMembersList" => "Can Restore List",
94
		"CanBackupMembersList" => "Can Backup List",
95
		"ArchiveUploadersList" => "Can Upload List",
96
		"ArchiveDownloadersList" => "Can Download List",
97
		"ArchiveDeletersList" => "Can Delete List",
98
	];
99
100
	/**
101
	 * @var array
102
	 */
103
	public static $searchable_fields = [
104
		"Name",
105
	];
106
107
	private static $singular_name = 'Capistrano Environment';
0 ignored issues
show
The property $singular_name is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
108
109
	private static $plural_name = 'Capistrano Environments';
110
111
	/**
112
	 * @var string
113
	 */
114
	private static $default_sort = 'Name';
115
116
	/**
117
	 * @var array
118
	 */
119
	public static $has_one = [
120
		"Project" => "DNProject",
121
		"CreateEnvironment" => "DNCreateEnvironment"
122
	];
123
124
	/**
125
	 * If this is set to a full pathfile, it will be used as template
126
	 * file when creating a new capistrano environment config file.
127
	 *
128
	 * If not set, the default 'environment.template' from the module
129
	 * root is used
130
	 *
131
	 * @config
132
	 * @var string
133
	 */
134
	private static $template_file = '';
0 ignored issues
show
The property $template_file is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
135
136
	/**
137
	 * Set this to true to allow editing of the environment files via the web admin
138
	 *
139
	 * @var bool
140
	 */
141
	private static $allow_web_editing = false;
0 ignored issues
show
The property $allow_web_editing is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
142
143
	/**
144
	 * @var array
145
	 */
146
	private static $casting = [
0 ignored issues
show
The property $casting is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
147
		'DeployHistory' => 'Text'
148
	];
149
150
	/**
151
	 * Allowed backends. A map of Injector identifier to human-readable label.
152
	 *
153
	 * @config
154
	 * @var array
155
	 */
156
	private static $allowed_backends = [];
0 ignored issues
show
The property $allowed_backends is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
157
158
	/**
159
	 * Used by the sync task
160
	 *
161
	 * @param string $path
162
	 * @return \DNEnvironment
163
	 */
164
	public static function create_from_path($path) {
165
		$e = DNEnvironment::create();
0 ignored issues
show
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
166
		$e->Filename = $path;
167
		$e->Name = basename($e->Filename, '.rb');
168
169
		// add each administrator member as a deployer of the new environment
170
		$adminGroup = Group::get()->filter('Code', 'administrators')->first();
171
		$e->DeployerGroups()->add($adminGroup);
0 ignored issues
show
It seems like $adminGroup defined by \Group::get()->filter('C...ministrators')->first() on line 170 can be null; however, ManyManyList::add() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
172
		return $e;
173
	}
174
175
	/**
176
	 * Get the deployment backend used for this environment.
177
	 *
178
	 * Enforces compliance with the allowed_backends setting; if the DNEnvironment.BackendIdentifier value is
179
	 * illegal then that value is ignored.
180
	 *
181
	 * @return DeploymentBackend
182
	 */
183
	public function Backend() {
184
		$backends = array_keys($this->config()->get('allowed_backends', Config::FIRST_SET));
185
		switch (sizeof($backends)) {
186
			// Nothing allowed, use the default value "DeploymentBackend"
187
			case 0:
188
				$backend = "DeploymentBackend";
189
				break;
190
191
			// Only 1 thing allowed, use that
192
			case 1:
193
				$backend = $backends[0];
194
				break;
195
196
			// Multiple choices, use our choice if it's legal, otherwise default to the first item on the list
197
			default:
198
				$backend = $this->BackendIdentifier;
199
				if (!in_array($backend, $backends)) {
200
					$backend = $backends[0];
201
				}
202
		}
203
204
		return Injector::inst()->get($backend);
205
	}
206
207
	/**
208
	 * @param SS_HTTPRequest $request
209
	 *
210
	 * @return DeploymentStrategy
211
	 */
212
	public function getDeployStrategy(\SS_HTTPRequest $request) {
213
		return $this->Backend()->planDeploy($this, $request->requestVars());
0 ignored issues
show
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...
214
	}
215
216
	/**
217
	 * Return the supported options for this environment.
218
	 * @return ArrayList
219
	 */
220
	public function getSupportedOptions() {
221
		return $this->Backend()->getDeployOptions($this);
222
	}
223
224
	public function Menu() {
225
		$list = new ArrayList();
226
227
		$controller = Controller::curr();
228
		$actionType = $controller->getField('CurrentActionType');
229
230
		$list->push(new ArrayData([
231
			'Link' => $this->DeploymentsLink(),
232
			'Title' => 'Deployments',
233
			'IsCurrent' => $this->isCurrent(),
234
			'IsSection' => $this->isSection() && ($actionType == DNRoot::ACTION_DEPLOY || $actionType == \EnvironmentOverview::ACTION_OVERVIEW)
0 ignored issues
show
This line exceeds maximum limit of 120 characters; contains 134 characters

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

Loading history...
235
		]));
236
237
		$this->extend('updateMenu', $list);
238
239
		return $list;
240
	}
241
242
	/**
243
	 * Return the current object from $this->Menu()
244
	 * Good for making titles and things
245
	 */
246
	public function CurrentMenu() {
247
		return $this->Menu()->filter('IsSection', true)->First();
248
	}
249
250
	/**
251
	 * Return a name for this environment.
252
	 *
253
	 * @param string $separator The string used when concatenating project with env name
254
	 * @return string
255
	 */
256
	public function getFullName($separator = ':') {
257
		return sprintf('%s%s%s', $this->Project()->Name, $separator, $this->Name);
258
	}
259
260
	/**
261
	 * URL for the environment that can be used if no explicit URL is set.
262
	 */
263
	public function getDefaultURL() {
264
		return null;
265
	}
266
267
	public function getBareURL() {
268
		$url = parse_url($this->URL);
269
		if (isset($url['host'])) {
270
			return strtolower($url['host']);
271
		}
272
	}
273
274
	public function getBareDefaultURL() {
275
		$url = parse_url($this->getDefaultURL());
276
		if (isset($url['host'])) {
277
			return strtolower($url['host']);
278
		}
279
	}
280
281
	/**
282
	 * Environments are only viewable by people that can view the environment.
283
	 *
284
	 * @param Member|null $member
285
	 * @return boolean
286
	 */
287
	public function canView($member = null) {
288
		if (!$member) {
289
			$member = Member::currentUser();
290
		}
291
		if (!$member) {
292
			return false;
293
		}
294
		// Must be logged in to check permissions
295
296
		if (Permission::checkMember($member, 'ADMIN')) {
297
			return true;
298
		}
299
300
		// if no Viewers or ViewerGroups defined, fallback to DNProject::canView permissions
301
		if ($this->Viewers()->exists() || $this->ViewerGroups()->exists()) {
302
			return $this->Viewers()->byID($member->ID)
303
				|| $member->inGroups($this->ViewerGroups());
304
		}
305
306
		return $this->Project()->canView($member);
307
	}
308
309
	/**
310
	 * Allow deploy only to some people.
311
	 *
312
	 * @param Member|null $member
313
	 * @return boolean
314
	 */
315 View Code Duplication
	public function canDeploy($member = null) {
0 ignored issues
show
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...
316
		if (!$member) {
317
			$member = Member::currentUser();
318
		}
319
		if (!$member) {
320
			return false;
321
		}
322
		// Must be logged in to check permissions
323
324
		if ($this->Usage === self::PRODUCTION || $this->Usage === self::UNSPECIFIED) {
325
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_DEPLOYMENT, $member)) {
0 ignored issues
show
$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...
326
				return true;
327
			}
328
		} else {
329
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_DEPLOYMENT, $member)) {
330
				return true;
331
			}
332
		}
333
334
		return $this->Deployers()->byID($member->ID)
335
			|| $member->inGroups($this->DeployerGroups());
336
	}
337
338
	/**
339
	 * Provide reason why the user cannot deploy.
340
	 *
341
	 * @return string
342
	 */
343
	public function getCannotDeployMessage() {
344
		return 'You cannot deploy to this environment.';
345
	}
346
347
	/**
348
	 * Allows only selected {@link Member} objects to restore {@link DNDataArchive} objects into this
349
	 * {@link DNEnvironment}.
350
	 *
351
	 * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
352
	 * @return boolean true if $member can restore, and false if they can't.
353
	 */
354 View Code Duplication
	public function canRestore($member = null) {
0 ignored issues
show
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...
355
		if (!$member) {
356
			$member = Member::currentUser();
357
		}
358
		if (!$member) {
359
			return false;
360
		}
361
		// Must be logged in to check permissions
362
363
		if ($this->Usage === self::PRODUCTION || $this->Usage === self::UNSPECIFIED) {
364
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) {
0 ignored issues
show
$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...
365
				return true;
366
			}
367
		} else {
368
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) {
0 ignored issues
show
$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...
369
				return true;
370
			}
371
		}
372
373
		return $this->CanRestoreMembers()->byID($member->ID)
374
			|| $member->inGroups($this->CanRestoreGroups());
375
	}
376
377
	/**
378
	 * Allows only selected {@link Member} objects to backup this {@link DNEnvironment} to a {@link DNDataArchive}
379
	 * file.
380
	 *
381
	 * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
382
	 * @return boolean true if $member can backup, and false if they can't.
383
	 */
384 View Code Duplication
	public function canBackup($member = null) {
0 ignored issues
show
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...
385
		$project = $this->Project();
386
		if ($project->HasDiskQuota() && $project->HasExceededDiskQuota()) {
387
			return false;
388
		}
389
390
		if (!$member) {
391
			$member = Member::currentUser();
392
		}
393
		// Must be logged in to check permissions
394
		if (!$member) {
395
			return false;
396
		}
397
398
		if ($this->Usage === self::PRODUCTION || $this->Usage === self::UNSPECIFIED) {
399
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) {
0 ignored issues
show
$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...
400
				return true;
401
			}
402
		} else {
403
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) {
0 ignored issues
show
$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...
404
				return true;
405
			}
406
		}
407
408
		return $this->CanBackupMembers()->byID($member->ID)
409
			|| $member->inGroups($this->CanBackupGroups());
410
	}
411
412
	/**
413
	 * Allows only selected {@link Member} objects to upload {@link DNDataArchive} objects linked to this
414
	 * {@link DNEnvironment}.
415
	 *
416
	 * Note: This is not uploading them to the actual environment itself (e.g. uploading to the live site) - it is the
417
	 * process of uploading a *.sspak file into Deploynaut for later 'restoring' to an environment. See
418
	 * {@link self::canRestore()}.
419
	 *
420
	 * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
421
	 * @return boolean true if $member can upload archives linked to this environment, false if they can't.
422
	 */
423 View Code Duplication
	public function canUploadArchive($member = null) {
0 ignored issues
show
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...
424
		$project = $this->Project();
425
		if ($project->HasDiskQuota() && $project->HasExceededDiskQuota()) {
426
			return false;
427
		}
428
429
		if (!$member) {
430
			$member = Member::currentUser();
431
		}
432
		if (!$member) {
433
			return false;
434
		}
435
		// Must be logged in to check permissions
436
437
		if ($this->Usage === self::PRODUCTION || $this->Usage === self::UNSPECIFIED) {
438
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) {
0 ignored issues
show
$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...
439
				return true;
440
			}
441
		} else {
442
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) {
0 ignored issues
show
$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...
443
				return true;
444
			}
445
		}
446
447
		return $this->ArchiveUploaders()->byID($member->ID)
448
			|| $member->inGroups($this->ArchiveUploaderGroups());
449
	}
450
451
	/**
452
	 * Allows only selected {@link Member} objects to download {@link DNDataArchive} objects from this
453
	 * {@link DNEnvironment}.
454
	 *
455
	 * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
456
	 * @return boolean true if $member can download archives from this environment, false if they can't.
457
	 */
458 View Code Duplication
	public function canDownloadArchive($member = null) {
0 ignored issues
show
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...
459
		if (!$member) {
460
			$member = Member::currentUser();
461
		}
462
		if (!$member) {
463
			return false;
464
		}
465
		// Must be logged in to check permissions
466
467
		if ($this->Usage === self::PRODUCTION || $this->Usage === self::UNSPECIFIED) {
468
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) {
0 ignored issues
show
$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...
469
				return true;
470
			}
471
		} else {
472
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) {
0 ignored issues
show
$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...
473
				return true;
474
			}
475
		}
476
477
		return $this->ArchiveDownloaders()->byID($member->ID)
478
			|| $member->inGroups($this->ArchiveDownloaderGroups());
479
	}
480
481
	/**
482
	 * Allows only selected {@link Member} objects to delete {@link DNDataArchive} objects from this
483
	 * {@link DNEnvironment}.
484
	 *
485
	 * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
486
	 * @return boolean true if $member can delete archives from this environment, false if they can't.
487
	 */
488 View Code Duplication
	public function canDeleteArchive($member = null) {
0 ignored issues
show
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...
489
		if (!$member) {
490
			$member = Member::currentUser();
491
		}
492
		if (!$member) {
493
			return false;
494
		}
495
		// Must be logged in to check permissions
496
497
		if ($this->Usage === self::PRODUCTION || $this->Usage === self::UNSPECIFIED) {
498
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) {
0 ignored issues
show
$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...
499
				return true;
500
			}
501
		} else {
502
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) {
0 ignored issues
show
$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...
503
				return true;
504
			}
505
		}
506
507
		return $this->ArchiveDeleters()->byID($member->ID)
508
			|| $member->inGroups($this->ArchiveDeleterGroups());
509
	}
510
511
	/**
512
	 * Get a string of groups/people that are allowed to deploy to this environment.
513
	 * Used in DNRoot_project.ss to list {@link Member}s who have permission to perform this action.
514
	 *
515
	 * @return string
516
	 */
517
	public function getDeployersList() {
518
		return implode(
519
			", ",
520
			array_merge(
521
				$this->DeployerGroups()->column("Title"),
522
				$this->Deployers()->column("FirstName")
523
			)
524
		);
525
	}
526
527
	/**
528
	 * Get a string of groups/people that are allowed to restore {@link DNDataArchive} objects into this environment.
529
	 *
530
	 * @return string
531
	 */
532
	public function getCanRestoreMembersList() {
533
		return implode(
534
			", ",
535
			array_merge(
536
				$this->CanRestoreGroups()->column("Title"),
537
				$this->CanRestoreMembers()->column("FirstName")
538
			)
539
		);
540
	}
541
542
	/**
543
	 * Get a string of groups/people that are allowed to backup {@link DNDataArchive} objects from this environment.
544
	 *
545
	 * @return string
546
	 */
547
	public function getCanBackupMembersList() {
548
		return implode(
549
			", ",
550
			array_merge(
551
				$this->CanBackupGroups()->column("Title"),
552
				$this->CanBackupMembers()->column("FirstName")
553
			)
554
		);
555
	}
556
557
	/**
558
	 * Get a string of groups/people that are allowed to upload {@link DNDataArchive}
559
	 *  objects linked to this environment.
560
	 *
561
	 * @return string
562
	 */
563
	public function getArchiveUploadersList() {
564
		return implode(
565
			", ",
566
			array_merge(
567
				$this->ArchiveUploaderGroups()->column("Title"),
568
				$this->ArchiveUploaders()->column("FirstName")
569
			)
570
		);
571
	}
572
573
	/**
574
	 * Get a string of groups/people that are allowed to download {@link DNDataArchive} objects from this environment.
575
	 *
576
	 * @return string
577
	 */
578
	public function getArchiveDownloadersList() {
579
		return implode(
580
			", ",
581
			array_merge(
582
				$this->ArchiveDownloaderGroups()->column("Title"),
583
				$this->ArchiveDownloaders()->column("FirstName")
584
			)
585
		);
586
	}
587
588
	/**
589
	 * Get a string of groups/people that are allowed to delete {@link DNDataArchive} objects from this environment.
590
	 *
591
	 * @return string
592
	 */
593
	public function getArchiveDeletersList() {
594
		return implode(
595
			", ",
596
			array_merge(
597
				$this->ArchiveDeleterGroups()->column("Title"),
598
				$this->ArchiveDeleters()->column("FirstName")
599
			)
600
		);
601
	}
602
603
	/**
604
	 * @return DNData
605
	 */
606
	public function DNData() {
607
		return DNData::inst();
608
	}
609
610
	/**
611
	 * Get the current deployed build for this environment
612
	 *
613
	 * Dear people of the future: If you are looking to optimize this, simply create a CurrentBuildSHA(), which can be
614
	 * a lot faster. I presume you came here because of the Project display template, which only needs a SHA.
615
	 *
616
	 * @return false|DNDeployment
617
	 */
618
	public function CurrentBuild() {
619
		// The DeployHistory function is far too slow to use for this
620
621
		/** @var DNDeployment $deploy */
622
		$deploy = DNDeployment::get()->filter([
623
			'EnvironmentID' => $this->ID,
624
			'State' => DNDeployment::STATE_COMPLETED
625
		])->sort('LastEdited DESC')->first();
626
627
		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...
628
			return false;
629
		}
630
631
		$repo = $this->Project()->getRepository();
632
		if (!$repo) {
633
			return $deploy;
634
		}
635
636
		try {
637
			$commit = $this->getCommit($deploy->SHA);
638
			if ($commit) {
639
				$deploy->Message = Convert::raw2xml($this->getCommitMessage($commit));
0 ignored issues
show
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...
640
				$deploy->Committer = Convert::raw2xml($commit->getCommitterName());
0 ignored issues
show
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...
641
				$deploy->CommitDate = $commit->getCommitterDate()->Format('d/m/Y g:ia');
0 ignored issues
show
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...
642
				$deploy->Author = Convert::raw2xml($commit->getAuthorName());
0 ignored issues
show
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...
643
				$deploy->AuthorDate = $commit->getAuthorDate()->Format('d/m/Y g:ia');
0 ignored issues
show
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...
644
			}
645
			// We can't find this SHA, so we ignore adding a commit message to the deployment
646
		} catch (Exception $ex) {
647
		}
648
649
		return $deploy;
650
	}
651
652
	/**
653
	 * This is a proxy call to gitonmy that caches the information per project and sha
654
	 *
655
	 * @param string $sha
656
	 * @return \Gitonomy\Git\Commit
657
	 */
658
	public function getCommit($sha) {
659
		return $this->Project()->getCommit($sha);
660
	}
661
662
	public function getCommitMessage(\Gitonomy\Git\Commit $commit) {
663
		return $this->Project()->getCommitMessage($commit);
664
	}
665
666
	public function getCommitSubjectMessage(\Gitonomy\Git\Commit $commit) {
667
		return $this->Project()->getCommitSubjectMessage($commit);
668
	}
669
670
	public function getCommitTags(\Gitonomy\Git\Commit $commit) {
671
		return $this->Project()->getCommitTags($commit);
672
	}
673
674
	/**
675
	 * A list of past deployments.
676
	 * @param string $orderBy - the name of a DB column to sort in descending order
677
	 * @return \ArrayList
678
	 */
679
	public function DeployHistory($orderBy = '') {
680
		$sort = [];
681
		if ($orderBy != '') {
682
			$sort[$orderBy] = 'DESC';
683
		}
684
		// default / fallback sort order
685
		$sort['LastEdited'] = 'DESC';
686
687
		$deployments = $this->Deployments()
688
			->where('"SHA" IS NOT NULL')
689
			->sort($sort);
690
691
		if (!$this->IsNewDeployEnabled()) {
692
			$deployments = $deployments->filter('State', [
693
				DNDeployment::STATE_COMPLETED,
694
				DNDeployment::STATE_FAILED,
695
				DNDeployment::STATE_INVALID
696
			]);
697
		}
698
		return $deployments;
699
	}
700
701
	/**
702
	 * Check if the new deployment form is enabled by whether the project has it,
703
	 * falling back to environment variables on whether it's enabled.
704
	 *
705
	 * @return bool
706
	 */
707
	public function IsNewDeployEnabled() {
708
		if ($this->Project()->IsNewDeployEnabled) {
0 ignored issues
show
The property IsNewDeployEnabled does not exist on object<DNProject>. 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...
709
			return true;
710
		}
711
		// Check for feature flags:
712
		// - FLAG_NEWDEPLOY_ENABLED: set to true to enable globally
713
		// - FLAG_NEWDEPLOY_ENABLED_FOR_MEMBERS: set to semicolon-separated list of email addresses of allowed users.
714
		if (defined('FLAG_NEWDEPLOY_ENABLED') && FLAG_NEWDEPLOY_ENABLED) {
715
			return true;
716
		}
717
		if (defined('FLAG_NEWDEPLOY_ENABLED_FOR_MEMBERS') && FLAG_NEWDEPLOY_ENABLED_FOR_MEMBERS) {
718
			$allowedMembers = explode(';', FLAG_NEWDEPLOY_ENABLED_FOR_MEMBERS);
719
			$member = Member::currentUser();
720
			if ($allowedMembers && $member && in_array($member->Email, $allowedMembers)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $allowedMembers of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
721
				return true;
722
			}
723
		}
724
		return false;
725
	}
726
727
	/**
728
	 * This provides the link to the deployments depending on whether
729
	 * the feature flag for the new deployment is enabled.
730
	 *
731
	 * @return string
732
	 */
733
	public function DeploymentsLink() {
734
		if ($this->IsNewDeployEnabled()) {
735
			return $this->Link(\EnvironmentOverview::ACTION_OVERVIEW);
736
		}
737
		return $this->Link();
738
	}
739
740
	/**
741
	 * @param string $action
742
	 *
743
	 * @return string
744
	 */
745
	public function Link($action = '') {
746
		return \Controller::join_links($this->Project()->Link(), 'environment', $this->Name, $action);
747
	}
748
749
	/**
750
	 * Is this environment currently at the root level of the controller that handles it?
751
	 * @return bool
752
	 */
753
	public function isCurrent() {
754
		return $this->isSection() && Controller::curr()->getAction() == 'environment';
755
	}
756
757
	/**
758
	 * Is this environment currently in a controller that is handling it or performing a sub-task?
759
	 * @return bool
760
	 */
761
	public function isSection() {
762
		$controller = Controller::curr();
763
		$environment = $controller->getField('CurrentEnvironment');
764
		return $environment && $environment->ID == $this->ID;
765
	}
766
767
	/**
768
	 * @return FieldList
769
	 */
770
	public function getCMSFields() {
771
		$fields = new FieldList(new TabSet('Root'));
772
773
		$project = $this->Project();
774
		if ($project && $project->exists()) {
775
			$viewerGroups = $project->Viewers();
776
			$groups = $viewerGroups->sort('Title')->map()->toArray();
777
			$members = [];
778
			foreach ($viewerGroups as $group) {
779
				foreach ($group->Members()->map() as $k => $v) {
780
					$members[$k] = $v;
781
				}
782
			}
783
			asort($members);
784
		} else {
785
			$groups = [];
786
			$members = [];
787
		}
788
789
		// Main tab
790
		$fields->addFieldsToTab('Root.Main', [
791
			// The Main.ProjectID
792
			TextField::create('ProjectName', 'Project')
793
				->setValue(($project = $this->Project()) ? $project->Name : null)
794
				->performReadonlyTransformation(),
795
796
			// The Main.Name
797
			TextField::create('Name', 'Environment name')
798
				->setDescription('A descriptive name for this environment, e.g. staging, uat, production'),
799
800
			$this->obj('Usage')->scaffoldFormField('Environment usage'),
801
802
			// The Main.URL field
803
			TextField::create('URL', 'Server URL')
804
				->setDescription('This url will be used to provide the front-end with a link to this environment'),
805
806
			// The Main.Filename
807
			TextField::create('Filename')
808
				->setDescription('The capistrano environment file name')
809
				->performReadonlyTransformation(),
810
		]);
811
812
		// Backend identifier - pick from a named list of configurations specified in YML config
813
		$backends = $this->config()->get('allowed_backends', Config::FIRST_SET);
814
		// If there's only 1 backend, then user selection isn't needed
815
		if (sizeof($backends) > 1) {
816
			$fields->addFieldToTab('Root.Main', DropdownField::create('BackendIdentifier', 'Deployment backend')
817
				->setSource($backends)
818
				->setDescription('What kind of deployment system should be used to deploy to this environment'));
819
		}
820
821
		$fields->addFieldsToTab('Root.UserPermissions', [
822
			// The viewers of the environment
823
			$this
824
				->buildPermissionField('ViewerGroups', 'Viewers', $groups, $members)
825
				->setTitle('Who can view this environment?')
826
				->setDescription('Groups or Users who can view this environment'),
827
828
			// The Main.Deployers
829
			$this
830
				->buildPermissionField('DeployerGroups', 'Deployers', $groups, $members)
831
				->setTitle('Who can deploy?')
832
				->setDescription('Groups or Users who can deploy to this environment'),
833
834
			// A box to select all snapshot options.
835
			$this
836
				->buildPermissionField('TickAllSnapshotGroups', 'TickAllSnapshot', $groups, $members)
837
				->setTitle("<em>All snapshot permissions</em>")
838
				->addExtraClass('tickall')
839
				->setDescription('UI shortcut to select all snapshot-related options - not written to the database.'),
840
841
			// The Main.CanRestoreMembers
842
			$this
843
				->buildPermissionField('CanRestoreGroups', 'CanRestoreMembers', $groups, $members)
844
				->setTitle('Who can restore?')
845
				->setDescription('Groups or Users who can restore archives from Deploynaut into this environment'),
846
847
			// The Main.CanBackupMembers
848
			$this
849
				->buildPermissionField('CanBackupGroups', 'CanBackupMembers', $groups, $members)
850
				->setTitle('Who can backup?')
851
				->setDescription('Groups or Users who can backup archives from this environment into Deploynaut'),
852
853
			// The Main.ArchiveDeleters
854
			$this
855
				->buildPermissionField('ArchiveDeleterGroups', 'ArchiveDeleters', $groups, $members)
856
				->setTitle('Who can delete?')
857
				->setDescription("Groups or Users who can delete archives from this environment's staging area."),
858
859
			// The Main.ArchiveUploaders
860
			$this
861
				->buildPermissionField('ArchiveUploaderGroups', 'ArchiveUploaders', $groups, $members)
862
				->setTitle('Who can upload?')
863
				->setDescription(
864
					'Users who can upload archives linked to this environment into Deploynaut.<br />' .
865
					'Linking them to an environment allows limiting download permissions (see below).'
866
				),
867
868
			// The Main.ArchiveDownloaders
869
			$this
870
				->buildPermissionField('ArchiveDownloaderGroups', 'ArchiveDownloaders', $groups, $members)
871
				->setTitle('Who can download?')
872
				->setDescription(<<<PHP
873
Users who can download archives from this environment to their computer.<br />
874
Since this implies access to the snapshot, it is also a prerequisite for restores
875
to other environments, alongside the "Who can restore" permission.<br>
876
Should include all users with upload permissions, otherwise they can't download
877
their own uploads.
878
PHP
879
				)
880
881
		]);
882
883
		// The Main.DeployConfig
884
		if ($this->Project()->exists()) {
885
			$this->setDeployConfigurationFields($fields);
886
		}
887
888
		// The DataArchives
889
		$dataArchiveConfig = GridFieldConfig_RecordViewer::create();
890
		$dataArchiveConfig->removeComponentsByType('GridFieldAddNewButton');
891
		if (class_exists('GridFieldBulkManager')) {
892
			$dataArchiveConfig->addComponent(new GridFieldBulkManager());
893
		}
894
		$dataArchive = GridField::create('DataArchives', 'Data Archives', $this->DataArchives(), $dataArchiveConfig);
895
		$fields->addFieldToTab('Root.DataArchive', $dataArchive);
896
897
		// Deployments
898
		$deploymentsConfig = GridFieldConfig_RecordEditor::create();
899
		$deploymentsConfig->removeComponentsByType('GridFieldAddNewButton');
900
		if (class_exists('GridFieldBulkManager')) {
901
			$deploymentsConfig->addComponent(new GridFieldBulkManager());
902
		}
903
		$deployments = GridField::create('Deployments', 'Deployments', $this->Deployments(), $deploymentsConfig);
904
		$fields->addFieldToTab('Root.Deployments', $deployments);
905
906
		Requirements::javascript('deploynaut/javascript/environment.js');
907
908
		// Add actions
909
		$action = new FormAction('check', 'Check Connection');
910
		$action->setUseButtonTag(true);
911
		$dataURL = Director::absoluteBaseURL() . 'naut/api/' . $this->Project()->Name . '/' . $this->Name . '/ping';
912
		$action->setAttribute('data-url', $dataURL);
913
		$fields->insertBefore($action, 'Name');
0 ignored issues
show
'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...
914
915
		// Allow extensions
916
		$this->extend('updateCMSFields', $fields);
917
		return $fields;
918
	}
919
920
	/**
921
	 */
922
	public function onBeforeWrite() {
923
		parent::onBeforeWrite();
924
		if ($this->Name && $this->Name . '.rb' != $this->Filename) {
925
			$this->Filename = $this->Name . '.rb';
926
		}
927
		$this->checkEnvironmentPath();
928
		$this->writeConfigFile();
929
	}
930
931
	public function onAfterWrite() {
932
		parent::onAfterWrite();
933
934
		if ($this->Usage === self::PRODUCTION || $this->Usage === self::UAT) {
935
			$conflicting = DNEnvironment::get()
0 ignored issues
show
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
936
				->filter('ProjectID', $this->ProjectID)
937
				->filter('Usage', $this->Usage)
938
				->exclude('ID', $this->ID);
939
940
			foreach ($conflicting as $otherEnvironment) {
941
				$otherEnvironment->Usage = self::UNSPECIFIED;
942
				$otherEnvironment->write();
943
			}
944
		}
945
	}
946
947
	/**
948
	 * Delete any related config files
949
	 */
950
	public function onAfterDelete() {
951
		parent::onAfterDelete();
952
953
		// Create a basic new environment config from a template
954
		if ($this->config()->get('allow_web_editing') && $this->envFileExists()) {
955
			unlink($this->getConfigFilename());
956
		}
957
958
		$deployments = $this->Deployments();
959
		if ($deployments && $deployments->exists()) {
960
			foreach ($deployments as $deployment) {
961
				$deployment->delete();
962
			}
963
		}
964
965
		$archives = $this->DataArchives();
966
		if ($archives && $archives->exists()) {
967
			foreach ($archives as $archive) {
968
				$archive->delete();
969
			}
970
		}
971
972
		$transfers = $this->DataTransfers();
0 ignored issues
show
Documentation Bug introduced by
The method DataTransfers 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...
973
		if ($transfers && $transfers->exists()) {
974
			foreach ($transfers as $transfer) {
975
				$transfer->delete();
976
			}
977
		}
978
979
		$pings = $this->Pings();
0 ignored issues
show
Documentation Bug introduced by
The method Pings 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...
980
		if ($pings && $pings->exists()) {
981
			foreach ($pings as $ping) {
982
				$ping->delete();
983
			}
984
		}
985
986
		$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...
987
		if ($create && $create->exists()) {
988
			$create->delete();
989
		}
990
	}
991
992
	/**
993
	 * Returns the path to the ruby config file
994
	 *
995
	 * @return string
996
	 */
997
	public function getConfigFilename() {
998
		if (!$this->Project()->exists()) {
999
			return '';
1000
		}
1001
		if (!$this->Filename) {
1002
			return '';
1003
		}
1004
		return $this->DNData()->getEnvironmentDir() . '/' . $this->Project()->Name . '/' . $this->Filename;
1005
	}
1006
1007
	/**
1008
	 * Helper function to convert a multi-dimensional array (associative or indexed) to an {@link ArrayList} or
1009
	 * {@link ArrayData} object structure, so that values can be used in templates.
1010
	 *
1011
	 * @param array $array The (single- or multi-dimensional) array to convert
1012
	 * @return object Either an {@link ArrayList} or {@link ArrayData} object, or the original item ($array) if $array
1013
	 * isn't an array.
1014
	 */
1015
	public static function array_to_viewabledata($array) {
1016
		// Don't transform non-arrays
1017
		if (!is_array($array)) {
1018
			return $array;
1019
		}
1020
1021
		// Figure out whether this is indexed or associative
1022
		$keys = array_keys($array);
1023
		$assoc = ($keys != array_keys($keys));
1024
		if ($assoc) {
1025
			// Treat as viewable data
1026
			$data = new ArrayData([]);
1027
			foreach ($array as $key => $value) {
1028
				$data->setField($key, self::array_to_viewabledata($value));
1029
			}
1030
			return $data;
1031
		} else {
1032
			// Treat this as basic non-associative list
1033
			$list = new ArrayList();
1034
			foreach ($array as $value) {
1035
				$list->push(self::array_to_viewabledata($value));
1036
			}
1037
			return $list;
1038
		}
1039
	}
1040
1041
	/**
1042
	 * Fetchs all deployments in progress. Limits to 1 hour to prevent deployments
1043
	 * if an old deployment is stuck.
1044
	 *
1045
	 * @return DataList
1046
	 */
1047
	public function runningDeployments() {
1048
		return DNDeployment::get()
1049
			->filter([
1050
				'EnvironmentID' => $this->ID,
1051
				'State' => [
1052
					DNDeployment::STATE_QUEUED,
1053
					DNDeployment::STATE_DEPLOYING,
1054
					DNDeployment::STATE_ABORTING
1055
				],
1056
				'Created:GreaterThan' => strtotime('-1 hour')
1057
			]);
1058
	}
1059
1060
	/**
1061
	 * @param string $sha
1062
	 * @return array
1063
	 */
1064
	protected function getCommitData($sha) {
1065
		try {
1066
			$repo = $this->Project()->getRepository();
1067
			if ($repo !== false) {
1068
				$commit = new \Gitonomy\Git\Commit($repo, $sha);
1069
				return [
1070
					'AuthorName' => (string) Convert::raw2xml($commit->getAuthorName()),
1071
					'AuthorEmail' => (string) Convert::raw2xml($commit->getAuthorEmail()),
1072
					'Message' => (string) Convert::raw2xml($this->getCommitMessage($commit)),
1073
					'ShortHash' => Convert::raw2xml($commit->getFixedShortHash(8)),
1074
					'Hash' => Convert::raw2xml($commit->getHash())
1075
				];
1076
			}
1077
		} catch (\Gitonomy\Git\Exception\ReferenceNotFoundException $exc) {
1078
			SS_Log::log($exc, SS_Log::WARN);
1079
		}
1080
		return [
1081
			'AuthorName' => '(unknown)',
1082
			'AuthorEmail' => '(unknown)',
1083
			'Message' => '(unknown)',
1084
			'ShortHash' => $sha,
1085
			'Hash' => '(unknown)',
1086
		];
1087
	}
1088
1089
	/**
1090
	 * Build a set of multi-select fields for assigning permissions to a pair of group and member many_many relations
1091
	 *
1092
	 * @param string $groupField Group field name
1093
	 * @param string $memberField Member field name
1094
	 * @param array $groups List of groups
1095
	 * @param array $members List of members
1096
	 * @return FieldGroup
1097
	 */
1098
	protected function buildPermissionField($groupField, $memberField, $groups, $members) {
1099
		return FieldGroup::create(
1100
			ListboxField::create($groupField, false, $groups)
1101
				->setMultiple(true)
1102
				->setAttribute('data-placeholder', 'Groups')
1103
				->setAttribute('placeholder', 'Groups')
1104
				->setAttribute('style', 'width: 400px;'),
1105
1106
			ListboxField::create($memberField, false, $members)
1107
				->setMultiple(true)
1108
				->setAttribute('data-placeholder', 'Members')
1109
				->setAttribute('placeholder', 'Members')
1110
				->setAttribute('style', 'width: 400px;')
1111
		);
1112
	}
1113
1114
	/**
1115
	 * @param FieldList $fields
1116
	 */
1117
	protected function setDeployConfigurationFields(&$fields) {
1118
		if (!$this->config()->get('allow_web_editing')) {
1119
			return;
1120
		}
1121
1122
		if ($this->envFileExists()) {
1123
			$deployConfig = new TextareaField('DeployConfig', 'Deploy config', $this->getEnvironmentConfig());
1124
			$deployConfig->setRows(40);
1125
			$fields->insertAfter($deployConfig, 'Filename');
0 ignored issues
show
'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...
1126
			return;
1127
		}
1128
1129
		$warning = 'Warning: This environment doesn\'t have deployment configuration.';
1130
		$noDeployConfig = new LabelField('noDeployConfig', $warning);
1131
		$noDeployConfig->addExtraClass('message warning');
1132
		$fields->insertAfter($noDeployConfig, 'Filename');
0 ignored issues
show
'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...
1133
		$createConfigField = new CheckboxField('CreateEnvConfig', 'Create Config');
1134
		$createConfigField->setDescription('Would you like to create the capistrano deploy configuration?');
1135
		$fields->insertAfter($createConfigField, 'noDeployConfig');
0 ignored issues
show
'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...
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
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
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
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
	 * @return string
1171
	 */
1172
	protected function getEnvironmentConfig() {
1173
		if (!$this->envFileExists()) {
1174
			return '';
1175
		}
1176
		return file_get_contents($this->getConfigFilename());
1177
	}
1178
1179
	/**
1180
	 * @return boolean
1181
	 */
1182
	protected function envFileExists() {
1183
		if (!$this->getConfigFilename()) {
1184
			return false;
1185
		}
1186
		return file_exists($this->getConfigFilename());
1187
	}
1188
1189
	protected function validate() {
1190
		$result = parent::validate();
1191
		$backend = $this->Backend();
1192
1193
		if (strcasecmp('test', $this->Name) === 0 && get_class($backend) == 'CapistranoDeploymentBackend') {
1194
			$result->error('"test" is not a valid environment name when using Capistrano backend.');
1195
		}
1196
1197
		return $result;
1198
	}
1199
1200
}
1201