diff --git a/_config.php b/_config.php index 6d59e6c..b288829 100644 --- a/_config.php +++ b/_config.php @@ -15,11 +15,11 @@ if (!file_exists(BASE_PATH . DIRECTORY_SEPARATOR . DMS_DIR)) { CMSMenu::remove_menu_item('DMSDocumentAddController'); ShortcodeParser::get('default')->register( - 'dms_document_link', + 'dms_document_link', array('DMSShortcodeHandler', 'handle') ); if ($config->get('DMSDocument_versions', 'enable_versions')) { - //using the same db relations for the versioned documents, as for the actual documents - $config->update('DMSDocument_versions', 'db', $config->get('DMSDocument', 'db')); + //using the same db relations for the versioned documents, as for the actual documents + $config->update('DMSDocument_versions', 'db', $config->get('DMSDocument', 'db')); } diff --git a/code/model/DMSDocument.php b/code/model/DMSDocument.php index 95961ba..6a565e9 100644 --- a/code/model/DMSDocument.php +++ b/code/model/DMSDocument.php @@ -2,6 +2,26 @@ /** * @package dms + * + * @property Varchar Filename + * @property Varchar Folder + * @property Varchar Title + * @property Text Description + * @property int ViewCount + * @property DateTime LastChanged + * @property Boolean EmbargoedIndefinitely + * @property Boolean EmbargoedUntilPublished + * @property DateTime EmbargoedUntilDate + * @property DateTime ExpireAtDate + * @property Enum DownloadBehavior + * @property Enum CanViewType Enum('Anyone, LoggedInUsers, OnlyTheseUsers', 'Anyone') + * @property Enum CanEditType Enum('LoggedInUsers, OnlyTheseUsers', 'LoggedInUsers') + * + * @method ManyManyList RelatedDocuments + * @method ManyManyList Tags + * @method ManyManyList ViewerGroups + * @method ManyManyList EditorGroups + * */ class DMSDocument extends DataObject implements DMSDocumentInterface { @@ -19,12 +39,16 @@ class DMSDocument extends DataObject implements DMSDocumentInterface "EmbargoedUntilDate" => 'SS_DateTime', "ExpireAtDate" => 'SS_DateTime', "DownloadBehavior" => 'Enum(array("open","download"), "download")', + "CanViewType" => "Enum('Anyone, LoggedInUsers, OnlyTheseUsers', 'Anyone')", + "CanEditType" => "Enum('LoggedInUsers, OnlyTheseUsers', 'LoggedInUsers')", ); private static $many_many = array( 'Pages' => 'SiteTree', 'RelatedDocuments' => 'DMSDocument', - 'Tags' => 'DMSTag' + 'Tags' => 'DMSTag', + 'ViewerGroups' => 'Group', + 'EditorGroups' => 'Group', ); private static $many_many_extraFields = array( @@ -67,11 +91,6 @@ class DMSDocument extends DataObject implements DMSDocumentInterface */ private static $default_download_behaviour = 'download'; - /** - * @param Member $member - * - * @return boolean - */ public function canView($member = null) { if (!$member || !(is_a($member, 'Member')) || is_numeric($member)) { @@ -87,18 +106,36 @@ class DMSDocument extends DataObject implements DMSDocumentInterface } } - if ($member && $member->ID) { + if (!$this->CanViewType || $this->CanViewType == 'Anyone') { return true; } - return false; + if ($member && Permission::checkMember($member, + array( + 'ADMIN', + 'SITETREE_EDIT_ALL', + 'SITETREE_VIEW_ALL', + ) + ) + ) { + return true; + } + + if ($this->isHidden()) { + return false; + } + + if ($this->CanViewType == 'LoggedInUsers') { + return $member && $member->exists(); + } + + if ($this->CanViewType == 'OnlyTheseUsers' && $this->ViewerGroups()->count()) { + return ($member && $member->inGroups($this->ViewerGroups())); + } + + return $this->canEdit($member); } - /** - * @param Member $member - * - * @return boolean - */ public function canEdit($member = null) { if (!$member || !(is_a($member, 'Member')) || is_numeric($member)) { @@ -113,7 +150,27 @@ class DMSDocument extends DataObject implements DMSDocumentInterface } } - return $this->canView(); + // Do early admin check + if ($member && Permission::checkMember($member, + array( + 'ADMIN', + 'SITETREE_EDIT_ALL', + 'SITETREE_VIEW_ALL', + ) + ) + ) { + return true; + } + + if ($this->CanEditType === 'LoggedInUsers') { + return $member && $member->exists(); + } + + if ($this->CanEditType === 'OnlyTheseUsers' && $this->EditorGroups()->count()) { + return $member && $member->inGroups($this->EditorGroups()); + } + + return ($member && Permission::checkMember($member, array('ADMIN', 'SITETREE_EDIT_ALL'))); } /** @@ -135,7 +192,7 @@ class DMSDocument extends DataObject implements DMSDocumentInterface } } - return $this->canView(); + return $this->canEdit($member); } /** @@ -294,7 +351,7 @@ class DMSDocument extends DataObject implements DMSDocumentInterface * @param string $category of a metadata category to add (required) * @param string $value of a metadata value to add (required) * @param bool $multiValue Boolean that determines if the category is - * multi-value or single-value (optional) + * multi-value or single-value (optional) * * @return DMSDocument */ @@ -445,12 +502,30 @@ class DMSDocument extends DataObject implements DMSDocumentInterface /** * Returns a link to download this document from the DMS store. + * Alternatively a basic javascript alert will be shown should the user not have view permissions. An extension + * point for this was also added. + * + * To extend use the following from within an Extension subclass: + * + * + * public function updateGetLink($result) + * { + * // Do something here + * } + * * * @return string */ public function getLink() { - return Controller::join_links(Director::baseURL(), 'dmsdocument/'.$this->ID); + $result = Controller::join_links(Director::baseURL(), 'dmsdocument/' . $this->ID); + if (!$this->canView()) { + $result = sprintf("javascript:alert('%s')", $this->getPermissionDeniedReason()); + } + + $this->extend('updateGetLink', $result); + + return $result; } /** @@ -1095,14 +1170,52 @@ class DMSDocument extends DataObject implements DMSDocumentInterface $actionsPanel->setName("ActionsPanel"); $actionsPanel->addExtraClass("DMSDocumentActionsPanel"); - $fields->push($actionsPanel); + $this->addPermissionsFields($fields); $this->extend('updateCMSFields', $fields); return $fields; } + /** + * Adds permissions selection fields to the FieldList. + * + * @param FieldList $fields + */ + public function addPermissionsFields($fields) + { + $showFields = array( + 'CanViewType' => '', + 'ViewerGroups' => 'hide', + 'CanEditType' => '', + 'EditorGroups' => 'hide', + ); + /** @var SiteTree $siteTree */ + $siteTree = singleton('SiteTree'); + $settingsFields = $siteTree->getSettingsFields(); + + foreach ($showFields as $name => $extraCss) { + $compositeName = "Root.Settings.$name"; + /** @var FormField $field */ + if ($field = $settingsFields->fieldByName($compositeName)) { + $field->addExtraClass($extraCss); + $title = str_replace('page', 'document', $field->Title()); + $field->setTitle($title); + + // Remove Inherited source option from DropdownField + if ($field instanceof DropdownField) { + $options = $field->getSource(); + unset($options['Inherit']); + $field->setSource($options); + } + $fields->push($field); + } + } + + $this->extend('updatePermissionsFields', $fields); + } + public function onBeforeWrite() { parent::onBeforeWrite(); @@ -1329,4 +1442,55 @@ class DMSDocument extends DataObject implements DMSDocumentInterface return $gridField; } + + /** + * Checks at least one group is selected if CanViewType || CanEditType == 'OnlyTheseUsers' + * + * @return ValidationResult + */ + protected function validate() + { + $valid = parent::validate(); + + if ($this->CanViewType == 'OnlyTheseUsers' && !$this->ViewerGroups()->count()) { + $valid->error( + _t( + 'DMSDocument.VALIDATIONERROR_NOVIEWERSELECTED', + "Selecting 'Only these people' from a viewers list needs at least one group selected." + ) + ); + } + + if ($this->CanEditType == 'OnlyTheseUsers' && !$this->EditorGroups()->count()) { + $valid->error( + _t( + 'DMSDocument.VALIDATIONERROR_NOEDITORSELECTED', + "Selecting 'Only these people' from a editors list needs at least one group selected." + ) + ); + } + + return $valid; + } + + /** + * Returns a reason as to why this document cannot be viewed. + * + * @return string + */ + public function getPermissionDeniedReason() + { + $result = ''; + + if ($this->CanViewType == 'LoggedInUsers') { + $result = _t('DMSDocument.PERMISSIONDENIEDREASON_LOGINREQUIRED', 'Please log in to view this document'); + } + + if ($this->CanViewType == 'OnlyTheseUsers') { + $result = _t('DMSDocument.PERMISSIONDENIEDREASON_NOTAUTHORISED', + 'You are not authorised to view this document'); + } + + return $result; + } } diff --git a/code/model/DMSDocument_Controller.php b/code/model/DMSDocument_Controller.php index ab32db2..7fa4f0b 100644 --- a/code/model/DMSDocument_Controller.php +++ b/code/model/DMSDocument_Controller.php @@ -54,39 +54,9 @@ class DMSDocument_Controller extends Controller { $doc = $this->getDocumentFromID($request); + if (!empty($doc)) { - $canView = false; - - // Runs through all pages that this page links to and sets canView - // to true if the user can view ONE of these pages - if (method_exists($doc, 'Pages')) { - $pages = $doc->Pages(); - if ($pages->Count() > 0) { - foreach ($pages as $page) { - if ($page->CanView()) { - // just one canView is enough to know that we can - // view the file - $canView = true; - break; - } - } - } else { - // if the document isn't on any page, then allow viewing of - // the document (because there is no canView() to consult) - $canView = true; - } - } - - // check for embargo or expiry - if ($doc->isHidden()) { - $canView = false; - } - - //admins can always download any document, even if otherwise hidden - $member = Member::currentUser(); - if ($member && Permission::checkMember($member, 'ADMIN')) { - $canView = true; - } + $canView = $doc->canView(); if ($canView) { $path = $doc->getFullPath(); @@ -129,8 +99,7 @@ class DMSDocument_Controller extends Controller //its ViewCount should be increased by 1 just before the browser sending the file to front. $doc->trackView(); - $this->sendFile($path, $mime, $doc->getFilenameWithoutID(), $disposition); - return; + return $this->sendFile($path, $mime, $doc->getFilenameWithoutID(), $disposition); } } } diff --git a/javascript/DMSDocumentCMSFields.js b/javascript/DMSDocumentCMSFields.js index ecbffde..82b138b 100644 --- a/javascript/DMSDocumentCMSFields.js +++ b/javascript/DMSDocumentCMSFields.js @@ -1,7 +1,7 @@ -(function ($) { +(function($) { "use strict"; - $.entwine('ss', function ($) { + $.entwine('ss', function($) { $('#DocumentTypeID ul li').entwine({ onadd: function () { @@ -11,7 +11,7 @@ this.addClass('selected'); } }, - onclick: function (e) { + onclick: function(e) { $('#DocumentTypeID').find('li.selected').removeClass('selected'); this.find('input').prop("checked", true); this.addClass('selected'); @@ -34,7 +34,22 @@ } } });*/ - + $('#CanViewType input, #CanEditType input').entwine({ + onchange: function () { + var dropDown = $(this).closest('.field').next(); + if ($(this).val() === 'OnlyTheseUsers') { + dropDown.removeClass('hide'); + } else { + dropDown.addClass('hide'); + } + }, + onadd: function () { + if ($(this).is(':checked') && $(this).val() === 'OnlyTheseUsers') { + var dropDown = $(this).closest('.field').next(); + dropDown.removeClass('hide'); + } + } + }); $('#Actions ul li').entwine({ onclick: function (e) { @@ -56,7 +71,6 @@ $('#Form_ItemEditForm_Embargo input, #Form_EditForm_Embargo input').entwine({ onchange: function () { - console.log('called'); //selected the date options if (this.attr('value') === 'Date') { $('.embargoDatetime').children().show(); diff --git a/templates/Includes/Document.ss b/templates/Includes/Document.ss index d074b01..2428804 100644 --- a/templates/Includes/Document.ss +++ b/templates/Includes/Document.ss @@ -1,4 +1,4 @@ -<% if $isHidden != true %> +<% if not $isHidden %>
<% if $Title %>

">$Title

diff --git a/tests/DMSDocumentTest.php b/tests/DMSDocumentTest.php index 10f3660..395a281 100644 --- a/tests/DMSDocumentTest.php +++ b/tests/DMSDocumentTest.php @@ -237,4 +237,103 @@ class DMSDocumentTest extends SapphireTest 'Related documents GridField does not have an "add new" button' ); } + + /* + * Tests whether the permissions fields are added + */ + public function testAddPermissionsFields() + { + $document = $this->objFromFixture('DMSDocument', 'd1'); + $fields = $document->getCMSFields(); + + $this->assertNotNull($fields->fieldByName('CanViewType')); + $this->assertNotNull($fields->fieldByName('ViewerGroups')); + } + + /** + * Test view permissions + */ + public function testCanView() + { + /** @var DMSDocument $document */ + $document = $this->objFromFixture('DMSDocument', 'doc-logged-in-users'); + // Make sure user is logged out + if ($member = Member::currentUser()) { + $member->logOut(); + } + + // Logged out user test + $this->assertFalse($document->canView()); + + // Logged in user test + $adminID = $this->logInWithPermission(); + $admin = Member::get()->byID($adminID); + $this->assertTrue($document->canView($admin)); + /** @var Member $member */ + $admin->logout(); + + // Check anyone + $document = $this->objFromFixture('DMSDocument', 'doc-anyone'); + $this->assertTrue($document->canView()); + + // Check OnlyTheseUsers + $document = $this->objFromFixture('DMSDocument', 'doc-only-these-users'); + $reportAdminID = $this->logInWithPermission('cable-guy'); + /** @var Member $reportAdmin */ + $reportAdmin = Member::get()->byID($reportAdminID); + $this->assertFalse($document->canView($reportAdmin)); + // Add reportAdmin to group + $reportAdmin->addToGroupByCode('content-author'); + $this->assertTrue($document->canView($reportAdmin)); + $reportAdmin->logout(); + } + + /** + * Tests edit permissions + */ + public function testCanEdit() + { + // Make sure user is logged out + if ($member = Member::currentUser()) { + $member->logOut(); + } + + /** @var DMSDocument $document1 */ + $document1 = $this->objFromFixture('DMSDocument', 'doc-logged-in-users'); + + // Logged out user test + $this->assertFalse($document1->canEdit()); + + //Logged in user test + $contentAuthor = $this->objFromFixture('Member', 'editor'); + $this->assertTrue($document1->canEdit($contentAuthor)); + + // Check OnlyTheseUsers + /** @var DMSDocument $document2 */ + $document2 = $this->objFromFixture('DMSDocument', 'doc-only-these-users'); + /** @var Member $cableGuy */ + $cableGuy = $this->objFromFixture('Member', 'non-editor'); + $this->assertFalse($document2->canEdit($cableGuy)); + + $cableGuy->addToGroupByCode('content-author'); + $this->assertTrue($document2->canEdit($cableGuy)); + } + + /** + * Test permission denied reasons for documents + */ + public function testGetPermissionDeniedReason() + { + /** @var DMSDocument $document1 */ + $doc1 = $this->objFromFixture('DMSDocument', 'doc-logged-in-users'); + $this->assertContains('Please log in to view this document', $doc1->getPermissionDeniedReason()); + + /** @var DMSDocument $doc2 */ + $doc2 = $this->objFromFixture('DMSDocument', 'doc-only-these-users'); + $this->assertContains('You are not authorised to view this document', $doc2->getPermissionDeniedReason()); + + /** @var DMSDocument $doc3 */ + $doc3 = $this->objFromFixture('DMSDocument', 'doc-anyone'); + $this->assertEquals('', $doc3->getPermissionDeniedReason()); + } } diff --git a/tests/DMSEmbargoTest.php b/tests/DMSEmbargoTest.php index 2880b1d..543c3dc 100644 --- a/tests/DMSEmbargoTest.php +++ b/tests/DMSEmbargoTest.php @@ -35,6 +35,7 @@ class DMSEmbargoTest extends SapphireTest $doc = new DMSDocument(); $doc->Filename = "DMS-test-lorum-file.pdf"; $doc->Folder = "tests"; + $doc->CanViewType = 'LoggedInUsers'; $docID = $doc->write(); //fake a request for a document diff --git a/tests/dmstest.yml b/tests/dmstest.yml index 3832895..9d101c4 100644 --- a/tests/dmstest.yml +++ b/tests/dmstest.yml @@ -36,6 +36,13 @@ DMSTag: t6: Category: tag6 Value: tag6value +Group: + content-author: + Code: content-author + Title: "Content Author" + cable-guy: + Code: cable-guy + Title: "Cable Guy" DMSDocument: d1: Filename: test-file-file-doesnt-exist-1 @@ -51,3 +58,32 @@ DMSDocument: Filename: file-with-relations Folder: 5 RelatedDocuments: =>DMSDocument.d1, =>DMSDocument.d2 + doc-logged-in-users: + FileName: doc-logged-in-users + CanViewType: LoggedInUsers + CanEditType: LoggedInUsers + Folder: 5 + Tags: =>DMSTag.t1, =>DMSTag.t2, =>DMSTag.t3, =>DMSTag.t4 + Pages: =>SiteTree.s1, =>SiteTree.s2, =>SiteTree.s3, =>SiteTree.s4, =>SiteTree.s5, =>SiteTree.s6 + doc-anyone: + FileName: doc-anyone + CanViewType: Anyone + Folder: 5 + Tags: =>DMSTag.t1, =>DMSTag.t2, =>DMSTag.t3, =>DMSTag.t4 + Pages: =>SiteTree.s1, =>SiteTree.s2, =>SiteTree.s3, =>SiteTree.s4, =>SiteTree.s5, =>SiteTree.s6 + doc-only-these-users: + FileName: doc-only-these-users + CanViewType: OnlyTheseUsers + CanEditType: OnlyTheseUsers + Folder: 5 + Tags: =>DMSTag.t1, =>DMSTag.t2, =>DMSTag.t3, =>DMSTag.t4 + Pages: =>SiteTree.s1, =>SiteTree.s2, =>SiteTree.s3, =>SiteTree.s4, =>SiteTree.s5, =>SiteTree.s6 + ViewerGroups: =>Group.content-author + EditorGroups: =>Group.content-author +Member: + editor: + Name: editor + Groups: =>Group.content-author + non-editor: + Name: cable-guy + Groups: =>Group.cable-guy