Completed
Pull Request — master (#489)
by Helpful
03:18
created

DNEnvironment::getPipelineFilename()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 11
rs 9.4286
cc 3
eloc 7
nc 3
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
    /**
46
     * If this is set to a full pathfile, it will be used as template
47
     * file when creating a new capistrano environment config file.
48
     *
49
     * If not set, the default 'environment.template' from the module
50
     * root is used
51
     *
52
     * @config
53
     * @var string
54
     */
55
    private static $template_file = '';
56
57
    /**
58
     * Set this to true to allow editing of the environment files via the web admin
59
     *
60
     * @var bool
61
     */
62
    private static $allow_web_editing = false;
63
64
    /**
65
     * @var array
66
     */
67
    private static $casting = array(
68
        'DeployHistory' => 'Text'
69
    );
70
71
    /**
72
     * Allowed backends. A map of Injector identifier to human-readable label.
73
     *
74
     * @config
75
     * @var array
76
     */
77
    private static $allowed_backends = array();
78
79
    /**
80
     * @var array
81
     */
82
    public static $db = array(
83
        "Filename" => "Varchar(255)",
84
        "Name" => "Varchar(255)",
85
        "URL" => "Varchar(255)",
86
        "BackendIdentifier" => "Varchar(255)", // Injector identifier of the DeploymentBackend
87
        "DryRunEnabled" => "Boolean", // True if the dry run button should be enabled on the frontend
88
        "Usage" => "Enum('Production, UAT, Test, Unspecified', 'Unspecified')"
89
    );
90
91
    /**
92
     * @var array
93
     */
94
    public static $has_one = array(
95
        "Project" => "DNProject",
96
        "CreateEnvironment" => "DNCreateEnvironment"
97
    );
98
99
    /**
100
     * @var array
101
     */
102
    public static $has_many = array(
103
        "Deployments" => "DNDeployment",
104
        "DataArchives" => "DNDataArchive",
105
        "Pipelines" => "Pipeline" // Only one Pipeline can be 'Running' at any one time. @see self::CurrentPipeline().
106
    );
107
108
    /**
109
     * @var array
110
     */
111
    public static $many_many = array(
112
        "Viewers"            => "Member", // Who can view this environment
113
        "ViewerGroups"       => "Group",
114
        "Deployers"          => "Member", // Who can deploy to this environment
115
        "DeployerGroups" => "Group",
116
        "CanRestoreMembers"  => "Member", // Who can restore archive files to this environment
117
        "CanRestoreGroups"  => "Group",
118
        "CanBackupMembers"   => "Member", // Who can backup archive files from this environment
119
        "CanBackupGroups"   => "Group",
120
        "ArchiveUploaders"   => "Member", // Who can upload archive files linked to this environment
121
        "ArchiveUploaderGroups" => "Group",
122
        "ArchiveDownloaders" => "Member", // Who can download archive files from this environment
123
        "ArchiveDownloaderGroups" => "Group",
124
        "ArchiveDeleters"    => "Member", // Who can delete archive files from this environment,
125
        "ArchiveDeleterGroups" => "Group",
126
        "PipelineApprovers"  => "Member", // Who can approve / reject pipelines from this environment
127
        "PipelineApproverGroups" => "Group",
128
        "PipelineCancellers"   => "Member", // Who can abort pipelines
129
        "PipelineCancellerGroups" => "Group"
130
    );
131
132
    /**
133
     * @var array
134
     */
135
    public static $summary_fields = array(
136
        "Name" => "Environment Name",
137
        "Usage" => "Usage",
138
        "URL" => "URL",
139
        "DeployersList" => "Can Deploy List",
140
        "CanRestoreMembersList" => "Can Restore List",
141
        "CanBackupMembersList" => "Can Backup List",
142
        "ArchiveUploadersList" => "Can Upload List",
143
        "ArchiveDownloadersList" => "Can Download List",
144
        "ArchiveDeletersList"  => "Can Delete List",
145
        "PipelineApproversList" => "Can Approve List",
146
        "PipelineCancellersList" => "Can Cancel List"
147
    );
148
149
    private static $singular_name = 'Capistrano Environment';
150
151
    private static $plural_name = 'Capistrano Environments';
152
153
    /**
154
     * @var array
155
     */
156
    public static $searchable_fields = array(
157
        "Name",
158
    );
159
160
    /**
161
     * @var string
162
     */
163
    private static $default_sort = 'Name';
164
165
    /**
166
     * Used by the sync task
167
     *
168
     * @param string $path
169
     * @return \DNEnvironment
170
     */
171
    public static function create_from_path($path)
172
    {
173
        $e = DNEnvironment::create();
174
        $e->Filename = $path;
175
        $e->Name = basename($e->Filename, '.rb');
176
177
        // add each administrator member as a deployer of the new environment
178
        $adminGroup = Group::get()->filter('Code', 'administrators')->first();
179
        $e->DeployerGroups()->add($adminGroup);
180
        return $e;
181
    }
182
183
    /**
184
     * Get the deployment backend used for this environment.
185
     *
186
     * Enforces compliance with the allowed_backends setting; if the DNEnvironment.BackendIdentifier value is
187
     * illegal then that value is ignored.
188
     *
189
     * @return DeploymentBackend
190
     */
191
    public function Backend()
192
    {
193
        $backends = array_keys($this->config()->get('allowed_backends', Config::FIRST_SET));
194
        switch (sizeof($backends)) {
195
        // Nothing allowed, use the default value "DeploymentBackend"
196
            case 0:
197
                $backend = "DeploymentBackend";
198
                break;
199
200
            // Only 1 thing allowed, use that
201
            case 1:
202
                $backend = $backends[0];
203
                break;
204
205
            // Multiple choices, use our choice if it's legal, otherwise default to the first item on the list
206
            default:
207
                $backend = $this->BackendIdentifier;
208
                if (!in_array($backend, $backends)) {
209
                    $backend = $backends[0];
210
                }
211
        }
212
213
        return Injector::inst()->get($backend);
214
    }
215
216
    public function Menu()
217
    {
218
        $list = new ArrayList();
219
220
        $controller = Controller::curr();
221
        $actionType = $controller->getField('CurrentActionType');
222
223
        $list->push(new ArrayData(array(
224
            'Link' => sprintf('naut/project/%s/environment/%s', $this->Project()->Name, $this->Name),
225
            'Title' => 'Deployments',
226
            'IsCurrent' => $this->isCurrent(),
227
            'IsSection' => $this->isSection() && $actionType == DNRoot::ACTION_DEPLOY
228
        )));
229
230
        $this->extend('updateMenu', $list);
231
232
        return $list;
233
    }
234
235
    /**
236
     * Return the current object from $this->Menu()
237
     * Good for making titles and things
238
     */
239
    public function CurrentMenu()
240
    {
241
        return $this->Menu()->filter('IsSection', true)->First();
242
    }
243
244
    /**
245
     * Return a name for this environment.
246
     *
247
     * @param string $separator The string used when concatenating project with env name
248
     * @return string
249
     */
250
    public function getFullName($separator = ':')
251
    {
252
        return sprintf('%s%s%s', $this->Project()->Name, $separator, $this->Name);
253
    }
254
255
    public function getBareURL()
256
    {
257
        $url = parse_url($this->URL);
258
        if (isset($url['host'])) {
259
            return strtolower($url['host']);
260
        }
261
    }
262
263
    /**
264
     * @return boolean true if there is a pipeline for the current environment.
265
     */
266
    public function HasPipelineSupport()
267
    {
268
        $config = $this->GenericPipelineConfig();
269
        return $config instanceof ArrayData && isset($config->Steps);
270
    }
271
272
    /**
273
     * Returns a {@link Pipeline} object that is linked to this environment, but isn't saved into the database. This
274
     * shouldn't be saved into the database unless you plan on starting an actual pipeline.
275
     *
276
     * @return Pipeline
277
     */
278
    public function GenericPipeline()
279
    {
280
        $pipeline = Pipeline::create();
281
        $pipeline->EnvironmentID = $this->ID;
282
        return $pipeline;
283
    }
284
285
    /**
286
     * Returns the parsed config, based on a {@link Pipeline} being created for this {@link DNEnvironment}.
287
     *
288
     * @return ArrayData
289
     */
290
    public function GenericPipelineConfig()
291
    {
292
        $config = $this->loadPipelineConfig();
293
        if ($config) {
294
            return self::array_to_viewabledata($config);
295
        }
296
    }
297
298
    /**
299
     * Extract pipeline configuration data from the source yml file
300
     *
301
     * @return array
302
     */
303
    public function loadPipelineConfig()
304
    {
305
        require_once 'thirdparty/spyc/spyc.php';
306
307
        $path = $this->getPipelineFilename();
308
        if (file_exists($path)) {
309
            return Spyc::YAMLLoad($path);
310
        }
311
    }
312
313
    /**
314
     * Returns the {@link DNEnvironment} object relating to the pipeline config for this environment. The environment
315
     * YAML file (e.g. project1-uat.yml; see docs/en/pipelines.md) contains two variable called `DependsOnProject` and
316
     * `DependsOnEnvironment` - these are used together to find the {@link DNEnvironment} that this environment should
317
     * rely on.
318
     */
319
    public function DependsOnEnvironment()
320
    {
321
        if ($this->HasPipelineSupport()) {
322
            $pipeline = $this->GenericPipeline();
323
            return $pipeline->getDependentEnvironment();
324
        }
325
326
        return null;
327
    }
328
329
    /**
330
     * @return bool true if there is a currently running Pipeline, and false if there isn't
331
     */
332
    public function HasCurrentPipeline()
333
    {
334
        return $this->CurrentPipeline() && $this->CurrentPipeline()->isInDB();
335
    }
336
337
    /**
338
     * This can be used to determine if there is a currently running pipeline (there can only be one running per
339
     * {@link DNEnvironment} at once), as well as getting the current pipeline to be shown in templates.
340
     *
341
     * @return DataObject|null The currently running pipeline, or null if there isn't any.
342
     */
343
    public function CurrentPipeline()
344
    {
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
    {
353
        // do we have a current pipeline
354
        if ($this->HasCurrentPipeline()) {
355
            return $this->CurrentPipeline()->canAbort();
356
        }
357
        return false;
358
    }
359
360
    /**
361
     * Environments are only viewable by people that can view the environment.
362
     *
363
     * @param Member|null $member
364
     * @return boolean
365
     */
366
    public function canView($member = null)
367
    {
368
        if (!$member) {
369
            $member = Member::currentUser();
370
        }
371
        if (!$member) {
372
            return false;
373
        }
374
        // Must be logged in to check permissions
375
376
        if (Permission::checkMember($member, 'ADMIN')) {
377
            return true;
378
        }
379
380
        // if no Viewers or ViewerGroups defined, fallback to DNProject::canView permissions
381
        if ($this->Viewers()->exists() || $this->ViewerGroups()->exists()) {
382
            return $this->Viewers()->byID($member->ID)
383
                || $member->inGroups($this->ViewerGroups());
384
        }
385
386
        return $this->Project()->canView($member);
1 ignored issue
show
Documentation introduced by
$member is of type object<DataObject>, but the function expects a object<Member>|null.

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...
387
    }
388
389
    /**
390
     * Allow deploy only to some people.
391
     *
392
     * @param Member|null $member
393
     * @return boolean
394
     */
395 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...
396
    {
397
        if (!$member) {
398
            $member = Member::currentUser();
399
        }
400
        if (!$member) {
401
            return false;
402
        }
403
        // Must be logged in to check permissions
404
405
        if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
406
            if ($this->Project()->allowed(DNRoot::ALLOW_PROD_DEPLOYMENT, $member)) {
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>, but the function expects a object<Member>|null.

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...
407
                return true;
408
            }
409
        } else {
410
            if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_DEPLOYMENT, $member)) {
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>, but the function expects a object<Member>|null.

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...
411
                return true;
412
            }
413
        }
414
415
        return $this->Deployers()->byID($member->ID)
416
            || $member->inGroups($this->DeployerGroups());
417
    }
418
419
    /**
420
     * Allows only selected {@link Member} objects to restore {@link DNDataArchive} objects into this
421
     * {@link DNEnvironment}.
422
     *
423
     * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
424
     * @return boolean true if $member can restore, and false if they can't.
425
     */
426 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...
427
    {
428
        if (!$member) {
429
            $member = Member::currentUser();
430
        }
431
        if (!$member) {
432
            return false;
433
        }
434
        // Must be logged in to check permissions
435
436
        if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
437
            if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) {
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>, but the function expects a object<Member>|null.

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...
438
                return true;
439
            }
440
        } else {
441
            if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) {
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>, but the function expects a object<Member>|null.

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...
442
                return true;
443
            }
444
        }
445
446
        return $this->CanRestoreMembers()->byID($member->ID)
447
            || $member->inGroups($this->CanRestoreGroups());
448
    }
449
450
    /**
451
     * Allows only selected {@link Member} objects to backup this {@link DNEnvironment} to a {@link DNDataArchive}
452
     * file.
453
     *
454
     * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
455
     * @return boolean true if $member can backup, and false if they can't.
456
     */
457 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...
458
    {
459
        $project = $this->Project();
460
        if ($project->HasDiskQuota() && $project->HasExceededDiskQuota()) {
461
            return false;
462
        }
463
464
        if (!$member) {
465
            $member = Member::currentUser();
466
        }
467
        // Must be logged in to check permissions
468
        if (!$member) {
469
            return false;
470
        }
471
472
        if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
473
            if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) {
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>, but the function expects a object<Member>|null.

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...
474
                return true;
475
            }
476
        } else {
477
            if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) {
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>, but the function expects a object<Member>|null.

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...
478
                return true;
479
            }
480
        }
481
482
        return $this->CanBackupMembers()->byID($member->ID)
483
            || $member->inGroups($this->CanBackupGroups());
484
    }
485
486
    /**
487
     * Allows only selected {@link Member} objects to upload {@link DNDataArchive} objects linked to this
488
     * {@link DNEnvironment}.
489
     *
490
     * Note: This is not uploading them to the actual environment itself (e.g. uploading to the live site) - it is the
491
     * process of uploading a *.sspak file into Deploynaut for later 'restoring' to an environment. See
492
     * {@link self::canRestore()}.
493
     *
494
     * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
495
     * @return boolean true if $member can upload archives linked to this environment, false if they can't.
496
     */
497 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...
498
    {
499
        $project = $this->Project();
500
        if ($project->HasDiskQuota() && $project->HasExceededDiskQuota()) {
501
            return false;
502
        }
503
504
        if (!$member) {
505
            $member = Member::currentUser();
506
        }
507
        if (!$member) {
508
            return false;
509
        }
510
        // Must be logged in to check permissions
511
512
        if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
513
            if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) {
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>, but the function expects a object<Member>|null.

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...
514
                return true;
515
            }
516
        } else {
517
            if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) {
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>, but the function expects a object<Member>|null.

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...
518
                return true;
519
            }
520
        }
521
522
        return $this->ArchiveUploaders()->byID($member->ID)
523
            || $member->inGroups($this->ArchiveUploaderGroups());
524
    }
525
526
    /**
527
     * Allows only selected {@link Member} objects to download {@link DNDataArchive} objects from this
528
     * {@link DNEnvironment}.
529
     *
530
     * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
531
     * @return boolean true if $member can download archives from this environment, false if they can't.
532
     */
533 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...
534
    {
535
        if (!$member) {
536
            $member = Member::currentUser();
537
        }
538
        if (!$member) {
539
            return false;
540
        }
541
        // Must be logged in to check permissions
542
543
        if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
544
            if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) {
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>, but the function expects a object<Member>|null.

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...
545
                return true;
546
            }
547
        } else {
548
            if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) {
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>, but the function expects a object<Member>|null.

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...
549
                return true;
550
            }
551
        }
552
553
        return $this->ArchiveDownloaders()->byID($member->ID)
554
            || $member->inGroups($this->ArchiveDownloaderGroups());
555
    }
556
557
    /**
558
     * Determine if the specified user can abort any pipelines
559
     *
560
     * @param Member|null $member
561
     * @return boolean
562
     */
563 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...
564
    {
565
        if (!$member) {
566
            $member = Member::currentUser();
567
        }
568
        if (!$member) {
569
            return false;
570
        }
571
572
        if (Permission::checkMember($member, 'ADMIN')) {
573
            return true;
574
        }
575
576
        return $this->PipelineCancellers()->byID($member->ID)
577
            || $member->inGroups($this->PipelineCancellerGroups());
578
    }
579
580
    /**
581
     * Determine if the specified user can approve any pipelines
582
     *
583
     * @param Member|null $member
584
     * @return boolean
585
     */
586 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...
587
    {
588
        if (!$member) {
589
            $member = Member::currentUser();
590
        }
591
        if (!$member) {
592
            return false;
593
        }
594
595
        if (Permission::checkMember($member, 'ADMIN')) {
596
            return true;
597
        }
598
        return $this->PipelineApprovers()->byID($member->ID)
599
            || $member->inGroups($this->PipelineApproverGroups());
600
    }
601
602
    /**
603
     * Allows only selected {@link Member} objects to delete {@link DNDataArchive} objects from this
604
     * {@link DNEnvironment}.
605
     *
606
     * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember();
607
     * @return boolean true if $member can delete archives from this environment, false if they can't.
608
     */
609 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...
610
    {
611
        if (!$member) {
612
            $member = Member::currentUser();
613
        }
614
        if (!$member) {
615
            return false;
616
        }
617
        // Must be logged in to check permissions
618
619
        if ($this->Usage==='Production' || $this->Usage==='Unspecified') {
620
            if ($this->Project()->allowed(DNRoot::ALLOW_PROD_SNAPSHOT, $member)) {
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>, but the function expects a object<Member>|null.

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...
621
                return true;
622
            }
623
        } else {
624
            if ($this->Project()->allowed(DNRoot::ALLOW_NON_PROD_SNAPSHOT, $member)) {
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>, but the function expects a object<Member>|null.

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