Completed
Push — master ( 1bceff...ab9d36 )
by Robbie
03:14 queued 01:48
created

DMSDocument::getPermissionsActionPanel()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 35
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 35
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 22
nc 4
nop 0
1
<?php
2
3
/**
4
 * @package dms
5
 *
6
 * @property Varchar Filename
7
 * @property Varchar Folder
8
 * @property Varchar Title
9
 * @property Text Description
10
 * @property int ViewCount
11
 * @property Boolean EmbargoedIndefinitely
12
 * @property Boolean EmbargoedUntilPublished
13
 * @property DateTime EmbargoedUntilDate
14
 * @property DateTime ExpireAtDate
15
 * @property Enum DownloadBehavior
16
 * @property Enum CanViewType Enum('Anyone, LoggedInUsers, OnlyTheseUsers', 'Anyone')
17
 * @property Enum CanEditType Enum('LoggedInUsers, OnlyTheseUsers', 'LoggedInUsers')
18
 *
19
 * @method ManyManyList RelatedDocuments
20
 * @method ManyManyList ViewerGroups
21
 * @method ManyManyList EditorGroups
22
 *
23
 * @method Member CreatedBy
24
 * @property Int CreatedByID
25
 * @method Member LastEditedBy
26
 * @property Int LastEditedByID
27
 *
28
 */
29
class DMSDocument extends DataObject implements DMSDocumentInterface
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
30
{
31
    private static $db = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $db is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
32
        "Filename" => "Varchar(255)", // eg. 3469~2011-energysaving-report.pdf
33
        "Folder" => "Varchar(255)",    // eg.	0
34
        "Title" => 'Varchar(1024)', // eg. "Energy Saving Report for Year 2011, New Zealand LandCorp"
35
        "Description" => 'Text',
36
        "ViewCount" => 'Int',
37
        "EmbargoedIndefinitely" => 'Boolean(false)',
38
        "EmbargoedUntilPublished" => 'Boolean(false)',
39
        "EmbargoedUntilDate" => 'SS_DateTime',
40
        "ExpireAtDate" => 'SS_DateTime',
41
        "DownloadBehavior" => 'Enum(array("open","download"), "download")',
42
        "CanViewType" => "Enum('Anyone, LoggedInUsers, OnlyTheseUsers', 'Anyone')",
43
        "CanEditType" => "Enum('LoggedInUsers, OnlyTheseUsers', 'LoggedInUsers')",
44
    );
45
46
    private static $belongs_many_many = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $belongs_many_many is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
47
        'Sets' => 'DMSDocumentSet'
48
    );
49
50
    private static $has_one = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $has_one is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
51
        'CoverImage' => 'Image',
52
        'CreatedBy' => 'Member',
53
        'LastEditedBy' => 'Member',
54
    );
55
56
    private static $many_many = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $many_many is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
57
        'RelatedDocuments' => 'DMSDocument',
58
        'ViewerGroups' => 'Group',
59
        'EditorGroups' => 'Group',
60
    );
61
62
    private static $display_fields = array(
0 ignored issues
show
Unused Code introduced by
The property $display_fields is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
63
        'ID' => 'ID',
64
        'Title' => 'Title',
65
        'FilenameWithoutID' => 'Filename',
66
        'LastEdited' => 'Last Edited'
67
    );
68
69
    private static $singular_name = 'Document';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $singular_name is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
70
71
    private static $plural_name = 'Documents';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $plural_name is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
72
73
    private static $summary_fields = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $summary_fields is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
74
        'Filename' => 'Filename',
75
        'Title' => 'Title',
76
        'getRelatedPages.count' => 'Page Use',
77
        'ViewCount' => 'ViewCount',
78
    );
79
80
    /**
81
     * @var string download|open
82
     * @config
83
     */
84
    private static $default_download_behaviour = 'download';
0 ignored issues
show
Unused Code introduced by
The property $default_download_behaviour is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
85
86
    /**
87
     * A key value map of the "actions" tabs that will be added to the CMS fields
88
     *
89
     * @var array
90
     */
91
    protected $actionTasks = array(
92
        'embargo' => 'Embargo',
93
        'expiry' => 'Expiry',
94
        'replace' => 'Replace',
95
        'find-usage' => 'Usage',
96
        'find-references' => 'References',
97
        'find-relateddocuments' => 'Related Documents',
98
        'permissions' => 'Permissions'
99
    );
100
101
    public function canView($member = null)
102
    {
103 View Code Duplication
        if (!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
104
            $member = Member::currentUser();
105
        }
106
107
        // extended access checks
108
        $results = $this->extend('canView', $member);
109
110
        if ($results && is_array($results)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $results of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
111
            if (!min($results)) {
112
                return false;
113
            }
114
        }
115
116
        if (!$this->CanViewType || $this->CanViewType == 'Anyone') {
117
            return true;
118
        }
119
        if ($member && Permission::checkMember($member, array(
120
                'ADMIN',
121
                'SITETREE_EDIT_ALL',
122
                'SITETREE_VIEW_ALL',
123
            ))
124
        ) {
125
            return true;
126
        }
127
128
        if ($this->isHidden()) {
129
            return false;
130
        }
131
132
        if ($this->CanViewType == 'LoggedInUsers') {
133
            return $member && $member->exists();
134
        }
135
136
        if ($this->CanViewType == 'OnlyTheseUsers' && $this->ViewerGroups()->count()) {
0 ignored issues
show
Documentation Bug introduced by
The method ViewerGroups does not exist on object<DMSDocument>? 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...
137
            return ($member && $member->inGroups($this->ViewerGroups()) || $this->canEdit($member));
0 ignored issues
show
Documentation Bug introduced by
The method ViewerGroups does not exist on object<DMSDocument>? 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...
138
        }
139
140
        return $this->canEdit($member);
141
    }
142
143
    public function canEdit($member = null)
144
    {
145 View Code Duplication
        if (!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
146
            $member = Member::currentUser();
147
        }
148
149
        $results = $this->extend('canEdit', $member);
150
151
        if ($results && is_array($results)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $results of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
152
            if (!min($results)) {
153
                return false;
154
            }
155
        }
156
157
        // Do early admin check
158
        if ($member && Permission::checkMember($member, array('ADMIN','SITETREE_EDIT_ALL'))) {
159
            return true;
160
        }
161
162
        if ($this->CanEditType === 'LoggedInUsers') {
163
            return $member && $member->exists();
164
        }
165
166
        if ($this->CanEditType === 'OnlyTheseUsers' && $this->EditorGroups()->count()) {
0 ignored issues
show
Documentation Bug introduced by
The method EditorGroups does not exist on object<DMSDocument>? 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...
167
            return $member && $member->inGroups($this->EditorGroups());
0 ignored issues
show
Documentation Bug introduced by
The method EditorGroups does not exist on object<DMSDocument>? 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...
168
        }
169
170
        return false;
171
    }
172
173
    /**
174
     * @param Member $member
0 ignored issues
show
Documentation introduced by
Should the type for parameter $member not be Member|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
175
     *
176
     * @return boolean
177
     */
178
    public function canCreate($member = null)
179
    {
180 View Code Duplication
        if (!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
181
            $member = Member::currentUser();
182
        }
183
184
        $results = $this->extend('canCreate', $member);
185
186
        if ($results && is_array($results)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $results of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
187
            if (!min($results)) {
188
                return false;
189
            }
190
        }
191
192
        // Do early admin check
193
        if ($member &&
194
            Permission::checkMember($member, array('CMS_ACCESS_DMSDocumentAdmin'))
195
        ) {
196
            return true;
197
        }
198
199
        return $this->canEdit($member);
200
    }
201
202
    /**
203
     * @param Member $member
0 ignored issues
show
Documentation introduced by
Should the type for parameter $member not be Member|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
204
     *
205
     * @return boolean
206
     */
207
    public function canDelete($member = null)
208
    {
209 View Code Duplication
        if (!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
210
            $member = Member::currentUser();
211
        }
212
213
        $results = $this->extend('canDelete', $member);
214
215
        if ($results && is_array($results)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $results of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
216
            if (!min($results)) {
217
                return false;
218
            }
219
        }
220
221
        return $this->canEdit($member);
222
    }
223
224
    /**
225
     * Increase ViewCount by 1, without update any other record fields such as
226
     * LastEdited.
227
     *
228
     * @return DMSDocument
229
     */
230
    public function trackView()
231
    {
232
        if ($this->ID > 0) {
233
            $count = $this->ViewCount + 1;
234
235
            $this->ViewCount = $count;
236
237
            DB::query("UPDATE \"DMSDocument\" SET \"ViewCount\"='$count' WHERE \"ID\"={$this->ID}");
238
            
239
            $this->extend('trackView');
240
        }
241
242
        return $this;
243
    }
244
245
    /**
246
     * Returns a link to download this document from the DMS store.
247
     * Alternatively a basic javascript alert will be shown should the user not have view permissions. An extension
248
     * point for this was also added.
249
     *
250
     * To extend use the following from within an Extension subclass:
251
     *
252
     * <code>
253
     * public function updateGetLink($result)
254
     * {
255
     *     // Do something here
256
     * }
257
     * </code>
258
     *
259
     * @return string
260
     */
261
    public function getLink()
262
    {
263
        $urlSegment = sprintf('%d-%s', $this->ID, URLSegmentFilter::create()->filter($this->getTitle()));
264
        $result = Controller::join_links(Director::baseURL(), 'dmsdocument/' . $urlSegment);
265
        if (!$this->canView()) {
266
            $result = sprintf("javascript:alert('%s')", $this->getPermissionDeniedReason());
267
        }
268
269
        $this->extend('updateGetLink', $result);
270
271
        return $result;
272
    }
273
274
    /**
275
     * @return string
276
     */
277
    public function Link()
278
    {
279
        return $this->getLink();
280
    }
281
282
    /**
283
     * Hides the document, so it does not show up when getByPage($myPage) is
284
     * called (without specifying the $showEmbargoed = true parameter).
285
     *
286
     * This is similar to expire, except that this method should be used to hide
287
     * documents that have not yet gone live.
288
     *
289
     * @param bool $write Save change to the database
290
     *
291
     * @return DMSDocument
292
     */
293
    public function embargoIndefinitely($write = true)
294
    {
295
        $this->EmbargoedIndefinitely = true;
296
297
        if ($write) {
298
            $this->write();
299
        }
300
301
        return $this;
302
    }
303
304
    /**
305
     * Hides the document until any page it is linked to is published
306
     *
307
     * @param bool $write Save change to database
308
     *
309
     * @return DMSDocument
310
     */
311
    public function embargoUntilPublished($write = true)
312
    {
313
        $this->EmbargoedUntilPublished = true;
314
315
        if ($write) {
316
            $this->write();
317
        }
318
319
        return $this;
320
    }
321
322
    /**
323
     * Returns if this is Document is embargoed or expired.
324
     *
325
     * Also, returns if the document should be displayed on the front-end,
326
     * respecting the current reading mode of the site and the embargo status.
327
     *
328
     * I.e. if a document is embargoed until published, then it should still
329
     * show up in draft mode.
330
     *
331
     * @return bool
332
     */
333
    public function isHidden()
334
    {
335
        $hidden = $this->isEmbargoed() || $this->isExpired();
336
        $readingMode = Versioned::get_reading_mode();
337
338
        if ($readingMode == "Stage.Stage") {
339
            if ($this->EmbargoedUntilPublished == true) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
340
                $hidden = false;
341
            }
342
        }
343
344
        return $hidden;
345
    }
346
347
    /**
348
     * Returns if this is Document is embargoed.
349
     *
350
     * @return bool
351
     */
352
    public function isEmbargoed()
353
    {
354
        if (is_object($this->EmbargoedUntilDate)) {
355
            $this->EmbargoedUntilDate = $this->EmbargoedUntilDate->Value;
0 ignored issues
show
Bug introduced by
The property Value does not seem to exist in DateTime.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
356
        }
357
358
        $embargoed = false;
359
360
        if ($this->EmbargoedIndefinitely) {
361
            $embargoed = true;
362
        } elseif ($this->EmbargoedUntilPublished) {
363
            $embargoed = true;
364
        } elseif (!empty($this->EmbargoedUntilDate)) {
365
            if (SS_Datetime::now()->Value < $this->EmbargoedUntilDate) {
366
                $embargoed = true;
367
            }
368
        }
369
370
        return $embargoed;
371
    }
372
373
    /**
374
     * Hides the document, so it does not show up when getByPage($myPage) is
375
     * called. Automatically un-hides the Document at a specific date.
376
     *
377
     * @param string $datetime date time value when this Document should expire.
378
     * @param bool $write
379
     *
380
     * @return DMSDocument
381
     */
382 View Code Duplication
    public function embargoUntilDate($datetime, $write = true)
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...
383
    {
384
        $this->EmbargoedUntilDate = DBField::create_field('SS_Datetime', $datetime)->Format('Y-m-d H:i:s');
385
386
        if ($write) {
387
            $this->write();
388
        }
389
390
        return $this;
391
    }
392
393
    /**
394
     * Clears any previously set embargos, so the Document always shows up in
395
     * all queries.
396
     *
397
     * @param bool $write
398
     *
399
     * @return DMSDocument
400
     */
401
    public function clearEmbargo($write = true)
402
    {
403
        $this->EmbargoedIndefinitely = false;
404
        $this->EmbargoedUntilPublished = false;
405
        $this->EmbargoedUntilDate = null;
406
407
        if ($write) {
408
            $this->write();
409
        }
410
411
        return $this;
412
    }
413
414
    /**
415
     * Returns if this is Document is expired.
416
     *
417
     * @return bool
418
     */
419
    public function isExpired()
420
    {
421
        if (is_object($this->ExpireAtDate)) {
422
            $this->ExpireAtDate = $this->ExpireAtDate->Value;
0 ignored issues
show
Bug introduced by
The property Value does not seem to exist in DateTime.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
423
        }
424
425
        $expired = false;
426
427
        if (!empty($this->ExpireAtDate)) {
428
            if (SS_Datetime::now()->Value >= $this->ExpireAtDate) {
429
                $expired = true;
430
            }
431
        }
432
433
        return $expired;
434
    }
435
436
    /**
437
     * Hides the document at a specific date, so it does not show up when
438
     * getByPage($myPage) is called.
439
     *
440
     * @param string $datetime date time value when this Document should expire
441
     * @param bool $write
442
     *
443
     * @return DMSDocument
444
     */
445 View Code Duplication
    public function expireAtDate($datetime, $write = true)
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...
446
    {
447
        $this->ExpireAtDate = DBField::create_field('SS_Datetime', $datetime)->Format('Y-m-d H:i:s');
448
449
        if ($write) {
450
            $this->write();
451
        }
452
453
        return $this;
454
    }
455
456
    /**
457
     * Clears any previously set expiry.
458
     *
459
     * @param bool $write
460
     *
461
     * @return DMSDocument
462
     */
463
    public function clearExpiry($write = true)
464
    {
465
        $this->ExpireAtDate = null;
466
467
        if ($write) {
468
            $this->write();
469
        }
470
471
        return $this;
472
    }
473
474
    /**
475
     * Returns a DataList of all previous Versions of this document (check the
476
     * LastEdited date of each object to find the correct one).
477
     *
478
     * If {@link DMSDocument_versions::$enable_versions} is disabled then an
479
     * Exception is thrown
480
     *
481
     * @throws Exception
482
     *
483
     * @return DataList List of Document objects
484
     */
485
    public function getVersions()
486
    {
487
        if (!DMSDocument_versions::$enable_versions) {
488
            throw new Exception("DMSDocument versions are disabled");
489
        }
490
491
        return DMSDocument_versions::get_versions($this);
492
    }
493
494
    /**
495
     * Returns the full filename of the document stored in this object.
496
     *
497
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
498
     */
499
    public function getFullPath()
500
    {
501
        if ($this->Filename) {
502
            return DMS::inst()->getStoragePath() . DIRECTORY_SEPARATOR
503
                . $this->Folder . DIRECTORY_SEPARATOR . $this->Filename;
504
        }
505
506
        return null;
507
    }
508
509
    /**
510
     * Returns the filename of this asset.
511
     *
512
     * @return string
513
     */
514
    public function getFilename()
515
    {
516
        if ($this->getField('Filename')) {
517
            return $this->getField('Filename');
518
        }
519
        return ASSETS_DIR . '/';
520
    }
521
522
    /**
523
     * @return string
524
     */
525
    public function getName()
526
    {
527
        return $this->getField('Title');
528
    }
529
530
531
    /**
532
     * Returns the filename of a document without the prefix, e.g. 0~filename.jpg -> filename.jpg
533
     *
534
     * @return string
535
     */
536
    public function getFilenameWithoutID()
537
    {
538
        $filenameParts = explode('~', $this->Filename);
539
        $filename = array_pop($filenameParts);
540
541
        return $filename;
542
    }
543
544
    /**
545
     * @return string
546
     */
547
    public function getStorageFolder()
548
    {
549
        return DMS::inst()->getStoragePath() . DIRECTORY_SEPARATOR . DMS::inst()->getStorageFolder($this->ID);
550
    }
551
552
    /**
553
     * Deletes the DMSDocument and its underlying file. Also calls the parent DataObject's delete method in
554
     * order to complete an cascade.
555
     *
556
     * @return void
557
     */
558
    public function delete()
559
    {
560
        // delete the file (and previous versions of files)
561
        $filesToDelete = array();
562
        $storageFolder = $this->getStorageFolder();
563
564
        if (file_exists($storageFolder)) {
565
            if ($handle = opendir($storageFolder)) {
566
                while (false !== ($entry = readdir($handle))) {
567
                    // only delete if filename starts the the relevant ID
568
                    if (strpos($entry, $this->ID.'~') === 0) {
569
                        $filesToDelete[] = $entry;
570
                    }
571
                }
572
573
                closedir($handle);
574
575
                //delete all this files that have the id of this document
576
                foreach ($filesToDelete as $file) {
577
                    $filePath = $storageFolder .DIRECTORY_SEPARATOR . $file;
578
579
                    if (is_file($filePath)) {
580
                        unlink($filePath);
581
                    }
582
                }
583
            }
584
        }
585
586
        // get rid of any versions have saved for this DMSDocument, too
587
        if (DMSDocument_versions::$enable_versions) {
588
            $versions = $this->getVersions();
589
590
            if ($versions->Count() > 0) {
591
                foreach ($versions as $v) {
592
                    $v->delete();
593
                }
594
            }
595
        }
596
597
        return parent::delete();
598
    }
599
600
    /**
601
     * Relate an existing file on the filesystem to the document.
602
     *
603
     * Copies the file to the new destination, as defined in {@link DMS::getStoragePath()}.
604
     *
605
     * @param string $filePath Path to file, relative to webroot.
606
     *
607
     * @return DMSDocument
608
     */
609
    public function storeDocument($filePath)
610
    {
611
        if (empty($this->ID)) {
612
            user_error("Document must be written to database before it can store documents", E_USER_ERROR);
613
        }
614
615
        // calculate all the path to copy the file to
616
        $fromFilename = basename($filePath);
617
        $toFilename = $this->ID. '~' . $fromFilename; //add the docID to the start of the Filename
618
        $toFolder = DMS::inst()->getStorageFolder($this->ID);
619
        $toPath = DMS::inst()->getStoragePath() . DIRECTORY_SEPARATOR . $toFolder . DIRECTORY_SEPARATOR . $toFilename;
620
621
        DMS::inst()->createStorageFolder(DMS::inst()->getStoragePath() . DIRECTORY_SEPARATOR . $toFolder);
622
623
        //copy the file into place
624
        $fromPath = BASE_PATH . DIRECTORY_SEPARATOR . $filePath;
625
626
        //version the existing file (copy it to a new "very specific" filename
627
        if (DMSDocument_versions::$enable_versions) {
628
            DMSDocument_versions::create_version($this);
629
        } else {    //otherwise delete the old document file
630
            $oldPath = $this->getFullPath();
631
            if (file_exists($oldPath)) {
632
                unlink($oldPath);
633
            }
634
        }
635
636
        copy($fromPath, $toPath);   //this will overwrite the existing file (if present)
637
638
        //write the filename of the stored document
639
        $this->Filename = $toFilename;
640
        $this->Folder = strval($toFolder);
641
642
        $extension = pathinfo($this->Filename, PATHINFO_EXTENSION);
643
644
        if (empty($this->Title)) {
645
            // don't overwrite existing document titles
646
            $this->Title = basename($filePath, '.'.$extension);
647
        }
648
649
        $this->write();
650
651
        return $this;
652
    }
653
654
    /**
655
     * Takes a File object or a String (path to a file) and copies it into the
656
     * DMS, replacing the original document file but keeping the rest of the
657
     * document unchanged.
658
     *
659
     * @param File|string $file path to a file to store
660
     *
661
     * @return DMSDocument object that we replaced the file in
662
     */
663
    public function replaceDocument($file)
664
    {
665
        $filePath = DMS::inst()->transformFileToFilePath($file);
666
        $doc = $this->storeDocument($filePath); // replace the document
667
668
        return $doc;
669
    }
670
671
672
    /**
673
     * Return the type of file for the given extension
674
     * on the current file name.
675
     *
676
     * @param string $ext
677
     *
678
     * @return string
679
     */
680
    public static function get_file_type($ext)
681
    {
682
        $types = array(
683
            'gif' => 'GIF image - good for diagrams',
684
            'jpg' => 'JPEG image - good for photos',
685
            'jpeg' => 'JPEG image - good for photos',
686
            'png' => 'PNG image - good general-purpose format',
687
            'ico' => 'Icon image',
688
            'tiff' => 'Tagged image format',
689
            'doc' => 'Word document',
690
            'xls' => 'Excel spreadsheet',
691
            'zip' => 'ZIP compressed file',
692
            'gz' => 'GZIP compressed file',
693
            'dmg' => 'Apple disk image',
694
            'pdf' => 'Adobe Acrobat PDF file',
695
            'mp3' => 'MP3 audio file',
696
            'wav' => 'WAV audo file',
697
            'avi' => 'AVI video file',
698
            'mpg' => 'MPEG video file',
699
            'mpeg' => 'MPEG video file',
700
            'js' => 'Javascript file',
701
            'css' => 'CSS file',
702
            'html' => 'HTML file',
703
            'htm' => 'HTML file'
704
        );
705
706
        return isset($types[$ext]) ? $types[$ext] : $ext;
707
    }
708
709
710
    /**
711
     * Returns the Description field with HTML <br> tags added when there is a
712
     * line break.
713
     *
714
     * @return string
715
     */
716
    public function getDescriptionWithLineBreak()
717
    {
718
        return nl2br($this->getField('Description'));
719
    }
720
721
    /**
722
     * @return FieldList
723
     */
724
    public function getCMSFields()
725
    {
726
        //include JS to handling showing and hiding of bottom "action" tabs
727
        Requirements::javascript(DMS_DIR . '/javascript/DMSDocumentCMSFields.js');
728
        Requirements::css(DMS_DIR . '/dist/css/cmsbundle.css');
729
730
        $fields = new FieldList();  //don't use the automatic scaffolding, it is slow and unnecessary here
731
732
        $extraTasks = '';   //additional text to inject into the list of tasks at the bottom of a DMSDocument CMSfield
0 ignored issues
show
Unused Code introduced by
$extraTasks is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
733
734
        //get list of shortcode page relations
735
        $relationFinder = new ShortCodeRelationFinder();
736
        $relationList = $relationFinder->getList($this->ID);
737
738
        $fieldsTop = $this->getFieldsForFile($relationList->count());
739
        $fields->add($fieldsTop);
740
741
        $fields->add(TextField::create('Title', _t('DMSDocument.TITLE', 'Title')));
742
        $fields->add(TextareaField::create('Description', _t('DMSDocument.DESCRIPTION', 'Description')));
743
744
        $coverImageField = UploadField::create('CoverImage', _t('DMSDocument.COVERIMAGE', 'Cover Image'));
745
        $coverImageField->getValidator()->setAllowedExtensions(array('jpg', 'jpeg', 'png', 'gif'));
746
        $coverImageField->setConfig('allowedMaxFileNumber', 1);
747
        $fields->add($coverImageField);
748
749
750
        $downloadBehaviorSource = array(
751
            'open' => _t('DMSDocument.OPENINBROWSER', 'Open in browser'),
752
            'download' => _t('DMSDocument.FORCEDOWNLOAD', 'Force download'),
753
        );
754
        $defaultDownloadBehaviour = Config::inst()->get('DMSDocument', 'default_download_behaviour');
755
        if (!isset($downloadBehaviorSource[$defaultDownloadBehaviour])) {
756
            user_error('Default download behaviour "' . $defaultDownloadBehaviour . '" not supported.', E_USER_WARNING);
757
        } else {
758
            $downloadBehaviorSource[$defaultDownloadBehaviour] .= ' (' . _t('DMSDocument.DEFAULT', 'default') . ')';
759
        }
760
761
        $fields->add(
762
            OptionsetField::create(
763
                'DownloadBehavior',
764
                _t('DMSDocument.DOWNLOADBEHAVIOUR', 'Download behavior'),
765
                $downloadBehaviorSource,
766
                $defaultDownloadBehaviour
767
            )
768
            ->setDescription(
769
                'How the visitor will view this file. <strong>Open in browser</strong> '
770
                . 'allows files to be opened in a new tab.'
771
            )
772
        );
773
774
        //create upload field to replace document
775
        $uploadField = new DMSUploadField('ReplaceFile', 'Replace file');
776
        $uploadField->setConfig('allowedMaxFileNumber', 1);
777
        $uploadField->setConfig('downloadTemplateName', 'ss-dmsuploadfield-downloadtemplate');
778
        $uploadField->setRecord($this);
779
780
        $gridFieldConfig = GridFieldConfig::create()->addComponents(
781
            new GridFieldToolbarHeader(),
782
            new GridFieldSortableHeader(),
783
            new GridFieldDataColumns(),
784
            new GridFieldPaginator(30),
785
            //new GridFieldEditButton(),
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
786
            new GridFieldDetailForm()
787
        );
788
789
        $gridFieldConfig->getComponentByType('GridFieldDataColumns')
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface GridFieldComponent as the method setDisplayFields() does only exist in the following implementations of said interface: GridFieldDataColumns, GridFieldEditableColumns, GridFieldExternalLink.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
790
            ->setDisplayFields(array(
791
                'Title' => 'Title',
792
                'ClassName' => 'Page Type',
793
                'ID' => 'Page ID'
794
            ))
795
            ->setFieldFormatting(array(
796
                'Title' => sprintf(
797
                    '<a class=\"cms-panel-link\" href=\"%s/$ID\">$Title</a>',
798
                    singleton('CMSPageEditController')->Link('show')
799
                )
800
            ));
801
802
        $pagesGrid = GridField::create(
803
            'Pages',
804
            _t('DMSDocument.RelatedPages', 'Related Pages'),
805
            $this->getRelatedPages(),
806
            $gridFieldConfig
807
        );
808
809
        $referencesGrid = GridField::create(
810
            'References',
811
            _t('DMSDocument.RelatedReferences', 'Related References'),
812
            $relationList,
813
            $gridFieldConfig
814
        );
815
816
        if (DMSDocument_versions::$enable_versions) {
817
            $versionsGridFieldConfig = GridFieldConfig::create()->addComponents(
818
                new GridFieldToolbarHeader(),
819
                new GridFieldSortableHeader(),
820
                new GridFieldDataColumns(),
821
                new GridFieldPaginator(30)
822
            );
823
            $versionsGridFieldConfig->getComponentByType('GridFieldDataColumns')
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface GridFieldComponent as the method setDisplayFields() does only exist in the following implementations of said interface: GridFieldDataColumns, GridFieldEditableColumns, GridFieldExternalLink.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
824
                ->setDisplayFields(Config::inst()->get('DMSDocument_versions', 'display_fields'))
825
                ->setFieldFormatting(
826
                    array(
827
                        'FilenameWithoutID' => '<a target="_blank" class="file-url" href="$Link">'
828
                            . '$FilenameWithoutID</a>'
829
                    )
830
                );
831
832
            $versionsGrid =  GridField::create(
833
                'Versions',
834
                _t('DMSDocument.Versions', 'Versions'),
835
                $this->getVersions(),
836
                $versionsGridFieldConfig
837
            );
838
            $this->addActionPanelTask('find-versions', 'Versions');
839
        }
840
841
        $embargoValue = 'None';
842
        if ($this->EmbargoedIndefinitely) {
843
            $embargoValue = 'Indefinitely';
844
        } elseif ($this->EmbargoedUntilPublished) {
845
            $embargoValue = 'Published';
846
        } elseif (!empty($this->EmbargoedUntilDate)) {
847
            $embargoValue = 'Date';
848
        }
849
        $embargo = new OptionsetField(
850
            'Embargo',
851
            _t('DMSDocument.EMBARGO', 'Embargo'),
852
            array(
853
                'None' => _t('DMSDocument.EMBARGO_NONE', 'None'),
854
                'Published' => _t('DMSDocument.EMBARGO_PUBLISHED', 'Hide document until page is published'),
855
                'Indefinitely' => _t('DMSDocument.EMBARGO_INDEFINITELY', 'Hide document indefinitely'),
856
                'Date' => _t('DMSDocument.EMBARGO_DATE', 'Hide until set date')
857
            ),
858
            $embargoValue
859
        );
860
        $embargoDatetime = DatetimeField::create('EmbargoedUntilDate', '');
861
        $embargoDatetime->getDateField()
862
            ->setConfig('showcalendar', true)
863
            ->setConfig('dateformat', 'dd-MM-yyyy')
864
            ->setConfig('datavalueformat', 'dd-MM-yyyy');
865
866
        $expiryValue = 'None';
867
        if (!empty($this->ExpireAtDate)) {
868
            $expiryValue = 'Date';
869
        }
870
        $expiry = new OptionsetField(
871
            'Expiry',
872
            'Expiry',
873
            array(
874
                'None' => 'None',
875
                'Date' => 'Set document to expire on'
876
            ),
877
            $expiryValue
878
        );
879
        $expiryDatetime = DatetimeField::create('ExpireAtDate', '');
880
        $expiryDatetime->getDateField()
881
            ->setConfig('showcalendar', true)
882
            ->setConfig('dateformat', 'dd-MM-yyyy')
883
            ->setConfig('datavalueformat', 'dd-MM-yyyy');
884
885
        // This adds all the actions details into a group.
886
        // Embargo, History, etc to go in here
887
        // These are toggled on and off via the Actions Buttons above
888
        // exit('hit');
0 ignored issues
show
Unused Code Comprehensibility introduced by
84% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
889
        $actionsPanel = FieldGroup::create(
890
            FieldGroup::create($embargo, $embargoDatetime)->addExtraClass('embargo'),
891
            FieldGroup::create($expiry, $expiryDatetime)->addExtraClass('expiry'),
892
            FieldGroup::create($uploadField)->addExtraClass('replace'),
893
            FieldGroup::create($pagesGrid)->addExtraClass('find-usage'),
894
            FieldGroup::create($referencesGrid)->addExtraClass('find-references'),
895
            FieldGroup::create($this->getPermissionsActionPanel())->addExtraClass('permissions')
896
        );
897
898
        if ($this->canEdit()) {
899
            $actionsPanel->push(FieldGroup::create($versionsGrid)->addExtraClass('find-versions'));
0 ignored issues
show
Bug introduced by
The variable $versionsGrid does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
900
            $actionsPanel->push(
901
                FieldGroup::create($this->getRelatedDocumentsGridField())->addExtraClass('find-relateddocuments')
902
            );
903
        } else {
904
            $this->removeActionPanelTask('find-relateddocuments')->removeActionPanelTask('find-versions');
905
        }
906
        $fields->add(LiteralField::create('BottomTaskSelection', $this->getActionTaskHtml()));
907
        $actionsPanel->setName('ActionsPanel');
908
        $actionsPanel->addExtraClass('dmsdocument-actionspanel');
909
        $fields->push($actionsPanel);
910
911
        $this->extend('updateCMSFields', $fields);
912
913
        return $fields;
914
    }
915
916
    /**
917
     * Adds permissions selection fields to a composite field and returns so it can be used in the "actions panel"
918
     *
919
     * @return CompositeField
920
     */
921
    public function getPermissionsActionPanel()
922
    {
923
        $fields = FieldList::create();
924
        $showFields = array(
925
            'CanViewType'  => '',
926
            'ViewerGroups' => 'hide',
927
            'CanEditType'  => '',
928
            'EditorGroups' => 'hide',
929
        );
930
        /** @var SiteTree $siteTree */
931
        $siteTree = singleton('SiteTree');
932
        $settingsFields = $siteTree->getSettingsFields();
933
934
        foreach ($showFields as $name => $extraCss) {
935
            $compositeName = "Root.Settings.$name";
936
            /** @var FormField $field */
937
            if ($field = $settingsFields->fieldByName($compositeName)) {
938
                $field->addExtraClass($extraCss);
939
                $title = str_replace('page', 'document', $field->Title());
940
                $field->setTitle($title);
941
942
                // Remove Inherited source option from DropdownField
943
                if ($field instanceof DropdownField) {
944
                    $options = $field->getSource();
945
                    unset($options['Inherit']);
946
                    $field->setSource($options);
947
                }
948
                $fields->push($field);
949
            }
950
        }
951
952
        $this->extend('updatePermissionsFields', $fields);
953
954
        return CompositeField::create($fields);
955
    }
956
957
    /**
958
     * Return a title to use on the frontend, preferably the "title", otherwise the filename without it's numeric ID
959
     *
960
     * @return string
961
     */
962
    public function getTitle()
963
    {
964
        if ($this->getField('Title')) {
965
            return $this->getField('Title');
966
        }
967
        return $this->FilenameWithoutID;
0 ignored issues
show
Bug introduced by
The property FilenameWithoutID does not seem to exist. Did you mean Filename?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
968
    }
969
970
    public function onBeforeWrite()
971
    {
972
        parent::onBeforeWrite();
973
974
        if (isset($this->Embargo)) {
975
            //set the embargo options from the OptionSetField created in the getCMSFields method
976
            //do not write after clearing the embargo (write happens automatically)
977
            $savedDate = $this->EmbargoedUntilDate;
978
            $this->clearEmbargo(false); // Clear all previous settings and re-apply them on save
979
980
            if ($this->Embargo == 'Published') {
0 ignored issues
show
Bug introduced by
The property Embargo does not seem to exist. Did you mean EmbargoedIndefinitely?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
981
                $this->embargoUntilPublished(false);
982
            }
983
            if ($this->Embargo == 'Indefinitely') {
0 ignored issues
show
Bug introduced by
The property Embargo does not seem to exist. Did you mean EmbargoedIndefinitely?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
984
                $this->embargoIndefinitely(false);
985
            }
986
            if ($this->Embargo == 'Date') {
0 ignored issues
show
Bug introduced by
The property Embargo does not seem to exist. Did you mean EmbargoedIndefinitely?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
987
                $this->embargoUntilDate($savedDate, false);
0 ignored issues
show
Documentation introduced by
$savedDate is of type object<DateTime>|null, but the function expects a string.

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...
988
            }
989
        }
990
991
        if (isset($this->Expiry)) {
992
            if ($this->Expiry == 'Date') {
0 ignored issues
show
Documentation introduced by
The property Expiry does not exist on object<DMSDocument>. 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...
993
                $this->expireAtDate($this->ExpireAtDate, false);
0 ignored issues
show
Documentation introduced by
$this->ExpireAtDate is of type object<DateTime>|null, but the function expects a string.

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...
994
            } else {
995
                $this->clearExpiry(false);
996
            } // Clear all previous settings
997
        }
998
999
        // Set user fields
1000
        if ($currentUserID = Member::currentUserID()) {
1001
            if (!$this->CreatedByID) {
1002
                $this->CreatedByID = $currentUserID;
1003
            }
1004
            $this->LastEditedByID = $currentUserID;
1005
        }
1006
    }
1007
1008
    /**
1009
     * Return the relative URL of an icon for the file type, based on the
1010
     * {@link appCategory()} value.
1011
     *
1012
     * Images are searched for in "dms/images/app_icons/".
1013
     *
1014
     * @return string
1015
     */
1016
    public function Icon($ext)
1017
    {
1018
        if (!Director::fileExists(DMS_DIR."/images/app_icons/{$ext}_32.png")) {
1019
            $ext = File::get_app_category($ext);
1020
        }
1021
1022
        if (!Director::fileExists(DMS_DIR."/images/app_icons/{$ext}_32.png")) {
1023
            $ext = "generic";
1024
        }
1025
1026
        return DMS_DIR."/images/app_icons/{$ext}_32.png";
1027
    }
1028
1029
    /**
1030
     * Return the extension of the file associated with the document
1031
     *
1032
     * @return string
1033
     */
1034
    public function getExtension()
1035
    {
1036
        return strtolower(pathinfo($this->Filename, PATHINFO_EXTENSION));
1037
    }
1038
1039
    /**
1040
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be string|false?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1041
     */
1042
    public function getSize()
1043
    {
1044
        $size = $this->getAbsoluteSize();
1045
        return ($size) ? File::format_size($size) : false;
1046
    }
1047
1048
    /**
1049
     * Return the size of the file associated with the document.
1050
     *
1051
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1052
     */
1053
    public function getAbsoluteSize()
1054
    {
1055
        return file_exists($this->getFullPath()) ? filesize($this->getFullPath()) : null;
1056
    }
1057
1058
    /**
1059
     * An alias to DMSDocument::getSize()
1060
     *
1061
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be string|false?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1062
     */
1063
    public function getFileSizeFormatted()
1064
    {
1065
        return $this->getSize();
1066
    }
1067
1068
1069
    /**
1070
     * @return FieldList
1071
     */
1072
    protected function getFieldsForFile($relationListCount)
1073
    {
1074
        $extension = $this->getExtension();
1075
1076
        $previewField = new LiteralField(
1077
            "ImageFull",
1078
            "<img id='thumbnailImage' class='thumbnail-preview' src='{$this->Icon($extension)}?r="
1079
            . rand(1, 100000) . "' alt='{$this->Title}' />\n"
1080
        );
1081
1082
        //count the number of pages this document is published on
1083
        $publishedOnCount = $this->getRelatedPages()->count();
1084
        $publishedOnValue = "$publishedOnCount pages";
1085
        if ($publishedOnCount == 1) {
1086
            $publishedOnValue = "$publishedOnCount page";
1087
        }
1088
1089
        $relationListCountValue = "$relationListCount pages";
1090
        if ($relationListCount == 1) {
1091
            $relationListCountValue = "$relationListCount page";
1092
        }
1093
1094
        $fields = new FieldGroup(
1095
            $filePreview = CompositeField::create(
1096
                CompositeField::create(
1097
                    $previewField
1098
                )->setName("FilePreviewImage")->addExtraClass('cms-file-info-preview'),
1099
                CompositeField::create(
1100
                    CompositeField::create(
1101
                        new ReadonlyField("ID", "ID number". ':', $this->ID),
1102
                        new ReadonlyField(
1103
                            "FileType",
1104
                            _t('AssetTableField.TYPE', 'File type') . ':',
1105
                            self::get_file_type($extension)
1106
                        ),
1107
                        new ReadonlyField(
1108
                            "Size",
1109
                            _t('AssetTableField.SIZE', 'File size') . ':',
1110
                            $this->getFileSizeFormatted()
1111
                        ),
1112
                        $urlField = new ReadonlyField(
1113
                            'ClickableURL',
1114
                            _t('AssetTableField.URL', 'URL'),
1115
                            sprintf(
1116
                                '<a href="%s" target="_blank" class="file-url">%s</a>',
1117
                                $this->getLink(),
1118
                                $this->getLink()
1119
                            )
1120
                        ),
1121
                        new ReadonlyField("FilenameWithoutIDField", "Filename". ':', $this->getFilenameWithoutID()),
1122
                        new DateField_Disabled(
1123
                            "Created",
1124
                            _t('AssetTableField.CREATED', 'First uploaded') . ':',
1125
                            $this->Created
1126
                        ),
1127
                        new DateField_Disabled(
1128
                            "LastEdited",
1129
                            _t('AssetTableField.LASTEDIT', 'Last changed') . ':',
1130
                            $this->LastEdited
1131
                        ),
1132
                        new ReadonlyField("PublishedOn", "Published on". ':', $publishedOnValue),
1133
                        new ReadonlyField("ReferencedOn", "Referenced on". ':', $relationListCountValue),
1134
                        new ReadonlyField("ViewCount", "View count". ':', $this->ViewCount)
1135
                    )->setName('FilePreviewDataFields')
1136
                )->setName("FilePreviewData")->addExtraClass('cms-file-info-data')
1137
            )->setName("FilePreview")->addExtraClass('cms-file-info')
1138
        );
1139
1140
        $fields->addExtraClass('dmsdocument-documentdetails');
1141
        $urlField->dontEscape = true;
1142
1143
        $this->extend('updateFieldsForFile', $fields);
1144
1145
        return $fields;
1146
    }
1147
1148
    /**
1149
     * Takes a file and adds it to the DMSDocument storage, replacing the
1150
     * current file.
1151
     *
1152
     * @param File $file
1153
     *
1154
     * @return $this
1155
     */
1156
    public function ingestFile($file)
1157
    {
1158
        $this->replaceDocument($file);
1159
        $file->delete();
1160
1161
        return $this;
1162
    }
1163
1164
    /**
1165
     * Get a data list of documents related to this document
1166
     *
1167
     * @return DataList
1168
     */
1169
    public function getRelatedDocuments()
1170
    {
1171
        $documents = $this->RelatedDocuments();
0 ignored issues
show
Bug introduced by
The method RelatedDocuments() does not exist on DMSDocument. Did you maybe mean getRelatedDocuments()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1172
1173
        $this->extend('updateRelatedDocuments', $documents);
1174
1175
        return $documents;
1176
    }
1177
1178
    /**
1179
     * Get a list of related pages for this document by going through the associated document sets
1180
     *
1181
     * @return ArrayList
1182
     */
1183
    public function getRelatedPages()
1184
    {
1185
        $pages = ArrayList::create();
1186
1187
        foreach ($this->Sets() as $documentSet) {
0 ignored issues
show
Documentation Bug introduced by
The method Sets does not exist on object<DMSDocument>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1188
            /** @var DocumentSet $documentSet */
1189
            $pages->add($documentSet->Page());
1190
        }
1191
        $pages->removeDuplicates();
1192
1193
        $this->extend('updateRelatedPages', $pages);
1194
1195
        return $pages;
1196
    }
1197
1198
    /**
1199
     * Get a GridField for managing related documents
1200
     *
1201
     * @return GridField
1202
     */
1203
    protected function getRelatedDocumentsGridField()
1204
    {
1205
        $gridField = GridField::create(
1206
            'RelatedDocuments',
1207
            _t('DMSDocument.RELATEDDOCUMENTS', 'Related Documents'),
1208
            $this->RelatedDocuments(),
0 ignored issues
show
Bug introduced by
The method RelatedDocuments() does not exist on DMSDocument. Did you maybe mean getRelatedDocuments()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1209
            new GridFieldConfig_RelationEditor
1210
        );
1211
1212
        $gridFieldConfig = $gridField->getConfig();
1213
        $gridFieldConfig->removeComponentsByType('GridFieldEditButton');
1214
        $gridFieldConfig->addComponent(new DMSGridFieldEditButton(), 'GridFieldDeleteAction');
1215
1216
        $gridField->getConfig()->removeComponentsByType('GridFieldAddNewButton');
1217
        // Move the autocompleter to the left
1218
        $gridField->getConfig()->removeComponentsByType('GridFieldAddExistingAutocompleter');
1219
        $gridField->getConfig()->addComponent(
1220
            $addExisting = new GridFieldAddExistingAutocompleter('buttons-before-left')
1221
        );
1222
1223
        // Ensure that current document doesn't get returned in the autocompleter
1224
        $addExisting->setSearchList($this->getRelatedDocumentsForAutocompleter());
1225
1226
        // Restrict search fields to specific fields only
1227
        $addExisting->setSearchFields(array('Title:PartialMatch', 'Filename:PartialMatch'));
1228
        $addExisting->setResultsFormat('$Filename');
1229
1230
        $this->extend('updateRelatedDocumentsGridField', $gridField);
1231
        return $gridField;
1232
    }
1233
1234
    /**
1235
     * Get the list of documents to show in "related documents". This can be modified via the extension point, for
1236
     * example if you wanted to exclude embargoed documents or something similar.
1237
     *
1238
     * @return DataList
1239
     */
1240
    protected function getRelatedDocumentsForAutocompleter()
1241
    {
1242
        $documents = DMSDocument::get()->exclude('ID', $this->ID);
1243
        $this->extend('updateRelatedDocumentsForAutocompleter', $documents);
1244
        return $documents;
1245
    }
1246
1247
    /**
1248
     * Checks at least one group is selected if CanViewType || CanEditType == 'OnlyTheseUsers'
1249
     *
1250
     * @return ValidationResult
1251
     */
1252
    protected function validate()
1253
    {
1254
        $valid = parent::validate();
1255
1256
        if ($this->CanViewType == 'OnlyTheseUsers' && !$this->ViewerGroups()->count()) {
0 ignored issues
show
Documentation Bug introduced by
The method ViewerGroups does not exist on object<DMSDocument>? 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...
1257
            $valid->error(
1258
                _t(
1259
                    'DMSDocument.VALIDATIONERROR_NOVIEWERSELECTED',
1260
                    "Selecting 'Only these people' from a viewers list needs at least one group selected."
1261
                )
1262
            );
1263
        }
1264
1265
        if ($this->CanEditType == 'OnlyTheseUsers' && !$this->EditorGroups()->count()) {
0 ignored issues
show
Documentation Bug introduced by
The method EditorGroups does not exist on object<DMSDocument>? 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...
1266
            $valid->error(
1267
                _t(
1268
                    'DMSDocument.VALIDATIONERROR_NOEDITORSELECTED',
1269
                    "Selecting 'Only these people' from a editors list needs at least one group selected."
1270
                )
1271
            );
1272
        }
1273
1274
        return $valid;
1275
    }
1276
1277
    /**
1278
     * Returns a reason as to why this document cannot be viewed.
1279
     *
1280
     * @return string
1281
     */
1282
    public function getPermissionDeniedReason()
1283
    {
1284
        $result = '';
1285
1286
        if ($this->CanViewType == 'LoggedInUsers') {
1287
            $result = _t('DMSDocument.PERMISSIONDENIEDREASON_LOGINREQUIRED', 'Please log in to view this document');
1288
        }
1289
1290
        if ($this->CanViewType == 'OnlyTheseUsers') {
1291
            $result = _t(
1292
                'DMSDocument.PERMISSIONDENIEDREASON_NOTAUTHORISED',
1293
                'You are not authorised to view this document'
1294
            );
1295
        }
1296
1297
        return $result;
1298
    }
1299
1300
    /**
1301
     * Add an "action panel" task
1302
     *
1303
     * @param  string $panelKey
1304
     * @param  string $title
1305
     * @return $this
1306
     */
1307
    public function addActionPanelTask($panelKey, $title)
1308
    {
1309
        $this->actionTasks[$panelKey] = $title;
1310
        return $this;
1311
    }
1312
1313
    /**
1314
     * Returns a HTML representation of the action tasks for the CMS
1315
     *
1316
     * @return string
1317
     */
1318
    public function getActionTaskHtml()
1319
    {
1320
        $html = '<div class="field dmsdocment-actions">'
1321
            . '<label class="left">' . _t('DMSDocument.ACTIONS_LABEL', 'Actions') . '</label>'
1322
            . '<ul>';
1323
1324
        foreach ($this->actionTasks as $panelKey => $title) {
1325
            $panelKey = Convert::raw2xml($panelKey);
1326
            $title = Convert::raw2xml($title);
1327
1328
            $html .= '<li class="ss-ui-button dmsdocument-action" data-panel="' . $panelKey . '">'
1329
                . _t('DMSDocument.ACTION_' . strtoupper($panelKey), $title)
0 ignored issues
show
Bug introduced by
It seems like $title defined by \Convert::raw2xml($title) on line 1326 can also be of type array; however, _t() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1330
                . '</li>';
1331
        }
1332
1333
        $html .= '</ul></div>';
1334
1335
        return $html;
1336
    }
1337
1338
    /**
1339
     * Removes an "action panel" tasks
1340
     *
1341
     * @param  string $panelKey
1342
     * @return $this
1343
     */
1344
    public function removeActionPanelTask($panelKey)
1345
    {
1346
        if (array_key_exists($panelKey, $this->actionTasks)) {
1347
            unset($this->actionTasks[$panelKey]);
1348
        }
1349
        return $this;
1350
    }
1351
}
1352