Completed
Pull Request — master (#421)
by Michael
1349:23 queued 1346:09
created

DNProject   F

Complexity

Total Complexity 128

Size/Duplication

Total Lines 984
Duplicated Lines 8.03 %

Coupling/Cohesion

Components 1
Dependencies 38

Importance

Changes 13
Bugs 2 Features 5
Metric Value
wmc 128
c 13
b 2
f 5
lcom 1
cbo 38
dl 79
loc 984
rs 1.0434

56 Methods

Rating   Name   Duplication   Size   Complexity  
A create_from_path() 0 12 3
A getUsedQuotaMB() 0 12 3
A getDiskQuotaMB() 0 10 3
A HasExceededDiskQuota() 0 3 1
A HasDiskQuota() 0 3 1
A DiskQuotaUsagePercent() 0 7 2
A Menu() 19 19 4
A isCurrent() 0 3 2
A CurrentMenu() 0 3 1
A isSection() 0 5 2
A canView() 0 11 3
A canRestore() 15 15 2
A canBackup() 15 15 2
A canUploadArchive() 15 15 2
A canDownloadArchive() 15 15 2
A canCreateEnvironments() 0 10 3
A DataArchives() 0 4 1
A PendingManualUploadDataArchives() 0 3 1
A getProcessEnv() 0 14 2
A getViewersList() 0 3 1
A DNData() 0 3 1
A DNBuildList() 0 3 1
A DNBranchList() 0 6 3
A DNTagList() 0 6 3
A getRepository() 0 7 2
A DNEnvironmentList() 0 16 3
A EnvironmentsByUsage() 0 3 1
A currentBuilds() 0 10 3
A Link() 0 3 1
A CreateEnvironmentLink() 0 6 2
A ToggleStarLink() 0 3 1
A IsStarred() 0 11 3
A APILink() 0 3 1
A getCMSFields() 0 55 2
A setCreateProjectFolderField() 0 17 3
A projectFolderExists() 0 6 2
A repoExists() 0 3 1
A cloneRepo() 0 7 1
A getLocalCVSPath() 0 3 1
A onBeforeWrite() 0 6 1
A checkProjectPath() 0 6 3
A checkCVSPath() 0 9 4
A onAfterDelete() 0 16 4
A getPublicKey() 0 7 2
A getPublicKeyPath() 0 6 2
A getPrivateKeyPath() 0 9 2
A getKeyDir() 0 11 2
A setEnvironmentFields() 0 18 3
A getRepositoryURL() 0 6 2
C getRepositoryInterface() 0 40 12
A getProjectFolderPath() 0 3 1
A whoIsAllowed() 0 3 1
A whoIsAllowedAny() 0 17 2
A allowed() 0 3 1
A allowedAny() 0 10 4
B validate() 0 21 6

How to fix   Duplicated Code    Complexity   

Duplicated Code

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

Common duplication problems, and corresponding solutions are:

Complex Class

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

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

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

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

1
<?php
2
3
/**
4
 * DNProject represents a project that relates to a group of target
5
 * environments.
6
 *
7
 * @property string Name
8
 * @property string CVSPath
9
 * @property int DiskQuotaMB
10
 *
11
 * @method HasManyList Environments()
12
 * @method ManyManyList Viewers()
13
 */
14
class DNProject extends DataObject {
15
16
	/**
17
	 * @var array
18
	 */
19
	public static $db = array(
20
		"Name" => "Varchar",
21
		"CVSPath" => "Varchar(255)",
22
		"DiskQuotaMB" => "Int",
23
		"AllowedEnvironmentType" => "Varchar(255)",
24
		"Client" => "Varchar(255)",
25
	);
26
27
	/**
28
	 * @var array
29
	 */
30
	public static $has_many = array(
31
		"Environments" => "DNEnvironment",
32
		"CreateEnvironments" => "DNCreateEnvironment"
33
	);
34
35
	/**
36
	 * @var array
37
	 */
38
	public static $many_many = array(
39
		"Viewers" => "Group",
40
		'StarredBy' => "Member"
41
	);
42
43
	/**
44
	 * @var array
45
	 */
46
	public static $summary_fields = array(
47
		"Name",
48
		"ViewersList",
49
	);
50
51
	/**
52
	 * @var array
53
	 */
54
	public static $searchable_fields = array(
55
		"Name",
56
	);
57
58
	/**
59
	 * @var string
60
	 */
61
	private static $singular_name = 'Project';
62
63
	/**
64
	 * @var string
65
	 */
66
	private static $plural_name = 'Projects';
67
68
	/**
69
	 * @var string
70
	 */
71
	private static $default_sort = 'Name';
72
73
	/**
74
	 * Display the repository URL on the project page.
75
	 *
76
	 * @var bool
77
	 */
78
	private static $show_repository_url = false;
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 = array();
87
88
	/**
89
	 * @var bool|Member
90
	 */
91
	protected static $_current_member_cache = null;
92
93
	/**
94
	 * Used by the sync task
95
	 *
96
	 * @param string $path
97
	 * @return \DNProject
98
	 */
99
	public static function create_from_path($path) {
100
		$project = DNProject::create();
101
		$project->Name = $path;
102
		$project->write();
103
104
		// add the administrators group as the viewers of the new project
105
		$adminGroup = Group::get()->filter('Code', 'administrators')->first();
106
		if($adminGroup && $adminGroup->exists()) {
107
			$project->Viewers()->add($adminGroup);
108
		}
109
		return $project;
110
	}
111
112
	/**
113
	 * Return the used quota in MB.
114
	 *
115
	 * @param int $round Number of decimal places to round to
116
	 * @return double The used quota size in MB
117
	 */
118
	public function getUsedQuotaMB($round = 2) {
119
		$size = 0;
120
121
		foreach($this->Environments() as $environment) {
122
			foreach($environment->DataArchives()->filter('IsBackup', 0) as $archive) {
123
				$size += $archive->ArchiveFile()->getAbsoluteSize();
124
			}
125
		}
126
127
		// convert bytes to megabytes and round
128
		return round(($size / 1024) / 1024, $round);
129
	}
130
131
	/**
132
	 * Getter for DiskQuotaMB field to provide a default for existing
133
	 * records that have no quota field set, as it will need to default
134
	 * to a globally set size.
135
	 *
136
	 * @return string|int The quota size in MB
137
	 */
138
	public function getDiskQuotaMB() {
139
		$size = $this->getField('DiskQuotaMB');
140
141
		if(empty($size)) {
142
			$defaults = $this->config()->get('defaults');
143
			$size = (isset($defaults['DiskQuotaMB'])) ? $defaults['DiskQuotaMB'] : 0;
144
		}
145
146
		return $size;
147
	}
148
149
	/**
150
	 * Has the disk quota been exceeded?
151
	 *
152
	 * @return boolean
153
	 */
154
	public function HasExceededDiskQuota() {
155
		return $this->getUsedQuotaMB(0) >= $this->getDiskQuotaMB();
156
	}
157
158
	/**
159
	 * Is there a disk quota set for this project?
160
	 *
161
	 * @return boolean
162
	 */
163
	public function HasDiskQuota() {
164
		return $this->getDiskQuotaMB() > 0;
165
	}
166
167
	/**
168
	 * Returns the current disk quota usage as a percentage
169
	 *
170
	 * @return int
171
	 */
172
	public function DiskQuotaUsagePercent() {
173
		$quota = $this->getDiskQuotaMB();
174
		if($quota > 0) {
175
			return $this->getUsedQuotaMB() * 100 / $quota;
176
		}
177
		return 100;
178
	}
179
180
	/**
181
	 * Get the menu to be shown on projects
182
	 *
183
	 * @return ArrayList
184
	 */
185 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...
186
		$list = new ArrayList();
187
188
		$controller = Controller::curr();
189
		$actionType = $controller->getField('CurrentActionType');
190
191
		if(DNRoot::FlagSnapshotsEnabled()) {
192
			$list->push(new ArrayData(array(
193
				'Link' => sprintf('naut/project/%s/snapshots', $this->Name),
194
				'Title' => 'Snapshots',
195
				'IsCurrent' => $this->isSection() && $controller->getAction() == 'snapshots',
196
				'IsSection' => $this->isSection() && $actionType == DNRoot::ACTION_SNAPSHOT
197
			)));
198
		}
199
200
		$this->extend('updateMenu', $list);
201
202
		return $list;
203
	}
204
205
	/**
206
	 * Is this project currently at the root level of the controller that handles it?
207
	 *
208
	 * @return bool
209
	 */
210
	public function isCurrent() {
211
		return $this->isSection() && Controller::curr()->getAction() == 'project';
212
	}
213
214
	/**
215
	 * Return the current object from $this->Menu()
216
	 * Good for making titles and things
217
	 *
218
	 * @return DataObject
219
	 */
220
	public function CurrentMenu() {
221
		return $this->Menu()->filter('IsSection', true)->First();
222
	}
223
224
	/**
225
	 * Is this project currently in a controller that is handling it or performing a sub-task?
226
	 *
227
	 * @return bool
228
	 */
229
	public function isSection() {
230
		$controller = Controller::curr();
231
		$project = $controller->getField('CurrentProject');
232
		return $project && $this->ID == $project->ID;
233
	}
234
235
	/**
236
	 * Restrict access to viewing this project
237
	 *
238
	 * @param Member|null $member
239
	 * @return boolean
240
	 */
241
	public function canView($member = null) {
242
		if(!$member) {
243
			$member = Member::currentUser();
244
		}
245
246
		if(Permission::checkMember($member, 'ADMIN')) {
247
			return true;
248
		}
249
250
		return $member->inGroups($this->Viewers());
251
	}
252
253
	/**
254
	 * @param Member|null $member
255
	 *
256
	 * @return bool
257
	 */
258 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...
259
		if ($this->allowedAny(
260
			array(
261
				DNRoot::ALLOW_PROD_SNAPSHOT,
262
				DNRoot::ALLOW_NON_PROD_SNAPSHOT
263
			),
264
			$member
265
		)) {
266
			return true;
267
		}
268
269
		return (bool)$this->Environments()->filterByCallback(function($env) use($member) {
270
			return $env->canRestore($member);
271
		})->Count();
272
	}
273
274
	/**
275
	 * @param Member|null $member
276
	 * @return bool
277
	 */
278 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...
279
		if ($this->allowedAny(
280
			array(
281
				DNRoot::ALLOW_PROD_SNAPSHOT,
282
				DNRoot::ALLOW_NON_PROD_SNAPSHOT
283
			),
284
			$member
285
		)) {
286
			return true;
287
		}
288
289
		return (bool)$this->Environments()->filterByCallback(function($env) use($member) {
290
			return $env->canBackup($member);
291
		})->Count();
292
	}
293
294
	/**
295
	 * @param Member|null $member
296
	 * @return bool
297
	 */
298 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...
299
		if ($this->allowedAny(
300
			array(
301
				DNRoot::ALLOW_PROD_SNAPSHOT,
302
				DNRoot::ALLOW_NON_PROD_SNAPSHOT
303
			),
304
			$member
305
		)) {
306
			return true;
307
		}
308
309
		return (bool)$this->Environments()->filterByCallback(function($env) use($member) {
310
			return $env->canUploadArchive($member);
311
		})->Count();
312
	}
313
314
	/**
315
	 * @param Member|null $member
316
	 * @return bool
317
	 */
318 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...
319
		if ($this->allowedAny(
320
			array(
321
				DNRoot::ALLOW_PROD_SNAPSHOT,
322
				DNRoot::ALLOW_NON_PROD_SNAPSHOT
323
			),
324
			$member
325
		)) {
326
			return true;
327
		}
328
329
		return (bool)$this->Environments()->filterByCallback(function($env) use($member) {
330
			return $env->canDownloadArchive($member);
331
		})->Count();
332
	}
333
334
	/**
335
	 * This is a permission check for the front-end only.
336
	 *
337
	 * Only admins can create environments for now. Also, we need to check the value
338
	 * of AllowedEnvironmentType which dictates which backend to use to render the form.
339
	 *
340
	 * @param Member|null $member
341
	 *
342
	 * @return bool
343
	 */
344
	public function canCreateEnvironments($member = null) {
345
		$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...
346
		if($envType) {
347
			$env = Injector::inst()->get($envType);
348
			if($env instanceof EnvironmentCreateBackend) {
349
				return $this->allowed(DNRoot::ALLOW_CREATE_ENVIRONMENT, $member);
350
			}
351
		}
352
		return false;
353
	}
354
355
	/**
356
	 * @return DataList
357
	 */
358
	public function DataArchives() {
359
		$envIds = $this->Environments()->column('ID');
360
		return DNDataArchive::get()->filter('EnvironmentID', $envIds);
361
	}
362
363
	/**
364
	 * Return all archives which are "manual upload requests",
365
	 * meaning they don't have a file attached to them (yet).
366
	 *
367
	 * @return DataList
368
	 */
369
	public function PendingManualUploadDataArchives() {
370
		return $this->DataArchives()->filter('ArchiveFileID', null);
371
	}
372
373
	/**
374
	 * Build an environment variable array to be used with this project.
375
	 *
376
	 * This is relevant if every project needs to use an individual SSH pubkey.
377
	 *
378
	 * Include this with all Gitonomy\Git\Repository, and
379
	 * \Symfony\Component\Process\Processes.
380
	 *
381
	 * @return array
382
	 */
383
	public function getProcessEnv() {
384
		if(file_exists($this->getPrivateKeyPath())) {
385
			// Key-pair is available, use it.
386
			$processEnv = array(
387
				'IDENT_KEY' => $this->getPrivateKeyPath(),
388
				'GIT_SSH' => BASE_PATH . "/deploynaut/git-deploy.sh"
389
			);
390
		} else {
391
			$processEnv = array();
392
		}
393
		$this->extend('updateProcessEnv', $processEnv);
394
395
		return $processEnv;
396
	}
397
398
	/**
399
	 * Get a string of people allowed to view this project
400
	 *
401
	 * @return string
402
	 */
403
	public function getViewersList() {
404
		return implode(", ", $this->Viewers()->column("Title"));
405
	}
406
407
	/**
408
	 * @return DNData
409
	 */
410
	public function DNData() {
411
		return DNData::inst();
412
	}
413
414
	/**
415
	 * Provides a DNBuildList of builds found in this project.
416
	 *
417
	 * @return DNReferenceList
418
	 */
419
	public function DNBuildList() {
420
		return DNReferenceList::create($this, $this->DNData());
421
	}
422
423
	/**
424
	 * Provides a list of the branches in this project.
425
	 *
426
	 * @return DNBranchList
427
	 */
428
	public function DNBranchList() {
429
		if($this->CVSPath && !$this->repoExists()) {
430
			$this->cloneRepo();
431
		}
432
		return DNBranchList::create($this, $this->DNData());
433
	}
434
435
	/**
436
	 * Provides a list of the tags in this project.
437
	 *
438
	 * @return DNReferenceList
439
	 */
440
	public function DNTagList() {
441
		if($this->CVSPath && !$this->repoExists()) {
442
			$this->cloneRepo();
443
		}
444
		return DNReferenceList::create($this, $this->DNData(), null, null, true);
445
	}
446
447
	/**
448
	 * @return false|Gitonomy\Git\Repository
449
	 */
450
	public function getRepository() {
451
		if(!$this->repoExists()) {
452
			return false;
453
		}
454
455
		return new Gitonomy\Git\Repository($this->getLocalCVSPath());
456
	}
457
458
	/**
459
	 * Provides a list of environments found in this project.
460
	 * CAUTION: filterByCallback will change this into an ArrayList!
461
	 *
462
	 * @return ArrayList
463
	 */
464
	public function DNEnvironmentList() {
465
466
		if(!self::$_current_member_cache) {
467
			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...
468
		}
469
470
		if(self::$_current_member_cache === false) {
471
			return new ArrayList();
472
		}
473
474
		$currentMember = self::$_current_member_cache;
475
		return $this->Environments()
476
			->filterByCallBack(function($item) use ($currentMember) {
477
				return $item->canView($currentMember);
478
			});
479
	}
480
481
	/**
482
	 * @param string $usage
483
	 * @return ArrayList
484
	 */
485
	public function EnvironmentsByUsage($usage) {
486
		return $this->DNEnvironmentList()->filter('Usage', $usage);
487
	}
488
489
	/**
490
	 * Returns a map of envrionment name to build name
491
	 *
492
	 * @return false|DNDeployment
493
	 */
494
	public function currentBuilds() {
495
		if(!isset(self::$relation_cache['currentBuilds.'.$this->ID])) {
496
			$currentBuilds = array();
497
			foreach($this->Environments() as $env) {
498
				$currentBuilds[$env->Name] = $env->CurrentBuild();
499
			}
500
			self::$relation_cache['currentBuilds.'.$this->ID] = $currentBuilds;
501
		}
502
		return self::$relation_cache['currentBuilds.'.$this->ID];
503
	}
504
505
	/**
506
	 * @param string
507
	 * @return string
508
	 */
509
	public function Link($action = '') {
510
		return Controller::join_links("naut", "project", $this->Name, $action);
511
	}
512
513
	/**
514
	 * @return string|null
515
	 */
516
	public function CreateEnvironmentLink() {
517
		if($this->canCreateEnvironments()) {
518
			return $this->Link('createenv');
519
		}
520
		return null;
521
	}
522
523
	/**
524
	 * @return string
525
	 */
526
	public function ToggleStarLink() {
527
		return $this->Link('/star');
528
	}
529
530
	/**
531
	 * @return bool
532
	 */
533
	public function IsStarred() {
534
		$member = Member::currentUser();
535
		if($member === null) {
536
			return false;
537
		}
538
		$favourited = $this->StarredBy()->filter('MemberID', $member->ID);
0 ignored issues
show
Documentation Bug introduced by
The method StarredBy 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...
539
		if($favourited->count() == 0) {
540
			return false;
541
		}
542
		return true;
543
	}
544
545
	/**
546
	 * @param string $action
547
	 * @return string
548
	 */
549
	public function APILink($action) {
550
		return Controller::join_links("naut", "api", $this->Name, $action);
551
	}
552
553
	/**
554
	 * @return FieldList
555
	 */
556
	public function getCMSFields() {
557
		$fields = parent::getCMSFields();
558
559
		/** @var GridField $environments */
560
		$environments = $fields->dataFieldByName("Environments");
561
562
		$fields->fieldByName("Root")->removeByName("Viewers");
563
		$fields->fieldByName("Root")->removeByName("Environments");
564
		$fields->fieldByName("Root")->removeByName("LocalCVSPath");
565
566
		$diskQuotaDesc = 'This is the maximum amount of disk space (in megabytes) that all environments within this '
567
			. 'project can use for stored snapshots';
568
		$fields->dataFieldByName('DiskQuotaMB')->setDescription($diskQuotaDesc);
569
570
		$projectNameDesc = 'Changing the name will <strong>reset</strong> the deploy configuration and avoid using non'
571
			. 'alphanumeric characters';
572
		$fields->fieldByName('Root.Main.Name')
573
			->setTitle('Project name')
574
			->setDescription($projectNameDesc);
575
576
		$fields->fieldByName('Root.Main.CVSPath')
577
			->setTitle('Git repository')
578
			->setDescription('E.g. [email protected]:silverstripe/silverstripe-installer.git');
579
580
		$workspaceField = new ReadonlyField('LocalWorkspace', 'Git workspace', $this->getLocalCVSPath());
581
		$workspaceField->setDescription('This is where the GIT repository are located on this server');
582
		$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...
583
584
		$readAccessGroups = ListboxField::create('Viewers', 'Project viewers', Group::get()->map()->toArray())
585
			->setMultiple(true)
586
			->setDescription('These groups can view the project in the front-end.');
587
		$fields->addFieldToTab("Root.Main", $readAccessGroups);
588
589
		$this->setCreateProjectFolderField($fields);
590
		$this->setEnvironmentFields($fields, $environments);
591
592
		$environmentTypes = ClassInfo::implementorsOf('EnvironmentCreateBackend');
593
		$types = array();
594
		foreach($environmentTypes as $type) {
595
			$types[$type] = $type;
596
		}
597
598
		$fields->addFieldsToTab('Root.Main', array(
599
			DropdownField::create(
600
				'AllowedEnvironmentType',
601
				'Allowed Environment Type',
602
				$types
603
			)->setDescription('This defined which form to show on the front end for '
604
				. 'environment creation. This will not affect backend functionality.')
605
			->setEmptyString(' - None - '),
606
			TextField::create('Client', 'Client'),
607
		));
608
609
		return $fields;
610
	}
611
612
	/**
613
	 * If there isn't a capistrano env project folder, show options to create one
614
	 *
615
	 * @param FieldList $fields
616
	 */
617
	public function setCreateProjectFolderField(&$fields) {
618
		// Check if the capistrano project folder exists
619
		if(!$this->Name) {
620
			return;
621
		}
622
623
		if($this->projectFolderExists()) {
624
			return;
625
		}
626
627
		$createFolderNotice = new LabelField('CreateEnvFolderNotice', 'Warning: No Capistrano project folder exists');
628
		$createFolderNotice->addExtraClass('message warning');
629
		$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...
630
		$createFolderField = new CheckboxField('CreateEnvFolder', 'Create folder');
631
		$createFolderField->setDescription('Would you like to create the capistrano project folder?');
632
		$fields->insertAfter($createFolderField, 'CreateEnvFolderNotice');
633
	}
634
635
	/**
636
	 * @return boolean
637
	 */
638
	public function projectFolderExists() {
639
		if(file_exists($this->DNData()->getEnvironmentDir().'/'.$this->Name)) {
640
			return true;
641
		}
642
		return false;
643
	}
644
645
	/**
646
	 * @return bool
647
	 */
648
	public function repoExists() {
649
		return file_exists(DEPLOYNAUT_LOCAL_VCS_PATH . '/' . $this->Name.'/HEAD');
650
	}
651
652
	/**
653
	 * Setup a asyncronous resque job to clone a git repository
654
	 */
655
	public function cloneRepo() {
656
		Resque::enqueue('git', 'CloneGitRepo', array(
657
			'repo' => $this->CVSPath,
658
			'path' => $this->getLocalCVSPath(),
659
			'env' => $this->getProcessEnv()
660
		));
661
	}
662
663
	/**
664
	 * @return string
665
	 */
666
	public function getLocalCVSPath() {
667
		return DEPLOYNAUT_LOCAL_VCS_PATH . '/' . $this->Name;
668
	}
669
670
	/**
671
	 * Checks for missing folders folder and schedules a git clone if the necessary
672
	 */
673
	public function onBeforeWrite() {
674
		parent::onBeforeWrite();
675
676
		$this->checkProjectPath();
677
		$this->checkCVSPath();
678
	}
679
680
	/**
681
	 * Ensure the path for this project has been created
682
	 */
683
	protected function checkProjectPath() {
684
		// Create the project capistrano folder
685
		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...
686
			mkdir($this->DNData()->getEnvironmentDir().'/'.$this->Name);
687
		}
688
	}
689
690
	/**
691
	 * Check if the CVSPath has been changed, and if so, ensure the repository has been updated
692
	 */
693
	protected function checkCVSPath() {
694
		$changedFields = $this->getChangedFields(true, 2);
695
		if(!$this->CVSPath) {
696
			return;
697
		}
698
		if(isset($changedFields['CVSPath']) || isset($changedFields['Name'])) {
699
			$this->cloneRepo();
700
		}
701
	}
702
703
	/**
704
	 * Delete related environments and folders
705
	 */
706
	public function onAfterDelete() {
707
		parent::onAfterDelete();
708
709
		// Delete related environments
710
		foreach($this->Environments() as $env) {
711
			$env->delete();
712
		}
713
714
		if(!file_exists($this->getProjectFolderPath())) {
715
			return;
716
		}
717
		// Create a basic new environment config from a template
718
		if(Config::inst()->get('DNEnvironment', 'allow_web_editing')) {
719
			FileSystem::removeFolder($this->getProjectFolderPath());
720
		}
721
	}
722
723
	/**
724
	 * Fetch the public key for this project.
725
	 *
726
	 * @return string|void
727
	 */
728
	public function getPublicKey() {
729
		$key = $this->getPublicKeyPath();
730
731
		if(file_exists($key)) {
732
			return file_get_contents($key);
733
		}
734
	}
735
736
	/**
737
	 * This returns that path of the public key if a key directory is set. It doesn't check whether the file exists.
738
	 *
739
	 * @return string|null
740
	 */
741
	public function getPublicKeyPath() {
742
		if($privateKey = $this->getPrivateKeyPath()) {
743
			return $privateKey . '.pub';
744
		}
745
		return null;
746
	}
747
748
	/**
749
	 * This returns that path of the private key if a key directory is set. It doesn't check whether the file exists.
750
	 *
751
	 * @return string|null
752
	 */
753
	public function getPrivateKeyPath() {
754
		$keyDir = $this->getKeyDir();
755
		if(!empty($keyDir)) {
756
			$filter = FileNameFilter::create();
757
			$name = $filter->filter($this->Name);
758
			return $keyDir . '/' . $name;
759
		}
760
		return null;
761
	}
762
763
	/**
764
	 * Returns the location of the projects key dir if one exists.
765
	 *
766
	 * @return string|null
767
	 */
768
	public function getKeyDir() {
769
		$keyDir = $this->DNData()->getKeyDir();
770
		if(!$keyDir) {
771
			return null;
772
		}
773
774
		$filter = FileNameFilter::create();
775
		$name = $filter->filter($this->Name);
776
777
		return $this->DNData()->getKeyDir() . '/' . $name;
778
	}
779
780
	/**
781
	 * Setup a gridfield for the environment configs
782
	 *
783
	 * @param FieldList $fields
784
	 * @param GridField $environments
785
	 */
786
	protected function setEnvironmentFields(&$fields, $environments) {
787
		if(!$environments) {
788
			return;
789
		}
790
791
		$environments->getConfig()->addComponent(new GridFieldAddNewMultiClass());
792
		$environments->getConfig()->removeComponentsByType('GridFieldAddNewButton');
793
		$environments->getConfig()->removeComponentsByType('GridFieldAddExistingAutocompleter');
794
		$environments->getConfig()->removeComponentsByType('GridFieldDeleteAction');
795
		$environments->getConfig()->removeComponentsByType('GridFieldPageCount');
796
		if(Config::inst()->get('DNEnvironment', 'allow_web_editing')) {
797
			$addNewRelease = new GridFieldAddNewButton('toolbar-header-right');
798
			$addNewRelease->setButtonName('Add');
799
			$environments->getConfig()->addComponent($addNewRelease);
800
		}
801
802
		$fields->addFieldToTab("Root.Main", $environments);
803
	}
804
805
	/**
806
	 * Provide current repository URL to the users.
807
	 *
808
	 * @return void|string
809
	 */
810
	public function getRepositoryURL() {
811
		$showUrl = Config::inst()->get($this->class, 'show_repository_url');
812
		if($showUrl) {
813
			return $this->CVSPath;
814
		}
815
	}
816
817
	/**
818
	 * Whitelist configuration that describes how to convert a repository URL into a link
819
	 * to a web user interface for that URL
820
	 *
821
	 * Consists of a hash of "full.lower.case.domain" => {configuration} key/value pairs
822
	 *
823
	 * {configuration} can either be boolean true to auto-detect both the host and the
824
	 * name of the UI provider, or a nested array that overrides either one or both
825
	 * of the auto-detected valyes
826
	 *
827
	 * @var array
828
	 */
829
	static private $repository_interfaces = array(
830
		'github.com' => array(
831
			'icon' => 'deploynaut/img/github.png'
832
		),
833
		'bitbucket.org' => array(
834
			'commit' => 'commits'
835
		),
836
		'repo.or.cz' => array(
837
			'scheme' => 'http',
838
			'name' => 'repo.or.cz',
839
			'regex' => array('^(.*)$' => '/w$1')
840
		),
841
842
		/* 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...
843
		'gitlab.mysite.com' => array(
844
			'icon' => 'deploynaut/img/git.png',
845
			'host' => 'gitlab.mysite.com',
846
			'name' => 'Gitlab',
847
			'regex' => array('.git$' => ''),
848
			'commit' => "commit"
849
		),
850
		*/
851
	);
852
853
	/**
854
	 * Get a ViewableData structure describing the UI tool that lets the user view the repository code
855
	 *
856
	 * @return ArrayData
857
	 */
858
	public function getRepositoryInterface() {
859
		$interfaces = $this->config()->repository_interfaces;
860
861
		/* Look for each whitelisted hostname */
862
		foreach($interfaces as $host => $interface) {
863
			/* See if the CVS Path is for this hostname, followed by some junk (maybe a port), then the path */
864
			if(preg_match('{^[^.]*' . $host . '(.*?)([/a-zA-Z].+)}', $this->CVSPath, $match)) {
865
866
				$path = $match[2];
867
868
				$scheme = isset($interface['scheme']) ? $interface['scheme'] : 'https';
869
				$host = isset($interface['host']) ? $interface['host'] : $host;
870
				$regex = isset($interface['regex']) ? $interface['regex'] : array('\.git$' => '');
871
872
				$components = explode('.', $host);
873
874
				foreach($regex as $pattern => $replacement) {
875
					$path = preg_replace('/' . $pattern . '/', $replacement, $path);
876
				}
877
878
				$uxurl = Controller::join_links($scheme . '://', $host, $path);
879
880
				if(array_key_exists('commit', $interface) && $interface['commit'] == false) {
881
					$commiturl = false;
882
				} else {
883
					$commiturl = Controller::join_links(
884
						$uxurl,
885
						isset($interface['commit']) ? $interface['commit'] : 'commit'
886
					);
887
				}
888
889
				return new ArrayData(array(
890
					'Name'      => isset($interface['name']) ? $interface['name'] : ucfirst($components[0]),
891
					'Icon'      => isset($interface['icon']) ? $interface['icon'] : 'deploynaut/img/git.png',
892
					'URL'       => $uxurl,
893
					'CommitURL' => $commiturl
894
				));
895
			}
896
		}
897
	}
898
899
	/**
900
	 * @return string
901
	 */
902
	protected function getProjectFolderPath() {
903
		return $this->DNData()->getEnvironmentDir().'/'.$this->Name;
904
	}
905
906
	/**
907
	 * Convenience wrapper for a single permission code.
908
	 *
909
	 * @param string $code
910
	 * @return SS_List
911
	 */
912
	public function whoIsAllowed($code) {
913
		return $this->whoIsAllowedAny(array($code));
914
	}
915
916
	/**
917
	 * List members who have $codes on this project.
918
	 * Does not support Permission::DENY_PERMISSION malarky, same as Permission::get_groups_by_permission anyway...
919
	 *
920
	 * @param array|string $codes
921
	 * @return SS_List
922
	 */
923
	public function whoIsAllowedAny($codes) {
924
		if(!is_array($codes)) $codes = array($codes);
925
926
		$SQLa_codes = Convert::raw2sql($codes);
927
		$SQL_codes = join("','", $SQLa_codes);
928
929
		return DataObject::get('Member')
930
			->where("\"PermissionRoleCode\".\"Code\" IN ('$SQL_codes') OR \"Permission\".\"Code\" IN ('$SQL_codes')")
931
			->filter("DNProject_Viewers.DNProjectID", $this->ID)
932
			->leftJoin('Group_Members', "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\"")
933
			->leftJoin('Group', "\"Group_Members\".\"GroupID\" = \"Group\".\"ID\"")
934
			->leftJoin('DNProject_Viewers', "\"DNProject_Viewers\".\"GroupID\" = \"Group\".\"ID\"")
935
			->leftJoin('Permission', "\"Permission\".\"GroupID\" = \"Group\".\"ID\"")
936
			->leftJoin('Group_Roles', "\"Group_Roles\".\"GroupID\" = \"Group\".\"ID\"")
937
			->leftJoin('PermissionRole', "\"Group_Roles\".\"PermissionRoleID\" = \"PermissionRole\".\"ID\"")
938
			->leftJoin('PermissionRoleCode', "\"PermissionRoleCode\".\"RoleID\" = \"PermissionRole\".\"ID\"");
939
	}
940
941
	/**
942
	 * Convenience wrapper for a single permission code.
943
	 *
944
	 * @param string $code
945
	 * @param Member|null $member
946
	 *
947
	 * @return bool
948
	 */
949
	public function allowed($code, $member = null) {
950
		return $this->allowedAny(array($code), $member);
951
	}
952
953
	/**
954
	 * Check if member has a permission code in this project.
955
	 *
956
	 * @param string $code
0 ignored issues
show
Bug introduced by
There is no parameter named $code. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
957
	 * @param Member|null $member
958
	 *
959
	 * @return bool
960
	 */
961
	public function allowedAny($codes, $member = null) {
962
		if (!$member) {
963
			$member = Member::currentUser();
964
		}
965
966
		if(Permission::checkMember($member, 'ADMIN')) return true;
967
968
		$hits = $this->whoIsAllowedAny($codes)->filter('Member.ID', $member->ID)->count();
969
		return ($hits>0 ? true : false);
970
	}
971
972
	/**
973
	 * @return ValidationResult
974
	 */
975
	protected function validate() {
976
		$validation = parent::validate();
977
		if($validation->valid()) {
978
			if(empty($this->Name)) {
979
				return $validation->error('The project must have a name.');
980
			}
981
982
			if(empty($this->CVSPath)) {
983
				return $validation->error('You must provide a repository URL.');
984
			}
985
986
			$existing = DNProject::get()->filter('Name', $this->Name);
987
			if($this->ID) {
988
				$existing = $existing->exclude('ID', $this->ID);
989
			}
990
			if($existing->count() > 0) {
991
				return $validation->error('A stack already exists with that name.');
992
			}
993
		}
994
		return $validation;
995
	}
996
997
}
998
999