Completed
Push — master ( e4ab2b...e8d46a )
by Franco
24s queued 18s
created

DMSDocument::getRelatedDocumentsGridField()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 26
rs 8.8571
c 0
b 0
f 0
cc 1
eloc 14
nc 1
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
        'ViewCount' => 'ViewCount',
77
        'getRelatedPages.count' => 'Page Use'
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
        if (!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
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
120 View Code Duplication
        if ($member && Permission::checkMember($member, array(
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...
121
                'ADMIN',
122
                'SITETREE_EDIT_ALL',
123
                'SITETREE_VIEW_ALL',
124
            ))
125
        ) {
126
            return true;
127
        }
128
129
        if ($this->isHidden()) {
130
            return false;
131
        }
132
133
        if ($this->CanViewType == 'LoggedInUsers') {
134
            return $member && $member->exists();
135
        }
136
137
        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...
138
            return ($member && $member->inGroups($this->ViewerGroups()));
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...
139
        }
140
141
        return $this->canEdit($member);
142
    }
143
144
    public function canEdit($member = null)
145
    {
146
        if (!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
147
            $member = Member::currentUser();
148
        }
149
150
        $results = $this->extend('canEdit', $member);
151
152
        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...
153
            if (!min($results)) {
154
                return false;
155
            }
156
        }
157
158
        // Do early admin check
159 View Code Duplication
        if ($member && Permission::checkMember(
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...
160
            $member,
161
            array(
162
                    'ADMIN',
163
                    'SITETREE_EDIT_ALL',
164
                    'SITETREE_VIEW_ALL',
165
                )
166
        )
167
        ) {
168
            return true;
169
        }
170
171
        if ($this->CanEditType === 'LoggedInUsers') {
172
            return $member && $member->exists();
173
        }
174
175
        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...
176
            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...
177
        }
178
179
        return ($member && Permission::checkMember($member, array('ADMIN', 'SITETREE_EDIT_ALL')));
180
    }
181
182
    /**
183
     * @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...
184
     *
185
     * @return boolean
186
     */
187 View Code Duplication
    public function canCreate($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...
188
    {
189
        if (!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
190
            $member = Member::currentUser();
191
        }
192
193
        $results = $this->extend('canCreate', $member);
194
195
        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...
196
            if (!min($results)) {
197
                return false;
198
            }
199
        }
200
201
        return $this->canEdit($member);
202
    }
203
204
    /**
205
     * @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...
206
     *
207
     * @return boolean
208
     */
209 View Code Duplication
    public function canDelete($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...
210
    {
211
        if (!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
212
            $member = Member::currentUser();
213
        }
214
215
        $results = $this->extend('canDelete', $member);
216
217
        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...
218
            if (!min($results)) {
219
                return false;
220
            }
221
        }
222
223
        return $this->canView();
224
    }
225
226
    /**
227
     * Increase ViewCount by 1, without update any other record fields such as
228
     * LastEdited.
229
     *
230
     * @return DMSDocument
231
     */
232
    public function trackView()
233
    {
234
        if ($this->ID > 0) {
235
            $count = $this->ViewCount + 1;
236
237
            $this->ViewCount = $count;
238
239
            DB::query("UPDATE \"DMSDocument\" SET \"ViewCount\"='$count' WHERE \"ID\"={$this->ID}");
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
        $result = Controller::join_links(Director::baseURL(), 'dmsdocument/' . $this->ID);
264
        if (!$this->canView()) {
265
            $result = sprintf("javascript:alert('%s')", $this->getPermissionDeniedReason());
266
        }
267
268
        $this->extend('updateGetLink', $result);
269
270
        return $result;
271
    }
272
273
    /**
274
     * @return string
275
     */
276
    public function Link()
277
    {
278
        return $this->getLink();
279
    }
280
281
    /**
282
     * Hides the document, so it does not show up when getByPage($myPage) is
283
     * called (without specifying the $showEmbargoed = true parameter).
284
     *
285
     * This is similar to expire, except that this method should be used to hide
286
     * documents that have not yet gone live.
287
     *
288
     * @param bool $write Save change to the database
289
     *
290
     * @return DMSDocument
291
     */
292
    public function embargoIndefinitely($write = true)
293
    {
294
        $this->EmbargoedIndefinitely = true;
295
296
        if ($write) {
297
            $this->write();
298
        }
299
300
        return $this;
301
    }
302
303
    /**
304
     * Hides the document until any page it is linked to is published
305
     *
306
     * @param bool $write Save change to database
307
     *
308
     * @return DMSDocument
309
     */
310
    public function embargoUntilPublished($write = true)
311
    {
312
        $this->EmbargoedUntilPublished = true;
313
314
        if ($write) {
315
            $this->write();
316
        }
317
318
        return $this;
319
    }
320
321
    /**
322
     * Returns if this is Document is embargoed or expired.
323
     *
324
     * Also, returns if the document should be displayed on the front-end,
325
     * respecting the current reading mode of the site and the embargo status.
326
     *
327
     * I.e. if a document is embargoed until published, then it should still
328
     * show up in draft mode.
329
     *
330
     * @return bool
331
     */
332
    public function isHidden()
333
    {
334
        $hidden = $this->isEmbargoed() || $this->isExpired();
335
        $readingMode = Versioned::get_reading_mode();
336
337
        if ($readingMode == "Stage.Stage") {
338
            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...
339
                $hidden = false;
340
            }
341
        }
342
343
        return $hidden;
344
    }
345
346
    /**
347
     * Returns if this is Document is embargoed.
348
     *
349
     * @return bool
350
     */
351
    public function isEmbargoed()
352
    {
353
        if (is_object($this->EmbargoedUntilDate)) {
354
            $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...
355
        }
356
357
        $embargoed = false;
358
359
        if ($this->EmbargoedIndefinitely) {
360
            $embargoed = true;
361
        } elseif ($this->EmbargoedUntilPublished) {
362
            $embargoed = true;
363
        } elseif (!empty($this->EmbargoedUntilDate)) {
364
            if (SS_Datetime::now()->Value < $this->EmbargoedUntilDate) {
365
                $embargoed = true;
366
            }
367
        }
368
369
        return $embargoed;
370
    }
371
372
    /**
373
     * Hides the document, so it does not show up when getByPage($myPage) is
374
     * called. Automatically un-hides the Document at a specific date.
375
     *
376
     * @param string $datetime date time value when this Document should expire.
377
     * @param bool $write
378
     *
379
     * @return DMSDocument
380
     */
381 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...
382
    {
383
        $this->EmbargoedUntilDate = DBField::create_field('SS_Datetime', $datetime)->Format('Y-m-d H:i:s');
384
385
        if ($write) {
386
            $this->write();
387
        }
388
389
        return $this;
390
    }
391
392
    /**
393
     * Clears any previously set embargos, so the Document always shows up in
394
     * all queries.
395
     *
396
     * @param bool $write
397
     *
398
     * @return DMSDocument
399
     */
400
    public function clearEmbargo($write = true)
401
    {
402
        $this->EmbargoedIndefinitely = false;
403
        $this->EmbargoedUntilPublished = false;
404
        $this->EmbargoedUntilDate = null;
405
406
        if ($write) {
407
            $this->write();
408
        }
409
410
        return $this;
411
    }
412
413
    /**
414
     * Returns if this is Document is expired.
415
     *
416
     * @return bool
417
     */
418
    public function isExpired()
419
    {
420
        if (is_object($this->ExpireAtDate)) {
421
            $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...
422
        }
423
424
        $expired = false;
425
426
        if (!empty($this->ExpireAtDate)) {
427
            if (SS_Datetime::now()->Value >= $this->ExpireAtDate) {
428
                $expired = true;
429
            }
430
        }
431
432
        return $expired;
433
    }
434
435
    /**
436
     * Hides the document at a specific date, so it does not show up when
437
     * getByPage($myPage) is called.
438
     *
439
     * @param string $datetime date time value when this Document should expire
440
     * @param bool $write
441
     *
442
     * @return DMSDocument
443
     */
444 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...
445
    {
446
        $this->ExpireAtDate = DBField::create_field('SS_Datetime', $datetime)->Format('Y-m-d H:i:s');
447
448
        if ($write) {
449
            $this->write();
450
        }
451
452
        return $this;
453
    }
454
455
    /**
456
     * Clears any previously set expiry.
457
     *
458
     * @param bool $write
459
     *
460
     * @return DMSDocument
461
     */
462
    public function clearExpiry($write = true)
463
    {
464
        $this->ExpireAtDate = null;
465
466
        if ($write) {
467
            $this->write();
468
        }
469
470
        return $this;
471
    }
472
473
    /**
474
     * Returns a DataList of all previous Versions of this document (check the
475
     * LastEdited date of each object to find the correct one).
476
     *
477
     * If {@link DMSDocument_versions::$enable_versions} is disabled then an
478
     * Exception is thrown
479
     *
480
     * @throws Exception
481
     *
482
     * @return DataList List of Document objects
483
     */
484
    public function getVersions()
485
    {
486
        if (!DMSDocument_versions::$enable_versions) {
487
            throw new Exception("DMSDocument versions are disabled");
488
        }
489
490
        return DMSDocument_versions::get_versions($this);
491
    }
492
493
    /**
494
     * Returns the full filename of the document stored in this object.
495
     *
496
     * @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...
497
     */
498
    public function getFullPath()
499
    {
500
        if ($this->Filename) {
501
            return DMS::inst()->getStoragePath() . DIRECTORY_SEPARATOR
502
                . $this->Folder . DIRECTORY_SEPARATOR . $this->Filename;
503
        }
504
505
        return null;
506
    }
507
508
    /**
509
     * Returns the filename of this asset.
510
     *
511
     * @return string
512
     */
513
    public function getFileName()
514
    {
515
        if ($this->getField('Filename')) {
516
            return $this->getField('Filename');
517
        } else {
518
            return ASSETS_DIR . '/';
519
        }
520
    }
521
522
    /**
523
     * @return string
524
     */
525
    public function getName()
526
    {
527
        return $this->getField('Title');
528
    }
529
530
531
    /**
532
     * @return string
533
     */
534
    public function getFilenameWithoutID()
535
    {
536
        $filenameParts = explode('~', $this->Filename);
537
        $filename = array_pop($filenameParts);
538
539
        return $filename;
540
    }
541
542
    /**
543
     * @return string
544
     */
545
    public function getStorageFolder()
546
    {
547
        return DMS::inst()->getStoragePath() . DIRECTORY_SEPARATOR . DMS::inst()->getStorageFolder($this->ID);
548
    }
549
550
    /**
551
     * Deletes the DMSDocument and its underlying file. Also calls the parent DataObject's delete method in
552
     * order to complete an cascade.
553
     *
554
     * @return void
555
     */
556
    public function delete()
557
    {
558
        // delete the file (and previous versions of files)
559
        $filesToDelete = array();
560
        $storageFolder = $this->getStorageFolder();
561
562
        if (file_exists($storageFolder)) {
563
            if ($handle = opendir($storageFolder)) {
564
                while (false !== ($entry = readdir($handle))) {
565
                    // only delete if filename starts the the relevant ID
566
                    if (strpos($entry, $this->ID.'~') === 0) {
567
                        $filesToDelete[] = $entry;
568
                    }
569
                }
570
571
                closedir($handle);
572
573
                //delete all this files that have the id of this document
574
                foreach ($filesToDelete as $file) {
575
                    $filePath = $storageFolder .DIRECTORY_SEPARATOR . $file;
576
577
                    if (is_file($filePath)) {
578
                        unlink($filePath);
579
                    }
580
                }
581
            }
582
        }
583
584
        // get rid of any versions have saved for this DMSDocument, too
585
        if (DMSDocument_versions::$enable_versions) {
586
            $versions = $this->getVersions();
587
588
            if ($versions->Count() > 0) {
589
                foreach ($versions as $v) {
590
                    $v->delete();
591
                }
592
            }
593
        }
594
595
        return parent::delete();
596
    }
597
598
    /**
599
     * Relate an existing file on the filesystem to the document.
600
     *
601
     * Copies the file to the new destination, as defined in {@link DMS::getStoragePath()}.
602
     *
603
     * @param string $filePath Path to file, relative to webroot.
604
     *
605
     * @return DMSDocument
606
     */
607
    public function storeDocument($filePath)
608
    {
609
        if (empty($this->ID)) {
610
            user_error("Document must be written to database before it can store documents", E_USER_ERROR);
611
        }
612
613
        // calculate all the path to copy the file to
614
        $fromFilename = basename($filePath);
615
        $toFilename = $this->ID. '~' . $fromFilename; //add the docID to the start of the Filename
616
        $toFolder = DMS::inst()->getStorageFolder($this->ID);
617
        $toPath = DMS::inst()->getStoragePath() . DIRECTORY_SEPARATOR . $toFolder . DIRECTORY_SEPARATOR . $toFilename;
618
619
        DMS::inst()->createStorageFolder(DMS::inst()->getStoragePath() . DIRECTORY_SEPARATOR . $toFolder);
620
621
        //copy the file into place
622
        $fromPath = BASE_PATH . DIRECTORY_SEPARATOR . $filePath;
623
624
        //version the existing file (copy it to a new "very specific" filename
625
        if (DMSDocument_versions::$enable_versions) {
626
            DMSDocument_versions::create_version($this);
627
        } else {    //otherwise delete the old document file
628
            $oldPath = $this->getFullPath();
629
            if (file_exists($oldPath)) {
630
                unlink($oldPath);
631
            }
632
        }
633
634
        copy($fromPath, $toPath);   //this will overwrite the existing file (if present)
635
636
        //write the filename of the stored document
637
        $this->Filename = $toFilename;
638
        $this->Folder = strval($toFolder);
639
640
        $extension = pathinfo($this->Filename, PATHINFO_EXTENSION);
641
642
        if (empty($this->Title)) {
643
            // don't overwrite existing document titles
644
            $this->Title = basename($filePath, '.'.$extension);
645
        }
646
647
        $this->write();
648
649
        return $this;
650
    }
651
652
    /**
653
     * Takes a File object or a String (path to a file) and copies it into the
654
     * DMS, replacing the original document file but keeping the rest of the
655
     * document unchanged.
656
     *
657
     * @param File|string $file path to a file to store
658
     *
659
     * @return DMSDocument object that we replaced the file in
660
     */
661
    public function replaceDocument($file)
662
    {
663
        $filePath = DMS::inst()->transformFileToFilePath($file);
664
        $doc = $this->storeDocument($filePath); // replace the document
665
666
        return $doc;
667
    }
668
669
670
    /**
671
     * Return the type of file for the given extension
672
     * on the current file name.
673
     *
674
     * @param string $ext
675
     *
676
     * @return string
677
     */
678
    public static function get_file_type($ext)
679
    {
680
        $types = array(
681
            'gif' => 'GIF image - good for diagrams',
682
            'jpg' => 'JPEG image - good for photos',
683
            'jpeg' => 'JPEG image - good for photos',
684
            'png' => 'PNG image - good general-purpose format',
685
            'ico' => 'Icon image',
686
            'tiff' => 'Tagged image format',
687
            'doc' => 'Word document',
688
            'xls' => 'Excel spreadsheet',
689
            'zip' => 'ZIP compressed file',
690
            'gz' => 'GZIP compressed file',
691
            'dmg' => 'Apple disk image',
692
            'pdf' => 'Adobe Acrobat PDF file',
693
            'mp3' => 'MP3 audio file',
694
            'wav' => 'WAV audo file',
695
            'avi' => 'AVI video file',
696
            'mpg' => 'MPEG video file',
697
            'mpeg' => 'MPEG video file',
698
            'js' => 'Javascript file',
699
            'css' => 'CSS file',
700
            'html' => 'HTML file',
701
            'htm' => 'HTML file'
702
        );
703
704
        return isset($types[$ext]) ? $types[$ext] : $ext;
705
    }
706
707
708
    /**
709
     * Returns the Description field with HTML <br> tags added when there is a
710
     * line break.
711
     *
712
     * @return string
713
     */
714
    public function getDescriptionWithLineBreak()
715
    {
716
        return nl2br($this->getField('Description'));
717
    }
718
719
    /**
720
     * @return FieldList
721
     */
722
    public function getCMSFields()
723
    {
724
        //include JS to handling showing and hiding of bottom "action" tabs
725
        Requirements::javascript(DMS_DIR . '/javascript/DMSDocumentCMSFields.js');
726
        Requirements::css(DMS_DIR . '/dist/css/cmsbundle.css');
727
728
        $fields = new FieldList();  //don't use the automatic scaffolding, it is slow and unnecessary here
729
730
        $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...
731
732
        //get list of shortcode page relations
733
        $relationFinder = new ShortCodeRelationFinder();
734
        $relationList = $relationFinder->getList($this->ID);
735
736
        $fieldsTop = $this->getFieldsForFile($relationList->count());
737
        $fields->add($fieldsTop);
738
739
        $fields->add(TextField::create('Title', _t('DMSDocument.TITLE', 'Title')));
740
        $fields->add(TextareaField::create('Description', _t('DMSDocument.DESCRIPTION', 'Description')));
741
742
        $coverImageField = UploadField::create('CoverImage', _t('DMSDocument.COVERIMAGE', 'Cover Image'));
743
        $coverImageField->getValidator()->setAllowedExtensions(array('jpg', 'jpeg', 'png', 'gif'));
744
        $coverImageField->setConfig('allowedMaxFileNumber', 1);
745
        $fields->add($coverImageField);
746
747
748
        $downloadBehaviorSource = array(
749
            'open' => _t('DMSDocument.OPENINBROWSER', 'Open in browser'),
750
            'download' => _t('DMSDocument.FORCEDOWNLOAD', 'Force download'),
751
        );
752
        $defaultDownloadBehaviour = Config::inst()->get('DMSDocument', 'default_download_behaviour');
753
        if (!isset($downloadBehaviorSource[$defaultDownloadBehaviour])) {
754
            user_error('Default download behaviour "' . $defaultDownloadBehaviour . '" not supported.', E_USER_WARNING);
755
        } else {
756
            $downloadBehaviorSource[$defaultDownloadBehaviour] .= ' (' . _t('DMSDocument.DEFAULT', 'default') . ')';
757
        }
758
759
        $fields->add(
760
            OptionsetField::create(
761
                'DownloadBehavior',
762
                _t('DMSDocument.DOWNLOADBEHAVIOUR', 'Download behavior'),
763
                $downloadBehaviorSource,
764
                $defaultDownloadBehaviour
765
            )
766
            ->setDescription(
767
                'How the visitor will view this file. <strong>Open in browser</strong> '
768
                . 'allows files to be opened in a new tab.'
769
            )
770
        );
771
772
        //create upload field to replace document
773
        $uploadField = new DMSUploadField('ReplaceFile', 'Replace file');
774
        $uploadField->setConfig('allowedMaxFileNumber', 1);
775
        $uploadField->setConfig('downloadTemplateName', 'ss-dmsuploadfield-downloadtemplate');
776
        $uploadField->setRecord($this);
777
778
        $gridFieldConfig = GridFieldConfig::create()->addComponents(
779
            new GridFieldToolbarHeader(),
780
            new GridFieldSortableHeader(),
781
            new GridFieldDataColumns(),
782
            new GridFieldPaginator(30),
783
            //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...
784
            new GridFieldDetailForm()
785
        );
786
787
        $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...
788
            ->setDisplayFields(array(
789
                'Title'=>'Title',
790
                'ClassName'=>'Page Type',
791
                'ID'=>'Page ID'
792
            ))
793
            ->setFieldFormatting(array(
794
                'Title'=>sprintf(
795
                    '<a class=\"cms-panel-link\" href=\"%s/$ID\">$Title</a>',
796
                    singleton('CMSPageEditController')->Link('show')
797
                )
798
            ));
799
800
        $pagesGrid = GridField::create(
801
            'Pages',
802
            _t('DMSDocument.RelatedPages', 'Related Pages'),
803
            $this->getRelatedPages(),
804
            $gridFieldConfig
805
        );
806
807
        $referencesGrid = GridField::create(
808
            'References',
809
            _t('DMSDocument.RelatedReferences', 'Related References'),
810
            $relationList,
811
            $gridFieldConfig
812
        );
813
814
        if (DMSDocument_versions::$enable_versions) {
815
            $versionsGridFieldConfig = GridFieldConfig::create()->addComponents(
816
                new GridFieldToolbarHeader(),
817
                new GridFieldSortableHeader(),
818
                new GridFieldDataColumns(),
819
                new GridFieldPaginator(30)
820
            );
821
            $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...
822
                ->setDisplayFields(Config::inst()->get('DMSDocument_versions', 'display_fields'))
823
                ->setFieldFormatting(
824
                    array(
825
                        'FilenameWithoutID' => '<a target=\'_blank\' class=\'file-url\' href=\'$Link\'>'
826
                            . '$FilenameWithoutID</a>'
827
                    )
828
                );
829
830
            $versionsGrid =  GridField::create(
831
                'Versions',
832
                _t('DMSDocument.Versions', 'Versions'),
833
                $this->getVersions(),
834
                $versionsGridFieldConfig
835
            );
836
            $this->addActionPanelTask('find-versions', 'Versions');
837
        }
838
839
        $fields->add(LiteralField::create('BottomTaskSelection', $this->getActionTaskHtml()));
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($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...
896
            FieldGroup::create($this->getRelatedDocumentsGridField())->addExtraClass('find-relateddocuments'),
897
            FieldGroup::create($this->getPermissionsActionPanel())->addExtraClass('permissions')
898
        );
899
900
        $actionsPanel->setName("ActionsPanel");
901
        $actionsPanel->addExtraClass("DMSDocumentActionsPanel");
902
        $fields->push($actionsPanel);
903
904
        $this->extend('updateCMSFields', $fields);
905
906
        return $fields;
907
    }
908
909
    /**
910
     * Adds permissions selection fields to a composite field and returns so it can be used in the "actions panel"
911
     *
912
     * @return CompositeField
913
     */
914
    public function getPermissionsActionPanel()
915
    {
916
        $fields = FieldList::create();
917
        $showFields = array(
918
            'CanViewType'  => '',
919
            'ViewerGroups' => 'hide',
920
            'CanEditType'  => '',
921
            'EditorGroups' => 'hide',
922
        );
923
        /** @var SiteTree $siteTree */
924
        $siteTree = singleton('SiteTree');
925
        $settingsFields = $siteTree->getSettingsFields();
926
927
        foreach ($showFields as $name => $extraCss) {
928
            $compositeName = "Root.Settings.$name";
929
            /** @var FormField $field */
930
            if ($field = $settingsFields->fieldByName($compositeName)) {
931
                $field->addExtraClass($extraCss);
932
                $title = str_replace('page', 'document', $field->Title());
933
                $field->setTitle($title);
934
935
                // Remove Inherited source option from DropdownField
936
                if ($field instanceof DropdownField) {
937
                    $options = $field->getSource();
938
                    unset($options['Inherit']);
939
                    $field->setSource($options);
940
                }
941
                $fields->push($field);
942
            }
943
        }
944
945
        $this->extend('updatePermissionsFields', $fields);
946
947
        return CompositeField::create($fields);
948
    }
949
950
    /**
951
     * Return a title to use on the frontend, preferably the "title", otherwise the filename without it's numeric ID
952
     *
953
     * @return string
954
     */
955
    public function getTitle()
956
    {
957
        if ($this->getField('Title')) {
958
            return $this->getField('Title');
959
        }
960
        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...
961
    }
962
963
    public function onBeforeWrite()
964
    {
965
        parent::onBeforeWrite();
966
967
        if (isset($this->Embargo)) {
968
            //set the embargo options from the OptionSetField created in the getCMSFields method
969
            //do not write after clearing the embargo (write happens automatically)
970
            $savedDate = $this->EmbargoedUntilDate;
971
            $this->clearEmbargo(false); // Clear all previous settings and re-apply them on save
972
973
            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...
974
                $this->embargoUntilPublished(false);
975
            }
976
            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...
977
                $this->embargoIndefinitely(false);
978
            }
979
            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...
980
                $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...
981
            }
982
        }
983
984
        if (isset($this->Expiry)) {
985
            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...
986
                $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...
987
            } else {
988
                $this->clearExpiry(false);
989
            } // Clear all previous settings
990
        }
991
992
        // Set user fields
993
        if ($currentUserID = Member::currentUserID()) {
994
            if (!$this->CreatedByID) {
995
                $this->CreatedByID = $currentUserID;
996
            }
997
            $this->LastEditedByID = $currentUserID;
998
        }
999
    }
1000
1001
    /**
1002
     * Return the relative URL of an icon for the file type, based on the
1003
     * {@link appCategory()} value.
1004
     *
1005
     * Images are searched for in "dms/images/app_icons/".
1006
     *
1007
     * @return string
1008
     */
1009
    public function Icon($ext)
1010
    {
1011
        if (!Director::fileExists(DMS_DIR."/images/app_icons/{$ext}_32.png")) {
1012
            $ext = File::get_app_category($ext);
1013
        }
1014
1015
        if (!Director::fileExists(DMS_DIR."/images/app_icons/{$ext}_32.png")) {
1016
            $ext = "generic";
1017
        }
1018
1019
        return DMS_DIR."/images/app_icons/{$ext}_32.png";
1020
    }
1021
1022
    /**
1023
     * Return the extension of the file associated with the document
1024
     *
1025
     * @return string
1026
     */
1027
    public function getExtension()
1028
    {
1029
        return strtolower(pathinfo($this->Filename, PATHINFO_EXTENSION));
1030
    }
1031
1032
    /**
1033
     * @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...
1034
     */
1035
    public function getSize()
1036
    {
1037
        $size = $this->getAbsoluteSize();
1038
        return ($size) ? File::format_size($size) : false;
1039
    }
1040
1041
    /**
1042
     * Return the size of the file associated with the document.
1043
     *
1044
     * @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...
1045
     */
1046
    public function getAbsoluteSize()
1047
    {
1048
        return file_exists($this->getFullPath()) ? filesize($this->getFullPath()) : null;
1049
    }
1050
1051
    /**
1052
     * An alias to DMSDocument::getSize()
1053
     *
1054
     * @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...
1055
     */
1056
    public function getFileSizeFormatted()
1057
    {
1058
        return $this->getSize();
1059
    }
1060
1061
1062
    /**
1063
     * @return FieldList
0 ignored issues
show
Documentation introduced by
Should the return type not be FieldGroup?

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...
1064
     */
1065
    protected function getFieldsForFile($relationListCount)
1066
    {
1067
        $extension = $this->getExtension();
1068
1069
        $previewField = new LiteralField(
1070
            "ImageFull",
1071
            "<img id='thumbnailImage' class='thumbnail-preview' src='{$this->Icon($extension)}?r="
1072
            . rand(1, 100000) . "' alt='{$this->Title}' />\n"
1073
        );
1074
1075
        //count the number of pages this document is published on
1076
        $publishedOnCount = $this->getRelatedPages()->count();
1077
        $publishedOnValue = "$publishedOnCount pages";
1078
        if ($publishedOnCount == 1) {
1079
            $publishedOnValue = "$publishedOnCount page";
1080
        }
1081
1082
        $relationListCountValue = "$relationListCount pages";
1083
        if ($relationListCount == 1) {
1084
            $relationListCountValue = "$relationListCount page";
1085
        }
1086
1087
        $fields = new FieldGroup(
1088
            $filePreview = CompositeField::create(
1089
                CompositeField::create(
1090
                    $previewField
1091
                )->setName("FilePreviewImage")->addExtraClass('cms-file-info-preview'),
1092
                CompositeField::create(
1093
                    CompositeField::create(
1094
                        new ReadonlyField("ID", "ID number". ':', $this->ID),
1095
                        new ReadonlyField(
1096
                            "FileType",
1097
                            _t('AssetTableField.TYPE', 'File type') . ':',
1098
                            self::get_file_type($extension)
1099
                        ),
1100
                        new ReadonlyField(
1101
                            "Size",
1102
                            _t('AssetTableField.SIZE', 'File size') . ':',
1103
                            $this->getFileSizeFormatted()
1104
                        ),
1105
                        $urlField = new ReadonlyField(
1106
                            'ClickableURL',
1107
                            _t('AssetTableField.URL', 'URL'),
1108
                            sprintf(
1109
                                '<a href="%s" target="_blank" class="file-url">%s</a>',
1110
                                $this->getLink(),
1111
                                $this->getLink()
1112
                            )
1113
                        ),
1114
                        new ReadonlyField("FilenameWithoutIDField", "Filename". ':', $this->getFilenameWithoutID()),
1115
                        new DateField_Disabled(
1116
                            "Created",
1117
                            _t('AssetTableField.CREATED', 'First uploaded') . ':',
1118
                            $this->Created
1119
                        ),
1120
                        new DateField_Disabled(
1121
                            "LastEdited",
1122
                            _t('AssetTableField.LASTEDIT', 'Last changed') . ':',
1123
                            $this->LastEdited
1124
                        ),
1125
                        new ReadonlyField("PublishedOn", "Published on". ':', $publishedOnValue),
1126
                        new ReadonlyField("ReferencedOn", "Referenced on". ':', $relationListCountValue),
1127
                        new ReadonlyField("ViewCount", "View count". ':', $this->ViewCount)
1128
                    )
1129
                )->setName("FilePreviewData")->addExtraClass('cms-file-info-data')
1130
            )->setName("FilePreview")->addExtraClass('cms-file-info')
1131
        );
1132
1133
        $fields->setName('FileP');
1134
        $urlField->dontEscape = true;
1135
1136
        return $fields;
1137
    }
1138
1139
    /**
1140
     * Takes a file and adds it to the DMSDocument storage, replacing the
1141
     * current file.
1142
     *
1143
     * @param File $file
1144
     *
1145
     * @return $this
1146
     */
1147
    public function ingestFile($file)
1148
    {
1149
        $this->replaceDocument($file);
1150
        $file->delete();
1151
1152
        return $this;
1153
    }
1154
1155
    /**
1156
     * Get a data list of documents related to this document
1157
     *
1158
     * @return DataList
1159
     */
1160
    public function getRelatedDocuments()
1161
    {
1162
        $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...
1163
1164
        $this->extend('updateRelatedDocuments', $documents);
1165
1166
        return $documents;
1167
    }
1168
1169
    /**
1170
     * Get a list of related pages for this document by going through the associated document sets
1171
     *
1172
     * @return ArrayList
1173
     */
1174
    public function getRelatedPages()
1175
    {
1176
        $pages = ArrayList::create();
1177
1178
        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...
1179
            /** @var DocumentSet $documentSet */
1180
            $pages->add($documentSet->Page());
1181
        }
1182
        $pages->removeDuplicates();
1183
1184
        $this->extend('updateRelatedPages', $pages);
1185
1186
        return $pages;
1187
    }
1188
1189
    /**
1190
     * Get a GridField for managing related documents
1191
     *
1192
     * @return GridField
1193
     */
1194
    protected function getRelatedDocumentsGridField()
1195
    {
1196
        $gridField = GridField::create(
1197
            'RelatedDocuments',
1198
            _t('DMSDocument.RELATEDDOCUMENTS', 'Related Documents'),
1199
            $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...
1200
            new GridFieldConfig_RelationEditor
1201
        );
1202
1203
        $gridField->getConfig()->removeComponentsByType('GridFieldAddNewButton');
1204
        // Move the autocompleter to the left
1205
        $gridField->getConfig()->removeComponentsByType('GridFieldAddExistingAutocompleter');
1206
        $gridField->getConfig()->addComponent(
1207
            $addExisting = new GridFieldAddExistingAutocompleter('buttons-before-left')
1208
        );
1209
1210
        // Ensure that current document doesn't get returned in the autocompleter
1211
        $addExisting->setSearchList($this->getRelatedDocumentsForAutocompleter());
1212
1213
        // Restrict search fields to specific fields only
1214
        $addExisting->setSearchFields(array('Title', 'Filename'));
1215
1216
        $this->extend('updateRelatedDocumentsGridField', $gridField);
1217
1218
        return $gridField;
1219
    }
1220
1221
    /**
1222
     * Get the list of documents to show in "related documents". This can be modified via the extension point, for
1223
     * example if you wanted to exclude embargoed documents or something similar.
1224
     *
1225
     * @return DataList
1226
     */
1227
    protected function getRelatedDocumentsForAutocompleter()
1228
    {
1229
        $documents = DMSDocument::get()->exclude('ID', $this->ID);
1230
        $this->extend('updateRelatedDocumentsForAutocompleter', $documents);
1231
        return $documents;
1232
    }
1233
1234
    /**
1235
     * Checks at least one group is selected if CanViewType || CanEditType == 'OnlyTheseUsers'
1236
     *
1237
     * @return ValidationResult
1238
     */
1239
    protected function validate()
1240
    {
1241
        $valid = parent::validate();
1242
1243
        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...
1244
            $valid->error(
1245
                _t(
1246
                    'DMSDocument.VALIDATIONERROR_NOVIEWERSELECTED',
1247
                    "Selecting 'Only these people' from a viewers list needs at least one group selected."
1248
                )
1249
            );
1250
        }
1251
1252
        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...
1253
            $valid->error(
1254
                _t(
1255
                    'DMSDocument.VALIDATIONERROR_NOEDITORSELECTED',
1256
                    "Selecting 'Only these people' from a editors list needs at least one group selected."
1257
                )
1258
            );
1259
        }
1260
1261
        return $valid;
1262
    }
1263
1264
    /**
1265
     * Returns a reason as to why this document cannot be viewed.
1266
     *
1267
     * @return string
1268
     */
1269
    public function getPermissionDeniedReason()
1270
    {
1271
        $result = '';
1272
1273
        if ($this->CanViewType == 'LoggedInUsers') {
1274
            $result = _t('DMSDocument.PERMISSIONDENIEDREASON_LOGINREQUIRED', 'Please log in to view this document');
1275
        }
1276
1277
        if ($this->CanViewType == 'OnlyTheseUsers') {
1278
            $result = _t(
1279
                'DMSDocument.PERMISSIONDENIEDREASON_NOTAUTHORISED',
1280
                'You are not authorised to view this document'
1281
            );
1282
        }
1283
1284
        return $result;
1285
    }
1286
1287
    /**
1288
     * Add an "action panel" task
1289
     *
1290
     * @param  string $panelKey
1291
     * @param  string $title
1292
     * @return $this
1293
     */
1294
    public function addActionPanelTask($panelKey, $title)
1295
    {
1296
        $this->actionTasks[$panelKey] = $title;
1297
        return $this;
1298
    }
1299
1300
    /**
1301
     * Returns a HTML representation of the action tasks for the CMS
1302
     *
1303
     * @return string
1304
     */
1305
    public function getActionTaskHtml()
1306
    {
1307
        $html = '<div id="Actions" class="field actions">'
1308
            . '<label class="left">' . _t('DMSDocument.ACTIONS_LABEL', 'Actions') . '</label>'
1309
            . '<ul>';
1310
1311
        foreach ($this->actionTasks as $panelKey => $title) {
1312
            $html .= '<li class="ss-ui-button" data-panel="' . $panelKey . '">'
1313
                . _t('DMSDocument.ACTION_' . strtoupper($panelKey), $title)
1314
                . '</li>';
1315
        }
1316
1317
        $html .= '</ul></div>';
1318
1319
        return $html;
1320
    }
1321
}
1322