Completed
Pull Request — master (#658)
by Stig
14:57 queued 08:07
created

DNProject::clearGitCache()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 0
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
		"CVSPath" => "Varchar(255)",
23
		"DiskQuotaMB" => "Int",
24
		"AllowedEnvironmentType" => "Varchar(255)",
25
	];
26
27
	/**
28
	 * @var array
29
	 */
30
	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...
31
		"Environments" => "DNEnvironment",
32
		"CreateEnvironments" => "DNCreateEnvironment"
33
	];
34
35
	/**
36
	 * @var array
37
	 */
38
	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...
39
		"Viewers" => "Group",
40
		'StarredBy' => "Member"
41
	];
42
43
	/**
44
	 * @var array
45
	 */
46
	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...
47
		"Name",
48
		"ViewersList",
49
	];
50
51
	/**
52
	 * @var array
53
	 */
54
	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...
55
		"Name",
56
	];
57
58
	/**
59
	 * @var string
60
	 */
61
	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...
62
63
	/**
64
	 * @var string
65
	 */
66
	private static $plural_name = 'Projects';
67
68
	/**
69
	 * @var string
70
	 */
71
	private static $default_sort = 'Name';
72
73
	/**
74
	 * In-memory cache for currentBuilds per environment since fetching them from
75
	 * disk is pretty resource hungry.
76
	 *
77
	 * @var array
78
	 */
79
	protected static $relation_cache = [];
80
81
	/**
82
	 * @var bool|Member
83
	 */
84
	protected static $_current_member_cache = null;
85
86
	/**
87
	 * Display the repository URL on the project page.
88
	 *
89
	 * @var bool
90
	 */
91
	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...
92
93
	/**
94
	 * In-memory cache to determine whether clone repo was called.
95
	 * @var array
96
	 */
97
	private static $has_cloned_cache = [];
98
99
	/**
100
	 * Whitelist configuration that describes how to convert a repository URL into a link
101
	 * to a web user interface for that URL
102
	 *
103
	 * Consists of a hash of "full.lower.case.domain" => {configuration} key/value pairs
104
	 *
105
	 * {configuration} can either be boolean true to auto-detect both the host and the
106
	 * name of the UI provider, or a nested array that overrides either one or both
107
	 * of the auto-detected valyes
108
	 *
109
	 * @var array
110
	 */
111
	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...
112
		'github.com' => [
113
			'icon' => 'deploynaut/img/github.png',
114
			'name' => 'Github.com',
115
		],
116
		'bitbucket.org' => [
117
			'commit' => 'commits',
118
			'name' => 'Bitbucket.org',
119
		],
120
		'repo.or.cz' => [
121
			'scheme' => 'http',
122
			'name' => 'repo.or.cz',
123
			'regex' => ['^(.*)$' => '/w$1'],
124
		],
125
126
		/* 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...
127
		'gitlab.mysite.com' => array(
128
			'icon' => 'deploynaut/img/git.png',
129
			'host' => 'gitlab.mysite.com',
130
			'name' => 'Gitlab',
131
			'regex' => array('.git$' => ''),
132
			'commit' => "commit"
133
		),
134
		*/
135
	];
136
137
	/**
138
	 * Used by the sync task
139
	 *
140
	 * @param string $path
141
	 * @return \DNProject
142
	 */
143
	public static function create_from_path($path) {
144
		$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...
145
		$project->Name = $path;
146
		$project->write();
147
148
		// add the administrators group as the viewers of the new project
149
		$adminGroup = Group::get()->filter('Code', 'administrators')->first();
150
		if ($adminGroup && $adminGroup->exists()) {
151
			$project->Viewers()->add($adminGroup);
152
		}
153
		return $project;
154
	}
155
156
	/**
157
	 * This will clear the cache for the git getters and should be called when the local git repo is updated
158
	 */
159
	public function clearGitCache() {
160
		$cache = self::get_git_cache();
161
		// we only need to clear the tag cache since everything else is cached by SHA, that is for commit and
162
		// commit message.
163
		$cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, ['gitonomy', 'tags', 'project_' . $this->ID]);
164
	}
165
166
	/**
167
	 * @return \Zend_Cache_Frontend_Output
168
	 */
169
	public static function get_git_cache() {
170
		return SS_Cache::factory('gitonomy', 'Output', [
171
			'automatic_serialization' => true,
172
			'lifetime' => 60 * 60 * 24 * 7 // seven days
173
		]);
174
	}
175
176
	/**
177
	 * Return the used quota in MB.
178
	 *
179
	 * @param int $round Number of decimal places to round to
180
	 * @return double The used quota size in MB
181
	 */
182
	public function getUsedQuotaMB($round = 2) {
183
		$size = 0;
184
185
		foreach ($this->Environments() as $environment) {
186
			foreach ($environment->DataArchives()->filter('IsBackup', 0) as $archive) {
187
				$size += $archive->ArchiveFile()->getAbsoluteSize();
188
			}
189
		}
190
191
		// convert bytes to megabytes and round
192
		return round(($size / 1024) / 1024, $round);
193
	}
194
195
	/**
196
	 * Getter for DiskQuotaMB field to provide a default for existing
197
	 * records that have no quota field set, as it will need to default
198
	 * to a globally set size.
199
	 *
200
	 * @return string|int The quota size in MB
201
	 */
202
	public function getDiskQuotaMB() {
203
		$size = $this->getField('DiskQuotaMB');
204
205
		if (empty($size)) {
206
			$defaults = $this->config()->get('defaults');
207
			$size = (isset($defaults['DiskQuotaMB'])) ? $defaults['DiskQuotaMB'] : 0;
208
		}
209
210
		return $size;
211
	}
212
213
	/**
214
	 * Has the disk quota been exceeded?
215
	 *
216
	 * @return boolean
217
	 */
218
	public function HasExceededDiskQuota() {
219
		return $this->getUsedQuotaMB(0) >= $this->getDiskQuotaMB();
220
	}
221
222
	/**
223
	 * Is there a disk quota set for this project?
224
	 *
225
	 * @return boolean
226
	 */
227
	public function HasDiskQuota() {
228
		return $this->getDiskQuotaMB() > 0;
229
	}
230
231
	/**
232
	 * Returns the current disk quota usage as a percentage
233
	 *
234
	 * @return int
235
	 */
236
	public function DiskQuotaUsagePercent() {
237
		$quota = $this->getDiskQuotaMB();
238
		if ($quota > 0) {
239
			return $this->getUsedQuotaMB() * 100 / $quota;
240
		}
241
		return 100;
242
	}
243
244
	/**
245
	 * Get the menu to be shown on projects
246
	 *
247
	 * @return ArrayList
248
	 */
249
	public function Menu() {
250
		$list = new ArrayList();
251
252
		$controller = Controller::curr();
253
		$actionType = $controller->getField('CurrentActionType');
254
255
		if (DNRoot::FlagSnapshotsEnabled() && $this->isProjectReady()) {
256
			$list->push(new ArrayData([
257
				'Link' => sprintf('naut/project/%s/snapshots', $this->Name),
258
				'Title' => 'Snapshots',
259
				'IsCurrent' => $this->isSection() && $controller->getAction() == 'snapshots',
260
				'IsSection' => $this->isSection() && $actionType == DNRoot::ACTION_SNAPSHOT
261
			]));
262
		}
263
264
		$this->extend('updateMenu', $list);
265
266
		return $list;
267
	}
268
269
	/**
270
	 * Is this project currently at the root level of the controller that handles it?
271
	 *
272
	 * @return bool
273
	 */
274
	public function isCurrent() {
275
		return $this->isSection() && Controller::curr()->getAction() == 'project';
276
	}
277
278
	/**
279
	 * Return the current object from $this->Menu()
280
	 * Good for making titles and things
281
	 *
282
	 * @return DataObject
283
	 */
284
	public function CurrentMenu() {
285
		return $this->Menu()->filter('IsSection', true)->First();
286
	}
287
288
	/**
289
	 * Is this project currently in a controller that is handling it or performing a sub-task?
290
	 *
291
	 * @return bool
292
	 */
293
	public function isSection() {
294
		$controller = Controller::curr();
295
		$project = $controller->getField('CurrentProject');
296
		return $project && $this->ID == $project->ID;
297
	}
298
299
	/**
300
	 * Restrict access to viewing this project
301
	 *
302
	 * @param Member|null $member
303
	 * @return boolean
304
	 */
305
	public function canView($member = null) {
306
		if (!$member) {
307
			$member = Member::currentUser();
308
		}
309
310
		if (Permission::checkMember($member, 'ADMIN')) {
311
			return true;
312
		}
313
314
		return $member->inGroups($this->Viewers());
315
	}
316
317
	/**
318
	 * @param Member|null $member
319
	 *
320
	 * @return bool
321
	 */
322 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...
323
		if ($this->allowedAny(
324
			[
325
				DNRoot::ALLOW_PROD_SNAPSHOT,
326
				DNRoot::ALLOW_NON_PROD_SNAPSHOT
327
			],
328
			$member
329
		)
330
		) {
331
			return true;
332
		}
333
334
		return (bool) $this->Environments()->filterByCallback(function ($env) use ($member) {
335
			return $env->canRestore($member);
336
		})->Count();
337
	}
338
339
	/**
340
	 * @param Member|null $member
341
	 * @return bool
342
	 */
343 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...
344
		if ($this->allowedAny(
345
			[
346
				DNRoot::ALLOW_PROD_SNAPSHOT,
347
				DNRoot::ALLOW_NON_PROD_SNAPSHOT
348
			],
349
			$member
350
		)
351
		) {
352
			return true;
353
		}
354
355
		return (bool) $this->Environments()->filterByCallback(function ($env) use ($member) {
356
			return $env->canBackup($member);
357
		})->Count();
358
	}
359
360
	/**
361
	 * @param Member|null $member
362
	 * @return bool
363
	 */
364 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...
365
		if ($this->allowedAny(
366
			[
367
				DNRoot::ALLOW_PROD_SNAPSHOT,
368
				DNRoot::ALLOW_NON_PROD_SNAPSHOT
369
			],
370
			$member
371
		)
372
		) {
373
			return true;
374
		}
375
376
		return (bool) $this->Environments()->filterByCallback(function ($env) use ($member) {
377
			return $env->canUploadArchive($member);
378
		})->Count();
379
	}
380
381
	/**
382
	 * @param Member|null $member
383
	 * @return bool
384
	 */
385 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...
386
		if ($this->allowedAny(
387
			[
388
				DNRoot::ALLOW_PROD_SNAPSHOT,
389
				DNRoot::ALLOW_NON_PROD_SNAPSHOT
390
			],
391
			$member
392
		)
393
		) {
394
			return true;
395
		}
396
397
		return (bool) $this->Environments()->filterByCallback(function ($env) use ($member) {
398
			return $env->canDownloadArchive($member);
399
		})->Count();
400
	}
401
402
	/**
403
	 * This is a permission check for the front-end only.
404
	 *
405
	 * Only admins can create environments for now. Also, we need to check the value
406
	 * of AllowedEnvironmentType which dictates which backend to use to render the form.
407
	 *
408
	 * @param Member|null $member
409
	 *
410
	 * @return bool
411
	 */
412
	public function canCreateEnvironments($member = null) {
413
		$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...
414
		if ($envType) {
415
			$env = Injector::inst()->get($envType);
416
			if ($env instanceof EnvironmentCreateBackend) {
417
				return $this->allowed(DNRoot::ALLOW_CREATE_ENVIRONMENT, $member);
418
			}
419
		}
420
		return false;
421
	}
422
423
	/**
424
	 * @return DataList
425
	 */
426
	public function DataArchives() {
427
		$envIds = $this->Environments()->column('ID');
428
		return DNDataArchive::get()->filter('EnvironmentID', $envIds);
429
	}
430
431
	/**
432
	 * Return all archives which are "manual upload requests",
433
	 * meaning they don't have a file attached to them (yet).
434
	 *
435
	 * @return DataList
436
	 */
437
	public function PendingManualUploadDataArchives() {
438
		return $this->DataArchives()->filter('ArchiveFileID', null);
439
	}
440
441
	/**
442
	 * Build an environment variable array to be used with this project.
443
	 *
444
	 * This is relevant if every project needs to use an individual SSH pubkey.
445
	 *
446
	 * Include this with all Gitonomy\Git\Repository, and
447
	 * \Symfony\Component\Process\Processes.
448
	 *
449
	 * @return array
450
	 */
451
	public function getProcessEnv() {
452
		if (file_exists($this->getPrivateKeyPath())) {
453
			// Key-pair is available, use it.
454
			$processEnv = [
455
				'IDENT_KEY' => $this->getPrivateKeyPath(),
456
				'GIT_SSH' => BASE_PATH . "/deploynaut/git-deploy.sh"
457
			];
458
		} else {
459
			$processEnv = [];
460
		}
461
		$this->extend('updateProcessEnv', $processEnv);
462
463
		return $processEnv;
464
	}
465
466
	/**
467
	 * Get a string of people allowed to view this project
468
	 *
469
	 * @return string
470
	 */
471
	public function getViewersList() {
472
		return implode(", ", $this->Viewers()->column("Title"));
473
	}
474
475
	/**
476
	 * @return DNData
477
	 */
478
	public function DNData() {
479
		return DNData::inst();
480
	}
481
482
	/**
483
	 * Provides a DNBuildList of builds found in this project.
484
	 *
485
	 * @return DNReferenceList
486
	 */
487
	public function DNBuildList() {
488
		return DNReferenceList::create($this, $this->DNData());
489
	}
490
491
	/**
492
	 * Provides a list of the branches in this project.
493
	 *
494
	 * @return DNBranchList
495
	 */
496
	public function DNBranchList() {
497
		if ($this->CVSPath && !$this->repoExists()) {
498
			$this->cloneRepo();
499
		}
500
		return DNBranchList::create($this, $this->DNData());
501
	}
502
503
	/**
504
	 * Provides a list of the tags in this project.
505
	 *
506
	 * @return DNReferenceList
507
	 */
508
	public function DNTagList() {
509
		if ($this->CVSPath && !$this->repoExists()) {
510
			$this->cloneRepo();
511
		}
512
		return DNReferenceList::create($this, $this->DNData(), null, null, true);
513
	}
514
515
	/**
516
	 * @return false|Gitonomy\Git\Repository
517
	 */
518
	public function getRepository() {
519
		if (!$this->repoExists()) {
520
			return false;
521
		}
522
523
		return new Gitonomy\Git\Repository($this->getLocalCVSPath());
524
	}
525
526
	/**
527
	 * Provides a list of environments found in this project.
528
	 * CAUTION: filterByCallback will change this into an ArrayList!
529
	 *
530
	 * @return ArrayList
531
	 */
532
	public function DNEnvironmentList() {
533
534
		if (!self::$_current_member_cache) {
535
			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...
536
		}
537
538
		if (self::$_current_member_cache === false) {
539
			return new ArrayList();
540
		}
541
542
		$currentMember = self::$_current_member_cache;
543
		return $this->Environments()
544
			->filterByCallBack(function ($item) use ($currentMember) {
545
				return $item->canView($currentMember);
546
			});
547
	}
548
549
	/**
550
	 * @param string $usage
551
	 * @return ArrayList
552
	 */
553
	public function EnvironmentsByUsage($usage) {
554
		return $this->DNEnvironmentList()->filter('Usage', $usage);
555
	}
556
557
	/**
558
	 * Returns a map of envrionment name to build name
559
	 *
560
	 * @return false|DNDeployment
561
	 */
562
	public function currentBuilds() {
563
		if (!isset(self::$relation_cache['currentBuilds.' . $this->ID])) {
564
			$currentBuilds = [];
565
			foreach ($this->Environments() as $env) {
566
				$currentBuilds[$env->Name] = $env->CurrentBuild();
567
			}
568
			self::$relation_cache['currentBuilds.' . $this->ID] = $currentBuilds;
569
		}
570
		return self::$relation_cache['currentBuilds.' . $this->ID];
571
	}
572
573
	/**
574
	 * @param string
575
	 * @return string
576
	 */
577
	public function Link($action = '') {
578
		return Controller::join_links("naut", "project", $this->Name, $action);
579
	}
580
581
	/**
582
	 * @return string|null
583
	 */
584
	public function CreateEnvironmentLink() {
585
		if ($this->canCreateEnvironments()) {
586
			return $this->Link('createenv');
587
		}
588
		return null;
589
	}
590
591
	/**
592
	 * @return string
593
	 */
594
	public function ToggleStarLink() {
595
		return $this->Link('/star');
596
	}
597
598
	/**
599
	 * @return bool
600
	 */
601
	public function IsStarred() {
602
		$member = Member::currentUser();
603
		if ($member === null) {
604
			return false;
605
		}
606
		$favourited = $this->StarredBy()->filter('MemberID', $member->ID);
607
		if ($favourited->count() == 0) {
608
			return false;
609
		}
610
		return true;
611
	}
612
613
	/**
614
	 * @param string $action
615
	 * @return string
616
	 */
617
	public function APILink($action) {
618
		return Controller::join_links("naut", "api", $this->Name, $action);
619
	}
620
621
	/**
622
	 * @return FieldList
623
	 */
624
	public function getCMSFields() {
625
		$fields = parent::getCMSFields();
626
627
		/** @var GridField $environments */
628
		$environments = $fields->dataFieldByName("Environments");
629
630
		$fields->fieldByName("Root")->removeByName("Viewers");
631
		$fields->fieldByName("Root")->removeByName("Environments");
632
		$fields->fieldByName("Root")->removeByName("LocalCVSPath");
633
634
		$diskQuotaDesc = 'This is the maximum amount of disk space (in megabytes) that all environments within this '
635
			. 'project can use for stored snapshots';
636
		$fields->dataFieldByName('DiskQuotaMB')->setDescription($diskQuotaDesc);
637
638
		$projectNameDesc = 'Changing the name will <strong>reset</strong> the deploy configuration and avoid using non'
639
			. 'alphanumeric characters';
640
		$fields->fieldByName('Root.Main.Name')
641
			->setTitle('Project name')
642
			->setDescription($projectNameDesc);
643
644
		$fields->fieldByName('Root.Main.CVSPath')
645
			->setTitle('Git repository')
646
			->setDescription('E.g. [email protected]:silverstripe/silverstripe-installer.git');
647
648
		$workspaceField = new ReadonlyField('LocalWorkspace', 'Git workspace', $this->getLocalCVSPath());
649
		$workspaceField->setDescription('This is where the GIT repository are located on this server');
650
		$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...
651
652
		$readAccessGroups = ListboxField::create('Viewers', 'Project viewers', Group::get()->map()->toArray())
653
			->setMultiple(true)
654
			->setDescription('These groups can view the project in the front-end.');
655
		$fields->addFieldToTab("Root.Main", $readAccessGroups);
656
657
		$this->setCreateProjectFolderField($fields);
658
		$this->setEnvironmentFields($fields, $environments);
659
660
		$environmentTypes = ClassInfo::implementorsOf('EnvironmentCreateBackend');
661
		$types = [];
662
		foreach ($environmentTypes as $type) {
663
			$types[$type] = $type;
664
		}
665
666
		$fields->addFieldsToTab('Root.Main', [
667
			DropdownField::create(
668
				'AllowedEnvironmentType',
669
				'Allowed Environment Type',
670
				$types
671
			)->setDescription('This defined which form to show on the front end for '
672
				. 'environment creation. This will not affect backend functionality.')
673
				->setEmptyString(' - None - '),
674
		]);
675
676
		return $fields;
677
	}
678
679
	/**
680
	 * If there isn't a capistrano env project folder, show options to create one
681
	 *
682
	 * @param FieldList $fields
683
	 */
684
	public function setCreateProjectFolderField(&$fields) {
685
		// Check if the capistrano project folder exists
686
		if (!$this->Name) {
687
			return;
688
		}
689
690
		if ($this->projectFolderExists()) {
691
			return;
692
		}
693
694
		$createFolderNotice = new LabelField('CreateEnvFolderNotice', 'Warning: No Capistrano project folder exists');
695
		$createFolderNotice->addExtraClass('message warning');
696
		$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...
697
		$createFolderField = new CheckboxField('CreateEnvFolder', 'Create folder');
698
		$createFolderField->setDescription('Would you like to create the capistrano project folder?');
699
		$fields->insertAfter($createFolderField, 'CreateEnvFolderNotice');
700
	}
701
702
	/**
703
	 * @return boolean
704
	 */
705
	public function projectFolderExists() {
706
		return file_exists($this->getProjectFolderPath());
707
	}
708
709
	/**
710
	 * @return bool
711
	 */
712
	public function repoExists() {
713
		return file_exists(sprintf('%s/HEAD', $this->getLocalCVSPath()));
714
	}
715
716
	/**
717
	 * Setup a job to clone a git repository.
718
	 * @return string resque token
719
	 */
720
	public function cloneRepo() {
721
		// Avoid this being called multiple times in the same request
722
		if (!isset(self::$has_cloned_cache[$this->ID])) {
723
			$fetch = DNGitFetch::create();
724
			$fetch->ProjectID = $this->ID;
725
			$fetch->write();
726
727
			// passing true here tells DNGitFetch to force a git clone, otherwise
728
			// it will just update the repo if it already exists. We want to ensure
729
			// we're always cloning a new repo in this case, as the git URL may have changed.
730
			$fetch->start(true);
731
732
			self::$has_cloned_cache[$this->ID] = true;
733
		}
734
	}
735
736
	/**
737
	 * @return string
738
	 */
739
	public function getLocalCVSPath() {
740
		return sprintf('%s/%s', DEPLOYNAUT_LOCAL_VCS_PATH, $this->Name);
741
	}
742
743
	public function onBeforeWrite() {
744
		parent::onBeforeWrite();
745
746
		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...
747
			mkdir($this->getProjectFolderPath());
748
		}
749
	}
750
751
	public function onAfterWrite() {
752
		parent::onAfterWrite();
753
754
		if (!$this->CVSPath) {
755
			return;
756
		}
757
758
		$changedFields = $this->getChangedFields(true, 2);
759
		if (isset($changedFields['CVSPath']) || isset($changedFields['Name'])) {
760
			$this->cloneRepo();
761
		}
762
	}
763
764
	/**
765
	 * Delete related environments and folders
766
	 */
767
	public function onAfterDelete() {
768
		parent::onAfterDelete();
769
770
		// Delete related environments
771
		foreach ($this->Environments() as $env) {
772
			$env->delete();
773
		}
774
775
		// Delete local repository
776
		if (file_exists($this->getLocalCVSPath())) {
777
			Filesystem::removeFolder($this->getLocalCVSPath());
778
		}
779
780
		// Delete project template
781
		if (file_exists($this->getProjectFolderPath()) && Config::inst()->get('DNEnvironment', 'allow_web_editing')) {
782
			Filesystem::removeFolder($this->getProjectFolderPath());
783
		}
784
785
		// Delete the deploy key
786
		if (file_exists($this->getKeyDir())) {
787
			Filesystem::removeFolder($this->getKeyDir());
788
		}
789
	}
790
791
	/**
792
	 * Fetch the public key for this project.
793
	 *
794
	 * @return string|void
795
	 */
796
	public function getPublicKey() {
797
		$key = $this->getPublicKeyPath();
798
799
		if (file_exists($key)) {
800
			return trim(file_get_contents($key));
801
		}
802
	}
803
804
	/**
805
	 * This returns that path of the public key if a key directory is set. It doesn't check whether the file exists.
806
	 *
807
	 * @return string|null
808
	 */
809
	public function getPublicKeyPath() {
810
		if ($privateKey = $this->getPrivateKeyPath()) {
811
			return $privateKey . '.pub';
812
		}
813
		return null;
814
	}
815
816
	/**
817
	 * This returns that path of the private key if a key directory is set. It doesn't check whether the file exists.
818
	 *
819
	 * @return string|null
820
	 */
821
	public function getPrivateKeyPath() {
822
		$keyDir = $this->getKeyDir();
823
		if (!empty($keyDir)) {
824
			$filter = FileNameFilter::create();
825
			$name = $filter->filter($this->Name);
826
			return $keyDir . '/' . $name;
827
		}
828
		return null;
829
	}
830
831
	/**
832
	 * Returns the location of the projects key dir if one exists.
833
	 *
834
	 * @return string|null
835
	 */
836
	public function getKeyDir() {
837
		$keyDir = $this->DNData()->getKeyDir();
838
		if (!$keyDir) {
839
			return null;
840
		}
841
842
		$filter = FileNameFilter::create();
843
		$name = $filter->filter($this->Name);
844
845
		return $this->DNData()->getKeyDir() . '/' . $name;
846
	}
847
848
	/**
849
	 * Provide current repository URL to the users.
850
	 *
851
	 * @return void|string
852
	 */
853
	public function getRepositoryURL() {
854
		$showUrl = Config::inst()->get($this->class, 'show_repository_url');
855
		if ($showUrl) {
856
			return $this->CVSPath;
857
		}
858
	}
859
860
	/**
861
	 * Get a ViewableData structure describing the UI tool that lets the user view the repository code
862
	 *
863
	 * @return ArrayData
864
	 */
865
	public function getRepositoryInterface() {
866
		$interfaces = $this->config()->repository_interfaces;
867
868
		/* Look for each whitelisted hostname */
869
		foreach ($interfaces as $host => $interface) {
870
			/* See if the CVS Path is for this hostname, followed by some junk (maybe a port), then the path */
871
			if (preg_match('{^[^.]*' . $host . '(.*?)([/a-zA-Z].+)}', $this->CVSPath, $match)) {
872
873
				$path = $match[2];
874
875
				$scheme = isset($interface['scheme']) ? $interface['scheme'] : 'https';
876
				$host = isset($interface['host']) ? $interface['host'] : $host;
877
				$regex = isset($interface['regex']) ? $interface['regex'] : ['\.git$' => ''];
878
879
				$components = explode('.', $host);
880
881
				foreach ($regex as $pattern => $replacement) {
882
					$path = preg_replace('/' . $pattern . '/', $replacement, $path);
883
				}
884
885
				$uxurl = Controller::join_links($scheme . '://', $host, $path);
886
887
				if (array_key_exists('commit', $interface) && $interface['commit'] == false) {
888
					$commiturl = false;
889
				} else {
890
					$commiturl = Controller::join_links(
891
						$uxurl,
892
						isset($interface['commit']) ? $interface['commit'] : 'commit'
893
					);
894
				}
895
896
				return new ArrayData([
897
					'Name' => isset($interface['name']) ? $interface['name'] : ucfirst($components[0]),
898
					'Icon' => isset($interface['icon']) ? $interface['icon'] : 'deploynaut/img/git.png',
899
					'URL' => $uxurl,
900
					'CommitURL' => $commiturl
901
				]);
902
			}
903
		}
904
	}
905
906
	/**
907
	 * Convenience wrapper for a single permission code.
908
	 *
909
	 * @param string $code
910
	 * @return SS_List
911
	 */
912
	public function whoIsAllowed($code) {
913
		return $this->whoIsAllowedAny([$code]);
914
	}
915
916
	/**
917
	 * List members who have $codes on this project.
918
	 * Does not support Permission::DENY_PERMISSION malarky, same as Permission::get_groups_by_permission anyway...
919
	 *
920
	 * @param array|string $codes
921
	 * @return SS_List
922
	 */
923
	public function whoIsAllowedAny($codes) {
924
		if (!is_array($codes)) {
925
			$codes = [$codes];
926
		}
927
928
		$SQLa_codes = Convert::raw2sql($codes);
929
		$SQL_codes = join("','", $SQLa_codes);
930
931
		return DataObject::get('Member')
932
			->where("\"PermissionRoleCode\".\"Code\" IN ('$SQL_codes') OR \"Permission\".\"Code\" IN ('$SQL_codes')")
933
			->filter("DNProject_Viewers.DNProjectID", $this->ID)
934
			->leftJoin('Group_Members', "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\"")
935
			->leftJoin('Group', "\"Group_Members\".\"GroupID\" = \"Group\".\"ID\"")
936
			->leftJoin('DNProject_Viewers', "\"DNProject_Viewers\".\"GroupID\" = \"Group\".\"ID\"")
937
			->leftJoin('Permission', "\"Permission\".\"GroupID\" = \"Group\".\"ID\"")
938
			->leftJoin('Group_Roles', "\"Group_Roles\".\"GroupID\" = \"Group\".\"ID\"")
939
			->leftJoin('PermissionRole', "\"Group_Roles\".\"PermissionRoleID\" = \"PermissionRole\".\"ID\"")
940
			->leftJoin('PermissionRoleCode', "\"PermissionRoleCode\".\"RoleID\" = \"PermissionRole\".\"ID\"");
941
	}
942
943
	/**
944
	 * Convenience wrapper for a single permission code.
945
	 *
946
	 * @param string $code
947
	 * @param Member|null $member
948
	 *
949
	 * @return bool
950
	 */
951
	public function allowed($code, $member = null) {
952
		return $this->allowedAny([$code], $member);
953
	}
954
955
	/**
956
	 * Checks if a group is allowed to the project and the permission code
957
	 *
958
	 * @param string $permissionCode
959
	 * @param Group $group
960
	 *
961
	 * @return bool
962
	 */
963
	public function groupAllowed($permissionCode, Group $group) {
964
		$viewers = $this->Viewers();
965
		if (!$viewers->find('ID', $group->ID)) {
966
			return false;
967
		}
968
		$groups = Permission::get_groups_by_permission($permissionCode);
969
		if (!$groups->find('ID', $group->ID)) {
970
			return false;
971
		}
972
		return true;
973
	}
974
975
	/**
976
	 * Check if member has a permission code in this project.
977
	 *
978
	 * @param array|string $codes
979
	 * @param Member|null $member
980
	 *
981
	 * @return bool
982
	 */
983
	public function allowedAny($codes, $member = null) {
984
		if (!$member) {
985
			$member = Member::currentUser();
986
		}
987
988
		if (Permission::checkMember($member, 'ADMIN')) {
989
			return true;
990
		}
991
992
		$hits = $this->whoIsAllowedAny($codes)->filter('Member.ID', $member->ID)->count();
993
		return ($hits > 0 ? true : false);
994
	}
995
996
	/**
997
	 * Checks if the environment has been fully built.
998
	 *
999
	 * @return bool
1000
	 */
1001
	public function isProjectReady() {
1002
		if ($this->getRunningInitialEnvironmentCreations()->count() > 0) {
1003
			// We're still creating the initial environments for this project so we're
1004
			// not quite done
1005
			return false;
1006
		}
1007
1008
		// Provide a hook for further checks. Logic stolen from
1009
		// {@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...
1010
		$isDone = $this->extend('isProjectReady');
1011
		if ($isDone && is_array($isDone)) {
1012
			$isDone = array_filter($isDone, function ($val) {
1013
				return !is_null($val);
1014
			});
1015
1016
			// If anything returns false then we're not ready.
1017
			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...
1018
				return min($isDone);
1019
			}
1020
		}
1021
1022
		return true;
1023
	}
1024
1025
	/**
1026
	 * Returns a list of environments still being created.
1027
	 *
1028
	 * @return SS_List
1029
	 */
1030
	public function getRunningEnvironmentCreations() {
1031
		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...
1032
			->filter('Status', ['Queued', 'Started']);
1033
	}
1034
1035
	/**
1036
	 * Returns a list of initial environments created for this project.
1037
	 *
1038
	 * @return DataList
1039
	 */
1040
	public function getInitialEnvironmentCreations() {
1041
		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...
1042
	}
1043
1044
	/**
1045
	 * Only returns initial environments that are being created.
1046
	 *
1047
	 * @return DataList
1048
	 */
1049
	public function getRunningInitialEnvironmentCreations() {
1050
		return $this->getInitialEnvironmentCreations()
1051
			->filter('Status', ['Queued', 'Started']);
1052
	}
1053
1054
	/**
1055
	 * Returns a list of completed initial environment creations. This includes failed tasks.
1056
	 *
1057
	 * @return DataList
1058
	 */
1059
	public function getCompleteInitialEnvironmentCreations() {
1060
		return $this->getInitialEnvironmentCreations()
1061
			->exclude('Status', ['Queued', 'Started']);
1062
	}
1063
1064
	/**
1065
	 * @param Member $member
1066
	 *
1067
	 * @return bool
1068
	 */
1069
	public function canCreate($member = null) {
1070
		if (!$member) {
1071
			$member = Member::currentUser();
1072
		}
1073
		if (!$member) {
1074
			return false;
1075
		}
1076
1077
		if (Permission::checkMember($member, 'ADMIN')) {
1078
			return true;
1079
		}
1080
1081
		// This calls canCreate on extensions.
1082
		return parent::canCreate($member);
1083
	}
1084
1085
	/**
1086
	 * This is a proxy call to gitonmy that caches the information per project and sha
1087
	 *
1088
	 * @param string $sha
1089
	 * @return false|\Gitonomy\Git\Commit
1090
	 */
1091
	public function getCommit($sha) {
1092
		$repo = $this->getRepository();
1093
		if (!$repo) {
1094
			return false;
1095
		}
1096
1097
		$cachekey = $this->ID . '_commit_' . $sha;
1098
		$cache = self::get_git_cache();
1099 View Code Duplication
		if (!($result = $cache->load($cachekey))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
1100
			$result = $repo->getCommit($sha);
1101
			$cache->save($result, $cachekey, ['gitonomy', 'commit', 'project_' . $this->ID]);
1102
		}
1103
		return $result;
1104
	}
1105
1106
	/**
1107
	 * @param \Gitonomy\Git\Commit $commit
1108
	 * @return string
1109
	 */
1110
	public function getCommitMessage(\Gitonomy\Git\Commit $commit) {
1111
		$cachekey = $this->ID . '_message_' . $commit->getRevision();
1112
		$cache = self::get_git_cache();
1113 View Code Duplication
		if (!($result = $cache->load($cachekey))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
1114
			$result = $commit->getMessage();
1115
			$cache->save($result, $cachekey, ['gitonomy', 'message', 'project_' . $this->ID]);
1116
		}
1117
		return $result;
1118
	}
1119
1120
	/**
1121
	 * @param \Gitonomy\Git\Commit $commit
1122
	 * @return mixed
1123
	 */
1124
	public function getCommitTags(\Gitonomy\Git\Commit $commit) {
1125
		$cachekey = $this->ID . '_tags_' . $commit->getRevision();
1126
		$cache = self::get_git_cache();
1127
		$result = $cache->load($cachekey);
1128
		// we check against false, because in many cases the tag list is an empty array
1129
		if ($result === false) {
1130
			$repo = $this->getRepository();
1131
			$result = $tags = $repo->getReferences()->resolveTags($commit->getRevision());
0 ignored issues
show
Unused Code introduced by
$tags is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1132
			$cache->save($result, $cachekey, ['gitonomy', 'tags', 'project_' . $this->ID]);
1133
		}
1134
		return $result;
1135
	}
1136
1137
	/**
1138
	 * Setup a gridfield for the environment configs
1139
	 *
1140
	 * @param FieldList $fields
1141
	 * @param GridField $environments
1142
	 */
1143
	protected function setEnvironmentFields(&$fields, $environments) {
1144
		if (!$environments) {
1145
			return;
1146
		}
1147
1148
		$environments->getConfig()->addComponent(new GridFieldAddNewMultiClass());
1149
		$environments->getConfig()->removeComponentsByType('GridFieldAddNewButton');
1150
		$environments->getConfig()->removeComponentsByType('GridFieldAddExistingAutocompleter');
1151
		$environments->getConfig()->removeComponentsByType('GridFieldDeleteAction');
1152
		$environments->getConfig()->removeComponentsByType('GridFieldPageCount');
1153
		if (Config::inst()->get('DNEnvironment', 'allow_web_editing')) {
1154
			$addNewRelease = new GridFieldAddNewButton('toolbar-header-right');
1155
			$addNewRelease->setButtonName('Add');
1156
			$environments->getConfig()->addComponent($addNewRelease);
1157
		}
1158
1159
		$fields->addFieldToTab("Root.Main", $environments);
1160
	}
1161
1162
	/**
1163
	 * @return string
1164
	 */
1165
	protected function getProjectFolderPath() {
1166
		return sprintf('%s/%s', $this->DNData()->getEnvironmentDir(), $this->Name);
1167
	}
1168
1169
	/**
1170
	 * @return ValidationResult
1171
	 */
1172
	protected function validate() {
1173
		$validation = parent::validate();
1174
		if ($validation->valid()) {
1175
			if (empty($this->Name)) {
1176
				return $validation->error('The stack must have a name.');
1177
			}
1178
1179
			// The name is used to build filepaths so should be restricted
1180
			if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-\_]+$/', $this->Name)) {
1181
				return $validation->error('Project name can only contain alphanumeric, hyphens and underscores.');
1182
			}
1183
1184
			if (empty($this->CVSPath)) {
1185
				return $validation->error('You must provide a repository URL.');
1186
			}
1187
1188
			$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...
1189
			if ($this->ID) {
1190
				$existing = $existing->exclude('ID', $this->ID);
1191
			}
1192
			if ($existing->count() > 0) {
1193
				return $validation->error('A stack already exists with that name.');
1194
			}
1195
		}
1196
		return $validation;
1197
	}
1198
1199
}
1200
1201