Completed
Pull Request — master (#168)
by Franco
02:28
created

DMSDocument::getFilename()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
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 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...
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()));
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 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...
159
            $member,
160
            array(
161
                    'ADMIN',
162
                    'SITETREE_EDIT_ALL',
163
                    'SITETREE_VIEW_ALL',
164
                )
165
        )
166
        ) {
167
            return true;
168
        }
169
170
        if ($this->CanEditType === 'LoggedInUsers') {
171
            return $member && $member->exists();
172
        }
173
174
        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...
175
            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...
176
        }
177
178
        return ($member && Permission::checkMember($member, array('ADMIN', 'SITETREE_EDIT_ALL')));
179
    }
180
181
    /**
182
     * @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...
183
     *
184
     * @return boolean
185
     */
186
    public function canCreate($member = null)
187
    {
188 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...
189
            $member = Member::currentUser();
190
        }
191
192
        $results = $this->extend('canCreate', $member);
193
194
        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...
195
            if (!min($results)) {
196
                return false;
197
            }
198
        }
199
200
        // Do early admin check
201
        if ($member &&
202
            Permission::checkMember(
203
                $member,
204
                array(
205
                    'CMS_ACCESS_DMSDocumentAdmin',
206
                )
207
            )
208
        ) {
209
            return true;
210
        }
211
212
        return $this->canEdit($member);
213
    }
214
215
    /**
216
     * @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...
217
     *
218
     * @return boolean
219
     */
220
    public function canDelete($member = null)
221
    {
222 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...
223
            $member = Member::currentUser();
224
        }
225
226
        $results = $this->extend('canDelete', $member);
227
228
        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...
229
            if (!min($results)) {
230
                return false;
231
            }
232
        }
233
234
        return $this->canEdit($member);
235
    }
236
237
    /**
238
     * Increase ViewCount by 1, without update any other record fields such as
239
     * LastEdited.
240
     *
241
     * @return DMSDocument
242
     */
243
    public function trackView()
244
    {
245
        if ($this->ID > 0) {
246
            $count = $this->ViewCount + 1;
247
248
            $this->ViewCount = $count;
249
250
            DB::query("UPDATE \"DMSDocument\" SET \"ViewCount\"='$count' WHERE \"ID\"={$this->ID}");
251
        }
252
253
        return $this;
254
    }
255
256
    /**
257
     * Returns a link to download this document from the DMS store.
258
     * Alternatively a basic javascript alert will be shown should the user not have view permissions. An extension
259
     * point for this was also added.
260
     *
261
     * To extend use the following from within an Extension subclass:
262
     *
263
     * <code>
264
     * public function updateGetLink($result)
265
     * {
266
     *     // Do something here
267
     * }
268
     * </code>
269
     *
270
     * @return string
271
     */
272
    public function getLink()
273
    {
274
        $urlSegment = sprintf('%d-%s', $this->ID, URLSegmentFilter::create()->filter($this->getTitle()));
275
        $result = Controller::join_links(Director::baseURL(), 'dmsdocument/' . $urlSegment);
276
        if (!$this->canView()) {
277
            $result = sprintf("javascript:alert('%s')", $this->getPermissionDeniedReason());
278
        }
279
280
        $this->extend('updateGetLink', $result);
281
282
        return $result;
283
    }
284
285
    /**
286
     * @return string
287
     */
288
    public function Link()
289
    {
290
        return $this->getLink();
291
    }
292
293
    /**
294
     * Hides the document, so it does not show up when getByPage($myPage) is
295
     * called (without specifying the $showEmbargoed = true parameter).
296
     *
297
     * This is similar to expire, except that this method should be used to hide
298
     * documents that have not yet gone live.
299
     *
300
     * @param bool $write Save change to the database
301
     *
302
     * @return DMSDocument
303
     */
304
    public function embargoIndefinitely($write = true)
305
    {
306
        $this->EmbargoedIndefinitely = true;
307
308
        if ($write) {
309
            $this->write();
310
        }
311
312
        return $this;
313
    }
314
315
    /**
316
     * Hides the document until any page it is linked to is published
317
     *
318
     * @param bool $write Save change to database
319
     *
320
     * @return DMSDocument
321
     */
322
    public function embargoUntilPublished($write = true)
323
    {
324
        $this->EmbargoedUntilPublished = true;
325
326
        if ($write) {
327
            $this->write();
328
        }
329
330
        return $this;
331
    }
332
333
    /**
334
     * Returns if this is Document is embargoed or expired.
335
     *
336
     * Also, returns if the document should be displayed on the front-end,
337
     * respecting the current reading mode of the site and the embargo status.
338
     *
339
     * I.e. if a document is embargoed until published, then it should still
340
     * show up in draft mode.
341
     *
342
     * @return bool
343
     */
344
    public function isHidden()
345
    {
346
        $hidden = $this->isEmbargoed() || $this->isExpired();
347
        $readingMode = Versioned::get_reading_mode();
348
349
        if ($readingMode == "Stage.Stage") {
350
            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...
351
                $hidden = false;
352
            }
353
        }
354
355
        return $hidden;
356
    }
357
358
    /**
359
     * Returns if this is Document is embargoed.
360
     *
361
     * @return bool
362
     */
363
    public function isEmbargoed()
364
    {
365
        if (is_object($this->EmbargoedUntilDate)) {
366
            $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...
367
        }
368
369
        $embargoed = false;
370
371
        if ($this->EmbargoedIndefinitely) {
372
            $embargoed = true;
373
        } elseif ($this->EmbargoedUntilPublished) {
374
            $embargoed = true;
375
        } elseif (!empty($this->EmbargoedUntilDate)) {
376
            if (SS_Datetime::now()->Value < $this->EmbargoedUntilDate) {
377
                $embargoed = true;
378
            }
379
        }
380
381
        return $embargoed;
382
    }
383
384
    /**
385
     * Hides the document, so it does not show up when getByPage($myPage) is
386
     * called. Automatically un-hides the Document at a specific date.
387
     *
388
     * @param string $datetime date time value when this Document should expire.
389
     * @param bool $write
390
     *
391
     * @return DMSDocument
392
     */
393 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...
394
    {
395
        $this->EmbargoedUntilDate = DBField::create_field('SS_Datetime', $datetime)->Format('Y-m-d H:i:s');
396
397
        if ($write) {
398
            $this->write();
399
        }
400
401
        return $this;
402
    }
403
404
    /**
405
     * Clears any previously set embargos, so the Document always shows up in
406
     * all queries.
407
     *
408
     * @param bool $write
409
     *
410
     * @return DMSDocument
411
     */
412
    public function clearEmbargo($write = true)
413
    {
414
        $this->EmbargoedIndefinitely = false;
415
        $this->EmbargoedUntilPublished = false;
416
        $this->EmbargoedUntilDate = null;
417
418
        if ($write) {
419
            $this->write();
420
        }
421
422
        return $this;
423
    }
424
425
    /**
426
     * Returns if this is Document is expired.
427
     *
428
     * @return bool
429
     */
430
    public function isExpired()
431
    {
432
        if (is_object($this->ExpireAtDate)) {
433
            $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...
434
        }
435
436
        $expired = false;
437
438
        if (!empty($this->ExpireAtDate)) {
439
            if (SS_Datetime::now()->Value >= $this->ExpireAtDate) {
440
                $expired = true;
441
            }
442
        }
443
444
        return $expired;
445
    }
446
447
    /**
448
     * Hides the document at a specific date, so it does not show up when
449
     * getByPage($myPage) is called.
450
     *
451
     * @param string $datetime date time value when this Document should expire
452
     * @param bool $write
453
     *
454
     * @return DMSDocument
455
     */
456 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...
457
    {
458
        $this->ExpireAtDate = DBField::create_field('SS_Datetime', $datetime)->Format('Y-m-d H:i:s');
459
460
        if ($write) {
461
            $this->write();
462
        }
463
464
        return $this;
465
    }
466
467
    /**
468
     * Clears any previously set expiry.
469
     *
470
     * @param bool $write
471
     *
472
     * @return DMSDocument
473
     */
474
    public function clearExpiry($write = true)
475
    {
476
        $this->ExpireAtDate = null;
477
478
        if ($write) {
479
            $this->write();
480
        }
481
482
        return $this;
483
    }
484
485
    /**
486
     * Returns a DataList of all previous Versions of this document (check the
487
     * LastEdited date of each object to find the correct one).
488
     *
489
     * If {@link DMSDocument_versions::$enable_versions} is disabled then an
490
     * Exception is thrown
491
     *
492
     * @throws Exception
493
     *
494
     * @return DataList List of Document objects
495
     */
496
    public function getVersions()
497
    {
498
        if (!DMSDocument_versions::$enable_versions) {
499
            throw new Exception("DMSDocument versions are disabled");
500
        }
501
502
        return DMSDocument_versions::get_versions($this);
503
    }
504
505
    /**
506
     * Returns the full filename of the document stored in this object.
507
     *
508
     * @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...
509
     */
510
    public function getFullPath()
511
    {
512
        if ($this->Filename) {
513
            return DMS::inst()->getStoragePath() . DIRECTORY_SEPARATOR
514
                . $this->Folder . DIRECTORY_SEPARATOR . $this->Filename;
515
        }
516
517
        return null;
518
    }
519
520
    /**
521
     * Returns the filename of this asset.
522
     *
523
     * @return string
524
     */
525
    public function getFilename()
526
    {
527
        if ($this->getField('Filename')) {
528
            return $this->getField('Filename');
529
        }
530
        return ASSETS_DIR . '/';
531
    }
532
533
    /**
534
     * @return string
535
     */
536
    public function getName()
537
    {
538
        return $this->getField('Title');
539
    }
540
541
542
    /**
543
     * Returns the filename of a document without the prefix, e.g. 0~filename.jpg -> filename.jpg
544
     *
545
     * @return string
546
     */
547
    public function getFilenameWithoutID()
548
    {
549
        $filenameParts = explode('~', $this->Filename);
550
        $filename = array_pop($filenameParts);
551
552
        return $filename;
553
    }
554
555
    /**
556
     * @return string
557
     */
558
    public function getStorageFolder()
559
    {
560
        return DMS::inst()->getStoragePath() . DIRECTORY_SEPARATOR . DMS::inst()->getStorageFolder($this->ID);
561
    }
562
563
    /**
564
     * Deletes the DMSDocument and its underlying file. Also calls the parent DataObject's delete method in
565
     * order to complete an cascade.
566
     *
567
     * @return void
568
     */
569
    public function delete()
570
    {
571
        // delete the file (and previous versions of files)
572
        $filesToDelete = array();
573
        $storageFolder = $this->getStorageFolder();
574
575
        if (file_exists($storageFolder)) {
576
            if ($handle = opendir($storageFolder)) {
577
                while (false !== ($entry = readdir($handle))) {
578
                    // only delete if filename starts the the relevant ID
579
                    if (strpos($entry, $this->ID.'~') === 0) {
580
                        $filesToDelete[] = $entry;
581
                    }
582
                }
583
584
                closedir($handle);
585
586
                //delete all this files that have the id of this document
587
                foreach ($filesToDelete as $file) {
588
                    $filePath = $storageFolder .DIRECTORY_SEPARATOR . $file;
589
590
                    if (is_file($filePath)) {
591
                        unlink($filePath);
592
                    }
593
                }
594
            }
595
        }
596
597
        // get rid of any versions have saved for this DMSDocument, too
598
        if (DMSDocument_versions::$enable_versions) {
599
            $versions = $this->getVersions();
600
601
            if ($versions->Count() > 0) {
602
                foreach ($versions as $v) {
603
                    $v->delete();
604
                }
605
            }
606
        }
607
608
        return parent::delete();
609
    }
610
611
    /**
612
     * Relate an existing file on the filesystem to the document.
613
     *
614
     * Copies the file to the new destination, as defined in {@link DMS::getStoragePath()}.
615
     *
616
     * @param string $filePath Path to file, relative to webroot.
617
     *
618
     * @return DMSDocument
619
     */
620
    public function storeDocument($filePath)
621
    {
622
        if (empty($this->ID)) {
623
            user_error("Document must be written to database before it can store documents", E_USER_ERROR);
624
        }
625
626
        // calculate all the path to copy the file to
627
        $fromFilename = basename($filePath);
628
        $toFilename = $this->ID. '~' . $fromFilename; //add the docID to the start of the Filename
629
        $toFolder = DMS::inst()->getStorageFolder($this->ID);
630
        $toPath = DMS::inst()->getStoragePath() . DIRECTORY_SEPARATOR . $toFolder . DIRECTORY_SEPARATOR . $toFilename;
631
632
        DMS::inst()->createStorageFolder(DMS::inst()->getStoragePath() . DIRECTORY_SEPARATOR . $toFolder);
633
634
        //copy the file into place
635
        $fromPath = BASE_PATH . DIRECTORY_SEPARATOR . $filePath;
636
637
        //version the existing file (copy it to a new "very specific" filename
638
        if (DMSDocument_versions::$enable_versions) {
639
            DMSDocument_versions::create_version($this);
640
        } else {    //otherwise delete the old document file
641
            $oldPath = $this->getFullPath();
642
            if (file_exists($oldPath)) {
643
                unlink($oldPath);
644
            }
645
        }
646
647
        copy($fromPath, $toPath);   //this will overwrite the existing file (if present)
648
649
        //write the filename of the stored document
650
        $this->Filename = $toFilename;
651
        $this->Folder = strval($toFolder);
652
653
        $extension = pathinfo($this->Filename, PATHINFO_EXTENSION);
654
655
        if (empty($this->Title)) {
656
            // don't overwrite existing document titles
657
            $this->Title = basename($filePath, '.'.$extension);
658
        }
659
660
        $this->write();
661
662
        return $this;
663
    }
664
665
    /**
666
     * Takes a File object or a String (path to a file) and copies it into the
667
     * DMS, replacing the original document file but keeping the rest of the
668
     * document unchanged.
669
     *
670
     * @param File|string $file path to a file to store
671
     *
672
     * @return DMSDocument object that we replaced the file in
673
     */
674
    public function replaceDocument($file)
675
    {
676
        $filePath = DMS::inst()->transformFileToFilePath($file);
677
        $doc = $this->storeDocument($filePath); // replace the document
678
679
        return $doc;
680
    }
681
682
683
    /**
684
     * Return the type of file for the given extension
685
     * on the current file name.
686
     *
687
     * @param string $ext
688
     *
689
     * @return string
690
     */
691
    public static function get_file_type($ext)
692
    {
693
        $types = array(
694
            'gif' => 'GIF image - good for diagrams',
695
            'jpg' => 'JPEG image - good for photos',
696
            'jpeg' => 'JPEG image - good for photos',
697
            'png' => 'PNG image - good general-purpose format',
698
            'ico' => 'Icon image',
699
            'tiff' => 'Tagged image format',
700
            'doc' => 'Word document',
701
            'xls' => 'Excel spreadsheet',
702
            'zip' => 'ZIP compressed file',
703
            'gz' => 'GZIP compressed file',
704
            'dmg' => 'Apple disk image',
705
            'pdf' => 'Adobe Acrobat PDF file',
706
            'mp3' => 'MP3 audio file',
707
            'wav' => 'WAV audo file',
708
            'avi' => 'AVI video file',
709
            'mpg' => 'MPEG video file',
710
            'mpeg' => 'MPEG video file',
711
            'js' => 'Javascript file',
712
            'css' => 'CSS file',
713
            'html' => 'HTML file',
714
            'htm' => 'HTML file'
715
        );
716
717
        return isset($types[$ext]) ? $types[$ext] : $ext;
718
    }
719
720
721
    /**
722
     * Returns the Description field with HTML <br> tags added when there is a
723
     * line break.
724
     *
725
     * @return string
726
     */
727
    public function getDescriptionWithLineBreak()
728
    {
729
        return nl2br($this->getField('Description'));
730
    }
731
732
    /**
733
     * @return FieldList
734
     */
735
    public function getCMSFields()
736
    {
737
        //include JS to handling showing and hiding of bottom "action" tabs
738
        Requirements::javascript(DMS_DIR . '/javascript/DMSDocumentCMSFields.js');
739
        Requirements::css(DMS_DIR . '/dist/css/cmsbundle.css');
740
741
        $fields = new FieldList();  //don't use the automatic scaffolding, it is slow and unnecessary here
742
743
        $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...
744
745
        //get list of shortcode page relations
746
        $relationFinder = new ShortCodeRelationFinder();
747
        $relationList = $relationFinder->getList($this->ID);
748
749
        $fieldsTop = $this->getFieldsForFile($relationList->count());
750
        $fields->add($fieldsTop);
751
752
        $fields->add(TextField::create('Title', _t('DMSDocument.TITLE', 'Title')));
753
        $fields->add(TextareaField::create('Description', _t('DMSDocument.DESCRIPTION', 'Description')));
754
755
        $coverImageField = UploadField::create('CoverImage', _t('DMSDocument.COVERIMAGE', 'Cover Image'));
756
        $coverImageField->getValidator()->setAllowedExtensions(array('jpg', 'jpeg', 'png', 'gif'));
757
        $coverImageField->setConfig('allowedMaxFileNumber', 1);
758
        $fields->add($coverImageField);
759
760
761
        $downloadBehaviorSource = array(
762
            'open' => _t('DMSDocument.OPENINBROWSER', 'Open in browser'),
763
            'download' => _t('DMSDocument.FORCEDOWNLOAD', 'Force download'),
764
        );
765
        $defaultDownloadBehaviour = Config::inst()->get('DMSDocument', 'default_download_behaviour');
766
        if (!isset($downloadBehaviorSource[$defaultDownloadBehaviour])) {
767
            user_error('Default download behaviour "' . $defaultDownloadBehaviour . '" not supported.', E_USER_WARNING);
768
        } else {
769
            $downloadBehaviorSource[$defaultDownloadBehaviour] .= ' (' . _t('DMSDocument.DEFAULT', 'default') . ')';
770
        }
771
772
        $fields->add(
773
            OptionsetField::create(
774
                'DownloadBehavior',
775
                _t('DMSDocument.DOWNLOADBEHAVIOUR', 'Download behavior'),
776
                $downloadBehaviorSource,
777
                $defaultDownloadBehaviour
778
            )
779
            ->setDescription(
780
                'How the visitor will view this file. <strong>Open in browser</strong> '
781
                . 'allows files to be opened in a new tab.'
782
            )
783
        );
784
785
        //create upload field to replace document
786
        $uploadField = new DMSUploadField('ReplaceFile', 'Replace file');
787
        $uploadField->setConfig('allowedMaxFileNumber', 1);
788
        $uploadField->setConfig('downloadTemplateName', 'ss-dmsuploadfield-downloadtemplate');
789
        $uploadField->setRecord($this);
790
791
        $gridFieldConfig = GridFieldConfig::create()->addComponents(
792
            new GridFieldToolbarHeader(),
793
            new GridFieldSortableHeader(),
794
            new GridFieldDataColumns(),
795
            new GridFieldPaginator(30),
796
            //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...
797
            new GridFieldDetailForm()
798
        );
799
800
        $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...
801
            ->setDisplayFields(array(
802
                'Title' => 'Title',
803
                'ClassName' => 'Page Type',
804
                'ID' => 'Page ID'
805
            ))
806
            ->setFieldFormatting(array(
807
                'Title' => sprintf(
808
                    '<a class=\"cms-panel-link\" href=\"%s/$ID\">$Title</a>',
809
                    singleton('CMSPageEditController')->Link('show')
810
                )
811
            ));
812
813
        $pagesGrid = GridField::create(
814
            'Pages',
815
            _t('DMSDocument.RelatedPages', 'Related Pages'),
816
            $this->getRelatedPages(),
817
            $gridFieldConfig
818
        );
819
820
        $referencesGrid = GridField::create(
821
            'References',
822
            _t('DMSDocument.RelatedReferences', 'Related References'),
823
            $relationList,
824
            $gridFieldConfig
825
        );
826
827
        if (DMSDocument_versions::$enable_versions) {
828
            $versionsGridFieldConfig = GridFieldConfig::create()->addComponents(
829
                new GridFieldToolbarHeader(),
830
                new GridFieldSortableHeader(),
831
                new GridFieldDataColumns(),
832
                new GridFieldPaginator(30)
833
            );
834
            $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...
835
                ->setDisplayFields(Config::inst()->get('DMSDocument_versions', 'display_fields'))
836
                ->setFieldFormatting(
837
                    array(
838
                        'FilenameWithoutID' => '<a target="_blank" class="file-url" href="$Link">'
839
                            . '$FilenameWithoutID</a>'
840
                    )
841
                );
842
843
            $versionsGrid =  GridField::create(
844
                'Versions',
845
                _t('DMSDocument.Versions', 'Versions'),
846
                $this->getVersions(),
847
                $versionsGridFieldConfig
848
            );
849
            $this->addActionPanelTask('find-versions', 'Versions');
850
        }
851
852
        $embargoValue = 'None';
853
        if ($this->EmbargoedIndefinitely) {
854
            $embargoValue = 'Indefinitely';
855
        } elseif ($this->EmbargoedUntilPublished) {
856
            $embargoValue = 'Published';
857
        } elseif (!empty($this->EmbargoedUntilDate)) {
858
            $embargoValue = 'Date';
859
        }
860
        $embargo = new OptionsetField(
861
            'Embargo',
862
            _t('DMSDocument.EMBARGO', 'Embargo'),
863
            array(
864
                'None' => _t('DMSDocument.EMBARGO_NONE', 'None'),
865
                'Published' => _t('DMSDocument.EMBARGO_PUBLISHED', 'Hide document until page is published'),
866
                'Indefinitely' => _t('DMSDocument.EMBARGO_INDEFINITELY', 'Hide document indefinitely'),
867
                'Date' => _t('DMSDocument.EMBARGO_DATE', 'Hide until set date')
868
            ),
869
            $embargoValue
870
        );
871
        $embargoDatetime = DatetimeField::create('EmbargoedUntilDate', '');
872
        $embargoDatetime->getDateField()
873
            ->setConfig('showcalendar', true)
874
            ->setConfig('dateformat', 'dd-MM-yyyy')
875
            ->setConfig('datavalueformat', 'dd-MM-yyyy');
876
877
        $expiryValue = 'None';
878
        if (!empty($this->ExpireAtDate)) {
879
            $expiryValue = 'Date';
880
        }
881
        $expiry = new OptionsetField(
882
            'Expiry',
883
            'Expiry',
884
            array(
885
                'None' => 'None',
886
                'Date' => 'Set document to expire on'
887
            ),
888
            $expiryValue
889
        );
890
        $expiryDatetime = DatetimeField::create('ExpireAtDate', '');
891
        $expiryDatetime->getDateField()
892
            ->setConfig('showcalendar', true)
893
            ->setConfig('dateformat', 'dd-MM-yyyy')
894
            ->setConfig('datavalueformat', 'dd-MM-yyyy');
895
896
        // This adds all the actions details into a group.
897
        // Embargo, History, etc to go in here
898
        // These are toggled on and off via the Actions Buttons above
899
        // 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...
900
        $actionsPanel = FieldGroup::create(
901
            FieldGroup::create($embargo, $embargoDatetime)->addExtraClass('embargo'),
902
            FieldGroup::create($expiry, $expiryDatetime)->addExtraClass('expiry'),
903
            FieldGroup::create($uploadField)->addExtraClass('replace'),
904
            FieldGroup::create($pagesGrid)->addExtraClass('find-usage'),
905
            FieldGroup::create($referencesGrid)->addExtraClass('find-references'),
906
            FieldGroup::create($this->getPermissionsActionPanel())->addExtraClass('permissions')
907
        );
908
909
        if ($this->canEdit()) {
910
            $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...
911
            $actionsPanel->push(
912
                FieldGroup::create($this->getRelatedDocumentsGridField())->addExtraClass('find-relateddocuments')
913
            );
914
        } else {
915
            $this->removeActionPanelTask('find-relateddocuments')->removeActionPanelTask('find-versions');
916
        }
917
        $fields->add(LiteralField::create('BottomTaskSelection', $this->getActionTaskHtml()));
918
        $actionsPanel->setName('ActionsPanel');
919
        $actionsPanel->addExtraClass('dmsdocument-actionspanel');
920
        $fields->push($actionsPanel);
921
922
        $this->extend('updateCMSFields', $fields);
923
924
        return $fields;
925
    }
926
927
    /**
928
     * Adds permissions selection fields to a composite field and returns so it can be used in the "actions panel"
929
     *
930
     * @return CompositeField
931
     */
932
    public function getPermissionsActionPanel()
933
    {
934
        $fields = FieldList::create();
935
        $showFields = array(
936
            'CanViewType'  => '',
937
            'ViewerGroups' => 'hide',
938
            'CanEditType'  => '',
939
            'EditorGroups' => 'hide',
940
        );
941
        /** @var SiteTree $siteTree */
942
        $siteTree = singleton('SiteTree');
943
        $settingsFields = $siteTree->getSettingsFields();
944
945
        foreach ($showFields as $name => $extraCss) {
946
            $compositeName = "Root.Settings.$name";
947
            /** @var FormField $field */
948
            if ($field = $settingsFields->fieldByName($compositeName)) {
949
                $field->addExtraClass($extraCss);
950
                $title = str_replace('page', 'document', $field->Title());
951
                $field->setTitle($title);
952
953
                // Remove Inherited source option from DropdownField
954
                if ($field instanceof DropdownField) {
955
                    $options = $field->getSource();
956
                    unset($options['Inherit']);
957
                    $field->setSource($options);
958
                }
959
                $fields->push($field);
960
            }
961
        }
962
963
        $this->extend('updatePermissionsFields', $fields);
964
965
        return CompositeField::create($fields);
966
    }
967
968
    /**
969
     * Return a title to use on the frontend, preferably the "title", otherwise the filename without it's numeric ID
970
     *
971
     * @return string
972
     */
973
    public function getTitle()
974
    {
975
        if ($this->getField('Title')) {
976
            return $this->getField('Title');
977
        }
978
        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...
979
    }
980
981
    public function onBeforeWrite()
982
    {
983
        parent::onBeforeWrite();
984
985
        if (isset($this->Embargo)) {
986
            //set the embargo options from the OptionSetField created in the getCMSFields method
987
            //do not write after clearing the embargo (write happens automatically)
988
            $savedDate = $this->EmbargoedUntilDate;
989
            $this->clearEmbargo(false); // Clear all previous settings and re-apply them on save
990
991
            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...
992
                $this->embargoUntilPublished(false);
993
            }
994
            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...
995
                $this->embargoIndefinitely(false);
996
            }
997
            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...
998
                $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...
999
            }
1000
        }
1001
1002
        if (isset($this->Expiry)) {
1003
            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...
1004
                $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...
1005
            } else {
1006
                $this->clearExpiry(false);
1007
            } // Clear all previous settings
1008
        }
1009
1010
        // Set user fields
1011
        if ($currentUserID = Member::currentUserID()) {
1012
            if (!$this->CreatedByID) {
1013
                $this->CreatedByID = $currentUserID;
1014
            }
1015
            $this->LastEditedByID = $currentUserID;
1016
        }
1017
    }
1018
1019
    /**
1020
     * Return the relative URL of an icon for the file type, based on the
1021
     * {@link appCategory()} value.
1022
     *
1023
     * Images are searched for in "dms/images/app_icons/".
1024
     *
1025
     * @return string
1026
     */
1027
    public function Icon($ext)
1028
    {
1029
        if (!Director::fileExists(DMS_DIR."/images/app_icons/{$ext}_32.png")) {
1030
            $ext = File::get_app_category($ext);
1031
        }
1032
1033
        if (!Director::fileExists(DMS_DIR."/images/app_icons/{$ext}_32.png")) {
1034
            $ext = "generic";
1035
        }
1036
1037
        return DMS_DIR."/images/app_icons/{$ext}_32.png";
1038
    }
1039
1040
    /**
1041
     * Return the extension of the file associated with the document
1042
     *
1043
     * @return string
1044
     */
1045
    public function getExtension()
1046
    {
1047
        return strtolower(pathinfo($this->Filename, PATHINFO_EXTENSION));
1048
    }
1049
1050
    /**
1051
     * @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...
1052
     */
1053
    public function getSize()
1054
    {
1055
        $size = $this->getAbsoluteSize();
1056
        return ($size) ? File::format_size($size) : false;
1057
    }
1058
1059
    /**
1060
     * Return the size of the file associated with the document.
1061
     *
1062
     * @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...
1063
     */
1064
    public function getAbsoluteSize()
1065
    {
1066
        return file_exists($this->getFullPath()) ? filesize($this->getFullPath()) : null;
1067
    }
1068
1069
    /**
1070
     * An alias to DMSDocument::getSize()
1071
     *
1072
     * @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...
1073
     */
1074
    public function getFileSizeFormatted()
1075
    {
1076
        return $this->getSize();
1077
    }
1078
1079
1080
    /**
1081
     * @return FieldList
1082
     */
1083
    protected function getFieldsForFile($relationListCount)
1084
    {
1085
        $extension = $this->getExtension();
1086
1087
        $previewField = new LiteralField(
1088
            "ImageFull",
1089
            "<img id='thumbnailImage' class='thumbnail-preview' src='{$this->Icon($extension)}?r="
1090
            . rand(1, 100000) . "' alt='{$this->Title}' />\n"
1091
        );
1092
1093
        //count the number of pages this document is published on
1094
        $publishedOnCount = $this->getRelatedPages()->count();
1095
        $publishedOnValue = "$publishedOnCount pages";
1096
        if ($publishedOnCount == 1) {
1097
            $publishedOnValue = "$publishedOnCount page";
1098
        }
1099
1100
        $relationListCountValue = "$relationListCount pages";
1101
        if ($relationListCount == 1) {
1102
            $relationListCountValue = "$relationListCount page";
1103
        }
1104
1105
        $fields = new FieldGroup(
1106
            $filePreview = CompositeField::create(
1107
                CompositeField::create(
1108
                    $previewField
1109
                )->setName("FilePreviewImage")->addExtraClass('cms-file-info-preview'),
1110
                CompositeField::create(
1111
                    CompositeField::create(
1112
                        new ReadonlyField("ID", "ID number". ':', $this->ID),
1113
                        new ReadonlyField(
1114
                            "FileType",
1115
                            _t('AssetTableField.TYPE', 'File type') . ':',
1116
                            self::get_file_type($extension)
1117
                        ),
1118
                        new ReadonlyField(
1119
                            "Size",
1120
                            _t('AssetTableField.SIZE', 'File size') . ':',
1121
                            $this->getFileSizeFormatted()
1122
                        ),
1123
                        $urlField = new ReadonlyField(
1124
                            'ClickableURL',
1125
                            _t('AssetTableField.URL', 'URL'),
1126
                            sprintf(
1127
                                '<a href="%s" target="_blank" class="file-url">%s</a>',
1128
                                $this->getLink(),
1129
                                $this->getLink()
1130
                            )
1131
                        ),
1132
                        new ReadonlyField("FilenameWithoutIDField", "Filename". ':', $this->getFilenameWithoutID()),
1133
                        new DateField_Disabled(
1134
                            "Created",
1135
                            _t('AssetTableField.CREATED', 'First uploaded') . ':',
1136
                            $this->Created
1137
                        ),
1138
                        new DateField_Disabled(
1139
                            "LastEdited",
1140
                            _t('AssetTableField.LASTEDIT', 'Last changed') . ':',
1141
                            $this->LastEdited
1142
                        ),
1143
                        new ReadonlyField("PublishedOn", "Published on". ':', $publishedOnValue),
1144
                        new ReadonlyField("ReferencedOn", "Referenced on". ':', $relationListCountValue),
1145
                        new ReadonlyField("ViewCount", "View count". ':', $this->ViewCount)
1146
                    )->setName('FilePreviewDataFields')
1147
                )->setName("FilePreviewData")->addExtraClass('cms-file-info-data')
1148
            )->setName("FilePreview")->addExtraClass('cms-file-info')
1149
        );
1150
1151
        $fields->addExtraClass('dmsdocument-documentdetails');
1152
        $urlField->dontEscape = true;
1153
1154
        $this->extend('updateFieldsForFile', $fields);
1155
1156
        return $fields;
1157
    }
1158
1159
    /**
1160
     * Takes a file and adds it to the DMSDocument storage, replacing the
1161
     * current file.
1162
     *
1163
     * @param File $file
1164
     *
1165
     * @return $this
1166
     */
1167
    public function ingestFile($file)
1168
    {
1169
        $this->replaceDocument($file);
1170
        $file->delete();
1171
1172
        return $this;
1173
    }
1174
1175
    /**
1176
     * Get a data list of documents related to this document
1177
     *
1178
     * @return DataList
1179
     */
1180
    public function getRelatedDocuments()
1181
    {
1182
        $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...
1183
1184
        $this->extend('updateRelatedDocuments', $documents);
1185
1186
        return $documents;
1187
    }
1188
1189
    /**
1190
     * Get a list of related pages for this document by going through the associated document sets
1191
     *
1192
     * @return ArrayList
1193
     */
1194
    public function getRelatedPages()
1195
    {
1196
        $pages = ArrayList::create();
1197
1198
        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...
1199
            /** @var DocumentSet $documentSet */
1200
            $pages->add($documentSet->Page());
1201
        }
1202
        $pages->removeDuplicates();
1203
1204
        $this->extend('updateRelatedPages', $pages);
1205
1206
        return $pages;
1207
    }
1208
1209
    /**
1210
     * Get a GridField for managing related documents
1211
     *
1212
     * @return GridField
1213
     */
1214
    protected function getRelatedDocumentsGridField()
1215
    {
1216
        $gridField = GridField::create(
1217
            'RelatedDocuments',
1218
            _t('DMSDocument.RELATEDDOCUMENTS', 'Related Documents'),
1219
            $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...
1220
            new GridFieldConfig_RelationEditor
1221
        );
1222
1223
        $gridField->getConfig()->removeComponentsByType('GridFieldAddNewButton');
1224
        // Move the autocompleter to the left
1225
        $gridField->getConfig()->removeComponentsByType('GridFieldAddExistingAutocompleter');
1226
        $gridField->getConfig()->addComponent(
1227
            $addExisting = new GridFieldAddExistingAutocompleter('buttons-before-left')
1228
        );
1229
1230
        // Ensure that current document doesn't get returned in the autocompleter
1231
        $addExisting->setSearchList($this->getRelatedDocumentsForAutocompleter());
1232
1233
        // Restrict search fields to specific fields only
1234
        $addExisting->setSearchFields(array('Title:PartialMatch', 'Filename:PartialMatch'));
1235
        $addExisting->setResultsFormat('$Filename');
1236
1237
        $this->extend('updateRelatedDocumentsGridField', $gridField);
1238
        return $gridField;
1239
    }
1240
1241
    /**
1242
     * Get the list of documents to show in "related documents". This can be modified via the extension point, for
1243
     * example if you wanted to exclude embargoed documents or something similar.
1244
     *
1245
     * @return DataList
1246
     */
1247
    protected function getRelatedDocumentsForAutocompleter()
1248
    {
1249
        $documents = DMSDocument::get()->exclude('ID', $this->ID);
1250
        $this->extend('updateRelatedDocumentsForAutocompleter', $documents);
1251
        return $documents;
1252
    }
1253
1254
    /**
1255
     * Checks at least one group is selected if CanViewType || CanEditType == 'OnlyTheseUsers'
1256
     *
1257
     * @return ValidationResult
1258
     */
1259
    protected function validate()
1260
    {
1261
        $valid = parent::validate();
1262
1263
        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...
1264
            $valid->error(
1265
                _t(
1266
                    'DMSDocument.VALIDATIONERROR_NOVIEWERSELECTED',
1267
                    "Selecting 'Only these people' from a viewers list needs at least one group selected."
1268
                )
1269
            );
1270
        }
1271
1272
        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...
1273
            $valid->error(
1274
                _t(
1275
                    'DMSDocument.VALIDATIONERROR_NOEDITORSELECTED',
1276
                    "Selecting 'Only these people' from a editors list needs at least one group selected."
1277
                )
1278
            );
1279
        }
1280
1281
        return $valid;
1282
    }
1283
1284
    /**
1285
     * Returns a reason as to why this document cannot be viewed.
1286
     *
1287
     * @return string
1288
     */
1289
    public function getPermissionDeniedReason()
1290
    {
1291
        $result = '';
1292
1293
        if ($this->CanViewType == 'LoggedInUsers') {
1294
            $result = _t('DMSDocument.PERMISSIONDENIEDREASON_LOGINREQUIRED', 'Please log in to view this document');
1295
        }
1296
1297
        if ($this->CanViewType == 'OnlyTheseUsers') {
1298
            $result = _t(
1299
                'DMSDocument.PERMISSIONDENIEDREASON_NOTAUTHORISED',
1300
                'You are not authorised to view this document'
1301
            );
1302
        }
1303
1304
        return $result;
1305
    }
1306
1307
    /**
1308
     * Add an "action panel" task
1309
     *
1310
     * @param  string $panelKey
1311
     * @param  string $title
1312
     * @return $this
1313
     */
1314
    public function addActionPanelTask($panelKey, $title)
1315
    {
1316
        $this->actionTasks[$panelKey] = $title;
1317
        return $this;
1318
    }
1319
1320
    /**
1321
     * Returns a HTML representation of the action tasks for the CMS
1322
     *
1323
     * @return string
1324
     */
1325
    public function getActionTaskHtml()
1326
    {
1327
        $html = '<div class="field dmsdocment-actions">'
1328
            . '<label class="left">' . _t('DMSDocument.ACTIONS_LABEL', 'Actions') . '</label>'
1329
            . '<ul>';
1330
1331
        foreach ($this->actionTasks as $panelKey => $title) {
1332
            $html .= '<li class="ss-ui-button dmsdocument-action" data-panel="' . $panelKey . '">'
1333
                . _t('DMSDocument.ACTION_' . strtoupper($panelKey), $title)
1334
                . '</li>';
1335
        }
1336
1337
        $html .= '</ul></div>';
1338
1339
        return $html;
1340
    }
1341
1342
    /**
1343
     * Removes an "action panel" tasks
1344
     *
1345
     * @param  string $panelKey
1346
     * @return $this
1347
     */
1348
    public function removeActionPanelTask($panelKey)
1349
    {
1350
        if (array_key_exists($panelKey, $this->actionTasks)) {
1351
            unset($this->actionTasks[$panelKey]);
1352
        }
1353
        return $this;
1354
    }
1355
}
1356