Completed
Pull Request — master (#877)
by Andrew
04:35
created

DNProject::getPrimaryEnvType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
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
		"IsNewDeployEnabled" => "Boolean",
23
		"IsVirtual" => "Boolean",
24
		"CVSPath" => "Varchar(255)",
25
		"DiskQuotaMB" => "Int",
26
		"AllowedEnvironmentType" => "Varchar(255)"
27
	];
28
29
	private static $defaults = [
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 $defaults 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...
30
		"IsVirtual" => false
31
	];
32
33
	/**
34
	 * @var array
35
	 */
36
	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...
37
		"Environments" => "DNEnvironment",
38
		"CreateEnvironments" => "DNCreateEnvironment",
39
		"Fetches" => "DNGitFetch"
40
	];
41
42
	/**
43
	 * @var array
44
	 */
45
	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...
46
		"Viewers" => "Group",
47
		"StarredBy" => "Member"
48
	];
49
50
	/**
51
	 * @var array
52
	 */
53
	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...
54
		"Name",
55
		"ViewersList",
56
	];
57
58
	/**
59
	 * @var array
60
	 */
61
	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...
62
		"Name",
63
	];
64
65
	/**
66
	 * @var string
67
	 */
68
	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...
69
70
	/**
71
	 * @var string
72
	 */
73
	private static $plural_name = 'Projects';
74
75
	/**
76
	 * @var string
77
	 */
78
	private static $default_sort = 'Name';
79
80
	/**
81
	 * In-memory cache for currentBuilds per environment since fetching them from
82
	 * disk is pretty resource hungry.
83
	 *
84
	 * @var array
85
	 */
86
	protected static $relation_cache = [];
87
88
	/**
89
	 * @var bool|Member
90
	 */
91
	protected static $_current_member_cache = null;
92
93
	/**
94
	 * Display the repository URL on the project page.
95
	 *
96
	 * @var bool
97
	 */
98
	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...
99
100
	/**
101
	 * In-memory cache to determine whether clone repo was called.
102
	 * @var array
103
	 */
104
	private static $has_cloned_cache = [];
105
106
	/**
107
	 * Whitelist configuration that describes how to convert a repository URL into a link
108
	 * to a web user interface for that URL
109
	 *
110
	 * Consists of a hash of "full.lower.case.domain" => {configuration} key/value pairs
111
	 *
112
	 * {configuration} can either be boolean true to auto-detect both the host and the
113
	 * name of the UI provider, or a nested array that overrides either one or both
114
	 * of the auto-detected values
115
	 *
116
	 * @var array
117
	 */
118
	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...
119
		'github.com' => [
120
			'icon' => 'deploynaut/img/github.png',
121
			'name' => 'Github.com',
122
		],
123
		'bitbucket.org' => [
124
			'commit' => 'commits',
125
			'name' => 'Bitbucket.org',
126
		],
127
		'repo.or.cz' => [
128
			'scheme' => 'http',
129
			'name' => 'repo.or.cz',
130
			'regex' => ['^(.*)$' => '/w$1'],
131
		],
132
133
		/* 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...
134
		'gitlab.mysite.com' => array(
135
			'icon' => 'deploynaut/img/git.png',
136
			'host' => 'gitlab.mysite.com',
137
			'name' => 'Gitlab',
138
			'regex' => array('.git$' => ''),
139
			'commit' => "commit"
140
		),
141
		*/
142
	];
143
144
	/**
145
	 * Used by the sync task
146
	 *
147
	 * @param string $path
148
	 * @return \DNProject
149
	 */
150
	public static function create_from_path($path) {
151
		$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...
152
		$project->Name = $path;
153
		$project->write();
154
155
		// add the administrators group as the viewers of the new project
156
		$adminGroup = Group::get()->filter('Code', 'administrators')->first();
157
		if ($adminGroup && $adminGroup->exists()) {
158
			$project->Viewers()->add($adminGroup);
159
		}
160
		return $project;
161
	}
162
163
	/**
164
	 * This will clear the cache for the git getters and should be called when the local git repo is updated
165
	 */
166
	public function clearGitCache() {
167
		$cache = self::get_git_cache();
168
		// we only need to clear the tag cache since everything else is cached by SHA, that is for commit and
169
		// commit message.
170
		$cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, ['gitonomy', 'tags', 'project_' . $this->ID]);
171
	}
172
173
	/**
174
	 * @return \Zend_Cache_Frontend_Output
175
	 */
176
	public static function get_git_cache() {
177
		return SS_Cache::factory('gitonomy', 'Output', [
178
			'automatic_serialization' => true,
179
			'lifetime' => 60 * 60 * 24 * 7 // seven days
180
		]);
181
	}
182
183
	/**
184
	 * Return the used quota in MB.
185
	 *
186
	 * @param int $round Number of decimal places to round to
187
	 * @return double The used quota size in MB
188
	 */
189
	public function getUsedQuotaMB($round = 2) {
190
		$size = 0;
191
192
		foreach ($this->Environments() as $environment) {
193
			foreach ($environment->DataArchives()->filter('IsBackup', 0) as $archive) {
194
				$size += $archive->ArchiveFile()->getAbsoluteSize();
195
			}
196
		}
197
198
		// convert bytes to megabytes and round
199
		return round(($size / 1024) / 1024, $round);
200
	}
201
202
	/**
203
	 * Getter for DiskQuotaMB field to provide a default for existing
204
	 * records that have no quota field set, as it will need to default
205
	 * to a globally set size.
206
	 *
207
	 * @return string|int The quota size in MB
208
	 */
209
	public function getDiskQuotaMB() {
210
		$size = $this->getField('DiskQuotaMB');
211
212
		if (empty($size)) {
213
			$defaults = $this->config()->get('defaults');
214
			$size = (isset($defaults['DiskQuotaMB'])) ? $defaults['DiskQuotaMB'] : 0;
215
		}
216
217
		return $size;
218
	}
219
220
	/**
221
	 * @return string
222
	 */
223
	public function getPrimaryEnvType() {
224
		$envClasses = $this->Environments()->column("ClassName");
225
		$topClass = array_count_values($envClasses);
226
		return str_replace("Environment", "", array_search(max($topClass), $topClass));
227
	}
228
229
	/**
230
	 * Has the disk quota been exceeded?
231
	 *
232
	 * @return boolean
233
	 */
234
	public function HasExceededDiskQuota() {
235
		return $this->getUsedQuotaMB(0) >= $this->getDiskQuotaMB();
236
	}
237
238
	/**
239
	 * Is there a disk quota set for this project?
240
	 *
241
	 * @return boolean
242
	 */
243
	public function HasDiskQuota() {
244
		return $this->getDiskQuotaMB() > 0;
245
	}
246
247
	/**
248
	 * Returns the current disk quota usage as a percentage
249
	 *
250
	 * @return int
251
	 */
252
	public function DiskQuotaUsagePercent() {
253
		$quota = $this->getDiskQuotaMB();
254
		if ($quota > 0) {
255
			return $this->getUsedQuotaMB() * 100 / $quota;
256
		}
257
		return 100;
258
	}
259
260
	/**
261
	 * Get the menu to be shown on projects
262
	 *
263
	 * @return ArrayList
264
	 */
265
	public function Menu() {
266
		$list = new ArrayList();
267
268
		$controller = Controller::curr();
269
		$actionType = $controller->getField('CurrentActionType');
270
271
		if ($this->isProjectReady()) {
272
			$list->push(new ArrayData([
273
				'Link' => $this->Link('snapshots'),
274
				'Title' => 'Snapshots',
275
				'IsCurrent' => $this->isSection() && $controller->getAction() == 'snapshots',
276
				'IsSection' => $this->isSection() && $actionType == DNRoot::ACTION_SNAPSHOT
277
			]));
278
		}
279
280
		$this->extend('updateMenu', $list);
281
282
		return $list;
283
	}
284
285
	/**
286
	 * Is this project currently at the root level of the controller that handles it?
287
	 *
288
	 * @return bool
289
	 */
290
	public function isCurrent() {
291
		return $this->isSection() && Controller::curr()->getAction() == 'project';
292
	}
293
294
	/**
295
	 * Return the current object from $this->Menu()
296
	 * Good for making titles and things
297
	 *
298
	 * @return DataObject
299
	 */
300
	public function CurrentMenu() {
301
		return $this->Menu()->filter('IsSection', true)->First();
302
	}
303
304
	/**
305
	 * Is this project currently in a controller that is handling it or performing a sub-task?
306
	 *
307
	 * @return bool
308
	 */
309
	public function isSection() {
310
		$controller = Controller::curr();
311
		$project = $controller->getField('CurrentProject');
312
		return $project && $this->ID == $project->ID;
313
	}
314
315
	/**
316
	 * Restrict access to viewing this project
317
	 *
318
	 * @param Member|null $member
319
	 * @return boolean
320
	 */
321
	public function canView($member = null) {
322
		if (!$member) {
323
			$member = Member::currentUser();
324
		}
325
326
		if (Permission::checkMember($member, 'ADMIN')) {
327
			return true;
328
		}
329
330
		return $member->inGroups($this->Viewers());
331
	}
332
333
	/**
334
	 * @param Member|null $member
335
	 *
336
	 * @return bool
337
	 */
338 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...
339
		if ($this->allowedAny(
340
			[
341
				DNRoot::ALLOW_PROD_SNAPSHOT,
342
				DNRoot::ALLOW_NON_PROD_SNAPSHOT
343
			],
344
			$member
345
		)
346
		) {
347
			return true;
348
		}
349
350
		return (bool) $this->Environments()->filterByCallback(function ($env) use ($member) {
351
			return $env->canRestore($member);
352
		})->Count();
353
	}
354
355
	/**
356
	 * @param Member|null $member
357
	 * @return bool
358
	 */
359 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...
360
		if ($this->allowedAny(
361
			[
362
				DNRoot::ALLOW_PROD_SNAPSHOT,
363
				DNRoot::ALLOW_NON_PROD_SNAPSHOT
364
			],
365
			$member
366
		)
367
		) {
368
			return true;
369
		}
370
371
		return (bool) $this->Environments()->filterByCallback(function ($env) use ($member) {
372
			return $env->canBackup($member);
373
		})->Count();
374
	}
375
376
	/**
377
	 * @param Member|null $member
378
	 * @return bool
379
	 */
380 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...
381
		if ($this->allowedAny(
382
			[
383
				DNRoot::ALLOW_PROD_SNAPSHOT,
384
				DNRoot::ALLOW_NON_PROD_SNAPSHOT
385
			],
386
			$member
387
		)
388
		) {
389
			return true;
390
		}
391
392
		return (bool) $this->Environments()->filterByCallback(function ($env) use ($member) {
393
			return $env->canUploadArchive($member);
394
		})->Count();
395
	}
396
397
	/**
398
	 * @param Member|null $member
399
	 * @return bool
400
	 */
401 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...
402
		if ($this->allowedAny(
403
			[
404
				DNRoot::ALLOW_PROD_SNAPSHOT,
405
				DNRoot::ALLOW_NON_PROD_SNAPSHOT
406
			],
407
			$member
408
		)
409
		) {
410
			return true;
411
		}
412
413
		return (bool) $this->Environments()->filterByCallback(function ($env) use ($member) {
414
			return $env->canDownloadArchive($member);
415
		})->Count();
416
	}
417
418
	/**
419
	 * This is a permission check for the front-end only.
420
	 *
421
	 * Only admins can create environments for now. Also, we need to check the value
422
	 * of AllowedEnvironmentType which dictates which backend to use to render the form.
423
	 *
424
	 * @param Member|null $member
425
	 *
426
	 * @return bool
427
	 */
428
	public function canCreateEnvironments($member = null) {
429
		$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...
430
		if ($envType) {
431
			$env = Injector::inst()->get($envType);
432
			if ($env instanceof EnvironmentCreateBackend) {
433
				return $this->allowed(DNRoot::ALLOW_CREATE_ENVIRONMENT, $member);
434
			}
435
		}
436
		return false;
437
	}
438
439
	/**
440
	 * @return DataList
441
	 */
442
	public function DataArchives() {
443
		$envIds = $this->Environments()->column('ID');
444
		return DNDataArchive::get()->filter('EnvironmentID', $envIds);
445
	}
446
447
	/**
448
	 * Return all archives which are "manual upload requests",
449
	 * meaning they don't have a file attached to them (yet).
450
	 *
451
	 * @return DataList
452
	 */
453
	public function PendingManualUploadDataArchives() {
454
		return $this->DataArchives()->filter('ArchiveFileID', null);
455
	}
456
457
	/**
458
	 * Build an environment variable array to be used with this project.
459
	 *
460
	 * This is relevant if every project needs to use an individual SSH pubkey.
461
	 *
462
	 * Include this with all Gitonomy\Git\Repository, and
463
	 * \Symfony\Component\Process\Processes.
464
	 *
465
	 * @return array
466
	 */
467
	public function getProcessEnv() {
468
		if (file_exists($this->getPrivateKeyPath())) {
469
			// Key-pair is available, use it.
470
			$processEnv = [
471
				'IDENT_KEY' => $this->getPrivateKeyPath(),
472
				'GIT_SSH' => BASE_PATH . "/deploynaut/git-deploy.sh"
473
			];
474
		} else {
475
			$processEnv = [];
476
		}
477
		$this->extend('updateProcessEnv', $processEnv);
478
479
		return $processEnv;
480
	}
481
482
	/**
483
	 * Get a string of people allowed to view this project
484
	 *
485
	 * @return string
486
	 */
487
	public function getViewersList() {
488
		return implode(", ", $this->Viewers()->column("Title"));
489
	}
490
491
	/**
492
	 * @return DNData
493
	 */
494
	public function DNData() {
495
		return DNData::inst();
496
	}
497
498
	/**
499
	 * Provides a DNBuildList of builds found in this project.
500
	 *
501
	 * @return DNReferenceList
502
	 */
503
	public function DNBuildList() {
504
		return DNReferenceList::create($this, $this->DNData());
505
	}
506
507
	/**
508
	 * Provides a list of the branches in this project.
509
	 *
510
	 * @return DNBranchList
511
	 */
512
	public function DNBranchList() {
513
		if ($this->CVSPath && !$this->repoExists()) {
514
			$this->cloneRepo();
515
		}
516
		return DNBranchList::create($this, $this->DNData());
517
	}
518
519
	/**
520
	 * Provides a list of the tags in this project.
521
	 *
522
	 * @return DNReferenceList
523
	 */
524
	public function DNTagList() {
525
		if ($this->CVSPath && !$this->repoExists()) {
526
			$this->cloneRepo();
527
		}
528
		return DNReferenceList::create($this, $this->DNData(), null, null, true);
529
	}
530
531
	/**
532
	 * @return false|Gitonomy\Git\Repository
533
	 */
534
	public function getRepository() {
535
		if (!$this->repoExists()) {
536
			return false;
537
		}
538
539
		return new \Gitonomy\Git\Repository($this->getLocalCVSPath());
540
	}
541
542
	/**
543
	 * Resolve a git reference like a branch or tag into a SHA.
544
	 * @return bool|string
545
	 */
546
	public function resolveRevision($value) {
547
		$repository = $this->getRepository();
548
		if (!$repository) {
549
			return false;
550
		}
551
552
		try {
553
			$revision = $this->repository->getRevision($value);
0 ignored issues
show
Bug introduced by
The property repository does not seem to exist. Did you mean show_repository_url?

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

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

Loading history...
554
			return $revision->getCommit()->getHash();
555
		} catch (\Gitonomy\Git\Exception\ReferenceNotFoundException $e) {
556
			return false;
557
		}
558
	}
559
560
	/**
561
	 * Provides a list of environments found in this project.
562
	 * CAUTION: filterByCallback will change this into an ArrayList!
563
	 *
564
	 * @return ArrayList
565
	 */
566
	public function DNEnvironmentList() {
567
568
		if (!self::$_current_member_cache) {
569
			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...
570
		}
571
572
		if (self::$_current_member_cache === false) {
573
			return new ArrayList();
574
		}
575
576
		$currentMember = self::$_current_member_cache;
577
		return $this->Environments()
578
			->filterByCallBack(function ($item) use ($currentMember) {
579
				return $item->canView($currentMember);
580
			});
581
	}
582
583
	/**
584
	 * @param string $usage
585
	 * @return ArrayList
586
	 */
587
	public function EnvironmentsByUsage($usage) {
588
		return $this->DNEnvironmentList()->filter('Usage', $usage);
589
	}
590
591
	/**
592
	 * Returns a map of envrionment name to build name
593
	 *
594
	 * @return false|DNDeployment
595
	 */
596
	public function currentBuilds() {
597
		if (!isset(self::$relation_cache['currentBuilds.' . $this->ID])) {
598
			$currentBuilds = [];
599
			foreach ($this->Environments() as $env) {
600
				$currentBuilds[$env->Name] = $env->CurrentBuild();
601
			}
602
			self::$relation_cache['currentBuilds.' . $this->ID] = $currentBuilds;
603
		}
604
		return self::$relation_cache['currentBuilds.' . $this->ID];
605
	}
606
607
	/**
608
	 * @param string
609
	 * @return string
610
	 */
611
	public function Link($action = '') {
612
		return Controller::join_links("naut", "project", $this->Name, $action);
613
	}
614
615
	/**
616
	 * @return string
617
	 */
618
	public function getCMSEditLink() {
619
		return Controller::join_links("admin", "naut", "DNProject", "EditForm", "field", "DNProject", "item", $this->ID, "edit");
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 123 characters

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

Loading history...
620
	}
621
	/**
622
	 * @return string|null
623
	 */
624
	public function CreateEnvironmentLink() {
625
		if ($this->canCreateEnvironments()) {
626
			return $this->Link('createenv');
627
		}
628
		return null;
629
	}
630
631
	/**
632
	 * @return string
633
	 */
634
	public function ToggleStarLink() {
635
		return $this->Link('/star');
636
	}
637
638
	/**
639
	 * @return bool
640
	 */
641
	public function IsStarred() {
642
		$member = Member::currentUser();
643
		if ($member === null) {
644
			return false;
645
		}
646
		$favourited = $this->StarredBy()->filter('MemberID', $member->ID);
647
		if ($favourited->count() == 0) {
648
			return false;
649
		}
650
		return true;
651
	}
652
653
	/**
654
	 * @param string $action
655
	 * @return string
656
	 */
657
	public function APILink($action) {
658
		return Controller::join_links("naut", "api", $this->Name, $action);
659
	}
660
661
	/**
662
	 * @return FieldList
663
	 */
664
	public function getCMSFields() {
665
		$fields = parent::getCMSFields();
666
667
		/** @var GridField $environments */
668
		$environments = $fields->dataFieldByName("Environments");
669
670
		$fields->fieldByName("Root")->removeByName("Viewers");
671
		$fields->fieldByName("Root")->removeByName("Environments");
672
		$fields->fieldByName("Root")->removeByName("LocalCVSPath");
673
674
		$diskQuotaDesc = 'This is the maximum amount of disk space (in megabytes) that all environments within this '
675
			. 'project can use for stored snapshots';
676
		$fields->dataFieldByName('DiskQuotaMB')->setDescription($diskQuotaDesc);
677
678
		$projectNameDesc = 'Changing the name will <strong>reset</strong> the deploy configuration and avoid using non'
679
			. 'alphanumeric characters';
680
		$fields->fieldByName('Root.Main.Name')
681
			->setTitle('Project name')
682
			->setDescription($projectNameDesc);
683
684
		$fields->fieldByName('Root.Main.IsNewDeployEnabled')
685
			->setTitle('New deploy form enabled for this project')
686
			->setDescription('Feature flag to change links to environment and deployments to the new deployment form for this project');
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 127 characters

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

Loading history...
687
688
		$fields->fieldByName('Root.Main.CVSPath')
689
			->setTitle('Git repository')
690
			->setDescription('E.g. [email protected]:silverstripe/silverstripe-installer.git');
691
692
		$workspaceField = new ReadonlyField('LocalWorkspace', 'Git workspace', $this->getLocalCVSPath());
693
		$workspaceField->setDescription('This is where the GIT repository are located on this server');
694
		$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...
695
696
		$readAccessGroups = ListboxField::create('Viewers', 'Project viewers', Group::get()->map()->toArray())
697
			->setMultiple(true)
698
			->setDescription('These groups can view the project in the front-end.');
699
		$fields->addFieldToTab("Root.Main", $readAccessGroups);
700
701
		$this->setCreateProjectFolderField($fields);
702
		$this->setEnvironmentFields($fields, $environments);
703
704
		$environmentTypes = ClassInfo::implementorsOf('EnvironmentCreateBackend');
705
		$types = [];
706
		foreach ($environmentTypes as $type) {
707
			$types[$type] = $type;
708
		}
709
710
		$fields->addFieldsToTab('Root.Main', [
711
			DropdownField::create(
712
				'AllowedEnvironmentType',
713
				'Allowed Environment Type',
714
				$types
715
			)->setDescription('This defined which form to show on the front end for '
716
				. 'environment creation. This will not affect backend functionality.')
717
				->setEmptyString(' - None - '),
718
		]);
719
720
		return $fields;
721
	}
722
723
	/**
724
	 * If there isn't a capistrano env project folder, show options to create one
725
	 *
726
	 * @param FieldList $fields
727
	 */
728
	public function setCreateProjectFolderField(&$fields) {
729
		// Check if the capistrano project folder exists
730
		if (!$this->Name) {
731
			return;
732
		}
733
734
		if ($this->projectFolderExists()) {
735
			return;
736
		}
737
738
		$createFolderNotice = new LabelField('CreateEnvFolderNotice', 'Warning: No Capistrano project folder exists');
739
		$createFolderNotice->addExtraClass('message warning');
740
		$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...
741
		$createFolderField = new CheckboxField('CreateEnvFolder', 'Create folder');
742
		$createFolderField->setDescription('Would you like to create the capistrano project folder?');
743
		$fields->insertAfter($createFolderField, 'CreateEnvFolderNotice');
744
	}
745
746
	/**
747
	 * @return boolean
748
	 */
749
	public function projectFolderExists() {
750
		return file_exists($this->getProjectFolderPath());
751
	}
752
753
	/**
754
	 * @return bool
755
	 */
756
	public function repoExists() {
757
		return file_exists(sprintf('%s/HEAD', $this->getLocalCVSPath()));
758
	}
759
760
	/**
761
	 * Setup a job to clone a git repository.
762
	 * @return string resque token
763
	 */
764
	public function cloneRepo() {
765
		// Avoid this being called multiple times in the same request
766
		if (!isset(self::$has_cloned_cache[$this->ID])) {
767
			$fetch = DNGitFetch::create();
768
			$fetch->ProjectID = $this->ID;
769
			$fetch->write();
770
771
			// passing true here tells DNGitFetch to force a git clone, otherwise
772
			// it will just update the repo if it already exists. We want to ensure
773
			// we're always cloning a new repo in this case, as the git URL may have changed.
774
			$fetch->start(true);
775
776
			self::$has_cloned_cache[$this->ID] = true;
777
		}
778
	}
779
780
	/**
781
	 * @return string
782
	 */
783
	public function getLocalCVSPath() {
784
		return sprintf('%s/%s', DEPLOYNAUT_LOCAL_VCS_PATH, $this->Name);
785
	}
786
787
	public function onBeforeWrite() {
788
		parent::onBeforeWrite();
789
790
		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...
791
			mkdir($this->getProjectFolderPath());
792
		}
793
	}
794
795
	public function onAfterWrite() {
796
		parent::onAfterWrite();
797
798
		if (!$this->CVSPath) {
799
			return;
800
		}
801
802
		$changedFields = $this->getChangedFields(true, 2);
803
		if (isset($changedFields['CVSPath']) || isset($changedFields['Name'])) {
804
			$this->cloneRepo();
805
		}
806
	}
807
808
	/**
809
	 * Delete related environments and folders
810
	 */
811
	public function onAfterDelete() {
812
		parent::onAfterDelete();
813
814
		$environments = $this->Environments();
815
		if ($environments && $environments->exists()) {
816
			foreach ($environments as $env) {
817
				$env->delete();
818
			}
819
		}
820
821
		$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...
822
		if ($fetches && $fetches->exists()) {
823
			foreach ($fetches as $fetch) {
824
				$fetch->delete();
825
			}
826
		}
827
828
		// Delete local repository
829
		if (file_exists($this->getLocalCVSPath())) {
830
			Filesystem::removeFolder($this->getLocalCVSPath());
831
		}
832
833
		// Delete project template
834
		if (file_exists($this->getProjectFolderPath()) && Config::inst()->get('DNEnvironment', 'allow_web_editing')) {
835
			Filesystem::removeFolder($this->getProjectFolderPath());
836
		}
837
838
		// Delete the deploy key
839
		if (file_exists($this->getKeyDir())) {
840
			Filesystem::removeFolder($this->getKeyDir());
841
		}
842
	}
843
844
	/**
845
	 * Fetch the public key for this project.
846
	 *
847
	 * @return string|void
848
	 */
849
	public function getPublicKey() {
850
		$key = $this->getPublicKeyPath();
851
852
		if (file_exists($key)) {
853
			return trim(file_get_contents($key));
854
		}
855
	}
856
857
	/**
858
	 * This returns that path of the public key if a key directory is set. It doesn't check whether the file exists.
859
	 *
860
	 * @return string|null
861
	 */
862
	public function getPublicKeyPath() {
863
		if ($privateKey = $this->getPrivateKeyPath()) {
864
			return $privateKey . '.pub';
865
		}
866
		return null;
867
	}
868
869
	/**
870
	 * This returns that path of the private key if a key directory is set. It doesn't check whether the file exists.
871
	 *
872
	 * @return string|null
873
	 */
874
	public function getPrivateKeyPath() {
875
		$keyDir = $this->getKeyDir();
876
		if (!empty($keyDir)) {
877
			$filter = FileNameFilter::create();
878
			$name = $filter->filter($this->Name);
879
			return $keyDir . '/' . $name;
880
		}
881
		return null;
882
	}
883
884
	/**
885
	 * Returns the location of the projects key dir if one exists.
886
	 *
887
	 * @return string|null
888
	 */
889
	public function getKeyDir() {
890
		$keyDir = $this->DNData()->getKeyDir();
891
		if (!$keyDir) {
892
			return null;
893
		}
894
895
		$filter = FileNameFilter::create();
896
		$name = $filter->filter($this->Name);
897
898
		return $this->DNData()->getKeyDir() . '/' . $name;
899
	}
900
901
	/**
902
	 * Provide current repository URL to the users.
903
	 *
904
	 * @return void|string
905
	 */
906
	public function getRepositoryURL() {
907
		$showUrl = Config::inst()->get($this->class, 'show_repository_url');
908
		if ($showUrl) {
909
			return $this->CVSPath;
910
		}
911
	}
912
913
	/**
914
	 * Get a ViewableData structure describing the UI tool that lets the user view the repository code
915
	 *
916
	 * @return ArrayData
917
	 */
918
	public function getRepositoryInterface() {
919
		$interfaces = $this->config()->repository_interfaces;
920
921
		/* Look for each whitelisted hostname */
922
		foreach ($interfaces as $host => $interface) {
923
			/* See if the CVS Path is for this hostname, followed by some junk (maybe a port), then the path */
924
			if (preg_match('{^[^.]*' . $host . '(.*?)([/a-zA-Z].+)}', $this->CVSPath, $match)) {
925
926
				$path = $match[2];
927
928
				$scheme = isset($interface['scheme']) ? $interface['scheme'] : 'https';
929
				$host = isset($interface['host']) ? $interface['host'] : $host;
930
				$regex = isset($interface['regex']) ? $interface['regex'] : ['\.git$' => ''];
931
932
				$components = explode('.', $host);
933
934
				foreach ($regex as $pattern => $replacement) {
935
					$path = preg_replace('/' . $pattern . '/', $replacement, $path);
936
				}
937
938
				$uxurl = Controller::join_links($scheme . '://', $host, $path);
939
940
				if (array_key_exists('commit', $interface) && $interface['commit'] == false) {
941
					$commiturl = false;
942
				} else {
943
					$commiturl = Controller::join_links(
944
						$uxurl,
945
						isset($interface['commit']) ? $interface['commit'] : 'commit'
946
					);
947
				}
948
949
				return new ArrayData([
950
					'Name' => isset($interface['name']) ? $interface['name'] : ucfirst($components[0]),
951
					'Icon' => isset($interface['icon']) ? $interface['icon'] : 'deploynaut/img/git.png',
952
					'URL' => $uxurl,
953
					'CommitURL' => $commiturl
954
				]);
955
			}
956
		}
957
	}
958
959
	/**
960
	 * Convenience wrapper for a single permission code.
961
	 *
962
	 * @param string $code
963
	 * @return SS_List
964
	 */
965
	public function whoIsAllowed($code) {
966
		return $this->whoIsAllowedAny([$code]);
967
	}
968
969
	/**
970
	 * List members who have $codes on this project.
971
	 * Does not support Permission::DENY_PERMISSION malarky, same as Permission::get_groups_by_permission anyway...
972
	 *
973
	 * @param array|string $codes
974
	 * @return SS_List
975
	 */
976
	public function whoIsAllowedAny($codes) {
977
		if (!is_array($codes)) {
978
			$codes = [$codes];
979
		}
980
981
		$SQLa_codes = Convert::raw2sql($codes);
982
		$SQL_codes = join("','", $SQLa_codes);
983
984
		return DataObject::get('Member')
985
			->where("\"PermissionRoleCode\".\"Code\" IN ('$SQL_codes') OR \"Permission\".\"Code\" IN ('$SQL_codes')")
986
			->filter("DNProject_Viewers.DNProjectID", $this->ID)
987
			->leftJoin('Group_Members', "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\"")
988
			->leftJoin('Group', "\"Group_Members\".\"GroupID\" = \"Group\".\"ID\"")
989
			->leftJoin('DNProject_Viewers', "\"DNProject_Viewers\".\"GroupID\" = \"Group\".\"ID\"")
990
			->leftJoin('Permission', "\"Permission\".\"GroupID\" = \"Group\".\"ID\"")
991
			->leftJoin('Group_Roles', "\"Group_Roles\".\"GroupID\" = \"Group\".\"ID\"")
992
			->leftJoin('PermissionRole', "\"Group_Roles\".\"PermissionRoleID\" = \"PermissionRole\".\"ID\"")
993
			->leftJoin('PermissionRoleCode', "\"PermissionRoleCode\".\"RoleID\" = \"PermissionRole\".\"ID\"");
994
	}
995
996
	/**
997
	 * Convenience wrapper for a single permission code.
998
	 *
999
	 * @param string $code
1000
	 * @param Member|null $member
1001
	 *
1002
	 * @return bool
1003
	 */
1004
	public function allowed($code, $member = null) {
1005
		return $this->allowedAny([$code], $member);
1006
	}
1007
1008
	/**
1009
	 * Checks if a group is allowed to the project and the permission code
1010
	 *
1011
	 * @param string $permissionCode
1012
	 * @param Group $group
1013
	 *
1014
	 * @return bool
1015
	 */
1016
	public function groupAllowed($permissionCode, Group $group) {
1017
		$viewers = $this->Viewers();
1018
		if (!$viewers->find('ID', $group->ID)) {
1019
			return false;
1020
		}
1021
		$groups = Permission::get_groups_by_permission($permissionCode);
1022
		if (!$groups->find('ID', $group->ID)) {
1023
			return false;
1024
		}
1025
		return true;
1026
	}
1027
1028
	/**
1029
	 * Check if member has a permission code in this project.
1030
	 *
1031
	 * @param array|string $codes
1032
	 * @param Member|null $member
1033
	 *
1034
	 * @return bool
1035
	 */
1036
	public function allowedAny($codes, $member = null) {
1037
		if (!$member) {
1038
			$member = Member::currentUser();
1039
		}
1040
1041
		if (Permission::checkMember($member, 'ADMIN')) {
1042
			return true;
1043
		}
1044
1045
		$hits = $this->whoIsAllowedAny($codes)->filter('Member.ID', $member->ID)->count();
1046
		return ($hits > 0 ? true : false);
1047
	}
1048
1049
	/**
1050
	 * Checks if the environment has been fully built.
1051
	 *
1052
	 * @return bool
1053
	 */
1054
	public function isProjectReady() {
1055
		if ($this->getRunningInitialEnvironmentCreations()->count() > 0) {
1056
			// We're still creating the initial environments for this project so we're
1057
			// not quite done
1058
			return false;
1059
		}
1060
1061
		// Provide a hook for further checks. Logic stolen from
1062
		// {@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...
1063
		$isDone = $this->extend('isProjectReady');
1064
		if ($isDone && is_array($isDone)) {
1065
			$isDone = array_filter($isDone, function ($val) {
1066
				return !is_null($val);
1067
			});
1068
1069
			// If anything returns false then we're not ready.
1070
			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...
1071
				return min($isDone);
1072
			}
1073
		}
1074
1075
		return true;
1076
	}
1077
1078
	/**
1079
	 * Returns a list of environments still being created.
1080
	 *
1081
	 * @return SS_List
1082
	 */
1083
	public function getRunningEnvironmentCreations() {
1084
		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...
1085
			->filter('Status', ['Queued', 'Started']);
1086
	}
1087
1088
	/**
1089
	 * Returns a list of initial environments created for this project.
1090
	 *
1091
	 * @return DataList
1092
	 */
1093
	public function getInitialEnvironmentCreations() {
1094
		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...
1095
	}
1096
1097
	/**
1098
	 * Only returns initial environments that are being created.
1099
	 *
1100
	 * @return DataList
1101
	 */
1102
	public function getRunningInitialEnvironmentCreations() {
1103
		return $this->getInitialEnvironmentCreations()
1104
			->filter('Status', ['Queued', 'Started']);
1105
	}
1106
1107
	/**
1108
	 * Returns a list of completed initial environment creations. This includes failed tasks.
1109
	 *
1110
	 * @return DataList
1111
	 */
1112
	public function getCompleteInitialEnvironmentCreations() {
1113
		return $this->getInitialEnvironmentCreations()
1114
			->exclude('Status', ['Queued', 'Started']);
1115
	}
1116
1117
	/**
1118
	 * @param Member $member
1119
	 *
1120
	 * @return bool
1121
	 */
1122
	public function canCreate($member = null) {
1123
		if (!$member) {
1124
			$member = Member::currentUser();
1125
		}
1126
		if (!$member) {
1127
			return false;
1128
		}
1129
1130
		if (Permission::checkMember($member, 'ADMIN')) {
1131
			return true;
1132
		}
1133
1134
		// This calls canCreate on extensions.
1135
		return parent::canCreate($member);
1136
	}
1137
1138
	/**
1139
	 * This is a proxy call to gitonmy that caches the information per project and sha
1140
	 *
1141
	 * @param string $sha
1142
	 * @return false|\Gitonomy\Git\Commit
1143
	 */
1144
	public function getCommit($sha) {
1145
		$repo = $this->getRepository();
1146
		if (!$repo) {
1147
			return false;
1148
		}
1149
1150
		$cachekey = $this->ID . '_commit_' . $sha;
1151
		$cache = self::get_git_cache();
1152
		if (!($result = $cache->load($cachekey))) {
1153
			try {
1154
				$result = $repo->getCommit($sha);
1155
			} catch (\Gitonomy\Git\Exception\ReferenceNotFoundException $e) {
1156
				return false;
1157
			}
1158
			$cache->save($result, $cachekey, ['gitonomy', 'commit', 'project_' . $this->ID]);
1159
		}
1160
		return $result;
1161
	}
1162
1163
	/**
1164
	 * @param \Gitonomy\Git\Commit $commit
1165
	 * @return string
1166
	 */
1167 View Code Duplication
	public function getCommitMessage(\Gitonomy\Git\Commit $commit) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1168
		$cachekey = $this->ID . '_message_' . $commit->getRevision();
1169
		$cache = self::get_git_cache();
1170
		if (!($result = $cache->load($cachekey))) {
1171
			$result = $commit->getMessage();
1172
			$cache->save($result, $cachekey, ['gitonomy', 'message', 'project_' . $this->ID]);
1173
		}
1174
		return $result;
1175
	}
1176
1177
	/**
1178
	 * get the commit "subject", getCommitMessage get the full message
1179
	 *
1180
	 * @param \Gitonomy\Git\Commit $commit
1181
	 * @return string
1182
	 */
1183 View Code Duplication
	public function getCommitSubjectMessage(\Gitonomy\Git\Commit $commit) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1184
		$cachekey = $this->ID . '_message_subject' . $commit->getRevision();
1185
		$cache = self::get_git_cache();
1186
		if (!($result = $cache->load($cachekey))) {
1187
			$result = $commit->getSubjectMessage();
1188
			$cache->save($result, $cachekey, ['gitonomy', 'message', 'project_' . $this->ID]);
1189
		}
1190
		return $result;
1191
	}
1192
1193
	/**
1194
	 * @param \Gitonomy\Git\Commit $commit
1195
	 * @return mixed
1196
	 */
1197
	public function getCommitTags(\Gitonomy\Git\Commit $commit) {
1198
		$cachekey = $this->ID . '_tags_' . $commit->getRevision();
1199
		$cache = self::get_git_cache();
1200
		$result = $cache->load($cachekey);
1201
		// we check against false, because in many cases the tag list is an empty array
1202
		if ($result === false) {
1203
			$repo = $this->getRepository();
1204
			$result = $repo->getReferences()->resolveTags($commit->getRevision());
1205
			$cache->save($result, $cachekey, ['gitonomy', 'tags', 'project_' . $this->ID]);
1206
		}
1207
		return $result;
1208
	}
1209
1210
	/**
1211
	 * Setup a gridfield for the environment configs
1212
	 *
1213
	 * @param FieldList $fields
1214
	 * @param GridField $environments
1215
	 */
1216
	protected function setEnvironmentFields(&$fields, $environments) {
1217
		if (!$environments) {
1218
			return;
1219
		}
1220
1221
		$environments->getConfig()->addComponent(new GridFieldAddNewMultiClass());
1222
		$environments->getConfig()->removeComponentsByType('GridFieldAddNewButton');
1223
		$environments->getConfig()->removeComponentsByType('GridFieldAddExistingAutocompleter');
1224
		$environments->getConfig()->removeComponentsByType('GridFieldDeleteAction');
1225
		$environments->getConfig()->removeComponentsByType('GridFieldPageCount');
1226
		if (Config::inst()->get('DNEnvironment', 'allow_web_editing')) {
1227
			$addNewRelease = new GridFieldAddNewButton('toolbar-header-right');
1228
			$addNewRelease->setButtonName('Add');
1229
			$environments->getConfig()->addComponent($addNewRelease);
1230
		}
1231
1232
		$fields->addFieldToTab("Root.Main", $environments);
1233
	}
1234
1235
	/**
1236
	 * @return string
1237
	 */
1238
	protected function getProjectFolderPath() {
1239
		return sprintf('%s/%s', $this->DNData()->getEnvironmentDir(), $this->Name);
1240
	}
1241
1242
	/**
1243
	 * @return ValidationResult
1244
	 */
1245
	protected function validate() {
1246
		$validation = parent::validate();
1247
		if ($validation->valid()) {
1248
			if (empty($this->Name)) {
1249
				return $validation->error('The stack must have a name.');
1250
			}
1251
1252
			// The name is used to build filepaths so should be restricted
1253
			if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-\_]+$/', $this->Name)) {
1254
				return $validation->error('Project name can only contain alphanumeric, hyphens and underscores.');
1255
			}
1256
1257
			if (empty($this->CVSPath)) {
1258
				return $validation->error('You must provide a repository URL.');
1259
			}
1260
1261
			$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...
1262
			if ($this->ID) {
1263
				$existing = $existing->exclude('ID', $this->ID);
1264
			}
1265
			if ($existing->count() > 0) {
1266
				return $validation->error('A stack already exists with that name.');
1267
			}
1268
		}
1269
		return $validation;
1270
	}
1271
1272
}
1273
1274