Completed
Pull Request — master (#742)
by Sean
04:18
created

DNProject::Menu()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 19
Code Lines 12

Duplication

Lines 19
Ratio 100 %

Importance

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