Completed
Pull Request — master (#518)
by Michael
03:13
created

DNEnvironment::getDefaultURL()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
/**
4
 * DNEnvironment
5
 *
6
 * This dataobject represents a target environment that source code can be deployed to.
7
 * Permissions are controlled by environment, see the various many-many relationships.
8
 *
9
 * @property string $Filename
10
 * @property string $Name
11
 * @property string $URL
12
 * @property string $BackendIdentifier
13
 * @property bool $DryRunEnabled
14
 * @property bool $Usage
15
 *
16
 * @method DNProject Project()
17
 * @property int $ProjectID
18
 *
19
 * @method HasManyList Deployments()
20
 * @method HasManyList DataArchives()
21
 * @method HasManyList Pipelines()
22
 *
23
 * @method ManyManyList Viewers()
24
 * @method ManyManyList ViewerGroups()
25
 * @method ManyManyList Deployers()
26
 * @method ManyManyList DeployerGroups()
27
 * @method ManyManyList CanRestoreMembers()
28
 * @method ManyManyList CanRestoreGroups()
29
 * @method ManyManyList CanBackupMembers()
30
 * @method ManyManyList CanBackupGroups()
31
 * @method ManyManyList ArchiveUploaders()
32
 * @method ManyManyList ArchiveUploaderGroups()
33
 * @method ManyManyList ArchiveDownloaders()
34
 * @method ManyManyList ArchiveDownloaderGroups()
35
 * @method ManyManyList ArchiveDeleters()
36
 * @method ManyManyList ArchiveDeleterGroups()
37
 * @method ManyManyList PipelineApprovers()
38
 * @method ManyManyList PipelineApproverGroups()
39
 * @method ManyManyList PipelineCancellers()
40
 * @method ManyManyList PipelineCancellerGroups()
41
 */
42
class DNEnvironment extends DataObject {
43
44
	/**
45
	 * If this is set to a full pathfile, it will be used as template
46
	 * file when creating a new capistrano environment config file.
47
	 *
48
	 * If not set, the default 'environment.template' from the module
49
	 * root is used
50
	 *
51
	 * @config
52
	 * @var string
53
	 */
54
	private static $template_file = '';
55
56
	/**
57
	 * Set this to true to allow editing of the environment files via the web admin
58
	 *
59
	 * @var bool
60
	 */
61
	private static $allow_web_editing = false;
62
63
	/**
64
	 * @var array
65
	 */
66
	private static $casting = array(
67
		'DeployHistory' => 'Text'
68
	);
69
70
	/**
71
	 * Allowed backends. A map of Injector identifier to human-readable label.
72
	 *
73
	 * @config
74
	 * @var array
75
	 */
76
	private static $allowed_backends = array();
77
78
	/**
79
	 * @var array
80
	 */
81
	public static $db = array(
82
		"Filename" => "Varchar(255)",
83
		"Name" => "Varchar(255)",
84
		"URL" => "Varchar(255)",
85
		"BackendIdentifier" => "Varchar(255)", // Injector identifier of the DeploymentBackend
86
		"DryRunEnabled" => "Boolean", // True if the dry run button should be enabled on the frontend
87
		"Usage" => "Enum('Production, UAT, Test, Unspecified', 'Unspecified')"
88
	);
89
90
	/**
91
	 * @var array
92
	 */
93
	public static $has_one = array(
94
		"Project" => "DNProject",
95
		"CreateEnvironment" => "DNCreateEnvironment"
96
	);
97
98
	/**
99
	 * @var array
100
	 */
101
	public static $has_many = array(
102
		"Deployments" => "DNDeployment",
103
		"DataArchives" => "DNDataArchive",
104
		"Pipelines" => "Pipeline" // Only one Pipeline can be 'Running' at any one time. @see self::CurrentPipeline().
105
	);
106
107
	/**
108
	 * @var array
109
	 */
110
	public static $many_many = array(
111
		"Viewers"            => "Member", // Who can view this environment
112
		"ViewerGroups"       => "Group",
113
		"Deployers"          => "Member", // Who can deploy to this environment
114
		"DeployerGroups" => "Group",
115
		"CanRestoreMembers"  => "Member", // Who can restore archive files to this environment
116
		"CanRestoreGroups"  => "Group",
117
		"CanBackupMembers"   => "Member", // Who can backup archive files from this environment
118
		"CanBackupGroups"   => "Group",
119
		"ArchiveUploaders"   => "Member", // Who can upload archive files linked to this environment
120
		"ArchiveUploaderGroups" => "Group",
121
		"ArchiveDownloaders" => "Member", // Who can download archive files from this environment
122
		"ArchiveDownloaderGroups" => "Group",
123
		"ArchiveDeleters"    => "Member", // Who can delete archive files from this environment,
124
		"ArchiveDeleterGroups" => "Group",
125
		"PipelineApprovers"  => "Member", // Who can approve / reject pipelines from this environment
126
		"PipelineApproverGroups" => "Group",
127
		"PipelineCancellers"   => "Member", // Who can abort pipelines
128
		"PipelineCancellerGroups" => "Group"
129
	);
130
131
	/**
132
	 * @var array
133
	 */
134
	public static $summary_fields = array(
135
		"Name" => "Environment Name",
136
		"Usage" => "Usage",
137
		"URL" => "URL",
138
		"DeployersList" => "Can Deploy List",
139
		"CanRestoreMembersList" => "Can Restore List",
140
		"CanBackupMembersList" => "Can Backup List",
141
		"ArchiveUploadersList" => "Can Upload List",
142
		"ArchiveDownloadersList" => "Can Download List",
143
		"ArchiveDeletersList"  => "Can Delete List",
144
		"PipelineApproversList" => "Can Approve List",
145
		"PipelineCancellersList" => "Can Cancel List"
146
	);
147
148
	private static $singular_name = 'Capistrano Environment';
149
150
	private static $plural_name = 'Capistrano Environments';
151
152
	/**
153
	 * @var array
154
	 */
155
	public static $searchable_fields = array(
156
		"Name",
157
	);
158
159
	/**
160
	 * @var string
161
	 */
162
	private static $default_sort = 'Name';
163
164
	/**
165
	 * Used by the sync task
166
	 *
167
	 * @param string $path
168
	 * @return \DNEnvironment
169
	 */
170
	public static function create_from_path($path) {
171
		$e = DNEnvironment::create();
172
		$e->Filename = $path;
173
		$e->Name = basename($e->Filename, '.rb');
174
175
		// add each administrator member as a deployer of the new environment
176
		$adminGroup = Group::get()->filter('Code', 'administrators')->first();
177
		$e->DeployerGroups()->add($adminGroup);
178
		return $e;
179
	}
180
181
	/**
182
	 * Get the deployment backend used for this environment.
183
	 *
184
	 * Enforces compliance with the allowed_backends setting; if the DNEnvironment.BackendIdentifier value is
185
	 * illegal then that value is ignored.
186
	 *
187
	 * @return DeploymentBackend
188
	 */
189
	public function Backend() {
190
		$backends = array_keys($this->config()->get('allowed_backends', Config::FIRST_SET));
191
		switch(sizeof($backends)) {
192
		// Nothing allowed, use the default value "DeploymentBackend"
193
			case 0:
194
				$backend = "DeploymentBackend";
195
				break;
196
197
			// Only 1 thing allowed, use that
198
			case 1:
199
				$backend = $backends[0];
200
				break;
201
202
			// Multiple choices, use our choice if it's legal, otherwise default to the first item on the list
203
			default:
204
				$backend = $this->BackendIdentifier;
205
				if(!in_array($backend, $backends)) {
206
					$backend = $backends[0];
207
				}
208
		}
209
210
		return Injector::inst()->get($backend);
211
	}
212
213
	public function Menu() {
214
		$list = new ArrayList();
215
216
		$controller = Controller::curr();
217
		$actionType = $controller->getField('CurrentActionType');
218
219
		$list->push(new ArrayData(array(
220
			'Link' => sprintf('naut/project/%s/environment/%s', $this->Project()->Name, $this->Name),
221
			'Title' => 'Deployments',
222
			'IsCurrent' => $this->isCurrent(),
223
			'IsSection' => $this->isSection() && $actionType == DNRoot::ACTION_DEPLOY
224
		)));
225
226
		$this->extend('updateMenu', $list);
227
228
		return $list;
229
	}
230
231
	/**
232
	 * Return the current object from $this->Menu()
233
	 * Good for making titles and things
234
	 */
235
	public function CurrentMenu() {
236
		return $this->Menu()->filter('IsSection', true)->First();
237
	}
238
239
	/**
240
	 * Return a name for this environment.
241
	 *
242
	 * @param string $separator The string used when concatenating project with env name
243
	 * @return string
244
	 */
245
	public function getFullName($separator = ':') {
246
		return sprintf('%s%s%s', $this->Project()->Name, $separator, $this->Name);
247
	}
248
249
	/**
250
	 * URL for the environment that can be used if no explicit URL is set.
251
	 */
252
	public function getDefaultURL() {
253
		return null;
254
	}
255
256
	public function getBareURL() {
257
		$url = parse_url($this->URL);
258
		if(isset($url['host'])) {
259
			return strtolower($url['host']);
260
		}
261
	}
262
263
	public function getBareDefaultURL() {
264
		$url = parse_url($this->getDefaultURL());
265
		if(isset($url['host'])) {
266
			return strtolower($url['host']);
267
		}
268
	}
269
270
	/**
271
	 * @return boolean true if there is a pipeline for the current environment.
272
	 */
273
	public function HasPipelineSupport() {
274
		$config = $this->GenericPipelineConfig();
275
		return $config instanceof ArrayData && isset($config->Steps);
276
	}
277
278
	/**
279
	 * Returns a {@link Pipeline} object that is linked to this environment, but isn't saved into the database. This
280
	 * shouldn't be saved into the database unless you plan on starting an actual pipeline.
281
	 *
282
	 * @return Pipeline
283
	 */
284
	public function GenericPipeline() {
285
		$pipeline = Pipeline::create();
286
		$pipeline->EnvironmentID = $this->ID;
287
		return $pipeline;
288
	}
289
290
	/**
291
	 * Returns the parsed config, based on a {@link Pipeline} being created for this {@link DNEnvironment}.
292
	 *
293
	 * @return ArrayData
294
	 */
295
	public function GenericPipelineConfig() {
296
		$config = $this->loadPipelineConfig();
297
		if($config) {
298
			return self::array_to_viewabledata($config);
299
		}
300
	}
301
302
	/**
303
	 * Extract pipeline configuration data from the source yml file
304
	 *
305
	 * @return array
306
	 */
307
	public function loadPipelineConfig() {
308
		require_once 'thirdparty/spyc/spyc.php';
309
310
		$path = $this->getPipelineFilename();
311
		if(file_exists($path)) {
312
			return Spyc::YAMLLoad($path);
313
		}
314
	}
315
316
	/**
317
	 * Returns the {@link DNEnvironment} object relating to the pipeline config for this environment. The environment
318
	 * YAML file (e.g. project1-uat.yml; see docs/en/pipelines.md) contains two variable called `DependsOnProject` and
319
	 * `DependsOnEnvironment` - these are used together to find the {@link DNEnvironment} that this environment should
320
	 * rely on.
321
	 */
322
	public function DependsOnEnvironment() {
323
		if($this->HasPipelineSupport()) {
324
			$pipeline = $this->GenericPipeline();
325
			return $pipeline->getDependentEnvironment();
326
		}
327
328
		return null;
329
	}
330
331
	/**
332
	 * @return bool true if there is a currently running Pipeline, and false if there isn't
333
	 */
334
	public function HasCurrentPipeline() {
335
		return $this->CurrentPipeline() && $this->CurrentPipeline()->isInDB();
336
	}
337
338
	/**
339
	 * This can be used to determine if there is a currently running pipeline (there can only be one running per
340
	 * {@link DNEnvironment} at once), as well as getting the current pipeline to be shown in templates.
341
	 *
342
	 * @return DataObject|null The currently running pipeline, or null if there isn't any.
343
	 */
344
	public function CurrentPipeline() {
345
		return $this->Pipelines()->filter('Status', array('Running', 'Rollback'))->first();
346
	}
347
348
	/**
349
	 * @return bool true if the current user can cancel a running pipeline
350
	 */
351
	public function CanCancelPipeline() {
352
		// do we have a current pipeline
353
		if($this->HasCurrentPipeline()) {
354
			return $this->CurrentPipeline()->canAbort();
355
		}
356
		return false;
357
	}
358
359
	/**
360
	 * Environments are only viewable by people that can view the environment.
361
	 *
362
	 * @param Member|null $member
363
	 * @return boolean
364
	 */
365
	public function canView($member = null) {
366
		if(!$member) {
367
			$member = Member::currentUser();
368
		}
369
		if(!$member) {
370
			return false;
371
		}
372
		// Must be logged in to check permissions
373
374
		if(Permission::checkMember($member, 'ADMIN')) {
375
			return true;
376
		}
377
378
		// if no Viewers or ViewerGroups defined, fallback to DNProject::canView permissions
379
		if($this->Viewers()->exists() || $this->ViewerGroups()->exists()) {
380
			return $this->Viewers()->byID($member->ID)
381
				|| $member->inGroups($this->ViewerGroups());
382
		}
383
384
		return $this->Project()->canView($member);
385
	}
386
387
	/**
388
	 * Allow deploy only to some people.
389
	 *
390
	 * @param Member|null $member
391
	 * @return boolean
392
	 */
393 View Code Duplication
	public function canDeploy($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...
394
		if(!$member) {
395
			$member = Member::currentUser();
396
		}
397
		if(!$member) {
398
			return false;
399
		}
400
		// Must be logged in to check permissions
401
402
		if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
403
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_DEPLOYMENT, $member)) return true;
404
		} else {
405
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_DEPLOYMENT, $member)) return true;
406
		}
407
408
		return $this->Deployers()->byID($member->ID)
409
			|| $member->inGroups($this->DeployerGroups());
410
	}
411
412
	/**
413
	 * Provide reason why the user cannot deploy.
414
	 *
415
	 * @return string
416
	 */
417
	public function getCannotDeployMessage() {
418
		return 'You cannot deploy to this environment.';
419
	}
420
421
	/**
422
	 * Allows only selected {@link Member} objects to restore {@link DNDataArchive} objects into this
423
	 * {@link DNEnvironment}.
424
	 *
425
	 * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
426
	 * @return boolean true if $member can restore, and false if they can't.
427
	 */
428 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...
429
		if(!$member) {
430
			$member = Member::currentUser();
431
		}
432
		if(!$member) {
433
			return false;
434
		}
435
		// Must be logged in to check permissions
436
437
		if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
438
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) return true;
439
		} else {
440
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) return true;
441
		}
442
443
		return $this->CanRestoreMembers()->byID($member->ID)
444
			|| $member->inGroups($this->CanRestoreGroups());
445
	}
446
447
	/**
448
	 * Allows only selected {@link Member} objects to backup this {@link DNEnvironment} to a {@link DNDataArchive}
449
	 * file.
450
	 *
451
	 * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
452
	 * @return boolean true if $member can backup, and false if they can't.
453
	 */
454 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...
455
		$project = $this->Project();
456
		if($project->HasDiskQuota() && $project->HasExceededDiskQuota()) {
457
			return false;
458
		}
459
460
		if(!$member) {
461
			$member = Member::currentUser();
462
		}
463
		// Must be logged in to check permissions
464
		if(!$member) {
465
			return false;
466
		}
467
468
		if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
469
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) return true;
470
		} else {
471
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) return true;
472
		}
473
474
		return $this->CanBackupMembers()->byID($member->ID)
475
			|| $member->inGroups($this->CanBackupGroups());
476
	}
477
478
	/**
479
	 * Allows only selected {@link Member} objects to upload {@link DNDataArchive} objects linked to this
480
	 * {@link DNEnvironment}.
481
	 *
482
	 * Note: This is not uploading them to the actual environment itself (e.g. uploading to the live site) - it is the
483
	 * process of uploading a *.sspak file into Deploynaut for later 'restoring' to an environment. See
484
	 * {@link self::canRestore()}.
485
	 *
486
	 * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
487
	 * @return boolean true if $member can upload archives linked to this environment, false if they can't.
488
	 */
489 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...
490
		$project = $this->Project();
491
		if($project->HasDiskQuota() && $project->HasExceededDiskQuota()) {
492
			return false;
493
		}
494
495
		if(!$member) {
496
			$member = Member::currentUser();
497
		}
498
		if(!$member) {
499
			return false;
500
		}
501
		// Must be logged in to check permissions
502
503
		if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
504
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) return true;
505
		} else {
506
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) return true;
507
		}
508
509
		return $this->ArchiveUploaders()->byID($member->ID)
510
			|| $member->inGroups($this->ArchiveUploaderGroups());
511
	}
512
513
	/**
514
	 * Allows only selected {@link Member} objects to download {@link DNDataArchive} objects from this
515
	 * {@link DNEnvironment}.
516
	 *
517
	 * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
518
	 * @return boolean true if $member can download archives from this environment, false if they can't.
519
	 */
520 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...
521
		if(!$member) {
522
			$member = Member::currentUser();
523
		}
524
		if(!$member) {
525
			return false;
526
		}
527
		// Must be logged in to check permissions
528
529
		if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
530
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) return true;
531
		} else {
532
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) return true;
533
		}
534
535
		return $this->ArchiveDownloaders()->byID($member->ID)
536
			|| $member->inGroups($this->ArchiveDownloaderGroups());
537
	}
538
539
	/**
540
	 * Determine if the specified user can abort any pipelines
541
	 *
542
	 * @param Member|null $member
543
	 * @return boolean
544
	 */
545 View Code Duplication
	public function canAbort($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...
546
		if(!$member) {
547
			$member = Member::currentUser();
548
		}
549
		if(!$member) {
550
			return false;
551
		}
552
553
		if(Permission::checkMember($member, 'ADMIN')) {
554
			return true;
555
		}
556
557
		return $this->PipelineCancellers()->byID($member->ID)
558
			|| $member->inGroups($this->PipelineCancellerGroups());
559
	}
560
561
	/**
562
	 * Determine if the specified user can approve any pipelines
563
	 *
564
	 * @param Member|null $member
565
	 * @return boolean
566
	 */
567 View Code Duplication
	public function canApprove($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...
568
		if(!$member) {
569
			$member = Member::currentUser();
570
		}
571
		if(!$member) {
572
			return false;
573
		}
574
575
		if(Permission::checkMember($member, 'ADMIN')) {
576
			return true;
577
		}
578
		return $this->PipelineApprovers()->byID($member->ID)
579
			|| $member->inGroups($this->PipelineApproverGroups());
580
	}
581
582
	/**
583
	 * Allows only selected {@link Member} objects to delete {@link DNDataArchive} objects from this
584
	 * {@link DNEnvironment}.
585
	 *
586
	 * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
587
	 * @return boolean true if $member can delete archives from this environment, false if they can't.
588
	 */
589 View Code Duplication
	public function canDeleteArchive($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...
590
		if(!$member) {
591
			$member = Member::currentUser();
592
		}
593
		if(!$member) {
594
			return false;
595
		}
596
		// Must be logged in to check permissions
597
598
		if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
599
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) return true;
600
		} else {
601
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) return true;
602
		}
603
604
		return $this->ArchiveDeleters()->byID($member->ID)
605
			|| $member->inGroups($this->ArchiveDeleterGroups());
606
	}
607
	/**
608
	 * Get a string of groups/people that are allowed to deploy to this environment.
609
	 * Used in DNRoot_project.ss to list {@link Member}s who have permission to perform this action.
610
	 *
611
	 * @return string
612
	 */
613
	public function getDeployersList() {
614
		return implode(
615
			", ",
616
			array_merge(
617
				$this->DeployerGroups()->column("Title"),
618
				$this->Deployers()->column("FirstName")
619
			)
620
		);
621
	}
622
623
	/**
624
	 * Get a string of groups/people that are allowed to restore {@link DNDataArchive} objects into this environment.
625
	 *
626
	 * @return string
627
	 */
628
	public function getCanRestoreMembersList() {
629
		return implode(
630
			", ",
631
			array_merge(
632
				$this->CanRestoreGroups()->column("Title"),
633
				$this->CanRestoreMembers()->column("FirstName")
634
			)
635
		);
636
	}
637
638
	/**
639
	 * Get a string of groups/people that are allowed to backup {@link DNDataArchive} objects from this environment.
640
	 *
641
	 * @return string
642
	 */
643
	public function getCanBackupMembersList() {
644
		return implode(
645
			", ",
646
			array_merge(
647
				$this->CanBackupGroups()->column("Title"),
648
				$this->CanBackupMembers()->column("FirstName")
649
			)
650
		);
651
	}
652
653
	/**
654
	 * Get a string of groups/people that are allowed to upload {@link DNDataArchive}
655
	 *  objects linked to this environment.
656
	 *
657
	 * @return string
658
	 */
659
	public function getArchiveUploadersList() {
660
		return implode(
661
			", ",
662
			array_merge(
663
				$this->ArchiveUploaderGroups()->column("Title"),
664
				$this->ArchiveUploaders()->column("FirstName")
665
			)
666
		);
667
	}
668
669
	/**
670
	 * Get a string of groups/people that are allowed to download {@link DNDataArchive} objects from this environment.
671
	 *
672
	 * @return string
673
	 */
674
	public function getArchiveDownloadersList() {
675
		return implode(
676
			", ",
677
			array_merge(
678
				$this->ArchiveDownloaderGroups()->column("Title"),
679
				$this->ArchiveDownloaders()->column("FirstName")
680
			)
681
		);
682
	}
683
684
	/**
685
	 * Get a string of groups/people that are allowed to delete {@link DNDataArchive} objects from this environment.
686
	 *
687
	 * @return string
688
	 */
689
	public function getArchiveDeletersList() {
690
		return implode(
691
			", ",
692
			array_merge(
693
				$this->ArchiveDeleterGroups()->column("Title"),
694
				$this->ArchiveDeleters()->column("FirstName")
695
			)
696
		);
697
	}
698
699
	/**
700
	 * Get a string of groups/people that are allowed to approve pipelines
701
	 *
702
	 * @return string
703
	 */
704
	public function getPipelineApproversList() {
705
		return implode(
706
			", ",
707
			array_merge(
708
				$this->PipelineApproverGroups()->column("Title"),
709
				$this->PipelineApprovers()->column("FirstName")
710
			)
711
		);
712
	}
713
714
	/**
715
	 * Get a string of groups/people that are allowed to cancel pipelines
716
	 *
717
	 * @return string
718
	 */
719
	public function getPipelineCancellersList() {
720
		return implode(
721
			", ",
722
			array_merge(
723
				$this->PipelineCancellerGroups()->column("Title"),
724
				$this->PipelineCancellers()->column("FirstName")
725
			)
726
		);
727
	}
728
729
	/**
730
	 * @return DNData
731
	 */
732
	public function DNData() {
733
		return DNData::inst();
734
	}
735
736
	/**
737
	 * Get the current deployed build for this environment
738
	 *
739
	 * Dear people of the future: If you are looking to optimize this, simply create a CurrentBuildSHA(), which can be
740
	 * a lot faster. I presume you came here because of the Project display template, which only needs a SHA.
741
	 *
742
	 * @return false|DNDeployment
743
	 */
744
	public function CurrentBuild() {
745
		// The DeployHistory function is far too slow to use for this
746
747
		/** @var DNDeployment $deploy */
748
		$deploy = DNDeployment::get()->filter(array(
749
			'EnvironmentID' => $this->ID,
750
			'Status' => 'Finished'
751
		))->sort('LastEdited DESC')->first();
752
753
		if(!$deploy || (!$deploy->SHA)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $deploy->SHA of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
754
			return false;
755
		}
756
757
		$repo = $this->Project()->getRepository();
758
		if(!$repo) {
759
			return $deploy;
760
		}
761
762
		try {
763
			$commit = $repo->getCommit($deploy->SHA);
764
			if($commit) {
765
				$deploy->Message = Convert::raw2xml($commit->getMessage());
0 ignored issues
show
Documentation introduced by
The property Message does not exist on object<DNDeployment>. 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...
766
				$deploy->Committer = Convert::raw2xml($commit->getCommitterName());
0 ignored issues
show
Documentation introduced by
The property Committer does not exist on object<DNDeployment>. 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...
767
				$deploy->CommitDate = $commit->getCommitterDate()->Format('d/m/Y g:ia');
0 ignored issues
show
Documentation introduced by
The property CommitDate does not exist on object<DNDeployment>. 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...
768
				$deploy->Author = Convert::raw2xml($commit->getAuthorName());
0 ignored issues
show
Documentation introduced by
The property Author does not exist on object<DNDeployment>. 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...
769
				$deploy->AuthorDate = $commit->getAuthorDate()->Format('d/m/Y g:ia');
0 ignored issues
show
Documentation introduced by
The property AuthorDate does not exist on object<DNDeployment>. 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...
770
			}
771
			// We can't find this SHA, so we ignore adding a commit message to the deployment
772
		} catch(Exception $ex) { }
773
774
		return $deploy;
775
	}
776
777
	/**
778
	 * A history of all builds deployed to this environment
779
	 *
780
	 * @return ArrayList
781
	 */
782
	public function DeployHistory() {
783
		return $this->Deployments()
784
			->where('SHA IS NOT NULL')
785
			->sort('LastEdited DESC');
786
	}
787
788
	/**
789
	 * @param string $sha
790
	 * @return array
791
	 */
792
	protected function getCommitData($sha) {
793
		try {
794
			$repo = $this->Project()->getRepository();
795
			if($repo !== false) {
796
				$commit = new \Gitonomy\Git\Commit($repo, $sha);
797
				return [
798
					'AuthorName' => (string)Convert::raw2xml($commit->getAuthorName()),
799
					'AuthorEmail' => (string)Convert::raw2xml($commit->getAuthorEmail()),
800
					'Message' => (string)Convert::raw2xml($commit->getMessage()),
801
					'ShortHash' => Convert::raw2xml($commit->getFixedShortHash(8)),
802
					'Hash' => Convert::raw2xml($commit->getHash())
803
				];
804
			}
805
		} catch(\Gitonomy\Git\Exception\ReferenceNotFoundException $exc) {
806
			SS_Log::log($exc, SS_Log::WARN);
807
		}
808
		return array(
809
			'AuthorName' => '(unknown)',
810
			'AuthorEmail' => '(unknown)',
811
			'Message' => '(unknown)',
812
			'ShortHash' => $sha,
813
			'Hash' => '(unknown)',
814
		);
815
	}
816
817
	/**
818
	 * @return string
819
	 */
820
	public function Link() {
821
		return $this->Project()->Link() . "/environment/" . $this->Name;
822
	}
823
824
	/**
825
	 * Is this environment currently at the root level of the controller that handles it?
826
	 * @return bool
827
	 */
828
	public function isCurrent() {
829
		return $this->isSection() && Controller::curr()->getAction() == 'environment';
830
	}
831
832
	/**
833
	 * Is this environment currently in a controller that is handling it or performing a sub-task?
834
	 * @return bool
835
	 */
836
	public function isSection() {
837
		$controller = Controller::curr();
838
		$environment = $controller->getField('CurrentEnvironment');
839
		return $environment && $environment->ID == $this->ID;
840
	}
841
842
843
	/**
844
	 * Build a set of multi-select fields for assigning permissions to a pair of group and member many_many relations
845
	 *
846
	 * @param string $groupField Group field name
847
	 * @param string $memberField Member field name
848
	 * @param array $groups List of groups
849
	 * @param array $members List of members
850
	 * @return FieldGroup
851
	 */
852
	protected function buildPermissionField($groupField, $memberField, $groups, $members) {
853
		return FieldGroup::create(
854
			ListboxField::create($groupField, false, $groups)
855
				->setMultiple(true)
856
				->setAttribute('data-placeholder', 'Groups')
857
				->setAttribute('placeholder', 'Groups')
858
				->setAttribute('style', 'width: 400px;'),
859
860
			ListboxField::create($memberField, false, $members)
861
				->setMultiple(true)
862
				->setAttribute('data-placeholder', 'Members')
863
				->setAttribute('placeholder', 'Members')
864
				->setAttribute('style', 'width: 400px;')
865
		);
866
	}
867
868
	/**
869
	 * @return FieldList
870
	 */
871
	public function getCMSFields() {
872
		$fields = new FieldList(new TabSet('Root'));
873
874
		$project = $this->Project();
875
		if($project && $project->exists()) {
876
			$viewerGroups = $project->Viewers();
877
			$groups = $viewerGroups->sort('Title')->map()->toArray();
878
			$members = array();
879
			foreach($viewerGroups as $group) {
880
				foreach($group->Members()->map() as $k => $v) {
881
					$members[$k] = $v;
882
				}
883
			}
884
			asort($members);
885
		} else {
886
			$groups = array();
887
			$members = array();
888
		}
889
890
		// Main tab
891
		$fields->addFieldsToTab('Root.Main', array(
892
			// The Main.ProjectID
893
			TextField::create('ProjectName', 'Project')
894
				->setValue(($project = $this->Project()) ? $project->Name : null)
895
				->performReadonlyTransformation(),
896
897
			// The Main.Name
898
			TextField::create('Name', 'Environment name')
899
				->setDescription('A descriptive name for this environment, e.g. staging, uat, production'),
900
901
902
			$this->obj('Usage')->scaffoldFormField('Environment usage'),
903
904
			// The Main.URL field
905
			TextField::create('URL', 'Server URL')
906
				->setDescription('This url will be used to provide the front-end with a link to this environment'),
907
908
			// The Main.Filename
909
			TextField::create('Filename')
910
				->setDescription('The capistrano environment file name')
911
				->performReadonlyTransformation(),
912
		));
913
914
		// Backend identifier - pick from a named list of configurations specified in YML config
915
		$backends = $this->config()->get('allowed_backends', Config::FIRST_SET);
916
		// If there's only 1 backend, then user selection isn't needed
917
		if(sizeof($backends) > 1) {
918
			$fields->addFieldToTab('Root.Main', DropdownField::create('BackendIdentifier', 'Deployment backend')
919
				->setSource($backends)
920
				->setDescription('What kind of deployment system should be used to deploy to this environment'));
921
		}
922
923
		$fields->addFieldsToTab('Root.UserPermissions', array(
924
			// The viewers of the environment
925
			$this
926
				->buildPermissionField('ViewerGroups', 'Viewers', $groups, $members)
927
				->setTitle('Who can view this environment?')
928
				->setDescription('Groups or Users who can view this environment'),
929
930
			// The Main.Deployers
931
			$this
932
				->buildPermissionField('DeployerGroups', 'Deployers', $groups, $members)
933
				->setTitle('Who can deploy?')
934
				->setDescription('Groups or Users who can deploy to this environment'),
935
936
			// A box to select all snapshot options.
937
			$this
938
				->buildPermissionField('TickAllSnapshotGroups', 'TickAllSnapshot', $groups, $members)
939
				->setTitle("<em>All snapshot permissions</em>")
940
				->addExtraClass('tickall')
941
				->setDescription('UI shortcut to select all snapshot-related options - not written to the database.'),
942
943
			// The Main.CanRestoreMembers
944
			$this
945
				->buildPermissionField('CanRestoreGroups', 'CanRestoreMembers', $groups, $members)
946
				->setTitle('Who can restore?')
947
				->setDescription('Groups or Users who can restore archives from Deploynaut into this environment'),
948
949
			// The Main.CanBackupMembers
950
			$this
951
				->buildPermissionField('CanBackupGroups', 'CanBackupMembers', $groups, $members)
952
				->setTitle('Who can backup?')
953
				->setDescription('Groups or Users who can backup archives from this environment into Deploynaut'),
954
955
			// The Main.ArchiveDeleters
956
			$this
957
				->buildPermissionField('ArchiveDeleterGroups', 'ArchiveDeleters', $groups, $members)
958
				->setTitle('Who can delete?')
959
				->setDescription("Groups or Users who can delete archives from this environment's staging area."),
960
961
			// The Main.ArchiveUploaders
962
			$this
963
				->buildPermissionField('ArchiveUploaderGroups', 'ArchiveUploaders', $groups, $members)
964
				->setTitle('Who can upload?')
965
				->setDescription(
966
					'Users who can upload archives linked to this environment into Deploynaut.<br />' .
967
					'Linking them to an environment allows limiting download permissions (see below).'
968
				),
969
970
			// The Main.ArchiveDownloaders
971
			$this
972
				->buildPermissionField('ArchiveDownloaderGroups', 'ArchiveDownloaders', $groups, $members)
973
				->setTitle('Who can download?')
974
				->setDescription(<<<PHP
975
Users who can download archives from this environment to their computer.<br />
976
Since this implies access to the snapshot, it is also a prerequisite for restores
977
to other environments, alongside the "Who can restore" permission.<br>
978
Should include all users with upload permissions, otherwise they can't download
979
their own uploads.
980
PHP
981
				),
982
983
			// The Main.PipelineApprovers
984
			$this
985
				->buildPermissionField('PipelineApproverGroups', 'PipelineApprovers', $groups, $members)
986
				->setTitle('Who can approve pipelines?')
987
				->setDescription('Users who can approve waiting deployment pipelines.'),
988
989
			// The Main.PipelineCancellers
990
			$this
991
				->buildPermissionField('PipelineCancellerGroups', 'PipelineCancellers', $groups, $members)
992
				->setTitle('Who can cancel pipelines?')
993
				->setDescription('Users who can cancel in-progess deployment pipelines.')
994
		));
995
996
		// The Main.DeployConfig
997
		if($this->Project()->exists()) {
998
			$this->setDeployConfigurationFields($fields);
999
		}
1000
1001
		// The DataArchives
1002
		$dataArchiveConfig = GridFieldConfig_RecordViewer::create();
1003
		$dataArchiveConfig->removeComponentsByType('GridFieldAddNewButton');
1004
		if(class_exists('GridFieldBulkManager')) {
1005
			$dataArchiveConfig->addComponent(new GridFieldBulkManager());
1006
		}
1007
		$dataArchive = GridField::create('DataArchives', 'Data Archives', $this->DataArchives(), $dataArchiveConfig);
1008
		$fields->addFieldToTab('Root.DataArchive', $dataArchive);
1009
1010
		// Pipeline templates
1011
		$this->setPipelineConfigurationFields($fields);
1012
1013
		// Pipelines
1014
		if($this->Pipelines()->Count()) {
1015
			$pipelinesConfig = GridFieldConfig_RecordEditor::create();
1016
			$pipelinesConfig->removeComponentsByType('GridFieldAddNewButton');
1017
			if(class_exists('GridFieldBulkManager')) {
1018
				$pipelinesConfig->addComponent(new GridFieldBulkManager());
1019
			}
1020
			$pipelines = GridField::create('Pipelines', 'Pipelines', $this->Pipelines(), $pipelinesConfig);
1021
			$fields->addFieldToTab('Root.Pipelines', $pipelines);
1022
		}
1023
1024
		// Deployments
1025
		$deploymentsConfig = GridFieldConfig_RecordEditor::create();
1026
		$deploymentsConfig->removeComponentsByType('GridFieldAddNewButton');
1027
		if(class_exists('GridFieldBulkManager')) {
1028
			$deploymentsConfig->addComponent(new GridFieldBulkManager());
1029
		}
1030
		$deployments = GridField::create('Deployments', 'Deployments', $this->Deployments(), $deploymentsConfig);
1031
		$fields->addFieldToTab('Root.Deployments', $deployments);
1032
1033
		Requirements::javascript('deploynaut/javascript/environment.js');
1034
1035
		// Add actions
1036
		$action = new FormAction('check', 'Check Connection');
1037
		$action->setUseButtonTag(true);
1038
		$dataURL = Director::absoluteBaseURL() . 'naut/api/' . $this->Project()->Name . '/' . $this->Name . '/ping';
1039
		$action->setAttribute('data-url', $dataURL);
1040
		$fields->insertBefore($action, '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...
1041
1042
		// Allow extensions
1043
		$this->extend('updateCMSFields', $fields);
1044
		return $fields;
1045
	}
1046
1047
	/**
1048
	 * @param FieldList $fields
1049
	 */
1050
	protected function setDeployConfigurationFields(&$fields) {
1051
		if(!$this->config()->get('allow_web_editing')) {
1052
			return;
1053
		}
1054
1055
		if($this->envFileExists()) {
1056
			$deployConfig = new TextareaField('DeployConfig', 'Deploy config', $this->getEnvironmentConfig());
1057
			$deployConfig->setRows(40);
1058
			$fields->insertAfter($deployConfig, 'Filename');
0 ignored issues
show
Documentation introduced by
'Filename' 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...
1059
			return;
1060
		}
1061
1062
		$warning = 'Warning: This environment doesn\'t have deployment configuration.';
1063
		$noDeployConfig = new LabelField('noDeployConfig', $warning);
1064
		$noDeployConfig->addExtraClass('message warning');
1065
		$fields->insertAfter($noDeployConfig, 'Filename');
0 ignored issues
show
Documentation introduced by
'Filename' 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...
1066
		$createConfigField = new CheckboxField('CreateEnvConfig', 'Create Config');
1067
		$createConfigField->setDescription('Would you like to create the capistrano deploy configuration?');
1068
		$fields->insertAfter($createConfigField, 'noDeployConfig');
0 ignored issues
show
Documentation introduced by
'noDeployConfig' 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...
1069
	}
1070
1071
	/**
1072
	 * @param FieldList $fields
1073
	 */
1074
	protected function setPipelineConfigurationFields($fields) {
1075
		if(!$this->config()->get('allow_web_editing')) {
1076
			return;
1077
		}
1078
		$config = $this->pipelineFileExists()
1079
			? file_get_contents($this->getPipelineFilename())
1080
			: '';
1081
		$deployConfig = new TextareaField('PipelineConfig', 'Pipeline config', $config);
1082
		$deployConfig->setRows(40);
1083
		if(!$this->pipelineFileExists()) {
1084
			$deployConfig->setDescription(
1085
				"No pipeline is configured for this environment. Saving content here will generate a new template."
1086
			);
1087
		}
1088
		$fields->addFieldsToTab('Root.PipelineSettings', array(
1089
			FieldGroup::create(
1090
				CheckboxField::create('DryRunEnabled', 'Enable dry-run?')
1091
			)
1092
				->setTitle('Pipeline Options')
1093
				->setDescription(
1094
					"Allows admins to run simulated pipelines without triggering deployments or notifications."
1095
				),
1096
			$deployConfig
1097
		));
1098
	}
1099
1100
	/**
1101
	 */
1102
	public function onBeforeWrite() {
1103
		parent::onBeforeWrite();
1104
		if($this->Name && $this->Name . '.rb' != $this->Filename) {
1105
			$this->Filename = $this->Name . '.rb';
1106
		}
1107
		$this->checkEnvironmentPath();
1108
		$this->writeConfigFile();
1109
		$this->writePipelineFile();
1110
	}
1111
1112
	public function onAfterWrite() {
1113
		parent::onAfterWrite();
1114
1115
		if($this->Usage == 'Production' || $this->Usage == 'UAT') {
1116
			$conflicting = DNEnvironment::get()
1117
				->filter('ProjectID', $this->ProjectID)
1118
				->filter('Usage', $this->Usage)
1119
				->exclude('ID', $this->ID);
1120
1121
			foreach($conflicting as $otherEnvironment) {
1122
				$otherEnvironment->Usage = 'Unspecified';
1123
				$otherEnvironment->write();
1124
			}
1125
		}
1126
	}
1127
1128
1129
	/**
1130
	 * Ensure that environment paths are setup on the local filesystem
1131
	 */
1132
	protected function checkEnvironmentPath() {
1133
		// Create folder if it doesn't exist
1134
		$configDir = dirname($this->getConfigFilename());
1135
		if(!file_exists($configDir) && $configDir) {
1136
			mkdir($configDir, 0777, true);
1137
		}
1138
	}
1139
1140
	/**
1141
	 * Write the deployment config file to filesystem
1142
	 */
1143
	protected function writeConfigFile() {
1144
		if(!$this->config()->get('allow_web_editing')) {
1145
			return;
1146
		}
1147
1148
		// Create a basic new environment config from a template
1149
		if(!$this->envFileExists()
1150
			&& $this->Filename
1151
			&& $this->CreateEnvConfig
0 ignored issues
show
Documentation introduced by
The property CreateEnvConfig does not exist on object<DNEnvironment>. 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...
1152
		) {
1153
			$templateFile = $this->config()->template_file ?: BASE_PATH . '/deploynaut/environment.template';
1154
			file_put_contents($this->getConfigFilename(), file_get_contents($templateFile));
1155
		} else if($this->envFileExists() && $this->DeployConfig) {
0 ignored issues
show
Documentation introduced by
The property DeployConfig does not exist on object<DNEnvironment>. 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...
1156
			file_put_contents($this->getConfigFilename(), $this->DeployConfig);
0 ignored issues
show
Documentation introduced by
The property DeployConfig does not exist on object<DNEnvironment>. 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...
1157
		}
1158
	}
1159
1160
	/**
1161
	 * Write the pipeline config file to filesystem
1162
	 */
1163
	protected function writePipelineFile() {
1164
		if(!$this->config()->get('allow_web_editing')) {
1165
			return;
1166
		}
1167
		$path = $this->getPipelineFilename();
1168
		if($this->PipelineConfig) {
0 ignored issues
show
Documentation introduced by
The property PipelineConfig does not exist on object<DNEnvironment>. 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...
1169
			// Update file
1170
			file_put_contents($path, $this->PipelineConfig);
0 ignored issues
show
Documentation introduced by
The property PipelineConfig does not exist on object<DNEnvironment>. 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...
1171
		} elseif($this->isChanged('PipelineConfig') && file_exists($path)) {
1172
			// Remove file if deleted
1173
			unlink($path);
1174
		}
1175
	}
1176
1177
	/**
1178
	 * Delete any related config files
1179
	 */
1180
	public function onAfterDelete() {
1181
		parent::onAfterDelete();
1182
		// Create a basic new environment config from a template
1183
		if($this->config()->get('allow_web_editing') && $this->envFileExists()) {
1184
			unlink($this->getConfigFilename());
1185
		}
1186
1187
		$create = $this->CreateEnvironment();
0 ignored issues
show
Documentation Bug introduced by
The method CreateEnvironment does not exist on object<DNEnvironment>? 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...
1188
		if($create && $create->exists()) {
1189
			$create->delete();
1190
		}
1191
	}
1192
1193
	/**
1194
	 * @return string
1195
	 */
1196
	protected function getEnvironmentConfig() {
1197
		if(!$this->envFileExists()) {
1198
			return '';
1199
		}
1200
		return file_get_contents($this->getConfigFilename());
1201
	}
1202
1203
	/**
1204
	 * @return boolean
1205
	 */
1206
	protected function envFileExists() {
1207
		if(!$this->getConfigFilename()) {
1208
			return false;
1209
		}
1210
		return file_exists($this->getConfigFilename());
1211
	}
1212
1213
	/**
1214
	 * Returns the path to the ruby config file
1215
	 *
1216
	 * @return string
1217
	 */
1218
	public function getConfigFilename() {
1219
		if(!$this->Project()->exists()) {
1220
			return '';
1221
		}
1222
		if(!$this->Filename) {
1223
			return '';
1224
		}
1225
		return $this->DNData()->getEnvironmentDir() . '/' . $this->Project()->Name . '/' . $this->Filename;
1226
	}
1227
1228
	/**
1229
	 * Returns the path to the {@link Pipeline} configuration for this environment.
1230
	 * Uses the same path and filename as the capistrano config, but with .yml extension.
1231
	 *
1232
	 * @return string
1233
	 */
1234
	public function getPipelineFilename() {
1235
		$name = $this->getConfigFilename();
1236
		if(!$name) {
1237
			return null;
1238
		}
1239
		$path = pathinfo($name);
1240
		if($path) {
1241
			return $path['dirname'] . '/' . $path['filename'] . '.yml';
1242
		}
1243
	}
1244
1245
	/**
1246
	 * Does this environment have a pipeline config file
1247
	 *
1248
	 * @return boolean
1249
	 */
1250
	protected function pipelineFileExists() {
1251
		$filename = $this->getPipelineFilename();
1252
		if(empty($filename)) {
1253
			return false;
1254
		}
1255
		return file_exists($filename);
1256
	}
1257
1258
	/**
1259
	 * Helper function to convert a multi-dimensional array (associative or indexed) to an {@link ArrayList} or
1260
	 * {@link ArrayData} object structure, so that values can be used in templates.
1261
	 *
1262
	 * @param array $array The (single- or multi-dimensional) array to convert
1263
	 * @return object Either an {@link ArrayList} or {@link ArrayData} object, or the original item ($array) if $array
1264
	 * isn't an array.
1265
	 */
1266
	public static function array_to_viewabledata($array) {
1267
		// Don't transform non-arrays
1268
		if(!is_array($array)) {
1269
			return $array;
1270
		}
1271
1272
		// Figure out whether this is indexed or associative
1273
		$keys = array_keys($array);
1274
		$assoc = ($keys != array_keys($keys));
1275
		if($assoc) {
1276
			// Treat as viewable data
1277
			$data = new ArrayData(array());
1278
			foreach($array as $key => $value) {
1279
				$data->setField($key, self::array_to_viewabledata($value));
1280
			}
1281
			return $data;
1282
		} else {
1283
			// Treat this as basic non-associative list
1284
			$list = new ArrayList();
1285
			foreach($array as $value) {
1286
				$list->push(self::array_to_viewabledata($value));
1287
			}
1288
			return $list;
1289
		}
1290
	}
1291
1292
1293
1294
	/**
1295
	 * Helper function to retrieve filtered commits from an environment
1296
	 * this environment depends on
1297
	 *
1298
	 * @return DataList
1299
	 */
1300
	public function getDependentFilteredCommits() {
1301
		// check if this environment depends on another environemnt
1302
		$dependsOnEnv = $this->DependsOnEnvironment();
1303
		if(empty($dependsOnEnv)) {
1304
			return null;
1305
		}
1306
1307
		// Check if there is a filter
1308
		$config = $this->GenericPipelineConfig();
1309
		$filter = isset($config->PipelineConfig->FilteredCommits)
1310
			? $config->PipelineConfig->FilteredCommits
1311
			: null;
1312
		if(empty($filter)) {
1313
			return null;
1314
		}
1315
1316
		// Create and execute filter
1317
		if(!class_exists($filter)) {
1318
			throw new Exception(sprintf("Class %s does not exist", $filter));
1319
		}
1320
		$commitClass = $filter::create();
1321
		// setup the environment to check for commits
1322
		$commitClass->env = $dependsOnEnv;
1323
		return $commitClass->getCommits();
1324
	}
1325
1326
	/**
1327
	 * Enable the maintenance page
1328
	 *
1329
	 * @param DeploynautLogFile $log
1330
	 */
1331
	public function enableMaintenace($log) {
1332
		$this->Backend()
1333
			->enableMaintenance($this, $log, $this->Project());
1334
	}
1335
1336
	/**
1337
	 * Disable maintenance page
1338
	 *
1339
	 * @param DeploynautLogFile $log
1340
	 */
1341
	public function disableMaintenance($log) {
1342
		$this->Backend()
1343
			->disableMaintenance($this, $log, $this->Project());
1344
	}
1345
1346
	protected function validate() {
1347
		$result = parent::validate();
1348
		$backend = $this->Backend();
1349
1350
		if(strcasecmp('test', $this->Name) === 0 && get_class($backend) == 'CapistranoDeploymentBackend') {
1351
			$result->error('"test" is not a valid environment name when using Capistrano backend.');
1352
		}
1353
1354
		return $result;
1355
	}
1356
1357
	/**
1358
	 * Fetchs all deployments in progress. Limits to 1 hour to prevent deployments
1359
	 * if an old deployment is stuck.
1360
	 *
1361
	 * @return DataList
1362
	 */
1363
	public function runningDeployments() {
1364
		return DNDeployment::get()
1365
			->filter([
1366
				'EnvironmentID' => $this->ID,
1367
				'Status' => ['Queued', 'Started'],
1368
				'Created:GreaterThan' => strtotime('-1 hour')
1369
			]);
1370
	}
1371
1372
}
1373
1374