Completed
Pull Request — master (#500)
by Michael
04:43
created

DNEnvironment::runningDeployments()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 8
rs 9.4285
cc 1
eloc 6
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
	public function getBareURL() {
250
		$url = parse_url($this->URL);
251
		if(isset($url['host'])) {
252
			return strtolower($url['host']);
253
		}
254
	}
255
256
	/**
257
	 * @return boolean true if there is a pipeline for the current environment.
258
	 */
259
	public function HasPipelineSupport() {
260
		$config = $this->GenericPipelineConfig();
261
		return $config instanceof ArrayData && isset($config->Steps);
262
	}
263
264
	/**
265
	 * Returns a {@link Pipeline} object that is linked to this environment, but isn't saved into the database. This
266
	 * shouldn't be saved into the database unless you plan on starting an actual pipeline.
267
	 *
268
	 * @return Pipeline
269
	 */
270
	public function GenericPipeline() {
271
		$pipeline = Pipeline::create();
272
		$pipeline->EnvironmentID = $this->ID;
273
		return $pipeline;
274
	}
275
276
	/**
277
	 * Returns the parsed config, based on a {@link Pipeline} being created for this {@link DNEnvironment}.
278
	 *
279
	 * @return ArrayData
280
	 */
281
	public function GenericPipelineConfig() {
282
		$config = $this->loadPipelineConfig();
283
		if($config) {
284
			return self::array_to_viewabledata($config);
285
		}
286
	}
287
288
	/**
289
	 * Extract pipeline configuration data from the source yml file
290
	 *
291
	 * @return array
292
	 */
293
	public function loadPipelineConfig() {
294
		require_once 'thirdparty/spyc/spyc.php';
295
296
		$path = $this->getPipelineFilename();
297
		if(file_exists($path)) {
298
			return Spyc::YAMLLoad($path);
299
		}
300
	}
301
302
	/**
303
	 * Returns the {@link DNEnvironment} object relating to the pipeline config for this environment. The environment
304
	 * YAML file (e.g. project1-uat.yml; see docs/en/pipelines.md) contains two variable called `DependsOnProject` and
305
	 * `DependsOnEnvironment` - these are used together to find the {@link DNEnvironment} that this environment should
306
	 * rely on.
307
	 */
308
	public function DependsOnEnvironment() {
309
		if($this->HasPipelineSupport()) {
310
			$pipeline = $this->GenericPipeline();
311
			return $pipeline->getDependentEnvironment();
312
		}
313
314
		return null;
315
	}
316
317
	/**
318
	 * @return bool true if there is a currently running Pipeline, and false if there isn't
319
	 */
320
	public function HasCurrentPipeline() {
321
		return $this->CurrentPipeline() && $this->CurrentPipeline()->isInDB();
322
	}
323
324
	/**
325
	 * This can be used to determine if there is a currently running pipeline (there can only be one running per
326
	 * {@link DNEnvironment} at once), as well as getting the current pipeline to be shown in templates.
327
	 *
328
	 * @return DataObject|null The currently running pipeline, or null if there isn't any.
329
	 */
330
	public function CurrentPipeline() {
331
		return $this->Pipelines()->filter('Status', array('Running', 'Rollback'))->first();
332
	}
333
334
	/**
335
	 * @return bool true if the current user can cancel a running pipeline
336
	 */
337
	public function CanCancelPipeline() {
338
		// do we have a current pipeline
339
		if($this->HasCurrentPipeline()) {
340
			return $this->CurrentPipeline()->canAbort();
341
		}
342
		return false;
343
	}
344
345
	/**
346
	 * Environments are only viewable by people that can view the environment.
347
	 *
348
	 * @param Member|null $member
349
	 * @return boolean
350
	 */
351
	public function canView($member = null) {
352
		if(!$member) {
353
			$member = Member::currentUser();
354
		}
355
		if(!$member) {
356
			return false;
357
		}
358
		// Must be logged in to check permissions
359
360
		if(Permission::checkMember($member, 'ADMIN')) {
361
			return true;
362
		}
363
364
		// if no Viewers or ViewerGroups defined, fallback to DNProject::canView permissions
365
		if($this->Viewers()->exists() || $this->ViewerGroups()->exists()) {
366
			return $this->Viewers()->byID($member->ID)
367
				|| $member->inGroups($this->ViewerGroups());
368
		}
369
370
		return $this->Project()->canView($member);
371
	}
372
373
	/**
374
	 * Allow deploy only to some people.
375
	 *
376
	 * @param Member|null $member
377
	 * @return boolean
378
	 */
379 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...
380
		if(!$member) {
381
			$member = Member::currentUser();
382
		}
383
		if(!$member) {
384
			return false;
385
		}
386
		// Must be logged in to check permissions
387
388
		if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
389
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_DEPLOYMENT, $member)) return true;
390
		} else {
391
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_DEPLOYMENT, $member)) return true;
392
		}
393
394
		return $this->Deployers()->byID($member->ID)
395
			|| $member->inGroups($this->DeployerGroups());
396
	}
397
398
	/**
399
	 * Provide reason why the user cannot deploy.
400
	 *
401
	 * @return string
402
	 */
403
	public function getCannotDeployMessage() {
404
		return 'You cannot deploy to this environment.';
405
	}
406
407
	/**
408
	 * Allows only selected {@link Member} objects to restore {@link DNDataArchive} objects into this
409
	 * {@link DNEnvironment}.
410
	 *
411
	 * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
412
	 * @return boolean true if $member can restore, and false if they can't.
413
	 */
414 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...
415
		if(!$member) {
416
			$member = Member::currentUser();
417
		}
418
		if(!$member) {
419
			return false;
420
		}
421
		// Must be logged in to check permissions
422
423
		if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
424
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) return true;
425
		} else {
426
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) return true;
427
		}
428
429
		return $this->CanRestoreMembers()->byID($member->ID)
430
			|| $member->inGroups($this->CanRestoreGroups());
431
	}
432
433
	/**
434
	 * Allows only selected {@link Member} objects to backup this {@link DNEnvironment} to a {@link DNDataArchive}
435
	 * file.
436
	 *
437
	 * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
438
	 * @return boolean true if $member can backup, and false if they can't.
439
	 */
440 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...
441
		$project = $this->Project();
442
		if($project->HasDiskQuota() && $project->HasExceededDiskQuota()) {
443
			return false;
444
		}
445
446
		if(!$member) {
447
			$member = Member::currentUser();
448
		}
449
		// Must be logged in to check permissions
450
		if(!$member) {
451
			return false;
452
		}
453
454
		if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
455
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) return true;
456
		} else {
457
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) return true;
458
		}
459
460
		return $this->CanBackupMembers()->byID($member->ID)
461
			|| $member->inGroups($this->CanBackupGroups());
462
	}
463
464
	/**
465
	 * Allows only selected {@link Member} objects to upload {@link DNDataArchive} objects linked to this
466
	 * {@link DNEnvironment}.
467
	 *
468
	 * Note: This is not uploading them to the actual environment itself (e.g. uploading to the live site) - it is the
469
	 * process of uploading a *.sspak file into Deploynaut for later 'restoring' to an environment. See
470
	 * {@link self::canRestore()}.
471
	 *
472
	 * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
473
	 * @return boolean true if $member can upload archives linked to this environment, false if they can't.
474
	 */
475 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...
476
		$project = $this->Project();
477
		if($project->HasDiskQuota() && $project->HasExceededDiskQuota()) {
478
			return false;
479
		}
480
481
		if(!$member) {
482
			$member = Member::currentUser();
483
		}
484
		if(!$member) {
485
			return false;
486
		}
487
		// Must be logged in to check permissions
488
489
		if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
490
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) return true;
491
		} else {
492
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) return true;
493
		}
494
495
		return $this->ArchiveUploaders()->byID($member->ID)
496
			|| $member->inGroups($this->ArchiveUploaderGroups());
497
	}
498
499
	/**
500
	 * Allows only selected {@link Member} objects to download {@link DNDataArchive} objects from this
501
	 * {@link DNEnvironment}.
502
	 *
503
	 * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
504
	 * @return boolean true if $member can download archives from this environment, false if they can't.
505
	 */
506 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...
507
		if(!$member) {
508
			$member = Member::currentUser();
509
		}
510
		if(!$member) {
511
			return false;
512
		}
513
		// Must be logged in to check permissions
514
515
		if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
516
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) return true;
517
		} else {
518
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) return true;
519
		}
520
521
		return $this->ArchiveDownloaders()->byID($member->ID)
522
			|| $member->inGroups($this->ArchiveDownloaderGroups());
523
	}
524
525
	/**
526
	 * Determine if the specified user can abort any pipelines
527
	 *
528
	 * @param Member|null $member
529
	 * @return boolean
530
	 */
531 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...
532
		if(!$member) {
533
			$member = Member::currentUser();
534
		}
535
		if(!$member) {
536
			return false;
537
		}
538
539
		if(Permission::checkMember($member, 'ADMIN')) {
540
			return true;
541
		}
542
543
		return $this->PipelineCancellers()->byID($member->ID)
544
			|| $member->inGroups($this->PipelineCancellerGroups());
545
	}
546
547
	/**
548
	 * Determine if the specified user can approve any pipelines
549
	 *
550
	 * @param Member|null $member
551
	 * @return boolean
552
	 */
553 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...
554
		if(!$member) {
555
			$member = Member::currentUser();
556
		}
557
		if(!$member) {
558
			return false;
559
		}
560
561
		if(Permission::checkMember($member, 'ADMIN')) {
562
			return true;
563
		}
564
		return $this->PipelineApprovers()->byID($member->ID)
565
			|| $member->inGroups($this->PipelineApproverGroups());
566
	}
567
568
	/**
569
	 * Allows only selected {@link Member} objects to delete {@link DNDataArchive} objects from this
570
	 * {@link DNEnvironment}.
571
	 *
572
	 * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
573
	 * @return boolean true if $member can delete archives from this environment, false if they can't.
574
	 */
575 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...
576
		if(!$member) {
577
			$member = Member::currentUser();
578
		}
579
		if(!$member) {
580
			return false;
581
		}
582
		// Must be logged in to check permissions
583
584
		if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
585
			if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) return true;
586
		} else {
587
			if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) return true;
588
		}
589
590
		return $this->ArchiveDeleters()->byID($member->ID)
591
			|| $member->inGroups($this->ArchiveDeleterGroups());
592
	}
593
	/**
594
	 * Get a string of groups/people that are allowed to deploy to this environment.
595
	 * Used in DNRoot_project.ss to list {@link Member}s who have permission to perform this action.
596
	 *
597
	 * @return string
598
	 */
599
	public function getDeployersList() {
600
		return implode(
601
			", ",
602
			array_merge(
603
				$this->DeployerGroups()->column("Title"),
604
				$this->Deployers()->column("FirstName")
605
			)
606
		);
607
	}
608
609
	/**
610
	 * Get a string of groups/people that are allowed to restore {@link DNDataArchive} objects into this environment.
611
	 *
612
	 * @return string
613
	 */
614
	public function getCanRestoreMembersList() {
615
		return implode(
616
			", ",
617
			array_merge(
618
				$this->CanRestoreGroups()->column("Title"),
619
				$this->CanRestoreMembers()->column("FirstName")
620
			)
621
		);
622
	}
623
624
	/**
625
	 * Get a string of groups/people that are allowed to backup {@link DNDataArchive} objects from this environment.
626
	 *
627
	 * @return string
628
	 */
629
	public function getCanBackupMembersList() {
630
		return implode(
631
			", ",
632
			array_merge(
633
				$this->CanBackupGroups()->column("Title"),
634
				$this->CanBackupMembers()->column("FirstName")
635
			)
636
		);
637
	}
638
639
	/**
640
	 * Get a string of groups/people that are allowed to upload {@link DNDataArchive}
641
	 *  objects linked to this environment.
642
	 *
643
	 * @return string
644
	 */
645
	public function getArchiveUploadersList() {
646
		return implode(
647
			", ",
648
			array_merge(
649
				$this->ArchiveUploaderGroups()->column("Title"),
650
				$this->ArchiveUploaders()->column("FirstName")
651
			)
652
		);
653
	}
654
655
	/**
656
	 * Get a string of groups/people that are allowed to download {@link DNDataArchive} objects from this environment.
657
	 *
658
	 * @return string
659
	 */
660
	public function getArchiveDownloadersList() {
661
		return implode(
662
			", ",
663
			array_merge(
664
				$this->ArchiveDownloaderGroups()->column("Title"),
665
				$this->ArchiveDownloaders()->column("FirstName")
666
			)
667
		);
668
	}
669
670
	/**
671
	 * Get a string of groups/people that are allowed to delete {@link DNDataArchive} objects from this environment.
672
	 *
673
	 * @return string
674
	 */
675
	public function getArchiveDeletersList() {
676
		return implode(
677
			", ",
678
			array_merge(
679
				$this->ArchiveDeleterGroups()->column("Title"),
680
				$this->ArchiveDeleters()->column("FirstName")
681
			)
682
		);
683
	}
684
685
	/**
686
	 * Get a string of groups/people that are allowed to approve pipelines
687
	 *
688
	 * @return string
689
	 */
690
	public function getPipelineApproversList() {
691
		return implode(
692
			", ",
693
			array_merge(
694
				$this->PipelineApproverGroups()->column("Title"),
695
				$this->PipelineApprovers()->column("FirstName")
696
			)
697
		);
698
	}
699
700
	/**
701
	 * Get a string of groups/people that are allowed to cancel pipelines
702
	 *
703
	 * @return string
704
	 */
705
	public function getPipelineCancellersList() {
706
		return implode(
707
			", ",
708
			array_merge(
709
				$this->PipelineCancellerGroups()->column("Title"),
710
				$this->PipelineCancellers()->column("FirstName")
711
			)
712
		);
713
	}
714
715
	/**
716
	 * @return DNData
717
	 */
718
	public function DNData() {
719
		return DNData::inst();
720
	}
721
722
	/**
723
	 * Get the current deployed build for this environment
724
	 *
725
	 * Dear people of the future: If you are looking to optimize this, simply create a CurrentBuildSHA(), which can be
726
	 * a lot faster. I presume you came here because of the Project display template, which only needs a SHA.
727
	 *
728
	 * @return false|DNDeployment
729
	 */
730
	public function CurrentBuild() {
731
		// The DeployHistory function is far too slow to use for this
732
733
		/** @var DNDeployment $deploy */
734
		$deploy = DNDeployment::get()->filter(array(
735
			'EnvironmentID' => $this->ID,
736
			'Status' => 'Finished'
737
		))->sort('LastEdited DESC')->first();
738
739
		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...
740
			return false;
741
		}
742
743
		$repo = $this->Project()->getRepository();
744
		if(!$repo) {
745
			return $deploy;
746
		}
747
748
		try {
749
			$commit = $repo->getCommit($deploy->SHA);
750
			if($commit) {
751
				$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...
752
				$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...
753
				$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...
754
				$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...
755
				$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...
756
			}
757
			// We can't find this SHA, so we ignore adding a commit message to the deployment
758
		} catch(Exception $ex) { }
759
760
		return $deploy;
761
	}
762
763
	/**
764
	 * A history of all builds deployed to this environment
765
	 *
766
	 * @return ArrayList
767
	 */
768
	public function DeployHistory() {
769
		return $this->Deployments()
770
			->where('SHA IS NOT NULL')
771
			->sort('LastEdited DESC');
772
	}
773
774
	/**
775
	 * @param string $sha
776
	 * @return array
777
	 */
778
	protected function getCommitData($sha) {
779
		try {
780
			$repo = $this->Project()->getRepository();
781
			if($repo !== false) {
782
				$commit = new \Gitonomy\Git\Commit($repo, $sha);
783
				return [
784
					'AuthorName' => (string)Convert::raw2xml($commit->getAuthorName()),
785
					'AuthorEmail' => (string)Convert::raw2xml($commit->getAuthorEmail()),
786
					'Message' => (string)Convert::raw2xml($commit->getMessage()),
787
					'ShortHash' => Convert::raw2xml($commit->getFixedShortHash(8)),
788
					'Hash' => Convert::raw2xml($commit->getHash())
789
				];
790
			}
791
		} catch(\Gitonomy\Git\Exception\ReferenceNotFoundException $exc) {
792
			SS_Log::log($exc, SS_Log::WARN);
793
		}
794
		return array(
795
			'AuthorName' => '(unknown)',
796
			'AuthorEmail' => '(unknown)',
797
			'Message' => '(unknown)',
798
			'ShortHash' => $sha,
799
			'Hash' => '(unknown)',
800
		);
801
	}
802
803
	/**
804
	 * @return string
805
	 */
806
	public function Link() {
807
		return $this->Project()->Link() . "/environment/" . $this->Name;
808
	}
809
810
	/**
811
	 * Is this environment currently at the root level of the controller that handles it?
812
	 * @return bool
813
	 */
814
	public function isCurrent() {
815
		return $this->isSection() && Controller::curr()->getAction() == 'environment';
816
	}
817
818
	/**
819
	 * Is this environment currently in a controller that is handling it or performing a sub-task?
820
	 * @return bool
821
	 */
822
	public function isSection() {
823
		$controller = Controller::curr();
824
		$environment = $controller->getField('CurrentEnvironment');
825
		return $environment && $environment->ID == $this->ID;
826
	}
827
828
829
	/**
830
	 * Build a set of multi-select fields for assigning permissions to a pair of group and member many_many relations
831
	 *
832
	 * @param string $groupField Group field name
833
	 * @param string $memberField Member field name
834
	 * @param array $groups List of groups
835
	 * @param array $members List of members
836
	 * @return FieldGroup
837
	 */
838
	protected function buildPermissionField($groupField, $memberField, $groups, $members) {
839
		return FieldGroup::create(
840
			ListboxField::create($groupField, false, $groups)
841
				->setMultiple(true)
842
				->setAttribute('data-placeholder', 'Groups')
843
				->setAttribute('placeholder', 'Groups')
844
				->setAttribute('style', 'width: 400px;'),
845
846
			ListboxField::create($memberField, false, $members)
847
				->setMultiple(true)
848
				->setAttribute('data-placeholder', 'Members')
849
				->setAttribute('placeholder', 'Members')
850
				->setAttribute('style', 'width: 400px;')
851
		);
852
	}
853
854
	/**
855
	 * @return FieldList
856
	 */
857
	public function getCMSFields() {
858
		$fields = new FieldList(new TabSet('Root'));
859
860
		$project = $this->Project();
861
		if($project && $project->exists()) {
862
			$viewerGroups = $project->Viewers();
863
			$groups = $viewerGroups->sort('Title')->map()->toArray();
864
			$members = array();
865
			foreach($viewerGroups as $group) {
866
				foreach($group->Members()->map() as $k => $v) {
867
					$members[$k] = $v;
868
				}
869
			}
870
			asort($members);
871
		} else {
872
			$groups = array();
873
			$members = array();
874
		}
875
876
		// Main tab
877
		$fields->addFieldsToTab('Root.Main', array(
878
			// The Main.ProjectID
879
			TextField::create('ProjectName', 'Project')
880
				->setValue(($project = $this->Project()) ? $project->Name : null)
881
				->performReadonlyTransformation(),
882
883
			// The Main.Name
884
			TextField::create('Name', 'Environment name')
885
				->setDescription('A descriptive name for this environment, e.g. staging, uat, production'),
886
887
888
			$this->obj('Usage')->scaffoldFormField('Environment usage'),
889
890
			// The Main.URL field
891
			TextField::create('URL', 'Server URL')
892
				->setDescription('This url will be used to provide the front-end with a link to this environment'),
893
894
			// The Main.Filename
895
			TextField::create('Filename')
896
				->setDescription('The capistrano environment file name')
897
				->performReadonlyTransformation(),
898
		));
899
900
		// Backend identifier - pick from a named list of configurations specified in YML config
901
		$backends = $this->config()->get('allowed_backends', Config::FIRST_SET);
902
		// If there's only 1 backend, then user selection isn't needed
903
		if(sizeof($backends) > 1) {
904
			$fields->addFieldToTab('Root.Main', DropdownField::create('BackendIdentifier', 'Deployment backend')
905
				->setSource($backends)
906
				->setDescription('What kind of deployment system should be used to deploy to this environment'));
907
		}
908
909
		$fields->addFieldsToTab('Root.UserPermissions', array(
910
			// The viewers of the environment
911
			$this
912
				->buildPermissionField('ViewerGroups', 'Viewers', $groups, $members)
913
				->setTitle('Who can view this environment?')
914
				->setDescription('Groups or Users who can view this environment'),
915
916
			// The Main.Deployers
917
			$this
918
				->buildPermissionField('DeployerGroups', 'Deployers', $groups, $members)
919
				->setTitle('Who can deploy?')
920
				->setDescription('Groups or Users who can deploy to this environment'),
921
922
			// A box to select all snapshot options.
923
			$this
924
				->buildPermissionField('TickAllSnapshotGroups', 'TickAllSnapshot', $groups, $members)
925
				->setTitle("<em>All snapshot permissions</em>")
926
				->addExtraClass('tickall')
927
				->setDescription('UI shortcut to select all snapshot-related options - not written to the database.'),
928
929
			// The Main.CanRestoreMembers
930
			$this
931
				->buildPermissionField('CanRestoreGroups', 'CanRestoreMembers', $groups, $members)
932
				->setTitle('Who can restore?')
933
				->setDescription('Groups or Users who can restore archives from Deploynaut into this environment'),
934
935
			// The Main.CanBackupMembers
936
			$this
937
				->buildPermissionField('CanBackupGroups', 'CanBackupMembers', $groups, $members)
938
				->setTitle('Who can backup?')
939
				->setDescription('Groups or Users who can backup archives from this environment into Deploynaut'),
940
941
			// The Main.ArchiveDeleters
942
			$this
943
				->buildPermissionField('ArchiveDeleterGroups', 'ArchiveDeleters', $groups, $members)
944
				->setTitle('Who can delete?')
945
				->setDescription("Groups or Users who can delete archives from this environment's staging area."),
946
947
			// The Main.ArchiveUploaders
948
			$this
949
				->buildPermissionField('ArchiveUploaderGroups', 'ArchiveUploaders', $groups, $members)
950
				->setTitle('Who can upload?')
951
				->setDescription(
952
					'Users who can upload archives linked to this environment into Deploynaut.<br />' .
953
					'Linking them to an environment allows limiting download permissions (see below).'
954
				),
955
956
			// The Main.ArchiveDownloaders
957
			$this
958
				->buildPermissionField('ArchiveDownloaderGroups', 'ArchiveDownloaders', $groups, $members)
959
				->setTitle('Who can download?')
960
				->setDescription(<<<PHP
961
Users who can download archives from this environment to their computer.<br />
962
Since this implies access to the snapshot, it is also a prerequisite for restores
963
to other environments, alongside the "Who can restore" permission.<br>
964
Should include all users with upload permissions, otherwise they can't download
965
their own uploads.
966
PHP
967
				),
968
969
			// The Main.PipelineApprovers
970
			$this
971
				->buildPermissionField('PipelineApproverGroups', 'PipelineApprovers', $groups, $members)
972
				->setTitle('Who can approve pipelines?')
973
				->setDescription('Users who can approve waiting deployment pipelines.'),
974
975
			// The Main.PipelineCancellers
976
			$this
977
				->buildPermissionField('PipelineCancellerGroups', 'PipelineCancellers', $groups, $members)
978
				->setTitle('Who can cancel pipelines?')
979
				->setDescription('Users who can cancel in-progess deployment pipelines.')
980
		));
981
982
		// The Main.DeployConfig
983
		if($this->Project()->exists()) {
984
			$this->setDeployConfigurationFields($fields);
985
		}
986
987
		// The DataArchives
988
		$dataArchiveConfig = GridFieldConfig_RecordViewer::create();
989
		$dataArchiveConfig->removeComponentsByType('GridFieldAddNewButton');
990
		if(class_exists('GridFieldBulkManager')) {
991
			$dataArchiveConfig->addComponent(new GridFieldBulkManager());
992
		}
993
		$dataArchive = GridField::create('DataArchives', 'Data Archives', $this->DataArchives(), $dataArchiveConfig);
994
		$fields->addFieldToTab('Root.DataArchive', $dataArchive);
995
996
		// Pipeline templates
997
		$this->setPipelineConfigurationFields($fields);
998
999
		// Pipelines
1000
		if($this->Pipelines()->Count()) {
1001
			$pipelinesConfig = GridFieldConfig_RecordEditor::create();
1002
			$pipelinesConfig->removeComponentsByType('GridFieldAddNewButton');
1003
			if(class_exists('GridFieldBulkManager')) {
1004
				$pipelinesConfig->addComponent(new GridFieldBulkManager());
1005
			}
1006
			$pipelines = GridField::create('Pipelines', 'Pipelines', $this->Pipelines(), $pipelinesConfig);
1007
			$fields->addFieldToTab('Root.Pipelines', $pipelines);
1008
		}
1009
1010
		// Deployments
1011
		$deploymentsConfig = GridFieldConfig_RecordEditor::create();
1012
		$deploymentsConfig->removeComponentsByType('GridFieldAddNewButton');
1013
		if(class_exists('GridFieldBulkManager')) {
1014
			$deploymentsConfig->addComponent(new GridFieldBulkManager());
1015
		}
1016
		$deployments = GridField::create('Deployments', 'Deployments', $this->Deployments(), $deploymentsConfig);
1017
		$fields->addFieldToTab('Root.Deployments', $deployments);
1018
1019
		Requirements::javascript('deploynaut/javascript/environment.js');
1020
1021
		// Add actions
1022
		$action = new FormAction('check', 'Check Connection');
1023
		$action->setUseButtonTag(true);
1024
		$dataURL = Director::absoluteBaseURL() . 'naut/api/' . $this->Project()->Name . '/' . $this->Name . '/ping';
1025
		$action->setAttribute('data-url', $dataURL);
1026
		$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...
1027
1028
		// Allow extensions
1029
		$this->extend('updateCMSFields', $fields);
1030
		return $fields;
1031
	}
1032
1033
	/**
1034
	 * @param FieldList $fields
1035
	 */
1036
	protected function setDeployConfigurationFields(&$fields) {
1037
		if(!$this->config()->get('allow_web_editing')) {
1038
			return;
1039
		}
1040
1041
		if($this->envFileExists()) {
1042
			$deployConfig = new TextareaField('DeployConfig', 'Deploy config', $this->getEnvironmentConfig());
1043
			$deployConfig->setRows(40);
1044
			$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...
1045
			return;
1046
		}
1047
1048
		$warning = 'Warning: This environment doesn\'t have deployment configuration.';
1049
		$noDeployConfig = new LabelField('noDeployConfig', $warning);
1050
		$noDeployConfig->addExtraClass('message warning');
1051
		$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...
1052
		$createConfigField = new CheckboxField('CreateEnvConfig', 'Create Config');
1053
		$createConfigField->setDescription('Would you like to create the capistrano deploy configuration?');
1054
		$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...
1055
	}
1056
1057
	/**
1058
	 * @param FieldList $fields
1059
	 */
1060
	protected function setPipelineConfigurationFields($fields) {
1061
		if(!$this->config()->get('allow_web_editing')) {
1062
			return;
1063
		}
1064
		$config = $this->pipelineFileExists()
1065
			? file_get_contents($this->getPipelineFilename())
1066
			: '';
1067
		$deployConfig = new TextareaField('PipelineConfig', 'Pipeline config', $config);
1068
		$deployConfig->setRows(40);
1069
		if(!$this->pipelineFileExists()) {
1070
			$deployConfig->setDescription(
1071
				"No pipeline is configured for this environment. Saving content here will generate a new template."
1072
			);
1073
		}
1074
		$fields->addFieldsToTab('Root.PipelineSettings', array(
1075
			FieldGroup::create(
1076
				CheckboxField::create('DryRunEnabled', 'Enable dry-run?')
1077
			)
1078
				->setTitle('Pipeline Options')
1079
				->setDescription(
1080
					"Allows admins to run simulated pipelines without triggering deployments or notifications."
1081
				),
1082
			$deployConfig
1083
		));
1084
	}
1085
1086
	/**
1087
	 */
1088
	public function onBeforeWrite() {
1089
		parent::onBeforeWrite();
1090
		if($this->Name && $this->Name . '.rb' != $this->Filename) {
1091
			$this->Filename = $this->Name . '.rb';
1092
		}
1093
		$this->checkEnvironmentPath();
1094
		$this->writeConfigFile();
1095
		$this->writePipelineFile();
1096
	}
1097
1098
	public function onAfterWrite() {
1099
		parent::onAfterWrite();
1100
1101
		if($this->Usage == 'Production' || $this->Usage == 'UAT') {
1102
			$conflicting = DNEnvironment::get()
1103
				->filter('ProjectID', $this->ProjectID)
1104
				->filter('Usage', $this->Usage)
1105
				->exclude('ID', $this->ID);
1106
1107
			foreach($conflicting as $otherEnvironment) {
1108
				$otherEnvironment->Usage = 'Unspecified';
1109
				$otherEnvironment->write();
1110
			}
1111
		}
1112
	}
1113
1114
1115
	/**
1116
	 * Ensure that environment paths are setup on the local filesystem
1117
	 */
1118
	protected function checkEnvironmentPath() {
1119
		// Create folder if it doesn't exist
1120
		$configDir = dirname($this->getConfigFilename());
1121
		if(!file_exists($configDir) && $configDir) {
1122
			mkdir($configDir, 0777, true);
1123
		}
1124
	}
1125
1126
	/**
1127
	 * Write the deployment config file to filesystem
1128
	 */
1129
	protected function writeConfigFile() {
1130
		if(!$this->config()->get('allow_web_editing')) {
1131
			return;
1132
		}
1133
1134
		// Create a basic new environment config from a template
1135
		if(!$this->envFileExists()
1136
			&& $this->Filename
1137
			&& $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...
1138
		) {
1139
			$templateFile = $this->config()->template_file ?: BASE_PATH . '/deploynaut/environment.template';
1140
			file_put_contents($this->getConfigFilename(), file_get_contents($templateFile));
1141
		} 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...
1142
			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...
1143
		}
1144
	}
1145
1146
	/**
1147
	 * Write the pipeline config file to filesystem
1148
	 */
1149
	protected function writePipelineFile() {
1150
		if(!$this->config()->get('allow_web_editing')) {
1151
			return;
1152
		}
1153
		$path = $this->getPipelineFilename();
1154
		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...
1155
			// Update file
1156
			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...
1157
		} elseif($this->isChanged('PipelineConfig') && file_exists($path)) {
1158
			// Remove file if deleted
1159
			unlink($path);
1160
		}
1161
	}
1162
1163
	/**
1164
	 * Delete any related config files
1165
	 */
1166
	public function onAfterDelete() {
1167
		parent::onAfterDelete();
1168
		// Create a basic new environment config from a template
1169
		if($this->config()->get('allow_web_editing') && $this->envFileExists()) {
1170
			unlink($this->getConfigFilename());
1171
		}
1172
1173
		$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...
1174
		if($create && $create->exists()) {
1175
			$create->delete();
1176
		}
1177
	}
1178
1179
	/**
1180
	 * @return string
1181
	 */
1182
	protected function getEnvironmentConfig() {
1183
		if(!$this->envFileExists()) {
1184
			return '';
1185
		}
1186
		return file_get_contents($this->getConfigFilename());
1187
	}
1188
1189
	/**
1190
	 * @return boolean
1191
	 */
1192
	protected function envFileExists() {
1193
		if(!$this->getConfigFilename()) {
1194
			return false;
1195
		}
1196
		return file_exists($this->getConfigFilename());
1197
	}
1198
1199
	/**
1200
	 * Returns the path to the ruby config file
1201
	 *
1202
	 * @return string
1203
	 */
1204
	public function getConfigFilename() {
1205
		if(!$this->Project()->exists()) {
1206
			return '';
1207
		}
1208
		if(!$this->Filename) {
1209
			return '';
1210
		}
1211
		return $this->DNData()->getEnvironmentDir() . '/' . $this->Project()->Name . '/' . $this->Filename;
1212
	}
1213
1214
	/**
1215
	 * Returns the path to the {@link Pipeline} configuration for this environment.
1216
	 * Uses the same path and filename as the capistrano config, but with .yml extension.
1217
	 *
1218
	 * @return string
1219
	 */
1220
	public function getPipelineFilename() {
1221
		$name = $this->getConfigFilename();
1222
		if(!$name) {
1223
			return null;
1224
		}
1225
		$path = pathinfo($name);
1226
		if($path) {
1227
			return $path['dirname'] . '/' . $path['filename'] . '.yml';
1228
		}
1229
	}
1230
1231
	/**
1232
	 * Does this environment have a pipeline config file
1233
	 *
1234
	 * @return boolean
1235
	 */
1236
	protected function pipelineFileExists() {
1237
		$filename = $this->getPipelineFilename();
1238
		if(empty($filename)) {
1239
			return false;
1240
		}
1241
		return file_exists($filename);
1242
	}
1243
1244
	/**
1245
	 * Helper function to convert a multi-dimensional array (associative or indexed) to an {@link ArrayList} or
1246
	 * {@link ArrayData} object structure, so that values can be used in templates.
1247
	 *
1248
	 * @param array $array The (single- or multi-dimensional) array to convert
1249
	 * @return object Either an {@link ArrayList} or {@link ArrayData} object, or the original item ($array) if $array
1250
	 * isn't an array.
1251
	 */
1252
	public static function array_to_viewabledata($array) {
1253
		// Don't transform non-arrays
1254
		if(!is_array($array)) {
1255
			return $array;
1256
		}
1257
1258
		// Figure out whether this is indexed or associative
1259
		$keys = array_keys($array);
1260
		$assoc = ($keys != array_keys($keys));
1261
		if($assoc) {
1262
			// Treat as viewable data
1263
			$data = new ArrayData(array());
1264
			foreach($array as $key => $value) {
1265
				$data->setField($key, self::array_to_viewabledata($value));
1266
			}
1267
			return $data;
1268
		} else {
1269
			// Treat this as basic non-associative list
1270
			$list = new ArrayList();
1271
			foreach($array as $value) {
1272
				$list->push(self::array_to_viewabledata($value));
1273
			}
1274
			return $list;
1275
		}
1276
	}
1277
1278
1279
1280
	/**
1281
	 * Helper function to retrieve filtered commits from an environment
1282
	 * this environment depends on
1283
	 *
1284
	 * @return DataList
1285
	 */
1286
	public function getDependentFilteredCommits() {
1287
		// check if this environment depends on another environemnt
1288
		$dependsOnEnv = $this->DependsOnEnvironment();
1289
		if(empty($dependsOnEnv)) {
1290
			return null;
1291
		}
1292
1293
		// Check if there is a filter
1294
		$config = $this->GenericPipelineConfig();
1295
		$filter = isset($config->PipelineConfig->FilteredCommits)
1296
			? $config->PipelineConfig->FilteredCommits
1297
			: null;
1298
		if(empty($filter)) {
1299
			return null;
1300
		}
1301
1302
		// Create and execute filter
1303
		if(!class_exists($filter)) {
1304
			throw new Exception(sprintf("Class %s does not exist", $filter));
1305
		}
1306
		$commitClass = $filter::create();
1307
		// setup the environment to check for commits
1308
		$commitClass->env = $dependsOnEnv;
1309
		return $commitClass->getCommits();
1310
	}
1311
1312
	/**
1313
	 * Enable the maintenance page
1314
	 *
1315
	 * @param DeploynautLogFile $log
1316
	 */
1317
	public function enableMaintenace($log) {
1318
		$this->Backend()
1319
			->enableMaintenance($this, $log, $this->Project());
1320
	}
1321
1322
	/**
1323
	 * Disable maintenance page
1324
	 *
1325
	 * @param DeploynautLogFile $log
1326
	 */
1327
	public function disableMaintenance($log) {
1328
		$this->Backend()
1329
			->disableMaintenance($this, $log, $this->Project());
1330
	}
1331
1332
	protected function validate() {
1333
		$result = parent::validate();
1334
		$backend = $this->Backend();
1335
1336
		if(strcasecmp('test', $this->Name) === 0 && get_class($backend) == 'CapistranoDeploymentBackend') {
1337
			$result->error('"test" is not a valid environment name when using Capistrano backend.');
1338
		}
1339
1340
		return $result;
1341
	}
1342
1343
	/**
1344
	 * Fetchs all deployments in progress. Limits to 1 hour to prevent deployments
1345
	 * if an old deployment is stuck.
1346
	 *
1347
	 * @return DataList
1348
	 */
1349
	public function runningDeployments() {
1350
		return DNDeployment::get()
1351
			->filter([
1352
				'EnvironmentID' => $this->ID,
1353
				'Status' => ['Queued', 'Started'],
1354
				'Created:GreaterThan' => strtotime('-1 hour')
1355
			]);
1356
	}
1357
1358
}
1359
1360