Completed
Pull Request — master (#110)
by Franco
02:08
created

DMSDocument::getPermissionDeniedReason()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 4
nop 0
1
<?php
2
3
/**
4
 * @package dms
5
 *
6
 * @property Varchar Filename
7
 * @property Varchar Folder
8
 * @property Varchar Title
9
 * @property Text Description
10
 * @property int ViewCount
11
 * @property DateTime LastChanged
12
 * @property Boolean EmbargoedIndefinitely
13
 * @property Boolean EmbargoedUntilPublished
14
 * @property DateTime EmbargoedUntilDate
15
 * @property DateTime ExpireAtDate
16
 * @property Enum DownloadBehavior
17
 * @property Enum CanViewType Enum('Anyone, LoggedInUsers, OnlyTheseUsers', 'Anyone')
18
 * @property Enum CanEditType Enum('LoggedInUsers, OnlyTheseUsers', 'LoggedInUsers')
19
 *
20
 * @method ManyManyList RelatedDocuments
21
 * @method ManyManyList Tags
22
 * @method ManyManyList ViewerGroups
23
 * @method ManyManyList EditorGroups
24
 *
25
 */
26
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...
27
{
28
    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...
29
        "Filename" => "Varchar(255)", // eg. 3469~2011-energysaving-report.pdf
30
        "Folder" => "Varchar(255)",    // eg.	0
31
        "Title" => 'Varchar(1024)', // eg. "Energy Saving Report for Year 2011, New Zealand LandCorp"
32
        "Description" => 'Text',
33
        "ViewCount" => 'Int',
34
        // When this document's file was created or last replaced (small changes like updating title don't count)
35
        "LastChanged" => 'SS_DateTime',
36
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 $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...
47
        'Pages' => 'SiteTree',
48
        'RelatedDocuments' => 'DMSDocument',
49
        'Tags' => 'DMSTag',
50
        'ViewerGroups' => 'Group',
51
        'EditorGroups' => 'Group',
52
    );
53
54
    private static $many_many_extraFields = 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_extraFields 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...
55
        'Pages' => array(
56
            'DocumentSort' => 'Int'
57
        )
58
    );
59
60
    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...
61
        'ID' => 'ID',
62
        'Title' => 'Title',
63
        'FilenameWithoutID' => 'Filename',
64
        'LastChanged' => 'LastChanged'
65
    );
66
67
    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...
68
69
    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...
70
71
    private static $searchable_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 $searchable_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...
72
        'ID' => array(
73
            'filter' => 'ExactMatchFilter',
74
            'field' => 'NumericField'
75
        ),
76
        'Title',
77
        'Filename',
78
        'LastChanged'
79
    );
80
81
    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...
82
        'Filename' => 'Filename',
83
        'Title' => 'Title',
84
        'ViewCount' => 'ViewCount',
85
        'getPages.count' => 'Page Use'
86
    );
87
88
    /**
89
     * @var string download|open
90
     * @config
91
     */
92
    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...
93
94
    public function canView($member = null)
95
    {
96
        if (!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
97
            $member = Member::currentUser();
98
        }
99
100
        if (!$this->CanViewType || $this->CanViewType == 'Anyone') {
101
            return true;
102
        }
103
104 View Code Duplication
        if ($member && Permission::checkMember($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...
105
                array(
106
                    'ADMIN',
107
                    'SITETREE_EDIT_ALL',
108
                    'SITETREE_VIEW_ALL',
109
                )
110
            )
111
        ) {
112
            return true;
113
        }
114
115
        if ($this->isHidden()) {
116
            return false;
117
        }
118
119
        if ($this->CanViewType == 'LoggedInUsers') {
120
            return $member && $member->exists();
121
        }
122
123
        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...
124
            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...
125
        }
126
127
        // extended access checks
128
        $results = $this->extend('canView', $member);
129
130
        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...
131
            if (!min($results)) {
132
                return false;
133
            }
134
        }
135
136
        if ($member && $member->ID) {
137
            return true;
138
        }
139
140
        return $this->canEdit($member);
141
    }
142
143
    public function canEdit($member = null)
144
    {
145
        if (!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
146
            $member = Member::currentUser();
147
        }
148
149
        // Do early admin check
150 View Code Duplication
        if ($member && Permission::checkMember($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...
151
                array(
152
                    'ADMIN',
153
                    'SITETREE_EDIT_ALL',
154
                    'SITETREE_VIEW_ALL',
155
                )
156
            )
157
        ) {
158
            return true;
159
        }
160
161
        if ($this->CanEditType === 'LoggedInUsers') {
162
            return $member && $member->exists();
163
        }
164
165
        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...
166
            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...
167
        }
168
169
        return ($member && Permission::checkMember($member, array('ADMIN', 'SITETREE_EDIT_ALL')));
170
    }
171
172
    /**
173
     * @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...
174
     *
175
     * @return boolean
176
     */
177
    public function canCreate($member = null)
178
    {
179
        $results = $this->extend('canCreate', $member);
180
181
        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...
182
            if (!min($results)) {
183
                return false;
184
            }
185
        }
186
187
        return $this->canEdit($member);
188
    }
189
190
    /**
191
     * @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...
192
     *
193
     * @return boolean
194
     */
195
    public function canDelete($member = null)
196
    {
197
        if (!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
198
            $member = Member::currentUser();
199
        }
200
201
        $results = $this->extend('canDelete', $member);
202
203
        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...
204
            if (!min($results)) {
205
                return false;
206
            }
207
        }
208
209
        return $this->canView();
210
    }
211
212
213
214
    /**
215
     * Associates this document with a Page. This method does nothing if the
216
     * association already exists.
217
     *
218
     * This could be a simple wrapper around $myDoc->Pages()->add($myPage) to
219
     * add a many_many relation.
220
     *
221
     * @param SiteTree $pageObject Page object to associate this Document with
222
     *
223
     * @return DMSDocument
224
     */
225
    public function addPage($pageObject)
226
    {
227
        $this->Pages()->add($pageObject);
228
229
        DB::query(
230
            "UPDATE \"DMSDocument_Pages\" SET \"DocumentSort\"=\"DocumentSort\"+1"
231
            . " WHERE \"SiteTreeID\" = $pageObject->ID"
232
        );
233
234
        return $this;
235
    }
236
237
    /**
238
     * Associates this DMSDocument with a set of Pages. This method loops
239
     * through a set of page ids, and then associates this DMSDocument with the
240
     * individual Page with the each page id in the set.
241
     *
242
     * @param array $pageIDs
243
     *
244
     * @return DMSDocument
245
     */
246
    public function addPages($pageIDs)
247
    {
248
        foreach ($pageIDs as $id) {
249
            $pageObject = DataObject::get_by_id("SiteTree", $id);
250
251
            if ($pageObject && $pageObject->exists()) {
252
                $this->addPage($pageObject);
0 ignored issues
show
Compatibility introduced by
$pageObject of type object<DataObject> is not a sub-type of object<SiteTree>. It seems like you assume a child class of the class DataObject to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
253
            }
254
        }
255
256
        return $this;
257
    }
258
259
    /**
260
     * Removes the association between this Document and a Page. This method
261
     * does nothing if the association does not exist.
262
     *
263
     * @param SiteTree $pageObject Page object to remove the association to
264
     *
265
     * @return DMSDocument
266
     */
267
    public function removePage($pageObject)
268
    {
269
        $this->Pages()->remove($pageObject);
0 ignored issues
show
Documentation introduced by
$pageObject is of type object<SiteTree>, but the function expects a object<DataClass>.

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...
270
271
        return $this;
272
    }
273
274
    /**
275
     * @see getPages()
276
     *
277
     * @return DataList
278
     */
279
    public function Pages()
280
    {
281
        $pages = $this->getManyManyComponents('Pages');
282
        $this->extend('updatePages', $pages);
283
284
        return $pages;
285
    }
286
287
    /**
288
     * Returns a list of the Page objects associated with this Document.
289
     *
290
     * @return DataList
291
     */
292
    public function getPages()
293
    {
294
        return $this->Pages();
295
    }
296
297
    /**
298
     * Removes all associated Pages from the DMSDocument
299
     *
300
     * @return DMSDocument
301
     */
302
    public function removeAllPages()
303
    {
304
        $this->Pages()->removeAll();
305
306
        return $this;
307
    }
308
309
    /**
310
     * Increase ViewCount by 1, without update any other record fields such as
311
     * LastEdited.
312
     *
313
     * @return DMSDocument
314
     */
315
    public function trackView()
316
    {
317
        if ($this->ID > 0) {
318
            $count = $this->ViewCount + 1;
319
320
            $this->ViewCount = $count;
321
322
            DB::query("UPDATE \"DMSDocument\" SET \"ViewCount\"='$count' WHERE \"ID\"={$this->ID}");
323
        }
324
325
        return $this;
326
    }
327
328
329
    /**
330
     * Adds a metadata tag to the Document. The tag has a category and a value.
331
     *
332
     * Each category can have multiple values by default. So:
333
     * addTag("fruit","banana") addTag("fruit", "apple") will add two items.
334
     *
335
     * However, if the third parameter $multiValue is set to 'false', then all
336
     * updates to a category only ever update a single value. So:
337
     * addTag("fruit","banana") addTag("fruit", "apple") would result in a
338
     * single metadata tag: fruit->apple.
339
     *
340
     * Can could be implemented as a key/value store table (although it is more
341
     * like category/value, because the same category can occur multiple times)
342
     *
343
     * @param string $category of a metadata category to add (required)
344
     * @param string $value of a metadata value to add (required)
345
     * @param bool $multiValue Boolean that determines if the category is
346
     *                  multi-value or single-value (optional)
347
     *
348
     * @return DMSDocument
349
     */
350
    public function addTag($category, $value, $multiValue = true)
351
    {
352
        if ($multiValue) {
353
            //check for a duplicate tag, don't add the duplicate
354
            $currentTag = $this->Tags()->filter(array('Category' => $category, 'Value' => $value));
0 ignored issues
show
Documentation Bug introduced by
The method Tags 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...
355
            if ($currentTag->Count() == 0) {
356
                //multi value tag
357
                $tag = new DMSTag();
358
                $tag->Category = $category;
0 ignored issues
show
Documentation introduced by
The property Category does not exist on object<DMSTag>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
359
                $tag->Value = $value;
0 ignored issues
show
Documentation introduced by
The property Value does not exist on object<DMSTag>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
360
                $tag->MultiValue = true;
0 ignored issues
show
Documentation introduced by
The property MultiValue does not exist on object<DMSTag>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
361
                $tag->write();
362
                $tag->Documents()->add($this);
0 ignored issues
show
Documentation Bug introduced by
The method Documents does not exist on object<DMSTag>? 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...
363
            } else {
364
                //add the relation between the tag and document
365
                foreach ($currentTag as $tagObj) {
366
                    $tagObj->Documents()->add($this);
367
                }
368
            }
369
        } else {
370
            //single value tag
371
            $currentTag = $this->Tags()->filter(array('Category' => $category));
0 ignored issues
show
Documentation Bug introduced by
The method Tags 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...
372
            $tag = null;
0 ignored issues
show
Unused Code introduced by
$tag 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...
373
            if ($currentTag->Count() == 0) {
374
                //create the single-value tag
375
                $tag = new DMSTag();
376
                $tag->Category = $category;
0 ignored issues
show
Documentation introduced by
The property Category does not exist on object<DMSTag>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
377
                $tag->Value = $value;
0 ignored issues
show
Documentation introduced by
The property Value does not exist on object<DMSTag>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
378
                $tag->MultiValue = false;
0 ignored issues
show
Documentation introduced by
The property MultiValue does not exist on object<DMSTag>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
379
                $tag->write();
380
            } else {
381
                //update the single value tag
382
                $tag = $currentTag->first();
383
                $tag->Value = $value;
384
                $tag->MultiValue = false;
385
                $tag->write();
386
            }
387
388
            // regardless of whether we created a new tag or are just updating an
389
            // existing one, add the relation
390
            $tag->Documents()->add($this);
391
        }
392
393
        return $this;
394
    }
395
396
    /**
397
     * @param string $category
398
     * @param string $value
0 ignored issues
show
Documentation introduced by
Should the type for parameter $value not be string|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...
399
     *
400
     * @return DataList
401
     */
402
    protected function getTagsObjects($category, $value = null)
403
    {
404
        $valueFilter = array("Category" => $category);
405
        if (!empty($value)) {
406
            $valueFilter['Value'] = $value;
407
        }
408
409
        $tags = $this->Tags()->filter($valueFilter);
0 ignored issues
show
Documentation Bug introduced by
The method Tags 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...
410
        return $tags;
411
    }
412
413
    /**
414
     * Fetches all tags associated with this DMSDocument within a given
415
     * category. If a value is specified this method tries to fetch that
416
     * specific tag.
417
     *
418
     * @param string $category metadata category to get
419
     * @param string $value value of the tag to get
0 ignored issues
show
Documentation introduced by
Should the type for parameter $value not be string|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...
420
     *
421
     * @return array Strings of all the tags or null if there is no match found
0 ignored issues
show
Documentation introduced by
Should the return type not be array|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...
422
     */
423
    public function getTagsList($category, $value = null)
424
    {
425
        $tags = $this->getTagsObjects($category, $value);
426
427
        $returnArray = null;
428
429
        if ($tags->Count() > 0) {
430
            $returnArray = array();
431
432
            foreach ($tags as $t) {
433
                $returnArray[] = $t->Value;
434
            }
435
        }
436
437
        return $returnArray;
438
    }
439
440
    /**
441
     * Removes a tag from the Document. If you only set a category, then all
442
     * values in that category are deleted.
443
     *
444
     * If you specify both a category and a value, then only that single
445
     * category/value pair is deleted.
446
     *
447
     * Nothing happens if the category or the value do not exist.
448
     *
449
     * @param string $category Category to remove
450
     * @param string $value Value to remove
0 ignored issues
show
Documentation introduced by
Should the type for parameter $value not be string|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...
451
     *
452
     * @return DMSDocument
453
     */
454
    public function removeTag($category, $value = null)
455
    {
456
        $tags = $this->getTagsObjects($category, $value);
457
458
        if ($tags->Count() > 0) {
459
            foreach ($tags as $t) {
460
                $documentList = $t->Documents();
461
462
                //remove the relation between the tag and the document
463
                $documentList->remove($this);
464
465
                //delete the entire tag if it has no relations left
466
                if ($documentList->Count() == 0) {
467
                    $t->delete();
468
                }
469
            }
470
        }
471
472
        return $this;
473
    }
474
475
    /**
476
     * Deletes all tags associated with this Document.
477
     *
478
     * @return DMSDocument
479
     */
480
    public function removeAllTags()
481
    {
482
        $allTags = $this->Tags();
0 ignored issues
show
Documentation Bug introduced by
The method Tags 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...
483
484
        foreach ($allTags as $tag) {
485
            $documentlist = $tag->Documents();
486
            $documentlist->remove($this);
487
            if ($tag->Documents()->Count() == 0) {
488
                $tag->delete();
489
            }
490
        }
491
492
        return $this;
493
    }
494
495
    /**
496
     * Returns a link to download this document from the DMS store.
497
     * Alternatively a basic javascript alert will be shown should the user not have view permissions. An extension
498
     * point for this was also added.
499
     *
500
     * To extend use the following from within an Extension subclass:
501
     *
502
     * <code>
503
     * public function updateGetLink($result)
504
     * {
505
     *     // Do something here
506
     * }
507
     * </code>
508
     *
509
     * @return string
510
     */
511
    public function getLink()
512
    {
513
        $result = Controller::join_links(Director::baseURL(), 'dmsdocument/' . $this->ID);
514
        if (!$this->canView()) {
515
            $result = sprintf("javascript:alert('%s')", $this->getPermissionDeniedReason());
516
        }
517
518
        $this->extend('updateGetLink', $result);
519
520
        return $result;
521
    }
522
523
    /**
524
     * @return string
525
     */
526
    public function Link()
527
    {
528
        return $this->getLink();
529
    }
530
531
    /**
532
     * Hides the document, so it does not show up when getByPage($myPage) is
533
     * called (without specifying the $showEmbargoed = true parameter).
534
     *
535
     * This is similar to expire, except that this method should be used to hide
536
     * documents that have not yet gone live.
537
     *
538
     * @param bool $write Save change to the database
539
     *
540
     * @return DMSDocument
541
     */
542
    public function embargoIndefinitely($write = true)
543
    {
544
        $this->EmbargoedIndefinitely = true;
545
546
        if ($write) {
547
            $this->write();
548
        }
549
550
        return $this;
551
    }
552
553
    /**
554
     * Hides the document until any page it is linked to is published
555
     *
556
     * @param bool $write Save change to database
557
     *
558
     * @return DMSDocument
559
     */
560
    public function embargoUntilPublished($write = true)
561
    {
562
        $this->EmbargoedUntilPublished = true;
563
564
        if ($write) {
565
            $this->write();
566
        }
567
568
        return $this;
569
    }
570
571
    /**
572
     * Returns if this is Document is embargoed or expired.
573
     *
574
     * Also, returns if the document should be displayed on the front-end,
575
     * respecting the current reading mode of the site and the embargo status.
576
     *
577
     * I.e. if a document is embargoed until published, then it should still
578
     * show up in draft mode.
579
     *
580
     * @return bool
581
     */
582
    public function isHidden()
583
    {
584
        $hidden = $this->isEmbargoed() || $this->isExpired();
585
        $readingMode = Versioned::get_reading_mode();
586
587
        if ($readingMode == "Stage.Stage") {
588
            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...
589
                $hidden = false;
590
            }
591
        }
592
593
        return $hidden;
594
    }
595
596
    /**
597
     * Returns if this is Document is embargoed.
598
     *
599
     * @return bool
600
     */
601
    public function isEmbargoed()
602
    {
603
        if (is_object($this->EmbargoedUntilDate)) {
604
            $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...
605
        }
606
607
        $embargoed = false;
608
609
        if ($this->EmbargoedIndefinitely) {
610
            $embargoed = true;
611
        } elseif ($this->EmbargoedUntilPublished) {
612
            $embargoed = true;
613
        } elseif (!empty($this->EmbargoedUntilDate)) {
614
            if (SS_Datetime::now()->Value < $this->EmbargoedUntilDate) {
615
                $embargoed = true;
616
            }
617
        }
618
619
        return $embargoed;
620
    }
621
622
    /**
623
     * Hides the document, so it does not show up when getByPage($myPage) is
624
     * called. Automatically un-hides the Document at a specific date.
625
     *
626
     * @param string $datetime date time value when this Document should expire.
627
     * @param bool $write
628
     *
629
     * @return DMSDocument
630
     */
631 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...
632
    {
633
        $this->EmbargoedUntilDate = DBField::create_field('SS_Datetime', $datetime)->Format('Y-m-d H:i:s');
634
635
        if ($write) {
636
            $this->write();
637
        }
638
639
        return $this;
640
    }
641
642
    /**
643
     * Clears any previously set embargos, so the Document always shows up in
644
     * all queries.
645
     *
646
     * @param bool $write
647
     *
648
     * @return DMSDocument
649
     */
650
    public function clearEmbargo($write = true)
651
    {
652
        $this->EmbargoedIndefinitely = false;
653
        $this->EmbargoedUntilPublished = false;
654
        $this->EmbargoedUntilDate = null;
655
656
        if ($write) {
657
            $this->write();
658
        }
659
660
        return $this;
661
    }
662
663
    /**
664
     * Returns if this is Document is expired.
665
     *
666
     * @return bool
667
     */
668
    public function isExpired()
669
    {
670
        if (is_object($this->ExpireAtDate)) {
671
            $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...
672
        }
673
674
        $expired = false;
675
676
        if (!empty($this->ExpireAtDate)) {
677
            if (SS_Datetime::now()->Value >= $this->ExpireAtDate) {
678
                $expired = true;
679
            }
680
        }
681
682
        return $expired;
683
    }
684
685
    /**
686
     * Hides the document at a specific date, so it does not show up when
687
     * getByPage($myPage) is called.
688
     *
689
     * @param string $datetime date time value when this Document should expire
690
     * @param bool $write
691
     *
692
     * @return DMSDocument
693
     */
694 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...
695
    {
696
        $this->ExpireAtDate = DBField::create_field('SS_Datetime', $datetime)->Format('Y-m-d H:i:s');
697
698
        if ($write) {
699
            $this->write();
700
        }
701
702
        return $this;
703
    }
704
705
    /**
706
     * Clears any previously set expiry.
707
     *
708
     * @param bool $write
709
     *
710
     * @return DMSDocument
711
     */
712
    public function clearExpiry($write = true)
713
    {
714
        $this->ExpireAtDate = null;
715
716
        if ($write) {
717
            $this->write();
718
        }
719
720
        return $this;
721
    }
722
723
    /**
724
     * Returns a DataList of all previous Versions of this document (check the
725
     * LastEdited date of each object to find the correct one).
726
     *
727
     * If {@link DMSDocument_versions::$enable_versions} is disabled then an
728
     * Exception is thrown
729
     *
730
     * @throws Exception
731
     *
732
     * @return DataList List of Document objects
733
     */
734
    public function getVersions()
735
    {
736
        if (!DMSDocument_versions::$enable_versions) {
737
            throw new Exception("DMSDocument versions are disabled");
738
        }
739
740
        return DMSDocument_versions::get_versions($this);
741
    }
742
743
    /**
744
     * Returns the full filename of the document stored in this object.
745
     *
746
     * @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...
747
     */
748
    public function getFullPath()
749
    {
750
        if ($this->Filename) {
751
            return DMS::get_dms_path() . DIRECTORY_SEPARATOR . $this->Folder . DIRECTORY_SEPARATOR . $this->Filename;
752
        }
753
754
        return null;
755
    }
756
757
    /**
758
     * Returns the filename of this asset.
759
     *
760
     * @return string
761
     */
762
    public function getFileName()
763
    {
764
        if ($this->getField('Filename')) {
765
            return $this->getField('Filename');
766
        } else {
767
            return ASSETS_DIR . '/';
768
        }
769
    }
770
771
    /**
772
     * @return string
773
     */
774
    public function getName()
775
    {
776
        return $this->getField('Title');
777
    }
778
779
780
    /**
781
     * @return string
782
     */
783
    public function getFilenameWithoutID()
784
    {
785
        $filenameParts = explode('~', $this->Filename);
786
        $filename = array_pop($filenameParts);
787
788
        return $filename;
789
    }
790
791
    /**
792
     * @return string
793
     */
794
    public function getStorageFolder()
795
    {
796
        return DMS::get_dms_path() . DIRECTORY_SEPARATOR . DMS::get_storage_folder($this->ID);
797
    }
798
799
    /**
800
     * Deletes the DMSDocument, its underlying file, as well as any tags related
801
     * to this DMSDocument. Also calls the parent DataObject's delete method in
802
     * order to complete an cascade.
803
     *
804
     * @return void
805
     */
806
    public function delete()
807
    {
808
        // remove tags
809
        $this->removeAllTags();
810
811
        // delete the file (and previous versions of files)
812
        $filesToDelete = array();
813
        $storageFolder = $this->getStorageFolder();
814
815
        if (file_exists($storageFolder)) {
816
            if ($handle = opendir($storageFolder)) {
817
                while (false !== ($entry = readdir($handle))) {
818
                    // only delete if filename starts the the relevant ID
819
                    if (strpos($entry, $this->ID.'~') === 0) {
820
                        $filesToDelete[] = $entry;
821
                    }
822
                }
823
824
                closedir($handle);
825
826
                //delete all this files that have the id of this document
827
                foreach ($filesToDelete as $file) {
828
                    $filePath = $storageFolder .DIRECTORY_SEPARATOR . $file;
829
830
                    if (is_file($filePath)) {
831
                        unlink($filePath);
832
                    }
833
                }
834
            }
835
        }
836
837
        $this->removeAllPages();
838
839
        // get rid of any versions have saved for this DMSDocument, too
840
        if (DMSDocument_versions::$enable_versions) {
841
            $versions = $this->getVersions();
842
843
            if ($versions->Count() > 0) {
844
                foreach ($versions as $v) {
845
                    $v->delete();
846
                }
847
            }
848
        }
849
850
        parent::delete();
851
    }
852
853
854
855
    /**
856
     * Relate an existing file on the filesystem to the document.
857
     *
858
     * Copies the file to the new destination, as defined in {@link get_DMS_path()}.
859
     *
860
     * @param string $filePath Path to file, relative to webroot.
861
     *
862
     * @return DMSDocument
863
     */
864
    public function storeDocument($filePath)
865
    {
866
        if (empty($this->ID)) {
867
            user_error("Document must be written to database before it can store documents", E_USER_ERROR);
868
        }
869
870
        // calculate all the path to copy the file to
871
        $fromFilename = basename($filePath);
872
        $toFilename = $this->ID. '~' . $fromFilename; //add the docID to the start of the Filename
873
        $toFolder = DMS::get_storage_folder($this->ID);
874
        $toPath = DMS::get_dms_path() . DIRECTORY_SEPARATOR . $toFolder . DIRECTORY_SEPARATOR . $toFilename;
875
876
        DMS::create_storage_folder(DMS::get_dms_path() . DIRECTORY_SEPARATOR . $toFolder);
877
878
        //copy the file into place
879
        $fromPath = BASE_PATH . DIRECTORY_SEPARATOR . $filePath;
880
881
        //version the existing file (copy it to a new "very specific" filename
882
        if (DMSDocument_versions::$enable_versions) {
883
            DMSDocument_versions::create_version($this);
884
        } else {    //otherwise delete the old document file
885
            $oldPath = $this->getFullPath();
886
            if (file_exists($oldPath)) {
887
                unlink($oldPath);
888
            }
889
        }
890
891
        copy($fromPath, $toPath);   //this will overwrite the existing file (if present)
892
893
        //write the filename of the stored document
894
        $this->Filename = $toFilename;
895
        $this->Folder = strval($toFolder);
896
897
        $extension = pathinfo($this->Filename, PATHINFO_EXTENSION);
898
899
        if (empty($this->Title)) {
900
            // don't overwrite existing document titles
901
            $this->Title = basename($filePath, '.'.$extension);
902
        }
903
904
        $this->LastChanged = SS_Datetime::now()->Rfc2822();
905
        $this->write();
906
907
        return $this;
908
    }
909
910
    /**
911
     * Takes a File object or a String (path to a file) and copies it into the
912
     * DMS, replacing the original document file but keeping the rest of the
913
     * document unchanged.
914
     *
915
     * @param File|string $file path to a file to store
916
     *
917
     * @return DMSDocument object that we replaced the file in
918
     */
919
    public function replaceDocument($file)
920
    {
921
        $filePath = DMS::transform_file_to_file_path($file);
922
        $doc = $this->storeDocument($filePath); // replace the document
923
924
        return $doc;
925
    }
926
927
928
    /**
929
     * Return the type of file for the given extension
930
     * on the current file name.
931
     *
932
     * @param string $ext
933
     *
934
     * @return string
935
     */
936
    public static function get_file_type($ext)
937
    {
938
        $types = array(
939
            'gif' => 'GIF image - good for diagrams',
940
            'jpg' => 'JPEG image - good for photos',
941
            'jpeg' => 'JPEG image - good for photos',
942
            'png' => 'PNG image - good general-purpose format',
943
            'ico' => 'Icon image',
944
            'tiff' => 'Tagged image format',
945
            'doc' => 'Word document',
946
            'xls' => 'Excel spreadsheet',
947
            'zip' => 'ZIP compressed file',
948
            'gz' => 'GZIP compressed file',
949
            'dmg' => 'Apple disk image',
950
            'pdf' => 'Adobe Acrobat PDF file',
951
            'mp3' => 'MP3 audio file',
952
            'wav' => 'WAV audo file',
953
            'avi' => 'AVI video file',
954
            'mpg' => 'MPEG video file',
955
            'mpeg' => 'MPEG video file',
956
            'js' => 'Javascript file',
957
            'css' => 'CSS file',
958
            'html' => 'HTML file',
959
            'htm' => 'HTML file'
960
        );
961
962
        return isset($types[$ext]) ? $types[$ext] : $ext;
963
    }
964
965
966
    /**
967
     * Returns the Description field with HTML <br> tags added when there is a
968
     * line break.
969
     *
970
     * @return string
971
     */
972
    public function getDescriptionWithLineBreak()
973
    {
974
        return nl2br($this->getField('Description'));
975
    }
976
977
    /**
978
     * @return FieldList
979
     */
980
    public function getCMSFields()
981
    {
982
        //include JS to handling showing and hiding of bottom "action" tabs
983
        Requirements::javascript(DMS_DIR.'/javascript/DMSDocumentCMSFields.js');
984
        Requirements::css(DMS_DIR.'/css/DMSDocumentCMSFields.css');
985
986
        $fields = new FieldList();  //don't use the automatic scaffolding, it is slow and unnecessary here
987
988
        $extraTasks = '';   //additional text to inject into the list of tasks at the bottom of a DMSDocument CMSfield
989
990
        //get list of shortcode page relations
991
        $relationFinder = new ShortCodeRelationFinder();
992
        $relationList = $relationFinder->getList($this->ID);
993
994
        $fieldsTop = $this->getFieldsForFile($relationList->count());
995
        $fields->add($fieldsTop);
996
997
        $fields->add(new TextField('Title', 'Title'));
998
        $fields->add(new TextareaField('Description', 'Description'));
999
1000
        $downloadBehaviorSource = array(
1001
            'open' => _t('DMSDocument.OPENINBROWSER', 'Open in browser'),
1002
            'download' => _t('DMSDocument.FORCEDOWNLOAD', 'Force download'),
1003
        );
1004
        $defaultDownloadBehaviour = Config::inst()->get('DMSDocument', 'default_download_behaviour');
1005
        if (!isset($downloadBehaviorSource[$defaultDownloadBehaviour])) {
1006
            user_error('Default download behaviour "' . $defaultDownloadBehaviour . '" not supported.', E_USER_WARNING);
1007
        } else {
1008
            $downloadBehaviorSource[$defaultDownloadBehaviour] .= ' (' . _t('DMSDocument.DEFAULT', 'default') . ')';
1009
        }
1010
1011
        $fields->add(
1012
            OptionsetField::create(
1013
                'DownloadBehavior',
1014
                _t('DMSDocument.DOWNLOADBEHAVIOUR', 'Download behavior'),
1015
                $downloadBehaviorSource,
1016
                $defaultDownloadBehaviour
1017
            )
1018
            ->setDescription(
1019
                'How the visitor will view this file. <strong>Open in browser</strong> '
1020
                . 'allows files to be opened in a new tab.'
1021
            )
1022
        );
1023
1024
        //create upload field to replace document
1025
        $uploadField = new DMSUploadField('ReplaceFile', 'Replace file');
1026
        $uploadField->setConfig('allowedMaxFileNumber', 1);
1027
        $uploadField->setConfig('downloadTemplateName', 'ss-dmsuploadfield-downloadtemplate');
1028
        $uploadField->setRecord($this);
1029
1030
        $gridFieldConfig = GridFieldConfig::create()->addComponents(
1031
            new GridFieldToolbarHeader(),
1032
            new GridFieldSortableHeader(),
1033
            new GridFieldDataColumns(),
1034
            new GridFieldPaginator(30),
1035
            //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...
1036
            new GridFieldDetailForm()
1037
        );
1038
1039
        $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...
1040
            ->setDisplayFields(array(
1041
                'Title'=>'Title',
1042
                'ClassName'=>'Page Type',
1043
                'ID'=>'Page ID'
1044
            ))
1045
            ->setFieldFormatting(array(
1046
                'Title'=>sprintf(
1047
                    '<a class=\"cms-panel-link\" href=\"%s/$ID\">$Title</a>',
1048
                    singleton('CMSPageEditController')->Link('show')
1049
                )
1050
            ));
1051
1052
        $pagesGrid = GridField::create(
1053
            'Pages',
1054
            _t('DMSDocument.RelatedPages', 'Related Pages'),
1055
            $this->Pages(),
1056
            $gridFieldConfig
1057
        );
1058
1059
        $referencesGrid = GridField::create(
1060
            'References',
1061
            _t('DMSDocument.RelatedReferences', 'Related References'),
1062
            $relationList,
1063
            $gridFieldConfig
1064
        );
1065
1066
        if (DMSDocument_versions::$enable_versions) {
1067
            $versionsGridFieldConfig = GridFieldConfig::create()->addComponents(
1068
                new GridFieldToolbarHeader(),
1069
                new GridFieldSortableHeader(),
1070
                new GridFieldDataColumns(),
1071
                new GridFieldPaginator(30)
1072
            );
1073
            $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...
1074
                ->setDisplayFields(Config::inst()->get('DMSDocument_versions', 'display_fields'))
1075
                ->setFieldCasting(array('LastChanged'=>"Datetime->Ago"))
1076
                ->setFieldFormatting(
1077
                    array(
1078
                        'FilenameWithoutID' => '<a target=\'_blank\' class=\'file-url\' href=\'$Link\'>'
1079
                            . '$FilenameWithoutID</a>'
1080
                    )
1081
                );
1082
1083
            $versionsGrid =  GridField::create(
1084
                'Versions',
1085
                _t('DMSDocument.Versions', 'Versions'),
1086
                $this->getVersions(),
1087
                $versionsGridFieldConfig
1088
            );
1089
            $extraTasks .= '<li class="ss-ui-button" data-panel="find-versions">Versions</li>';
1090
        }
1091
1092
        $fields->add(new LiteralField(
1093
            'BottomTaskSelection',
1094
            '<div id="Actions" class="field actions"><label class="left">Actions</label><ul>'
1095
            . '<li class="ss-ui-button" data-panel="embargo">Embargo</li>'
1096
            . '<li class="ss-ui-button" data-panel="expiry">Expiry</li>'
1097
            . '<li class="ss-ui-button" data-panel="replace">Replace</li>'
1098
            . '<li class="ss-ui-button" data-panel="find-usage">Usage</li>'
1099
            . '<li class="ss-ui-button" data-panel="find-references">References</li>'
1100
            . '<li class="ss-ui-button" data-panel="find-relateddocuments">Related Documents</li>'
1101
            . $extraTasks
1102
            . '</ul></div>'
1103
        ));
1104
1105
        $embargoValue = 'None';
1106
        if ($this->EmbargoedIndefinitely) {
1107
            $embargoValue = 'Indefinitely';
1108
        } elseif ($this->EmbargoedUntilPublished) {
1109
            $embargoValue = 'Published';
1110
        } elseif (!empty($this->EmbargoedUntilDate)) {
1111
            $embargoValue = 'Date';
1112
        }
1113
        $embargo = new OptionsetField(
1114
            'Embargo',
1115
            'Embargo',
1116
            array(
1117
                'None' => 'None',
1118
                'Published' => 'Hide document until page is published',
1119
                'Indefinitely' => 'Hide document indefinitely',
1120
                'Date' => 'Hide until set date'
1121
            ),
1122
            $embargoValue
1123
        );
1124
        $embargoDatetime = DatetimeField::create('EmbargoedUntilDate', '');
1125
        $embargoDatetime->getDateField()
1126
            ->setConfig('showcalendar', true)
1127
            ->setConfig('dateformat', 'dd-MM-yyyy')
1128
            ->setConfig('datavalueformat', 'dd-MM-yyyy');
1129
1130
        $expiryValue = 'None';
1131
        if (!empty($this->ExpireAtDate)) {
1132
            $expiryValue = 'Date';
1133
        }
1134
        $expiry = new OptionsetField(
1135
            'Expiry',
1136
            'Expiry',
1137
            array(
1138
                'None' => 'None',
1139
                'Date' => 'Set document to expire on'
1140
            ),
1141
            $expiryValue
1142
        );
1143
        $expiryDatetime = DatetimeField::create('ExpireAtDate', '');
1144
        $expiryDatetime->getDateField()
1145
            ->setConfig('showcalendar', true)
1146
            ->setConfig('dateformat', 'dd-MM-yyyy')
1147
            ->setConfig('datavalueformat', 'dd-MM-yyyy');
1148
1149
        // This adds all the actions details into a group.
1150
        // Embargo, History, etc to go in here
1151
        // These are toggled on and off via the Actions Buttons above
1152
        // 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...
1153
        $actionsPanel = FieldGroup::create(
1154
            FieldGroup::create($embargo, $embargoDatetime)->addExtraClass('embargo'),
1155
            FieldGroup::create($expiry, $expiryDatetime)->addExtraClass('expiry'),
1156
            FieldGroup::create($uploadField)->addExtraClass('replace'),
1157
            FieldGroup::create($pagesGrid)->addExtraClass('find-usage'),
1158
            FieldGroup::create($referencesGrid)->addExtraClass('find-references'),
1159
            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...
1160
            FieldGroup::create($this->getRelatedDocumentsGridField())->addExtraClass('find-relateddocuments')
1161
        );
1162
1163
        $actionsPanel->setName("ActionsPanel");
1164
        $actionsPanel->addExtraClass("DMSDocumentActionsPanel");
1165
        $fields->push($actionsPanel);
1166
1167
        $this->addPermissionsFields($fields);
1168
        $this->extend('updateCMSFields', $fields);
1169
1170
        return $fields;
1171
    }
1172
1173
    /**
1174
     * Adds permissions selection fields to the FieldList.
1175
     *
1176
     * @param FieldList $fields
1177
     */
1178
    public function addPermissionsFields($fields)
1179
    {
1180
        $showFields = array(
1181
            'CanViewType'  => '',
1182
            'ViewerGroups' => 'hide',
1183
            'CanEditType'  => '',
1184
            'EditorGroups' => 'hide',
1185
        );
1186
        /** @var SiteTree $siteTree */
1187
        $siteTree = singleton('SiteTree');
1188
        $settingsFields = $siteTree->getSettingsFields();
1189
1190
        foreach ($showFields as $name => $extraCss) {
1191
            $compositeName = "Root.Settings.$name";
1192
            /** @var FormField $field */
1193
            if ($field = $settingsFields->fieldByName($compositeName)) {
1194
                $field->addExtraClass($extraCss);
1195
                $title = str_replace('page', 'document', $field->Title());
1196
                $field->setTitle($title);
1197
1198
                // Remove Inherited source option from DropdownField
1199
                if ($field instanceof DropdownField) {
1200
                    $options = $field->getSource();
1201
                    unset($options['Inherit']);
1202
                    $field->setSource($options);
1203
                }
1204
                $fields->push($field);
1205
            }
1206
        }
1207
1208
        $this->extend('updatePermissionsFields', $fields);
1209
    }
1210
1211
    public function onBeforeWrite()
1212
    {
1213
        parent::onBeforeWrite();
1214
1215
        if (isset($this->Embargo)) {
1216
            //set the embargo options from the OptionSetField created in the getCMSFields method
1217
            //do not write after clearing the embargo (write happens automatically)
1218
            $savedDate = $this->EmbargoedUntilDate;
1219
            $this->clearEmbargo(false); //clear all previous settings and re-apply them on save
1220
1221
            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...
1222
                $this->embargoUntilPublished(false);
1223
            }
1224
            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...
1225
                $this->embargoIndefinitely(false);
1226
            }
1227
            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...
1228
                $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...
1229
            }
1230
        }
1231
1232
        if (isset($this->Expiry)) {
1233
            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...
1234
                $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...
1235
            } else {
1236
                $this->clearExpiry(false);
1237
            } //clear all previous settings
1238
        }
1239
    }
1240
1241
    /**
1242
     * Return the relative URL of an icon for the file type, based on the
1243
     * {@link appCategory()} value.
1244
     *
1245
     * Images are searched for in "dms/images/app_icons/".
1246
     *
1247
     * @return string
1248
     */
1249
    public function Icon($ext)
1250
    {
1251
        if (!Director::fileExists(DMS_DIR."/images/app_icons/{$ext}_32.png")) {
1252
            $ext = File::get_app_category($ext);
1253
        }
1254
1255
        if (!Director::fileExists(DMS_DIR."/images/app_icons/{$ext}_32.png")) {
1256
            $ext = "generic";
1257
        }
1258
1259
        return DMS_DIR."/images/app_icons/{$ext}_32.png";
1260
    }
1261
1262
    /**
1263
     * Return the extension of the file associated with the document
1264
     *
1265
     * @return string
1266
     */
1267
    public function getExtension()
1268
    {
1269
        return strtolower(pathinfo($this->Filename, PATHINFO_EXTENSION));
1270
    }
1271
1272
    /**
1273
     * @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...
1274
     */
1275
    public function getSize()
1276
    {
1277
        $size = $this->getAbsoluteSize();
1278
        return ($size) ? File::format_size($size) : false;
1279
    }
1280
1281
    /**
1282
     * Return the size of the file associated with the document.
1283
     *
1284
     * @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...
1285
     */
1286
    public function getAbsoluteSize()
1287
    {
1288
        return file_exists($this->getFullPath()) ? filesize($this->getFullPath()) : null;
1289
    }
1290
1291
    /**
1292
     * An alias to DMSDocument::getSize()
1293
     *
1294
     * @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...
1295
     */
1296
    public function getFileSizeFormatted()
1297
    {
1298
        return $this->getSize();
1299
    }
1300
1301
1302
    /**
1303
     * @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...
1304
     */
1305
    protected function getFieldsForFile($relationListCount)
1306
    {
1307
        $extension = $this->getExtension();
1308
1309
        $previewField = new LiteralField(
1310
            "ImageFull",
1311
            "<img id='thumbnailImage' class='thumbnail-preview' src='{$this->Icon($extension)}?r="
1312
            . rand(1, 100000) . "' alt='{$this->Title}' />\n"
1313
        );
1314
1315
        //count the number of pages this document is published on
1316
        $publishedOnCount = $this->Pages()->Count();
1317
        $publishedOnValue = "$publishedOnCount pages";
1318
        if ($publishedOnCount == 1) {
1319
            $publishedOnValue = "$publishedOnCount page";
1320
        }
1321
1322
        $relationListCountValue = "$relationListCount pages";
1323
        if ($relationListCount == 1) {
1324
            $relationListCountValue = "$relationListCount page";
1325
        }
1326
1327
        $fields = new FieldGroup(
1328
            $filePreview = CompositeField::create(
1329
                CompositeField::create(
1330
                    $previewField
1331
                )->setName("FilePreviewImage")->addExtraClass('cms-file-info-preview'),
1332
                CompositeField::create(
1333
                    CompositeField::create(
1334
                        new ReadonlyField("ID", "ID number". ':', $this->ID),
1335
                        new ReadonlyField(
1336
                            "FileType",
1337
                            _t('AssetTableField.TYPE', 'File type') . ':',
1338
                            self::get_file_type($extension)
1339
                        ),
1340
                        new ReadonlyField(
1341
                            "Size",
1342
                            _t('AssetTableField.SIZE', 'File size') . ':',
1343
                            $this->getFileSizeFormatted()
1344
                        ),
1345
                        $urlField = new ReadonlyField(
1346
                            'ClickableURL',
1347
                            _t('AssetTableField.URL', 'URL'),
1348
                            sprintf(
1349
                                '<a href="%s" target="_blank" class="file-url">%s</a>',
1350
                                $this->getLink(),
1351
                                $this->getLink()
1352
                            )
1353
                        ),
1354
                        new ReadonlyField("FilenameWithoutIDField", "Filename". ':', $this->getFilenameWithoutID()),
1355
                        new DateField_Disabled(
1356
                            "Created",
1357
                            _t('AssetTableField.CREATED', 'First uploaded') . ':',
1358
                            $this->Created
1359
                        ),
1360
                        new DateField_Disabled(
1361
                            "LastEdited",
1362
                            _t('AssetTableField.LASTEDIT', 'Last changed') . ':',
1363
                            $this->LastEdited
1364
                        ),
1365
                        new DateField_Disabled(
1366
                            "LastChanged",
1367
                            _t('AssetTableField.LASTCHANGED', 'Last replaced') . ':',
1368
                            $this->LastChanged
1369
                        ),
1370
                        new ReadonlyField("PublishedOn", "Published on". ':', $publishedOnValue),
1371
                        new ReadonlyField("ReferencedOn", "Referenced on". ':', $relationListCountValue),
1372
                        new ReadonlyField("ViewCount", "View count". ':', $this->ViewCount)
1373
                    )
1374
                )->setName("FilePreviewData")->addExtraClass('cms-file-info-data')
1375
            )->setName("FilePreview")->addExtraClass('cms-file-info')
1376
        );
1377
1378
        $fields->setName('FileP');
1379
        $urlField->dontEscape = true;
1380
1381
        return $fields;
1382
    }
1383
1384
    /**
1385
     * Takes a file and adds it to the DMSDocument storage, replacing the
1386
     * current file.
1387
     *
1388
     * @param File $file
1389
     *
1390
     * @return $this
1391
     */
1392
    public function ingestFile($file)
1393
    {
1394
        $this->replaceDocument($file);
1395
        $file->delete();
1396
1397
        return $this;
1398
    }
1399
1400
    /**
1401
     * Get a data list of documents related to this document
1402
     *
1403
     * @return DataList
1404
     */
1405
    public function getRelatedDocuments()
1406
    {
1407
        $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...
1408
1409
        $this->extend('updateRelatedDocuments', $documents);
1410
1411
        return $documents;
1412
    }
1413
1414
    /**
1415
     * Get a GridField for managing related documents
1416
     *
1417
     * @return GridField
1418
     */
1419
    protected function getRelatedDocumentsGridField()
1420
    {
1421
        $gridField = GridField::create(
1422
            'RelatedDocuments',
1423
            _t('DMSDocument.RELATEDDOCUMENTS', 'Related Documents'),
1424
            $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...
1425
            new GridFieldConfig_RelationEditor
1426
        );
1427
1428
        $gridField->getConfig()->removeComponentsByType('GridFieldAddNewButton');
1429
        // Move the autocompleter to the left
1430
        $gridField->getConfig()->removeComponentsByType('GridFieldAddExistingAutocompleter');
1431
        $gridField->getConfig()->addComponent(new GridFieldAddExistingAutocompleter('buttons-before-left'));
1432
1433
        $this->extend('updateRelatedDocumentsGridField', $gridField);
1434
1435
        return $gridField;
1436
    }
1437
1438
    /**
1439
     * Checks at least one group is selected if CanViewType || CanEditType == 'OnlyTheseUsers'
1440
     *
1441
     * @return ValidationResult
1442
     */
1443
    protected function validate()
1444
    {
1445
        $valid = parent::validate();
1446
1447
        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...
1448
            $valid->error(
1449
                _t(
1450
                    'DMSDocument.VALIDATIONERROR_NOVIEWERSELECTED',
1451
                    "Selecting 'Only these people' from a viewers list needs at least one group selected."
1452
                )
1453
            );
1454
        }
1455
1456
        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...
1457
            $valid->error(
1458
                _t(
1459
                    'DMSDocument.VALIDATIONERROR_NOEDITORSELECTED',
1460
                    "Selecting 'Only these people' from a editors list needs at least one group selected."
1461
                )
1462
            );
1463
        }
1464
1465
        return $valid;
1466
    }
1467
1468
    /**
1469
     * Returns a reason as to why this document cannot be viewed.
1470
     *
1471
     * @return string
1472
     */
1473
    public function getPermissionDeniedReason()
1474
    {
1475
        $result = '';
1476
1477
        if ($this->CanViewType == 'LoggedInUsers') {
1478
            $result = _t('DMSDocument.PERMISSIONDENIEDREASON_LOGINREQUIRED', 'Please log in to view this document');
1479
        }
1480
1481
        if ($this->CanViewType == 'OnlyTheseUsers') {
1482
            $result = _t('DMSDocument.PERMISSIONDENIEDREASON_NOTAUTHORISED',
1483
                'You are not authorised to view this document');
1484
        }
1485
1486
        return $result;
1487
    }
1488
}
1489