Category: JavaScript

  • Silverstripe 3 – Per user page access permissions

    Most of the times group access in SilverStripe is sufficient for controlling user access, but for this project I had a specific situation where every user needs to have a dedicated page. In order to avoid unnecessary editing and creating groups for each individual user, I decided to extend SiteTree and create a page with per user access control.

    So, I’ve created a new page type, since I only needed it for a single page type, but you can extend/decorate SiteTree any way you like.

    Let’s get to the code. This goes to mysite/code/FilePage.php

    <?php
    
    class FilePage extends Page {
    
    	static $db = array(
    		"CanViewTypeExtended" => "Enum('Anyone, LoggedInUsers, OnlyTheseUsers, OnlyTheseMembers, Inherit', 'Inherit')",
    	);
    
    	static $has_one = array(
    		"ViewerMember" => "Member",
    	);
    
    	public function getSettingsFields(){
    		global $project;
    		$fields = parent::getSettingsFields();
    
    		Requirements::javascript($project.'/javascript/CMSMain.EditFormMember.js'); //Not sure where to put this :)
    
    		$members = Member::map_in_groups();
    		$field = new DropdownField("ViewerMemberID", "Viewer Members", $members);
    		$field->setEmptyString('(Select one)');
    		$fields->addFieldToTab("Root.Settings", $field, 'CanEditType');
    
    		$viewersOptionsField = $fields->removeByName('CanViewType');
    		$viewersOptions = new OptionsetField(
    						"CanViewTypeExtended", 
    						_t('SiteTree.ACCESSHEADER', "Who can view this page?")
    					);
    		$viewersOptionsSource = array();
    		$viewersOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
    		$viewersOptionsSource["Anyone"] = _t('SiteTree.ACCESSANYONE', "Anyone");
    		$viewersOptionsSource["LoggedInUsers"] = _t('SiteTree.ACCESSLOGGEDIN', "Logged-in users");
    		$viewersOptionsSource["OnlyTheseUsers"] = _t('SiteTree.ACCESSONLYTHESE', "Only these people (choose from list)");
    		$viewersOptionsSource["OnlyTheseMembers"] = _t('SiteTree.ACCESSONLYTHESEMEMBERS', "Only these members (choose from list)");
    		$viewersOptions->setSource($viewersOptionsSource);
    		$fields->addFieldToTab("Root.Settings", $viewersOptions, 'ViewerGroups');
    
    		return $fields;
    	}
    
    	//	Override the SiteTree canView function to implement the new variable
    	public function canView($member = null) {
    		if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
    			$member = Member::currentUserID();
    		}
    
    		// admin override
    		if($member && Permission::checkMember($member, array("ADMIN", "SITETREE_VIEW_ALL"))) return true;
    
    		// Standard mechanism for accepting permission changes from extensions
    		$extended = $this->extendedCan('canView', $member);
    		if($extended !== null) return $extended;
    
    		// check for empty spec
    		if(!$this->CanViewTypeExtended || $this->CanViewTypeExtended == 'Anyone') return true;
    
    		// check for inherit
    		if($this->CanViewTypeExtended == 'Inherit') {
    			if($this->ParentID) return $this->Parent()->canView($member);
    			else return $this->getSiteConfig()->canView($member);
    		}
    
    		// check for any logged-in users
    		if($this->CanViewTypeExtended == 'LoggedInUsers' && $member) {
    			return true;
    		}
    
    		// check for specific groups
    		if($member && is_numeric($member)) $member = DataObject::get_by_id('Member', $member);
    		if(
    			$this->CanViewTypeExtended == 'OnlyTheseUsers' 
    			&& $member 
    			&& $member->inGroups($this->ViewerGroups())
    		) return true;
    
                    //Check for specific Member
    		if(
    			$this->CanViewTypeExtended == 'OnlyTheseMembers' 
    			&& $member 
    			&& $member->ID == $this->ViewerMemberID
    		) return true;
    
    		return false;
    	}
    }

    So, let’s break it down a bit:

    I’ve created a new variable – CanViewTypeExtended to replace SiteTree’s CanViewType, since I couldn’t find the way to add an option (If you know a way, feel free to drop a line). It replicates SiteTree’s CanViewType with OnlyTheseMembers option added, which is our per user access type.

    Then, we have has one ViewerMember, which holds actual user ID for single user (I’ve limited it to one user, since using any more would be a group).

    The rest is pretty basic, mostly copied from SiteTree with a bit of additions, getSettingsFields  is a standard function for updating the Settings tab in CMS. There we have first included the JavaScript file, which will be shown later, and is only for decoration – showing and hiding fields based on selection.

    Then we have created Member selection field, to pick a member to which the access will be granted, and replicated creating CanViewType field from SiteTree.php with addition of our new OnlyTheseMembers option.

    After this is saved, all that is left is to check user permissions in canView() method. Since we don’t use CanViewType any more, but have replaced it with CanViewTypeExtended, the entire function is copied from SiteTree.php, except for the last part which grants the access if current member is our selected member:

    if($this->CanViewTypeExtended == ‘OnlyTheseMembers’ && $member && $member->ID == $this->ViewerMemberID)
    return true;

    So, here’s the remaining js file, which goes to mysite/javascript/CMSMain.EditFormMember.js

    /**
     * File: CMSMain.EditFormMember.js
     */
    (function($) {
    	$.entwine('ss', function($){
    		/**
    		 * Class: .cms-edit-form #CanViewTypeExtended
    		 * 
    		 * Toggle display of Member dropdown in "access" tab,
    		 * based on selection of radiobuttons.
    		 */
    		$('.cms-edit-form #CanViewTypeExtended').entwine({
    			// Constructor: onmatch
    			onmatch: function() {
    				// TODO Decouple
    				var dropdownMembers = $('#ViewerMemberID');
    				var dropdownGroups = $('#ViewerGroups');
    
    				this.find('.optionset :input').bind('change', function(e) {
    					var wrapper = $(this).closest('.middleColumn').parent('div');
    					var wrapper2 = dropdownMembers.closest('.middleColumn').parent('div');
    					if(e.target.value == 'OnlyTheseMembers') {
    						wrapper.addClass('remove-splitter');
    						dropdownMembers['show']();
    						dropdownGroups['hide']();
    					}else if(e.target.value == 'OnlyTheseUsers') {
    						wrapper.addClass('remove-splitter');
    						dropdownMembers['hide']();
    						dropdownGroups['show']();
    					}else{
    						wrapper.removeClass('remove-splitter');
    						dropdownMembers['hide']();
    						dropdownGroups['hide']();
    					}
    				});
    
    				// initial state
    				var currentVal = this.find('input[name=' + this.attr('id') + ']:checked').val();
    				dropdownMembers[currentVal == 'OnlyTheseMembers' ? 'show' : 'hide']();
    				dropdownGroups[currentVal == 'OnlyTheseUsers' ? 'show' : 'hide']();
    
    				this._super();
    			},
    			onunmatch: function() {
    				this._super();
    			}
    		});
    
    	});
    }(jQuery));

     

  • Integrate CKFinder with TinyMCE

    I know TinyMCE isn’t very popular lately, since it’s not free any more, but I was asked by a client to fix the file uploading with TinyMCE in their CMS. It uses the last free version of TinyMCE – 1.41, and used to have TinyBrowser integrated for file browsing. As the TinyBrowser stopped working and support is equal to nothing, I decided to integrate CKFinder instead.

    It wasn’t an easy task, but bit by bit i somehow managed it. What’s even worse, their CMS didn’t use any frameworks, so I decided to keep it that way, and wrote pure javascript. The script isn’t very nice, but it works.

    OK, let’s get to the coding part. After importing the needed .js files first thing that needed to be done is defining the custom function in TinyMCE initialization that will be called when the Browse button is pressed.

    <script type="text/javascript">
    tinyMCE.init({ language : "sr", mode : "textareas", theme : "advanced", plugins : "safari,spellchecker,pagebreak,style,layer,table,save,advhr,advimage,advlink,emotions,iespell,inlinepopups,insertdatetime,preview,media,searchreplace,print,contextmenu,paste,directionality,fullscreen,noneditable,visualchars,nonbreaking,xhtmlxtras,template", theme_advanced_buttons1 : "bold,italic,underline,strikethrough,|,pastetext,pasteword,|,bullist,numlist,|,outdent,indent,blockquote,|,undo,redo,|,link,unlink,anchor,image,|,forecolor,backcolor,|,code,fullscreen", theme_advanced_buttons2 : "", theme_advanced_buttons3 : "", theme_advanced_buttons4 : "", theme_advanced_toolbar_location : "top", theme_advanced_toolbar_align : "left", theme_advanced_statusbar_location : "bottom", relative_urls : false, file_browser_callback : 'myFileBrowser', external_link_list_url : "/js/myexternallist.js" });
    </script>

     

    Here it’s named myFileBrowser (file_browser_callback : ‘myFileBrowser’). Nothing much to say about that, so let’s skip to myFileBrowser function, which launches the CKBrowser itself.

    <script type="text/javascript">
    function myFileBrowser (field_name, url, type, win) {
    // You can use the "CKFinder" class to render CKFinder in a page:
    var finder = new CKFinder();
    // The path for the installation of CKFinder (default = "/ckfinder/").
    finder.basePath = '/js/ckfinder/';
    // Name of a function which is called when a file is selected in
    CKFinder.finder.selectActionFunction = SetFileField;
    // Additional data to be passed to the selectActionFunction in a second argument.
    // We'll use this feature to pass the Id of a field that will be updated.
    finder.selectActionData = field_name;
    // Launch CKFinder
    finder.popup();
    }
    </script>

     

    I left the comments from the example I found for using CKBrowser with a textfield. So, basicaly we just init the finder and set what function will be called when the file is selected. SetFileField is the function that is left to be created, and file_name will be contained in the data array passed to the function. Let’s get onto it:

    <script type="text/javascript">
    function SetFileField( fileUrl, data ){
    var frId = getElementsByClassName("clearlooks2", "div")[0].getElementsByTagName("iframe")[0].id;
    document.getElementById(frId).contentWindow.document.getElementById(data["selectActionData"]).value = fileUrl;
    }
    </script>

     

    This part was a bit tricky, since TinyMCE opens iFrames in modal dialogs I couldn’t just access the text field by it’s id, but had to access it via iFrames id. To do that, as the frames id is generated again with each modal opening, I had to find the current assigned id. I used a bit of trick there, by accessing the overlay div by class, as it is always static (clearlooks2), and then getting element by tag name “iframe”. Once I had that prepared, I was able to get the field by id and populate it’s contents. In my case, as there was no framework support, I used Robert’s Ultimate getElementsByClassName to get the element by class.

    I don’t know if this will be useful to someone, as TinyMCE is disappearing from the scene, but if anyone get’s in a similar situation, I’m sure this will help.