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

DMSDocument::getRelatedDocumentsGridField()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 11
nc 1
nop 0
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 94 and the first side effect is on line 26.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
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
Bug introduced by
Possible parse error: class missing opening or closing brace
Loading history...
27
{
28
    private static $db = array(
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(
47
        'Pages' => 'SiteTree',
48
        'RelatedDocuments' => 'DMSDocument',
49
        'Tags' => 'DMSTag',
50
        'ViewerGroups' => 'Group',
51
        'EditorGroups' => 'Group',
52
    );
53
54
    private static $many_many_extraFields = array(
55
        'Pages' => array(
56
            'DocumentSort' => 'Int'
57
        )
58
    );
59
60
    private static $display_fields = array(
61
        'ID' => 'ID',
62
        'Title' => 'Title',
63
        'FilenameWithoutID' => 'Filename',
64
        'LastChanged' => 'LastChanged'
65
    );
66
67
    private static $singular_name = 'Document';
68
69
    private static $plural_name = 'Documents';
70
71
    private static $searchable_fields = array(
72
        'ID' => array(
73
            'filter' => 'ExactMatchFilter',
74
            'field' => 'NumericField'
75
        ),
76
        'Title',
77
        'Filename',
78
        'LastChanged'
79
    );
80
81
    private static $summary_fields = array(
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';
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
        if ($member && Permission::checkMember($member,
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()) {
124
            $result = ($member && $member->inGroups($this->ViewerGroups()));
125
126
            return $result;
127
        }
128
129
        return $this->canEdit($member);
130
    }
131
132
    public function canEdit($member = null)
133
    {
134
        if (!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
135
            $member = Member::currentUser();
136
        }
137
138
<<<<<<< HEAD
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

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