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

DMSDocument::addPermissionsFields()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 32
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

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