DNProject   F
last analyzed

Complexity

Total Complexity 168

Size/Duplication

Total Lines 1253
Duplicated Lines 6.23 %

Coupling/Cohesion

Components 1
Dependencies 38

Importance

Changes 0
Metric Value
wmc 168
lcom 1
cbo 38
dl 78
loc 1253
rs 0.5217
c 0
b 0
f 0

71 Methods

Rating   Name   Duplication   Size   Complexity  
A create_from_path() 0 12 3
A clearGitCache() 0 6 1
A get_git_cache() 0 6 1
A getUsedQuotaMB() 0 12 3
A getDiskQuotaMB() 0 10 3
A getPrimaryEnvType() 0 5 1
A HasExceededDiskQuota() 0 3 1
A HasDiskQuota() 0 3 1
A DiskQuotaUsagePercent() 0 7 2
A Menu() 0 19 4
A isCurrent() 0 3 2
A CurrentMenu() 0 3 1
A isSection() 0 5 2
A canView() 0 11 3
A canRestore() 15 16 2
A canBackup() 15 16 2
A canUploadArchive() 15 16 2
A canDownloadArchive() 15 16 2
A canCreateEnvironments() 0 10 3
A DataArchives() 0 4 1
A PendingManualUploadDataArchives() 0 3 1
A getProcessEnv() 0 14 2
A getViewersList() 0 3 1
A DNData() 0 3 1
A DNBuildList() 0 3 1
A DNBranchList() 0 6 3
A DNTagList() 0 6 3
A getRepository() 0 7 2
A resolveRevision() 0 13 3
A DNEnvironmentList() 0 16 3
A EnvironmentsByUsage() 0 3 1
A currentBuilds() 0 10 3
A Link() 0 3 1
A getCMSEditLink() 0 3 1
A CreateEnvironmentLink() 0 6 2
A ToggleStarLink() 0 3 1
A IsStarred() 0 11 3
A APILink() 0 3 1
A getCMSFields() 0 58 2
A setCreateProjectFolderField() 0 17 3
A projectFolderExists() 0 3 1
A repoExists() 0 3 1
A cloneRepo() 0 15 2
A getLocalCVSPath() 0 3 1
A onBeforeWrite() 0 7 3
A onAfterWrite() 0 12 4
C onAfterDelete() 0 32 11
A getPublicKey() 0 7 2
A getPublicKeyPath() 0 6 2
A getPrivateKeyPath() 0 9 2
A getKeyDir() 0 11 2
A getRepositoryURL() 0 6 2
C getRepositoryInterface() 0 40 12
A whoIsAllowed() 0 3 1
A whoIsAllowedAny() 0 19 2
A allowed() 0 3 1
A groupAllowed() 0 11 3
A allowedAny() 0 12 4
B isProjectReady() 0 23 5
A getRunningEnvironmentCreations() 0 4 1
A getInitialEnvironmentCreations() 0 3 1
A getRunningInitialEnvironmentCreations() 0 4 1
A getCompleteInitialEnvironmentCreations() 0 4 1
A canCreate() 0 15 4
A getCommit() 0 18 4
A getCommitMessage() 9 9 2
A getCommitSubjectMessage() 9 9 2
A getCommitTags() 0 12 2
A setEnvironmentFields() 0 18 3
A getProjectFolderPath() 0 3 1
C validate() 0 26 7

How to fix   Duplicated Code    Complexity   

Duplicated Code

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

Common duplication problems, and corresponding solutions are:

Complex Class

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

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

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

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

1
<?php
2
3
/**
4
 * DNProject represents a project that relates to a group of target
5
 * environments.
6
 *
7
 * @property string Name
8
 * @property string CVSPath
9
 * @property int DiskQuotaMB
10
 *
11
 * @method HasManyList Environments()
12
 * @method ManyManyList Viewers()
13
 * @method ManyManyList StarredBy()
14
 */
15
class DNProject extends DataObject {
0 ignored issues
show
Coding Style introduced by
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...
Coding Style introduced by
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...
Coding Style introduced by
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...
Coding Style introduced by
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...
Coding Style introduced by
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...
Coding Style introduced by
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...
Coding Style introduced by
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...
Coding Style introduced by
The property $show_repository_url 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...
Coding Style introduced by
The property $relation_cache 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...
Coding Style introduced by
The property $has_cloned_cache 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...
Coding Style introduced by
The property $_current_member_cache 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...
Coding Style introduced by
The property $repository_interfaces 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...
16
17
	/**
18
	 * @var array
19
	 */
20
	private static $db = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
21
		"Name" => "Varchar",
22
		"IsNewDeployEnabled" => "Boolean",
23
		"CVSPath" => "Varchar(255)",
24
		"DiskQuotaMB" => "Int",
25
		"AllowedEnvironmentType" => "Varchar(255)"
26
	];
27
28
	/**
29
	 * @var array
30
	 */
31
	private static $has_many = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
32
		"Environments" => "DNEnvironment",
33
		"CreateEnvironments" => "DNCreateEnvironment",
34
		"Fetches" => "DNGitFetch"
35
	];
36
37
	/**
38
	 * @var array
39
	 */
40
	private static $many_many = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $many_many 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...
41
		"Viewers" => "Group",
42
		"StarredBy" => "Member"
43
	];
44
45
	/**
46
	 * @var array
47
	 */
48
	private static $summary_fields = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
49
		"Name",
50
		"ViewersList",
51
	];
52
53
	/**
54
	 * @var array
55
	 */
56
	private static $searchable_fields = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
57
		"Name",
58
	];
59
60
	/**
61
	 * @var string
62
	 */
63
	private static $singular_name = 'Project';
0 ignored issues
show
Unused Code introduced by
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...
64
65
	/**
66
	 * @var string
67
	 */
68
	private static $plural_name = 'Projects';
69
70
	/**
71
	 * @var string
72
	 */
73
	private static $default_sort = 'Name';
74
75
	/**
76
	 * In-memory cache for currentBuilds per environment since fetching them from
77
	 * disk is pretty resource hungry.
78
	 *
79
	 * @var array
80
	 */
81
	protected static $relation_cache = [];
82
83
	/**
84
	 * @var bool|Member
85
	 */
86
	protected static $_current_member_cache = null;
87
88
	/**
89
	 * Display the repository URL on the project page.
90
	 *
91
	 * @var bool
92
	 */
93
	private static $show_repository_url = false;
0 ignored issues
show
Unused Code introduced by
The property $show_repository_url 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...
94
95
	/**
96
	 * In-memory cache to determine whether clone repo was called.
97
	 * @var array
98
	 */
99
	private static $has_cloned_cache = [];
100
101
	/**
102
	 * Whitelist configuration that describes how to convert a repository URL into a link
103
	 * to a web user interface for that URL
104
	 *
105
	 * Consists of a hash of "full.lower.case.domain" => {configuration} key/value pairs
106
	 *
107
	 * {configuration} can either be boolean true to auto-detect both the host and the
108
	 * name of the UI provider, or a nested array that overrides either one or both
109
	 * of the auto-detected values
110
	 *
111
	 * @var array
112
	 */
113
	private static $repository_interfaces = [
0 ignored issues
show
Unused Code introduced by
The property $repository_interfaces 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...
114
		'github.com' => [
115
			'icon' => 'deploynaut/img/github.png',
116
			'name' => 'Github.com',
117
		],
118
		'bitbucket.org' => [
119
			'commit' => 'commits',
120
			'name' => 'Bitbucket.org',
121
		],
122
		'repo.or.cz' => [
123
			'scheme' => 'http',
124
			'name' => 'repo.or.cz',
125
			'regex' => ['^(.*)$' => '/w$1'],
126
		],
127
128
		/* Example for adding your own gitlab repository and override all auto-detected values (with their defaults)
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
129
		'gitlab.mysite.com' => array(
130
			'icon' => 'deploynaut/img/git.png',
131
			'host' => 'gitlab.mysite.com',
132
			'name' => 'Gitlab',
133
			'regex' => array('.git$' => ''),
134
			'commit' => "commit"
135
		),
136
		*/
137
	];
138
139
	/**
140
	 * Used by the sync task
141
	 *
142
	 * @param string $path
143
	 * @return \DNProject
144
	 */
145
	public static function create_from_path($path) {
146
		$project = DNProject::create();
0 ignored issues
show
Coding Style introduced by
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...
147
		$project->Name = $path;
148
		$project->write();
149
150
		// add the administrators group as the viewers of the new project
151
		$adminGroup = Group::get()->filter('Code', 'administrators')->first();
152
		if ($adminGroup && $adminGroup->exists()) {
153
			$project->Viewers()->add($adminGroup);
154
		}
155
		return $project;
156
	}
157
158
	/**
159
	 * This will clear the cache for the git getters and should be called when the local git repo is updated
160
	 */
161
	public function clearGitCache() {
162
		$cache = self::get_git_cache();
163
		// we only need to clear the tag cache since everything else is cached by SHA, that is for commit and
164
		// commit message.
165
		$cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, ['gitonomy', 'tags', 'project_' . $this->ID]);
166
	}
167
168
	/**
169
	 * @return \Zend_Cache_Frontend_Output
170
	 */
171
	public static function get_git_cache() {
172
		return SS_Cache::factory('gitonomy', 'Output', [
173
			'automatic_serialization' => true,
174
			'lifetime' => 60 * 60 * 24 * 7 // seven days
175
		]);
176
	}
177
178
	/**
179
	 * Return the used quota in MB.
180
	 *
181
	 * @param int $round Number of decimal places to round to
182
	 * @return double The used quota size in MB
183
	 */
184
	public function getUsedQuotaMB($round = 2) {
185
		$size = 0;
186
187
		foreach ($this->Environments() as $environment) {
188
			foreach ($environment->DataArchives()->filter('IsBackup', 0) as $archive) {
189
				$size += $archive->ArchiveFile()->getAbsoluteSize();
190
			}
191
		}
192
193
		// convert bytes to megabytes and round
194
		return round(($size / 1024) / 1024, $round);
195
	}
196
197
	/**
198
	 * Getter for DiskQuotaMB field to provide a default for existing
199
	 * records that have no quota field set, as it will need to default
200
	 * to a globally set size.
201
	 *
202
	 * @return string|int The quota size in MB
203
	 */
204
	public function getDiskQuotaMB() {
205
		$size = $this->getField('DiskQuotaMB');
206
207
		if (empty($size)) {
208
			$defaults = $this->config()->get('defaults');
209
			$size = (isset($defaults['DiskQuotaMB'])) ? $defaults['DiskQuotaMB'] : 0;
210
		}
211
212
		return $size;
213
	}
214
215
	/**
216
	 * @return string
217
	 */
218
	public function getPrimaryEnvType() {
219
		$envClasses = $this->Environments()->column("ClassName");
220
		$topClass = array_count_values($envClasses);
221
		return str_replace("Environment", "", array_search(max($topClass), $topClass));
222
	}
223
224
	/**
225
	 * Has the disk quota been exceeded?
226
	 *
227
	 * @return boolean
228
	 */
229
	public function HasExceededDiskQuota() {
230
		return $this->getUsedQuotaMB(0) >= $this->getDiskQuotaMB();
231
	}
232
233
	/**
234
	 * Is there a disk quota set for this project?
235
	 *
236
	 * @return boolean
237
	 */
238
	public function HasDiskQuota() {
239
		return $this->getDiskQuotaMB() > 0;
240
	}
241
242
	/**
243
	 * Returns the current disk quota usage as a percentage
244
	 *
245
	 * @return int
246
	 */
247
	public function DiskQuotaUsagePercent() {
248
		$quota = $this->getDiskQuotaMB();
249
		if ($quota > 0) {
250
			return $this->getUsedQuotaMB() * 100 / $quota;
251
		}
252
		return 100;
253
	}
254
255
	/**
256
	 * Get the menu to be shown on projects
257
	 *
258
	 * @return ArrayList
259
	 */
260
	public function Menu() {
261
		$list = new ArrayList();
262
263
		$controller = Controller::curr();
264
		$actionType = $controller->getField('CurrentActionType');
265
266
		if ($this->isProjectReady()) {
267
			$list->push(new ArrayData([
268
				'Link' => $this->Link('snapshots'),
269
				'Title' => 'Snapshots',
270
				'IsCurrent' => $this->isSection() && $controller->getAction() == 'snapshots',
271
				'IsSection' => $this->isSection() && $actionType == DNRoot::ACTION_SNAPSHOT
272
			]));
273
		}
274
275
		$this->extend('updateMenu', $list);
276
277
		return $list;
278
	}
279
280
	/**
281
	 * Is this project currently at the root level of the controller that handles it?
282
	 *
283
	 * @return bool
284
	 */
285
	public function isCurrent() {
286
		return $this->isSection() && Controller::curr()->getAction() == 'project';
287
	}
288
289
	/**
290
	 * Return the current object from $this->Menu()
291
	 * Good for making titles and things
292
	 *
293
	 * @return DataObject
294
	 */
295
	public function CurrentMenu() {
296
		return $this->Menu()->filter('IsSection', true)->First();
297
	}
298
299
	/**
300
	 * Is this project currently in a controller that is handling it or performing a sub-task?
301
	 *
302
	 * @return bool
303
	 */
304
	public function isSection() {
305
		$controller = Controller::curr();
306
		$project = $controller->getField('CurrentProject');
307
		return $project && $this->ID == $project->ID;
308
	}
309
310
	/**
311
	 * Restrict access to viewing this project
312
	 *
313
	 * @param Member|null $member
314
	 * @return boolean
315
	 */
316
	public function canView($member = null) {
317
		if (!$member) {
318
			$member = Member::currentUser();
319
		}
320
321
		if (Permission::checkMember($member, 'ADMIN')) {
322
			return true;
323
		}
324
325
		return $member->inGroups($this->Viewers());
326
	}
327
328
	/**
329
	 * @param Member|null $member
330
	 *
331
	 * @return bool
332
	 */
333 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...
334
		if ($this->allowedAny(
335
			[
336
				DNRoot::ALLOW_PROD_SNAPSHOT,
337
				DNRoot::ALLOW_NON_PROD_SNAPSHOT
338
			],
339
			$member
340
		)
341
		) {
342
			return true;
343
		}
344
345
		return (bool) $this->Environments()->filterByCallback(function ($env) use ($member) {
346
			return $env->canRestore($member);
347
		})->Count();
348
	}
349
350
	/**
351
	 * @param Member|null $member
352
	 * @return bool
353
	 */
354 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...
355
		if ($this->allowedAny(
356
			[
357
				DNRoot::ALLOW_PROD_SNAPSHOT,
358
				DNRoot::ALLOW_NON_PROD_SNAPSHOT
359
			],
360
			$member
361
		)
362
		) {
363
			return true;
364
		}
365
366
		return (bool) $this->Environments()->filterByCallback(function ($env) use ($member) {
367
			return $env->canBackup($member);
368
		})->Count();
369
	}
370
371
	/**
372
	 * @param Member|null $member
373
	 * @return bool
374
	 */
375 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...
376
		if ($this->allowedAny(
377
			[
378
				DNRoot::ALLOW_PROD_SNAPSHOT,
379
				DNRoot::ALLOW_NON_PROD_SNAPSHOT
380
			],
381
			$member
382
		)
383
		) {
384
			return true;
385
		}
386
387
		return (bool) $this->Environments()->filterByCallback(function ($env) use ($member) {
388
			return $env->canUploadArchive($member);
389
		})->Count();
390
	}
391
392
	/**
393
	 * @param Member|null $member
394
	 * @return bool
395
	 */
396 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...
397
		if ($this->allowedAny(
398
			[
399
				DNRoot::ALLOW_PROD_SNAPSHOT,
400
				DNRoot::ALLOW_NON_PROD_SNAPSHOT
401
			],
402
			$member
403
		)
404
		) {
405
			return true;
406
		}
407
408
		return (bool) $this->Environments()->filterByCallback(function ($env) use ($member) {
409
			return $env->canDownloadArchive($member);
410
		})->Count();
411
	}
412
413
	/**
414
	 * This is a permission check for the front-end only.
415
	 *
416
	 * Only admins can create environments for now. Also, we need to check the value
417
	 * of AllowedEnvironmentType which dictates which backend to use to render the form.
418
	 *
419
	 * @param Member|null $member
420
	 *
421
	 * @return bool
422
	 */
423
	public function canCreateEnvironments($member = null) {
424
		$envType = $this->AllowedEnvironmentType;
0 ignored issues
show
Documentation introduced by
The property AllowedEnvironmentType does not exist on object<DNProject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
425
		if ($envType) {
426
			$env = Injector::inst()->get($envType);
427
			if ($env instanceof EnvironmentCreateBackend) {
428
				return $this->allowed(DNRoot::ALLOW_CREATE_ENVIRONMENT, $member);
429
			}
430
		}
431
		return false;
432
	}
433
434
	/**
435
	 * @return DataList
436
	 */
437
	public function DataArchives() {
438
		$envIds = $this->Environments()->column('ID');
439
		return DNDataArchive::get()->filter('EnvironmentID', $envIds);
440
	}
441
442
	/**
443
	 * Return all archives which are "manual upload requests",
444
	 * meaning they don't have a file attached to them (yet).
445
	 *
446
	 * @return DataList
447
	 */
448
	public function PendingManualUploadDataArchives() {
449
		return $this->DataArchives()->filter('ArchiveFileID', null);
450
	}
451
452
	/**
453
	 * Build an environment variable array to be used with this project.
454
	 *
455
	 * This is relevant if every project needs to use an individual SSH pubkey.
456
	 *
457
	 * Include this with all Gitonomy\Git\Repository, and
458
	 * \Symfony\Component\Process\Processes.
459
	 *
460
	 * @return array
461
	 */
462
	public function getProcessEnv() {
463
		if (file_exists($this->getPrivateKeyPath())) {
464
			// Key-pair is available, use it.
465
			$processEnv = [
466
				'IDENT_KEY' => $this->getPrivateKeyPath(),
467
				'GIT_SSH' => BASE_PATH . "/deploynaut/git-deploy.sh"
468
			];
469
		} else {
470
			$processEnv = [];
471
		}
472
		$this->extend('updateProcessEnv', $processEnv);
473
474
		return $processEnv;
475
	}
476
477
	/**
478
	 * Get a string of people allowed to view this project
479
	 *
480
	 * @return string
481
	 */
482
	public function getViewersList() {
483
		return implode(", ", $this->Viewers()->column("Title"));
484
	}
485
486
	/**
487
	 * @return DNData
488
	 */
489
	public function DNData() {
490
		return DNData::inst();
491
	}
492
493
	/**
494
	 * Provides a DNBuildList of builds found in this project.
495
	 *
496
	 * @return DNReferenceList
497
	 */
498
	public function DNBuildList() {
499
		return DNReferenceList::create($this, $this->DNData());
500
	}
501
502
	/**
503
	 * Provides a list of the branches in this project.
504
	 *
505
	 * @return DNBranchList
506
	 */
507
	public function DNBranchList() {
508
		if ($this->CVSPath && !$this->repoExists()) {
509
			$this->cloneRepo();
510
		}
511
		return DNBranchList::create($this, $this->DNData());
512
	}
513
514
	/**
515
	 * Provides a list of the tags in this project.
516
	 *
517
	 * @return DNReferenceList
518
	 */
519
	public function DNTagList() {
520
		if ($this->CVSPath && !$this->repoExists()) {
521
			$this->cloneRepo();
522
		}
523
		return DNReferenceList::create($this, $this->DNData(), null, null, true);
524
	}
525
526
	/**
527
	 * @return false|Gitonomy\Git\Repository
528
	 */
529
	public function getRepository() {
530
		if (!$this->repoExists()) {
531
			return false;
532
		}
533
534
		return new \Gitonomy\Git\Repository($this->getLocalCVSPath());
535
	}
536
537
	/**
538
	 * Resolve a git reference like a branch or tag into a SHA.
539
	 * @return bool|string
540
	 */
541
	public function resolveRevision($value) {
542
		$repository = $this->getRepository();
543
		if (!$repository) {
544
			return false;
545
		}
546
547
		try {
548
			$revision = $this->repository->getRevision($value);
0 ignored issues
show
Bug introduced by
The property repository does not seem to exist. Did you mean show_repository_url?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
549
			return $revision->getCommit()->getHash();
550
		} catch (\Gitonomy\Git\Exception\ReferenceNotFoundException $e) {
551
			return false;
552
		}
553
	}
554
555
	/**
556
	 * Provides a list of environments found in this project.
557
	 * CAUTION: filterByCallback will change this into an ArrayList!
558
	 *
559
	 * @return ArrayList
560
	 */
561
	public function DNEnvironmentList() {
562
563
		if (!self::$_current_member_cache) {
564
			self::$_current_member_cache = Member::currentUser();
0 ignored issues
show
Documentation Bug introduced by
It seems like \Member::currentUser() can also be of type object<DataObject>. However, the property $_current_member_cache is declared as type boolean|object<Member>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
565
		}
566
567
		if (self::$_current_member_cache === false) {
568
			return new ArrayList();
569
		}
570
571
		$currentMember = self::$_current_member_cache;
572
		return $this->Environments()
573
			->filterByCallBack(function ($item) use ($currentMember) {
574
				return $item->canView($currentMember);
575
			});
576
	}
577
578
	/**
579
	 * @param string $usage
580
	 * @return ArrayList
581
	 */
582
	public function EnvironmentsByUsage($usage) {
583
		return $this->DNEnvironmentList()->filter('Usage', $usage);
584
	}
585
586
	/**
587
	 * Returns a map of envrionment name to build name
588
	 *
589
	 * @return false|DNDeployment
590
	 */
591
	public function currentBuilds() {
592
		if (!isset(self::$relation_cache['currentBuilds.' . $this->ID])) {
593
			$currentBuilds = [];
594
			foreach ($this->Environments() as $env) {
595
				$currentBuilds[$env->Name] = $env->CurrentBuild();
596
			}
597
			self::$relation_cache['currentBuilds.' . $this->ID] = $currentBuilds;
598
		}
599
		return self::$relation_cache['currentBuilds.' . $this->ID];
600
	}
601
602
	/**
603
	 * @param string
604
	 * @return string
605
	 */
606
	public function Link($action = '') {
607
		return Controller::join_links("naut", "project", $this->Name, $action);
608
	}
609
610
	/**
611
	 * @return string
612
	 */
613
	public function getCMSEditLink() {
614
		return Controller::join_links("admin", "naut", "DNProject", "EditForm", "field", "DNProject", "item", $this->ID, "edit");
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 123 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...
615
	}
616
	/**
617
	 * @return string|null
618
	 */
619
	public function CreateEnvironmentLink() {
620
		if ($this->canCreateEnvironments()) {
621
			return $this->Link('createenv');
622
		}
623
		return null;
624
	}
625
626
	/**
627
	 * @return string
628
	 */
629
	public function ToggleStarLink() {
630
		return $this->Link('/star');
631
	}
632
633
	/**
634
	 * @return bool
635
	 */
636
	public function IsStarred() {
637
		$member = Member::currentUser();
638
		if ($member === null) {
639
			return false;
640
		}
641
		$favourited = $this->StarredBy()->filter('MemberID', $member->ID);
642
		if ($favourited->count() == 0) {
643
			return false;
644
		}
645
		return true;
646
	}
647
648
	/**
649
	 * @param string $action
650
	 * @return string
651
	 */
652
	public function APILink($action) {
653
		return Controller::join_links("naut", "api", $this->Name, $action);
654
	}
655
656
	/**
657
	 * @return FieldList
658
	 */
659
	public function getCMSFields() {
660
		$fields = parent::getCMSFields();
661
662
		/** @var GridField $environments */
663
		$environments = $fields->dataFieldByName("Environments");
664
665
		$fields->fieldByName("Root")->removeByName("Viewers");
666
		$fields->fieldByName("Root")->removeByName("Environments");
667
		$fields->fieldByName("Root")->removeByName("LocalCVSPath");
668
669
		$diskQuotaDesc = 'This is the maximum amount of disk space (in megabytes) that all environments within this '
670
			. 'project can use for stored snapshots';
671
		$fields->dataFieldByName('DiskQuotaMB')->setDescription($diskQuotaDesc);
672
673
		$projectNameDesc = 'Changing the name will <strong>reset</strong> the deploy configuration and avoid using non'
674
			. 'alphanumeric characters';
675
		$fields->fieldByName('Root.Main.Name')
676
			->setTitle('Project name')
677
			->setDescription($projectNameDesc);
678
679
		$fields->fieldByName('Root.Main.IsNewDeployEnabled')
680
			->setTitle('New deploy form enabled for this project')
681
			->setDescription('Feature flag to change links to environment and deployments to the new deployment form for this project');
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 127 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...
682
683
		$fields->fieldByName('Root.Main.CVSPath')
684
			->setTitle('Git repository')
685
			->setDescription('E.g. [email protected]:silverstripe/silverstripe-installer.git');
686
687
		$workspaceField = new ReadonlyField('LocalWorkspace', 'Git workspace', $this->getLocalCVSPath());
688
		$workspaceField->setDescription('This is where the GIT repository are located on this server');
689
		$fields->insertAfter($workspaceField, 'CVSPath');
0 ignored issues
show
Documentation introduced by
'CVSPath' 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...
690
691
		$readAccessGroups = ListboxField::create('Viewers', 'Project viewers', Group::get()->map()->toArray())
692
			->setMultiple(true)
693
			->setDescription('These groups can view the project in the front-end.');
694
		$fields->addFieldToTab("Root.Main", $readAccessGroups);
695
696
		$this->setCreateProjectFolderField($fields);
697
		$this->setEnvironmentFields($fields, $environments);
698
699
		$environmentTypes = ClassInfo::implementorsOf('EnvironmentCreateBackend');
700
		$types = [];
701
		foreach ($environmentTypes as $type) {
702
			$types[$type] = $type;
703
		}
704
705
		$fields->addFieldsToTab('Root.Main', [
706
			DropdownField::create(
707
				'AllowedEnvironmentType',
708
				'Allowed Environment Type',
709
				$types
710
			)->setDescription('This defined which form to show on the front end for '
711
				. 'environment creation. This will not affect backend functionality.')
712
				->setEmptyString(' - None - '),
713
		]);
714
715
		return $fields;
716
	}
717
718
	/**
719
	 * If there isn't a capistrano env project folder, show options to create one
720
	 *
721
	 * @param FieldList $fields
722
	 */
723
	public function setCreateProjectFolderField(&$fields) {
724
		// Check if the capistrano project folder exists
725
		if (!$this->Name) {
726
			return;
727
		}
728
729
		if ($this->projectFolderExists()) {
730
			return;
731
		}
732
733
		$createFolderNotice = new LabelField('CreateEnvFolderNotice', 'Warning: No Capistrano project folder exists');
734
		$createFolderNotice->addExtraClass('message warning');
735
		$fields->insertBefore($createFolderNotice, '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...
736
		$createFolderField = new CheckboxField('CreateEnvFolder', 'Create folder');
737
		$createFolderField->setDescription('Would you like to create the capistrano project folder?');
738
		$fields->insertAfter($createFolderField, 'CreateEnvFolderNotice');
739
	}
740
741
	/**
742
	 * @return boolean
743
	 */
744
	public function projectFolderExists() {
745
		return file_exists($this->getProjectFolderPath());
746
	}
747
748
	/**
749
	 * @return bool
750
	 */
751
	public function repoExists() {
752
		return file_exists(sprintf('%s/HEAD', $this->getLocalCVSPath()));
753
	}
754
755
	/**
756
	 * Setup a job to clone a git repository.
757
	 * @return string resque token
758
	 */
759
	public function cloneRepo() {
760
		// Avoid this being called multiple times in the same request
761
		if (!isset(self::$has_cloned_cache[$this->ID])) {
762
			$fetch = DNGitFetch::create();
763
			$fetch->ProjectID = $this->ID;
764
			$fetch->write();
765
766
			// passing true here tells DNGitFetch to force a git clone, otherwise
767
			// it will just update the repo if it already exists. We want to ensure
768
			// we're always cloning a new repo in this case, as the git URL may have changed.
769
			$fetch->start(true);
770
771
			self::$has_cloned_cache[$this->ID] = true;
772
		}
773
	}
774
775
	/**
776
	 * @return string
777
	 */
778
	public function getLocalCVSPath() {
779
		return sprintf('%s/%s', DEPLOYNAUT_LOCAL_VCS_PATH, $this->Name);
780
	}
781
782
	public function onBeforeWrite() {
783
		parent::onBeforeWrite();
784
785
		if ($this->CreateEnvFolder && !file_exists($this->getProjectFolderPath())) {
0 ignored issues
show
Documentation introduced by
The property CreateEnvFolder 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...
786
			mkdir($this->getProjectFolderPath());
787
		}
788
	}
789
790
	public function onAfterWrite() {
791
		parent::onAfterWrite();
792
793
		if (!$this->CVSPath) {
794
			return;
795
		}
796
797
		$changedFields = $this->getChangedFields(true, 2);
798
		if (isset($changedFields['CVSPath']) || isset($changedFields['Name'])) {
799
			$this->cloneRepo();
800
		}
801
	}
802
803
	/**
804
	 * Delete related environments and folders
805
	 */
806
	public function onAfterDelete() {
807
		parent::onAfterDelete();
808
809
		$environments = $this->Environments();
810
		if ($environments && $environments->exists()) {
811
			foreach ($environments as $env) {
812
				$env->delete();
813
			}
814
		}
815
816
		$fetches = $this->Fetches();
0 ignored issues
show
Documentation Bug introduced by
The method Fetches does not exist on object<DNProject>? 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...
817
		if ($fetches && $fetches->exists()) {
818
			foreach ($fetches as $fetch) {
819
				$fetch->delete();
820
			}
821
		}
822
823
		// Delete local repository
824
		if (file_exists($this->getLocalCVSPath())) {
825
			Filesystem::removeFolder($this->getLocalCVSPath());
826
		}
827
828
		// Delete project template
829
		if (file_exists($this->getProjectFolderPath()) && Config::inst()->get('DNEnvironment', 'allow_web_editing')) {
830
			Filesystem::removeFolder($this->getProjectFolderPath());
831
		}
832
833
		// Delete the deploy key
834
		if (file_exists($this->getKeyDir())) {
835
			Filesystem::removeFolder($this->getKeyDir());
836
		}
837
	}
838
839
	/**
840
	 * Fetch the public key for this project.
841
	 *
842
	 * @return string|void
843
	 */
844
	public function getPublicKey() {
845
		$key = $this->getPublicKeyPath();
846
847
		if (file_exists($key)) {
848
			return trim(file_get_contents($key));
849
		}
850
	}
851
852
	/**
853
	 * This returns that path of the public key if a key directory is set. It doesn't check whether the file exists.
854
	 *
855
	 * @return string|null
856
	 */
857
	public function getPublicKeyPath() {
858
		if ($privateKey = $this->getPrivateKeyPath()) {
859
			return $privateKey . '.pub';
860
		}
861
		return null;
862
	}
863
864
	/**
865
	 * This returns that path of the private key if a key directory is set. It doesn't check whether the file exists.
866
	 *
867
	 * @return string|null
868
	 */
869
	public function getPrivateKeyPath() {
870
		$keyDir = $this->getKeyDir();
871
		if (!empty($keyDir)) {
872
			$filter = FileNameFilter::create();
873
			$name = $filter->filter($this->Name);
874
			return $keyDir . '/' . $name;
875
		}
876
		return null;
877
	}
878
879
	/**
880
	 * Returns the location of the projects key dir if one exists.
881
	 *
882
	 * @return string|null
883
	 */
884
	public function getKeyDir() {
885
		$keyDir = $this->DNData()->getKeyDir();
886
		if (!$keyDir) {
887
			return null;
888
		}
889
890
		$filter = FileNameFilter::create();
891
		$name = $filter->filter($this->Name);
892
893
		return $this->DNData()->getKeyDir() . '/' . $name;
894
	}
895
896
	/**
897
	 * Provide current repository URL to the users.
898
	 *
899
	 * @return void|string
900
	 */
901
	public function getRepositoryURL() {
902
		$showUrl = Config::inst()->get($this->class, 'show_repository_url');
903
		if ($showUrl) {
904
			return $this->CVSPath;
905
		}
906
	}
907
908
	/**
909
	 * Get a ViewableData structure describing the UI tool that lets the user view the repository code
910
	 *
911
	 * @return ArrayData
912
	 */
913
	public function getRepositoryInterface() {
914
		$interfaces = $this->config()->repository_interfaces;
915
916
		/* Look for each whitelisted hostname */
917
		foreach ($interfaces as $host => $interface) {
918
			/* See if the CVS Path is for this hostname, followed by some junk (maybe a port), then the path */
919
			if (preg_match('{^[^.]*' . $host . '(.*?)([/a-zA-Z].+)}', $this->CVSPath, $match)) {
920
921
				$path = $match[2];
922
923
				$scheme = isset($interface['scheme']) ? $interface['scheme'] : 'https';
924
				$host = isset($interface['host']) ? $interface['host'] : $host;
925
				$regex = isset($interface['regex']) ? $interface['regex'] : ['\.git$' => ''];
926
927
				$components = explode('.', $host);
928
929
				foreach ($regex as $pattern => $replacement) {
930
					$path = preg_replace('/' . $pattern . '/', $replacement, $path);
931
				}
932
933
				$uxurl = Controller::join_links($scheme . '://', $host, $path);
934
935
				if (array_key_exists('commit', $interface) && $interface['commit'] == false) {
936
					$commiturl = false;
937
				} else {
938
					$commiturl = Controller::join_links(
939
						$uxurl,
940
						isset($interface['commit']) ? $interface['commit'] : 'commit'
941
					);
942
				}
943
944
				return new ArrayData([
945
					'Name' => isset($interface['name']) ? $interface['name'] : ucfirst($components[0]),
946
					'Icon' => isset($interface['icon']) ? $interface['icon'] : 'deploynaut/img/git.png',
947
					'URL' => $uxurl,
948
					'CommitURL' => $commiturl
949
				]);
950
			}
951
		}
952
	}
953
954
	/**
955
	 * Convenience wrapper for a single permission code.
956
	 *
957
	 * @param string $code
958
	 * @return SS_List
959
	 */
960
	public function whoIsAllowed($code) {
961
		return $this->whoIsAllowedAny([$code]);
962
	}
963
964
	/**
965
	 * List members who have $codes on this project.
966
	 * Does not support Permission::DENY_PERMISSION malarky, same as Permission::get_groups_by_permission anyway...
967
	 *
968
	 * @param array|string $codes
969
	 * @return SS_List
970
	 */
971
	public function whoIsAllowedAny($codes) {
972
		if (!is_array($codes)) {
973
			$codes = [$codes];
974
		}
975
976
		$SQLa_codes = Convert::raw2sql($codes);
977
		$SQL_codes = join("','", $SQLa_codes);
978
979
		return DataObject::get('Member')
980
			->where("\"PermissionRoleCode\".\"Code\" IN ('$SQL_codes') OR \"Permission\".\"Code\" IN ('$SQL_codes')")
981
			->filter("DNProject_Viewers.DNProjectID", $this->ID)
982
			->leftJoin('Group_Members', "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\"")
983
			->leftJoin('Group', "\"Group_Members\".\"GroupID\" = \"Group\".\"ID\"")
984
			->leftJoin('DNProject_Viewers', "\"DNProject_Viewers\".\"GroupID\" = \"Group\".\"ID\"")
985
			->leftJoin('Permission', "\"Permission\".\"GroupID\" = \"Group\".\"ID\"")
986
			->leftJoin('Group_Roles', "\"Group_Roles\".\"GroupID\" = \"Group\".\"ID\"")
987
			->leftJoin('PermissionRole', "\"Group_Roles\".\"PermissionRoleID\" = \"PermissionRole\".\"ID\"")
988
			->leftJoin('PermissionRoleCode', "\"PermissionRoleCode\".\"RoleID\" = \"PermissionRole\".\"ID\"");
989
	}
990
991
	/**
992
	 * Convenience wrapper for a single permission code.
993
	 *
994
	 * @param string $code
995
	 * @param Member|null $member
996
	 *
997
	 * @return bool
998
	 */
999
	public function allowed($code, $member = null) {
1000
		return $this->allowedAny([$code], $member);
1001
	}
1002
1003
	/**
1004
	 * Checks if a group is allowed to the project and the permission code
1005
	 *
1006
	 * @param string $permissionCode
1007
	 * @param Group $group
1008
	 *
1009
	 * @return bool
1010
	 */
1011
	public function groupAllowed($permissionCode, Group $group) {
1012
		$viewers = $this->Viewers();
1013
		if (!$viewers->find('ID', $group->ID)) {
1014
			return false;
1015
		}
1016
		$groups = Permission::get_groups_by_permission($permissionCode);
1017
		if (!$groups->find('ID', $group->ID)) {
1018
			return false;
1019
		}
1020
		return true;
1021
	}
1022
1023
	/**
1024
	 * Check if member has a permission code in this project.
1025
	 *
1026
	 * @param array|string $codes
1027
	 * @param Member|null $member
1028
	 *
1029
	 * @return bool
1030
	 */
1031
	public function allowedAny($codes, $member = null) {
1032
		if (!$member) {
1033
			$member = Member::currentUser();
1034
		}
1035
1036
		if (Permission::checkMember($member, 'ADMIN')) {
1037
			return true;
1038
		}
1039
1040
		$hits = $this->whoIsAllowedAny($codes)->filter('Member.ID', $member->ID)->count();
1041
		return ($hits > 0 ? true : false);
1042
	}
1043
1044
	/**
1045
	 * Checks if the environment has been fully built.
1046
	 *
1047
	 * @return bool
1048
	 */
1049
	public function isProjectReady() {
1050
		if ($this->getRunningInitialEnvironmentCreations()->count() > 0) {
1051
			// We're still creating the initial environments for this project so we're
1052
			// not quite done
1053
			return false;
1054
		}
1055
1056
		// Provide a hook for further checks. Logic stolen from
1057
		// {@see DataObject::extendedCan()}
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1058
		$isDone = $this->extend('isProjectReady');
1059
		if ($isDone && is_array($isDone)) {
1060
			$isDone = array_filter($isDone, function ($val) {
1061
				return !is_null($val);
1062
			});
1063
1064
			// If anything returns false then we're not ready.
1065
			if ($isDone) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $isDone 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...
1066
				return min($isDone);
1067
			}
1068
		}
1069
1070
		return true;
1071
	}
1072
1073
	/**
1074
	 * Returns a list of environments still being created.
1075
	 *
1076
	 * @return SS_List
1077
	 */
1078
	public function getRunningEnvironmentCreations() {
1079
		return $this->CreateEnvironments()
0 ignored issues
show
Bug introduced by
The method CreateEnvironments() does not exist on DNProject. Did you maybe mean canCreateEnvironments()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1080
			->filter('Status', ['Queued', 'Started']);
1081
	}
1082
1083
	/**
1084
	 * Returns a list of initial environments created for this project.
1085
	 *
1086
	 * @return DataList
1087
	 */
1088
	public function getInitialEnvironmentCreations() {
1089
		return $this->CreateEnvironments()->filter('IsInitialEnvironment', true);
0 ignored issues
show
Bug introduced by
The method CreateEnvironments() does not exist on DNProject. Did you maybe mean canCreateEnvironments()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1090
	}
1091
1092
	/**
1093
	 * Only returns initial environments that are being created.
1094
	 *
1095
	 * @return DataList
1096
	 */
1097
	public function getRunningInitialEnvironmentCreations() {
1098
		return $this->getInitialEnvironmentCreations()
1099
			->filter('Status', ['Queued', 'Started']);
1100
	}
1101
1102
	/**
1103
	 * Returns a list of completed initial environment creations. This includes failed tasks.
1104
	 *
1105
	 * @return DataList
1106
	 */
1107
	public function getCompleteInitialEnvironmentCreations() {
1108
		return $this->getInitialEnvironmentCreations()
1109
			->exclude('Status', ['Queued', 'Started']);
1110
	}
1111
1112
	/**
1113
	 * @param Member $member
1114
	 *
1115
	 * @return bool
1116
	 */
1117
	public function canCreate($member = null) {
1118
		if (!$member) {
1119
			$member = Member::currentUser();
1120
		}
1121
		if (!$member) {
1122
			return false;
1123
		}
1124
1125
		if (Permission::checkMember($member, 'ADMIN')) {
1126
			return true;
1127
		}
1128
1129
		// This calls canCreate on extensions.
1130
		return parent::canCreate($member);
1131
	}
1132
1133
	/**
1134
	 * This is a proxy call to gitonmy that caches the information per project and sha
1135
	 *
1136
	 * @param string $sha
1137
	 * @return false|\Gitonomy\Git\Commit
1138
	 */
1139
	public function getCommit($sha) {
1140
		$repo = $this->getRepository();
1141
		if (!$repo) {
1142
			return false;
1143
		}
1144
1145
		$cachekey = $this->ID . '_commit_' . $sha;
1146
		$cache = self::get_git_cache();
1147
		if (!($result = $cache->load($cachekey))) {
1148
			try {
1149
				$result = $repo->getCommit($sha);
1150
			} catch (\Gitonomy\Git\Exception\ReferenceNotFoundException $e) {
1151
				return false;
1152
			}
1153
			$cache->save($result, $cachekey, ['gitonomy', 'commit', 'project_' . $this->ID]);
1154
		}
1155
		return $result;
1156
	}
1157
1158
	/**
1159
	 * @param \Gitonomy\Git\Commit $commit
1160
	 * @return string
1161
	 */
1162 View Code Duplication
	public function getCommitMessage(\Gitonomy\Git\Commit $commit) {
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...
1163
		$cachekey = $this->ID . '_message_' . $commit->getRevision();
1164
		$cache = self::get_git_cache();
1165
		if (!($result = $cache->load($cachekey))) {
1166
			$result = $commit->getMessage();
1167
			$cache->save($result, $cachekey, ['gitonomy', 'message', 'project_' . $this->ID]);
1168
		}
1169
		return $result;
1170
	}
1171
1172
	/**
1173
	 * get the commit "subject", getCommitMessage get the full message
1174
	 *
1175
	 * @param \Gitonomy\Git\Commit $commit
1176
	 * @return string
1177
	 */
1178 View Code Duplication
	public function getCommitSubjectMessage(\Gitonomy\Git\Commit $commit) {
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...
1179
		$cachekey = $this->ID . '_message_subject' . $commit->getRevision();
1180
		$cache = self::get_git_cache();
1181
		if (!($result = $cache->load($cachekey))) {
1182
			$result = $commit->getSubjectMessage();
1183
			$cache->save($result, $cachekey, ['gitonomy', 'message', 'project_' . $this->ID]);
1184
		}
1185
		return $result;
1186
	}
1187
1188
	/**
1189
	 * @param \Gitonomy\Git\Commit $commit
1190
	 * @return mixed
1191
	 */
1192
	public function getCommitTags(\Gitonomy\Git\Commit $commit) {
1193
		$cachekey = $this->ID . '_tags_' . $commit->getRevision();
1194
		$cache = self::get_git_cache();
1195
		$result = $cache->load($cachekey);
1196
		// we check against false, because in many cases the tag list is an empty array
1197
		if ($result === false) {
1198
			$repo = $this->getRepository();
1199
			$result = $repo->getReferences()->resolveTags($commit->getRevision());
1200
			$cache->save($result, $cachekey, ['gitonomy', 'tags', 'project_' . $this->ID]);
1201
		}
1202
		return $result;
1203
	}
1204
1205
	/**
1206
	 * Setup a gridfield for the environment configs
1207
	 *
1208
	 * @param FieldList $fields
1209
	 * @param GridField $environments
1210
	 */
1211
	protected function setEnvironmentFields(&$fields, $environments) {
1212
		if (!$environments) {
1213
			return;
1214
		}
1215
1216
		$environments->getConfig()->addComponent(new GridFieldAddNewMultiClass());
1217
		$environments->getConfig()->removeComponentsByType('GridFieldAddNewButton');
1218
		$environments->getConfig()->removeComponentsByType('GridFieldAddExistingAutocompleter');
1219
		$environments->getConfig()->removeComponentsByType('GridFieldDeleteAction');
1220
		$environments->getConfig()->removeComponentsByType('GridFieldPageCount');
1221
		if (Config::inst()->get('DNEnvironment', 'allow_web_editing')) {
1222
			$addNewRelease = new GridFieldAddNewButton('toolbar-header-right');
1223
			$addNewRelease->setButtonName('Add');
1224
			$environments->getConfig()->addComponent($addNewRelease);
1225
		}
1226
1227
		$fields->addFieldToTab("Root.Main", $environments);
1228
	}
1229
1230
	/**
1231
	 * @return string
1232
	 */
1233
	protected function getProjectFolderPath() {
1234
		return sprintf('%s/%s', $this->DNData()->getEnvironmentDir(), $this->Name);
1235
	}
1236
1237
	/**
1238
	 * @return ValidationResult
1239
	 */
1240
	protected function validate() {
1241
		$validation = parent::validate();
1242
		if ($validation->valid()) {
1243
			if (empty($this->Name)) {
1244
				return $validation->error('The stack must have a name.');
1245
			}
1246
1247
			// The name is used to build filepaths so should be restricted
1248
			if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-\_]+$/', $this->Name)) {
1249
				return $validation->error('Project name can only contain alphanumeric, hyphens and underscores.');
1250
			}
1251
1252
			if (empty($this->CVSPath)) {
1253
				return $validation->error('You must provide a repository URL.');
1254
			}
1255
1256
			$existing = DNProject::get()->filter('Name', $this->Name);
0 ignored issues
show
Coding Style introduced by
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...
1257
			if ($this->ID) {
1258
				$existing = $existing->exclude('ID', $this->ID);
1259
			}
1260
			if ($existing->count() > 0) {
1261
				return $validation->error('A stack already exists with that name.');
1262
			}
1263
		}
1264
		return $validation;
1265
	}
1266
1267
}
1268
1269