Completed
Push — master ( d37947...f5a873 )
by Mateusz
1297:50 queued 1294:31
created

DNProject::allowed()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
nop 2
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
	public function Menu() {
186
		$list = new ArrayList();
187
188
		$controller = Controller::curr();
189
		$actionType = $controller->getField('CurrentActionType');
190
191
		if(DNRoot::FlagSnapshotsEnabled() && $this->isProjectReady()) {
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
	 * @return string resque token
656
	 */
657
	public function cloneRepo() {
658
		return Resque::enqueue('git', 'CloneGitRepo', array(
659
			'repo' => $this->CVSPath,
660
			'path' => $this->getLocalCVSPath(),
661
			'env' => $this->getProcessEnv()
662
		));
663
	}
664
665
	/**
666
	 * @return string
667
	 */
668
	public function getLocalCVSPath() {
669
		return DEPLOYNAUT_LOCAL_VCS_PATH . '/' . $this->Name;
670
	}
671
672
	/**
673
	 * Checks for missing folders folder and schedules a git clone if the necessary
674
	 */
675
	public function onBeforeWrite() {
676
		parent::onBeforeWrite();
677
678
		$this->checkProjectPath();
679
		$this->checkCVSPath();
680
	}
681
682
	/**
683
	 * Ensure the path for this project has been created
684
	 */
685
	protected function checkProjectPath() {
686
		// Create the project capistrano folder
687
		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...
688
			mkdir($this->DNData()->getEnvironmentDir().'/'.$this->Name);
689
		}
690
	}
691
692
	/**
693
	 * Check if the CVSPath has been changed, and if so, ensure the repository has been updated
694
	 */
695
	protected function checkCVSPath() {
696
		$changedFields = $this->getChangedFields(true, 2);
697
		if(!$this->CVSPath) {
698
			return;
699
		}
700
		if(isset($changedFields['CVSPath']) || isset($changedFields['Name'])) {
701
			$this->cloneRepo();
702
		}
703
	}
704
705
	/**
706
	 * Delete related environments and folders
707
	 */
708
	public function onAfterDelete() {
709
		parent::onAfterDelete();
710
711
		// Delete related environments
712
		foreach($this->Environments() as $env) {
713
			$env->delete();
714
		}
715
716
		if(!file_exists($this->getProjectFolderPath())) {
717
			return;
718
		}
719
		// Create a basic new environment config from a template
720
		if(Config::inst()->get('DNEnvironment', 'allow_web_editing')) {
721
			FileSystem::removeFolder($this->getProjectFolderPath());
722
		}
723
	}
724
725
	/**
726
	 * Fetch the public key for this project.
727
	 *
728
	 * @return string|void
729
	 */
730
	public function getPublicKey() {
731
		$key = $this->getPublicKeyPath();
732
733
		if(file_exists($key)) {
734
			return trim(file_get_contents($key));
735
		}
736
	}
737
738
	/**
739
	 * This returns that path of the public key if a key directory is set. It doesn't check whether the file exists.
740
	 *
741
	 * @return string|null
742
	 */
743
	public function getPublicKeyPath() {
744
		if($privateKey = $this->getPrivateKeyPath()) {
745
			return $privateKey . '.pub';
746
		}
747
		return null;
748
	}
749
750
	/**
751
	 * This returns that path of the private key if a key directory is set. It doesn't check whether the file exists.
752
	 *
753
	 * @return string|null
754
	 */
755
	public function getPrivateKeyPath() {
756
		$keyDir = $this->getKeyDir();
757
		if(!empty($keyDir)) {
758
			$filter = FileNameFilter::create();
759
			$name = $filter->filter($this->Name);
760
			return $keyDir . '/' . $name;
761
		}
762
		return null;
763
	}
764
765
	/**
766
	 * Returns the location of the projects key dir if one exists.
767
	 *
768
	 * @return string|null
769
	 */
770
	public function getKeyDir() {
771
		$keyDir = $this->DNData()->getKeyDir();
772
		if(!$keyDir) {
773
			return null;
774
		}
775
776
		$filter = FileNameFilter::create();
777
		$name = $filter->filter($this->Name);
778
779
		return $this->DNData()->getKeyDir() . '/' . $name;
780
	}
781
782
	/**
783
	 * Setup a gridfield for the environment configs
784
	 *
785
	 * @param FieldList $fields
786
	 * @param GridField $environments
787
	 */
788
	protected function setEnvironmentFields(&$fields, $environments) {
789
		if(!$environments) {
790
			return;
791
		}
792
793
		$environments->getConfig()->addComponent(new GridFieldAddNewMultiClass());
794
		$environments->getConfig()->removeComponentsByType('GridFieldAddNewButton');
795
		$environments->getConfig()->removeComponentsByType('GridFieldAddExistingAutocompleter');
796
		$environments->getConfig()->removeComponentsByType('GridFieldDeleteAction');
797
		$environments->getConfig()->removeComponentsByType('GridFieldPageCount');
798
		if(Config::inst()->get('DNEnvironment', 'allow_web_editing')) {
799
			$addNewRelease = new GridFieldAddNewButton('toolbar-header-right');
800
			$addNewRelease->setButtonName('Add');
801
			$environments->getConfig()->addComponent($addNewRelease);
802
		}
803
804
		$fields->addFieldToTab("Root.Main", $environments);
805
	}
806
807
	/**
808
	 * Provide current repository URL to the users.
809
	 *
810
	 * @return void|string
811
	 */
812
	public function getRepositoryURL() {
813
		$showUrl = Config::inst()->get($this->class, 'show_repository_url');
814
		if($showUrl) {
815
			return $this->CVSPath;
816
		}
817
	}
818
819
	/**
820
	 * Whitelist configuration that describes how to convert a repository URL into a link
821
	 * to a web user interface for that URL
822
	 *
823
	 * Consists of a hash of "full.lower.case.domain" => {configuration} key/value pairs
824
	 *
825
	 * {configuration} can either be boolean true to auto-detect both the host and the
826
	 * name of the UI provider, or a nested array that overrides either one or both
827
	 * of the auto-detected valyes
828
	 *
829
	 * @var array
830
	 */
831
	static private $repository_interfaces = array(
832
		'github.com' => array(
833
			'icon' => 'deploynaut/img/github.png'
834
		),
835
		'bitbucket.org' => array(
836
			'commit' => 'commits'
837
		),
838
		'repo.or.cz' => array(
839
			'scheme' => 'http',
840
			'name' => 'repo.or.cz',
841
			'regex' => array('^(.*)$' => '/w$1')
842
		),
843
844
		/* 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...
845
		'gitlab.mysite.com' => array(
846
			'icon' => 'deploynaut/img/git.png',
847
			'host' => 'gitlab.mysite.com',
848
			'name' => 'Gitlab',
849
			'regex' => array('.git$' => ''),
850
			'commit' => "commit"
851
		),
852
		*/
853
	);
854
855
	/**
856
	 * Get a ViewableData structure describing the UI tool that lets the user view the repository code
857
	 *
858
	 * @return ArrayData
859
	 */
860
	public function getRepositoryInterface() {
861
		$interfaces = $this->config()->repository_interfaces;
862
863
		/* Look for each whitelisted hostname */
864
		foreach($interfaces as $host => $interface) {
865
			/* See if the CVS Path is for this hostname, followed by some junk (maybe a port), then the path */
866
			if(preg_match('{^[^.]*' . $host . '(.*?)([/a-zA-Z].+)}', $this->CVSPath, $match)) {
867
868
				$path = $match[2];
869
870
				$scheme = isset($interface['scheme']) ? $interface['scheme'] : 'https';
871
				$host = isset($interface['host']) ? $interface['host'] : $host;
872
				$regex = isset($interface['regex']) ? $interface['regex'] : array('\.git$' => '');
873
874
				$components = explode('.', $host);
875
876
				foreach($regex as $pattern => $replacement) {
877
					$path = preg_replace('/' . $pattern . '/', $replacement, $path);
878
				}
879
880
				$uxurl = Controller::join_links($scheme . '://', $host, $path);
881
882
				if(array_key_exists('commit', $interface) && $interface['commit'] == false) {
883
					$commiturl = false;
884
				} else {
885
					$commiturl = Controller::join_links(
886
						$uxurl,
887
						isset($interface['commit']) ? $interface['commit'] : 'commit'
888
					);
889
				}
890
891
				return new ArrayData(array(
892
					'Name'      => isset($interface['name']) ? $interface['name'] : ucfirst($components[0]),
893
					'Icon'      => isset($interface['icon']) ? $interface['icon'] : 'deploynaut/img/git.png',
894
					'URL'       => $uxurl,
895
					'CommitURL' => $commiturl
896
				));
897
			}
898
		}
899
	}
900
901
	/**
902
	 * @return string
903
	 */
904
	protected function getProjectFolderPath() {
905
		return $this->DNData()->getEnvironmentDir().'/'.$this->Name;
906
	}
907
908
	/**
909
	 * Convenience wrapper for a single permission code.
910
	 *
911
	 * @param string $code
912
	 * @return SS_List
913
	 */
914
	public function whoIsAllowed($code) {
915
		return $this->whoIsAllowedAny(array($code));
916
	}
917
918
	/**
919
	 * List members who have $codes on this project.
920
	 * Does not support Permission::DENY_PERMISSION malarky, same as Permission::get_groups_by_permission anyway...
921
	 *
922
	 * @param array|string $codes
923
	 * @return SS_List
924
	 */
925
	public function whoIsAllowedAny($codes) {
926
		if(!is_array($codes)) $codes = array($codes);
927
928
		$SQLa_codes = Convert::raw2sql($codes);
929
		$SQL_codes = join("','", $SQLa_codes);
930
931
		return DataObject::get('Member')
932
			->where("\"PermissionRoleCode\".\"Code\" IN ('$SQL_codes') OR \"Permission\".\"Code\" IN ('$SQL_codes')")
933
			->filter("DNProject_Viewers.DNProjectID", $this->ID)
934
			->leftJoin('Group_Members', "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\"")
935
			->leftJoin('Group', "\"Group_Members\".\"GroupID\" = \"Group\".\"ID\"")
936
			->leftJoin('DNProject_Viewers', "\"DNProject_Viewers\".\"GroupID\" = \"Group\".\"ID\"")
937
			->leftJoin('Permission', "\"Permission\".\"GroupID\" = \"Group\".\"ID\"")
938
			->leftJoin('Group_Roles', "\"Group_Roles\".\"GroupID\" = \"Group\".\"ID\"")
939
			->leftJoin('PermissionRole', "\"Group_Roles\".\"PermissionRoleID\" = \"PermissionRole\".\"ID\"")
940
			->leftJoin('PermissionRoleCode', "\"PermissionRoleCode\".\"RoleID\" = \"PermissionRole\".\"ID\"");
941
	}
942
943
	/**
944
	 * Convenience wrapper for a single permission code.
945
	 *
946
	 * @param string $code
947
	 * @param Member|null $member
948
	 *
949
	 * @return bool
950
	 */
951
	public function allowed($code, $member = null) {
952
		return $this->allowedAny(array($code), $member);
953
	}
954
955
	/**
956
	 * Checks if a group is allowed to the project and the permission code
957
	 *
958
	 * @param string $permissionCode
959
	 * @param Group $group
960
	 *
961
	 * @return bool
962
	 */
963
	public function groupAllowed($permissionCode, Group $group) {
964
		$viewers = $this->Viewers();
965
		if(!$viewers->find('ID', $group->ID)) {
966
			return false;
967
		}
968
		$groups = Permission::get_groups_by_permission($permissionCode);
969
		if(!$groups->find('ID', $group->ID)) {
970
			return false;
971
		}
972
		return true;
973
	}
974
975
	/**
976
	 * Check if member has a permission code in this project.
977
	 *
978
	 * @param array|string $codes
979
	 * @param Member|null $member
980
	 *
981
	 * @return bool
982
	 */
983
	public function allowedAny($codes, $member = null) {
984
		if (!$member) {
985
			$member = Member::currentUser();
986
		}
987
988
		if(Permission::checkMember($member, 'ADMIN')) return true;
989
990
		$hits = $this->whoIsAllowedAny($codes)->filter('Member.ID', $member->ID)->count();
991
		return ($hits>0 ? true : false);
992
	}
993
994
	/**
995
	 * Checks if the environment has been fully built.
996
	 *
997
	 * @return bool
998
	 */
999
	public function isProjectReady() {
1000
		if($this->getRunningInitialEnvironmentCreations()->count() > 0) {
1001
			// We're still creating the initial environments for this project so we're 
1002
			// not quite done
1003
			return false;
1004
		}
1005
1006
		// Provide a hook for further checks. Logic stolen from 
1007
		// {@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...
1008
		$isDone = $this->extend('isProjectReady');
1009
		if($isDone && is_array($isDone)) {
1010
			$isDone = array_filter($isDone, function($val) {
1011
				return !is_null($val);
1012
			});
1013
1014
			// If anything returns false then we're not ready.
1015
			if($isDone) return min($isDone);
1016
		}
1017
1018
		return true;
1019
	}
1020
1021
	/**
1022
	 * Returns a list of environments still being created.
1023
	 *
1024
	 * @return SS_List
1025
	 */
1026
	public function getRunningEnvironmentCreations() {
1027
		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...
1028
			->filter('Status', ['Queued', 'Started']);
1029
	}
1030
1031
	/**
1032
	 * Returns a list of initial environments created for this project.
1033
	 * 
1034
	 * @return DataList
1035
	 */
1036
	public function getInitialEnvironmentCreations() {
1037
		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...
1038
	}
1039
1040
	/**
1041
	 * Only returns initial environments that are being created.
1042
	 *
1043
	 * @return DataList
1044
	 */
1045
	public function getRunningInitialEnvironmentCreations() {
1046
		return $this->getInitialEnvironmentCreations()
1047
			->filter('Status', ['Queued', 'Started']);
1048
	}
1049
1050
	/**
1051
	 * Returns a list of completed initial environment creations. This includes failed tasks.
1052
	 *
1053
	 * @return DataList
1054
	 */
1055
	public function getCompleteInitialEnvironmentCreations() {
1056
		return $this->getInitialEnvironmentCreations()
1057
			->exclude('Status', ['Queued', 'Started']);
1058
	}
1059
1060
	/**
1061
	 * @return ValidationResult
1062
	 */
1063
	protected function validate() {
1064
		$validation = parent::validate();
1065
		if($validation->valid()) {
1066
			if(empty($this->Name)) {
1067
				return $validation->error('The stack must have a name.');
1068
			}
1069
1070
			if(empty($this->CVSPath)) {
1071
				return $validation->error('You must provide a repository URL.');
1072
			}
1073
1074
			$existing = DNProject::get()->filter('Name', $this->Name);
1075
			if($this->ID) {
1076
				$existing = $existing->exclude('ID', $this->ID);
1077
			}
1078
			if($existing->count() > 0) {
1079
				return $validation->error('A stack already exists with that name.');
1080
			}
1081
		}
1082
		return $validation;
1083
	}
1084
1085
}
1086
1087