Making docs gender agnostic

This commit is contained in:
Daniel Hensby 2015-03-07 12:32:04 +00:00
parent 0898487ad2
commit d2a3da2203
12 changed files with 243 additions and 245 deletions

View File

@ -10,7 +10,7 @@ jQuery.noConflict();
// Entwine's 'fromWindow::onresize' does not trigger on IE8. Use synthetic event. // Entwine's 'fromWindow::onresize' does not trigger on IE8. Use synthetic event.
var cb = function() {$('.cms-container').trigger('windowresize');}; var cb = function() {$('.cms-container').trigger('windowresize');};
// Workaround to avoid IE8 infinite loops when elements are resized as a result of this event // Workaround to avoid IE8 infinite loops when elements are resized as a result of this event
if($.browser.msie && parseInt($.browser.version, 10) < 9) { if($.browser.msie && parseInt($.browser.version, 10) < 9) {
var newWindowWidth = $(window).width(), newWindowHeight = $(window).height(); var newWindowWidth = $(window).width(), newWindowHeight = $(window).height();
if(newWindowWidth != windowWidth || newWindowHeight != windowHeight) { if(newWindowWidth != windowWidth || newWindowHeight != windowHeight) {
@ -26,7 +26,7 @@ jQuery.noConflict();
// setup jquery.entwine // setup jquery.entwine
$.entwine.warningLevel = $.entwine.WARN_LEVEL_BESTPRACTISE; $.entwine.warningLevel = $.entwine.WARN_LEVEL_BESTPRACTISE;
$.entwine('ss', function($) { $.entwine('ss', function($) {
/* /*
* Handle messages sent via nested iframes * Handle messages sent via nested iframes
* Messages should be raised via postMessage with an object with the 'type' parameter given. * Messages should be raised via postMessage with an object with the 'type' parameter given.
@ -35,20 +35,20 @@ jQuery.noConflict();
* type should be one of: * type should be one of:
* - 'event' - Will trigger the given event (specified by 'event') on the target * - 'event' - Will trigger the given event (specified by 'event') on the target
* - 'callback' - Will call the given method (specified by 'callback') on the target * - 'callback' - Will call the given method (specified by 'callback') on the target
*/ */
$(window).on("message", function(e) { $(window).on("message", function(e) {
var target, var target,
event = e.originalEvent, event = e.originalEvent,
data = JSON.parse(event.data); data = JSON.parse(event.data);
// Reject messages outside of the same origin // Reject messages outside of the same origin
if($.path.parseUrl(window.location.href).domain !== $.path.parseUrl(event.origin).domain) return; if($.path.parseUrl(window.location.href).domain !== $.path.parseUrl(event.origin).domain) return;
// Get target of this action // Get target of this action
target = typeof(data.target) === 'undefined' target = typeof(data.target) === 'undefined'
? $(window) ? $(window)
: $(data.target); : $(data.target);
// Determine action // Determine action
switch(data.type) { switch(data.type) {
case 'event': case 'event':
@ -59,18 +59,18 @@ jQuery.noConflict();
break; break;
} }
}); });
/** /**
* Position the loading spinner animation below the ss logo * Position the loading spinner animation below the ss logo
*/ */
var positionLoadingSpinner = function() { var positionLoadingSpinner = function() {
var offset = 120; // offset from the ss logo var offset = 120; // offset from the ss logo
var spinner = $('.ss-loading-screen .loading-animation'); var spinner = $('.ss-loading-screen .loading-animation');
var top = ($(window).height() - spinner.height()) / 2; var top = ($(window).height() - spinner.height()) / 2;
spinner.css('top', top + offset); spinner.css('top', top + offset);
spinner.show(); spinner.show();
}; };
// apply an select element only when it is ready, ie. when it is rendered into a template // apply an select element only when it is ready, ie. when it is rendered into a template
// with css applied and got a width value. // with css applied and got a width value.
var applyChosen = function(el) { var applyChosen = function(el) {
@ -89,13 +89,13 @@ jQuery.noConflict();
setTimeout(function() { setTimeout(function() {
// Make sure it's visible before applying the ui // Make sure it's visible before applying the ui
el.show(); el.show();
applyChosen(el); }, applyChosen(el); },
500); 500);
} }
}; };
/** /**
* Compare URLs, but normalize trailing slashes in * Compare URLs, but normalize trailing slashes in
* URL to work around routing weirdnesses in SS_HTTPRequest. * URL to work around routing weirdnesses in SS_HTTPRequest.
* Also normalizes relative URLs by prefixing them with the <base>. * Also normalizes relative URLs by prefixing them with the <base>.
*/ */
@ -105,11 +105,11 @@ jQuery.noConflict();
url2 = $.path.isAbsoluteUrl(url2) ? url2 : $.path.makeUrlAbsolute(url2, baseUrl); url2 = $.path.isAbsoluteUrl(url2) ? url2 : $.path.makeUrlAbsolute(url2, baseUrl);
var url1parts = $.path.parseUrl(url1), url2parts = $.path.parseUrl(url2); var url1parts = $.path.parseUrl(url1), url2parts = $.path.parseUrl(url2);
return ( return (
url1parts.pathname.replace(/\/*$/, '') == url2parts.pathname.replace(/\/*$/, '') && url1parts.pathname.replace(/\/*$/, '') == url2parts.pathname.replace(/\/*$/, '') &&
url1parts.search == url2parts.search url1parts.search == url2parts.search
); );
}; };
$(window).bind('resize', positionLoadingSpinner).trigger('resize'); $(window).bind('resize', positionLoadingSpinner).trigger('resize');
// global ajax handlers // global ajax handlers
@ -142,7 +142,7 @@ jQuery.noConflict();
reathenticate = xhr.getResponseHeader('X-Reauthenticate'), reathenticate = xhr.getResponseHeader('X-Reauthenticate'),
msgType = (xhr.status < 200 || xhr.status > 399) ? 'bad' : 'good', msgType = (xhr.status < 200 || xhr.status > 399) ? 'bad' : 'good',
ignoredMessages = ['OK']; ignoredMessages = ['OK'];
// Enable reauthenticate dialog if requested // Enable reauthenticate dialog if requested
if(reathenticate) { if(reathenticate) {
$('.cms-container').showLoginDialog(); $('.cms-container').showLoginDialog();
@ -158,14 +158,14 @@ jQuery.noConflict();
/** /**
* Main LeftAndMain interface with some control panel and an edit form. * Main LeftAndMain interface with some control panel and an edit form.
* *
* Events: * Events:
* ajaxsubmit - ... * ajaxsubmit - ...
* validate - ... * validate - ...
* aftersubmitform - ... * aftersubmitform - ...
*/ */
$('.cms-container').entwine({ $('.cms-container').entwine({
/** /**
* Tracks current panel request. * Tracks current panel request.
*/ */
@ -177,7 +177,7 @@ jQuery.noConflict();
FragmentXHR: {}, FragmentXHR: {},
StateChangeCount: 0, StateChangeCount: 0,
/** /**
* Options for the threeColumnCompressor layout algorithm. * Options for the threeColumnCompressor layout algorithm.
* *
@ -198,7 +198,7 @@ jQuery.noConflict();
// Browser detection // Browser detection
if($.browser.msie && parseInt($.browser.version, 10) < 8) { if($.browser.msie && parseInt($.browser.version, 10) < 8) {
$('.ss-loading-screen').append( $('.ss-loading-screen').append(
'<p class="ss-loading-incompat-warning"><span class="notice">' + '<p class="ss-loading-incompat-warning"><span class="notice">' +
'Your browser is not compatible with the CMS interface. Please use Internet Explorer 8+, Google Chrome or Mozilla Firefox.' + 'Your browser is not compatible with the CMS interface. Please use Internet Explorer 8+, Google Chrome or Mozilla Firefox.' +
'</span></p>' '</span></p>'
).css('z-index', $('.ss-loading-screen').css('z-index')+1); ).css('z-index', $('.ss-loading-screen').css('z-index')+1);
@ -207,7 +207,7 @@ jQuery.noConflict();
this._super(); this._super();
return; return;
} }
// Initialize layouts // Initialize layouts
this.redraw(); this.redraw();
@ -216,7 +216,7 @@ jQuery.noConflict();
$('body').removeClass('loading'); $('body').removeClass('loading');
$(window).unbind('resize', positionLoadingSpinner); $(window).unbind('resize', positionLoadingSpinner);
this.restoreTabState(); this.restoreTabState();
this._super(); this._super();
}, },
@ -326,9 +326,9 @@ jQuery.noConflict();
* Proxy around History.pushState() which handles non-HTML5 fallbacks, * Proxy around History.pushState() which handles non-HTML5 fallbacks,
* as well as global change tracking. Change tracking needs to be synchronous rather than event/callback * as well as global change tracking. Change tracking needs to be synchronous rather than event/callback
* based because the user needs to be able to abort the action completely. * based because the user needs to be able to abort the action completely.
* *
* See handleStateChange() for more details. * See handleStateChange() for more details.
* *
* Parameters: * Parameters:
* - {String} url * - {String} url
* - {String} title New window title * - {String} title New window title
@ -343,20 +343,20 @@ jQuery.noConflict();
// Check change tracking (can't use events as we need a way to cancel the current state change) // Check change tracking (can't use events as we need a way to cancel the current state change)
var contentEls = this._findFragments(data.pjax ? data.pjax.split(',') : ['Content']); var contentEls = this._findFragments(data.pjax ? data.pjax.split(',') : ['Content']);
var trackedEls = contentEls.find(':data(changetracker)').add(contentEls.filter(':data(changetracker)')); var trackedEls = contentEls.find(':data(changetracker)').add(contentEls.filter(':data(changetracker)'));
if(trackedEls.length) { if(trackedEls.length) {
var abort = false; var abort = false;
trackedEls.each(function() { trackedEls.each(function() {
if(!$(this).confirmUnsavedChanges()) abort = true; if(!$(this).confirmUnsavedChanges()) abort = true;
}); });
if(abort) return; if(abort) return;
} }
// Save tab selections so we can restore them later // Save tab selections so we can restore them later
this.saveTabState(); this.saveTabState();
if(window.History.enabled) { if(window.History.enabled) {
$.extend(data, {__forceReferer: forceReferer}); $.extend(data, {__forceReferer: forceReferer});
// Active menu item is set based on X-Controller ajax header, // Active menu item is set based on X-Controller ajax header,
@ -382,31 +382,31 @@ jQuery.noConflict();
/** /**
* Function: submitForm * Function: submitForm
* *
* Parameters: * Parameters:
* {DOMElement} form - The form to be submitted. Needs to be passed * {DOMElement} form - The form to be submitted. Needs to be passed
* in to avoid entwine methods/context being removed through replacing the node itself. * in to avoid entwine methods/context being removed through replacing the node itself.
* {DOMElement} button - The pressed button (optional) * {DOMElement} button - The pressed button (optional)
* {Function} callback - Called in complete() handler of jQuery.ajax() * {Function} callback - Called in complete() handler of jQuery.ajax()
* {Object} ajaxOptions - Object literal to merge into $.ajax() call * {Object} ajaxOptions - Object literal to merge into $.ajax() call
* *
* Returns: * Returns:
* (boolean) * (boolean)
*/ */
submitForm: function(form, button, callback, ajaxOptions) { submitForm: function(form, button, callback, ajaxOptions) {
var self = this; var self = this;
// look for save button // look for save button
if(!button) button = this.find('.Actions :submit[name=action_save]'); if(!button) button = this.find('.Actions :submit[name=action_save]');
// default to first button if none given - simulates browser behaviour // default to first button if none given - simulates browser behaviour
if(!button) button = this.find('.Actions :submit:first'); if(!button) button = this.find('.Actions :submit:first');
form.trigger('beforesubmitform'); form.trigger('beforesubmitform');
this.trigger('submitform', {form: form, button: button}); this.trigger('submitform', {form: form, button: button});
// set button to "submitting" state // set button to "submitting" state
$(button).addClass('loading'); $(button).addClass('loading');
// validate if required // validate if required
var validationResult = form.validate(); var validationResult = form.validate();
if(typeof validationResult!=='undefined' && !validationResult) { if(typeof validationResult!=='undefined' && !validationResult) {
@ -417,12 +417,12 @@ jQuery.noConflict();
return false; return false;
} }
// get all data from the form // get all data from the form
var formData = form.serializeArray(); var formData = form.serializeArray();
// add button action // add button action
formData.push({name: $(button).attr('name'), value:'1'}); formData.push({name: $(button).attr('name'), value:'1'});
// Artificial HTTP referer, IE doesn't submit them via ajax. // Artificial HTTP referer, IE doesn't submit them via ajax.
// Also rewrites anchors to their page counterparts, which is important // Also rewrites anchors to their page counterparts, which is important
// as automatic browser ajax response redirects seem to discard the hash/fragment. // as automatic browser ajax response redirects seem to discard the hash/fragment.
// TODO Replaces trailing slashes added by History after locale (e.g. admin/?locale=en/) // TODO Replaces trailing slashes added by History after locale (e.g. admin/?locale=en/)
@ -437,7 +437,7 @@ jQuery.noConflict();
// sending back different `X-Pjax` headers and content // sending back different `X-Pjax` headers and content
jQuery.ajax(jQuery.extend({ jQuery.ajax(jQuery.extend({
headers: {"X-Pjax" : "CurrentForm,Breadcrumbs"}, headers: {"X-Pjax" : "CurrentForm,Breadcrumbs"},
url: form.attr('action'), url: form.attr('action'),
data: formData, data: formData,
type: 'POST', type: 'POST',
complete: function() { complete: function() {
@ -453,7 +453,7 @@ jQuery.noConflict();
newContentEls.filter('form').trigger('aftersubmitform', {status: status, xhr: xhr, formData: formData}); newContentEls.filter('form').trigger('aftersubmitform', {status: status, xhr: xhr, formData: formData});
} }
}, ajaxOptions)); }, ajaxOptions));
return false; return false;
}, },
@ -462,21 +462,21 @@ jQuery.noConflict();
* To trigger loading, pass a new URL to window.History.pushState(). * To trigger loading, pass a new URL to window.History.pushState().
* Use loadPanel() as a pushState() wrapper as it provides some additional functionality * Use loadPanel() as a pushState() wrapper as it provides some additional functionality
* like global changetracking and user aborts. * like global changetracking and user aborts.
* *
* Due to the nature of history management, no callbacks are allowed. * Due to the nature of history management, no callbacks are allowed.
* Use the 'beforestatechange' and 'afterstatechange' events instead, * Use the 'beforestatechange' and 'afterstatechange' events instead,
* or overwrite the beforeLoad() and afterLoad() methods on the * or overwrite the beforeLoad() and afterLoad() methods on the
* DOM element you're loading the new content into. * DOM element you're loading the new content into.
* Although you can pass data into pushState(), it shouldn't contain * Although you can pass data into pushState(), it shouldn't contain
* DOM elements or callback closures. * DOM elements or callback closures.
* *
* The passed URL should allow reconstructing important interface state * The passed URL should allow reconstructing important interface state
* without additional parameters, in the following use cases: * without additional parameters, in the following use cases:
* - Explicit loading through History.pushState() * - Explicit loading through History.pushState()
* - Implicit loading through browser navigation event triggered by the user (forward or back) * - Implicit loading through browser navigation event triggered by the user (forward or back)
* - Full window refresh without ajax * - Full window refresh without ajax
* For example, a ModelAdmin search event should contain the search terms * For example, a ModelAdmin search event should contain the search terms
* as URL parameters, and the result display should automatically appear * as URL parameters, and the result display should automatically appear
* if the URL is loaded without ajax. * if the URL is loaded without ajax.
*/ */
handleStateChange: function() { handleStateChange: function() {
@ -502,22 +502,22 @@ jQuery.noConflict();
// that can be reloaded without reloading the whole window. // that can be reloaded without reloading the whole window.
if(contentEls.length < fragmentsArr.length) { if(contentEls.length < fragmentsArr.length) {
fragments = 'Content', fragmentsArr = ['Content']; fragments = 'Content', fragmentsArr = ['Content'];
contentEls = this._findFragments(fragmentsArr); contentEls = this._findFragments(fragmentsArr);
} }
this.trigger('beforestatechange', {state: state, element: contentEls}); this.trigger('beforestatechange', {state: state, element: contentEls});
// Set Pjax headers, which can declare a preference for the returned view. // Set Pjax headers, which can declare a preference for the returned view.
// The actually returned view isn't always decided upon when the request // The actually returned view isn't always decided upon when the request
// is fired, so the server might decide to change it based on its own logic. // is fired, so the server might decide to change it based on its own logic.
headers['X-Pjax'] = fragments; headers['X-Pjax'] = fragments;
// Set 'fake' referer - we call pushState() before making the AJAX request, so we have to // Set 'fake' referer - we call pushState() before making the AJAX request, so we have to
// set our own referer here // set our own referer here
if (typeof state.data.__forceReferer !== 'undefined') { if (typeof state.data.__forceReferer !== 'undefined') {
// Ensure query string is properly encoded if present // Ensure query string is properly encoded if present
var url = state.data.__forceReferer; var url = state.data.__forceReferer;
try { try {
// Prevent double-encoding by attempting to decode // Prevent double-encoding by attempting to decode
url = decodeURI(url); url = decodeURI(url);
@ -528,7 +528,7 @@ jQuery.noConflict();
headers['X-Backurl'] = encodeURI(url); headers['X-Backurl'] = encodeURI(url);
} }
} }
contentEls.addClass('loading'); contentEls.addClass('loading');
var xhr = $.ajax({ var xhr = $.ajax({
headers: headers, headers: headers,
@ -543,7 +543,7 @@ jQuery.noConflict();
self.trigger('afterstatechange', {data: data, status: status, xhr: xhr, element: els, state: state}); self.trigger('afterstatechange', {data: data, status: status, xhr: xhr, element: els, state: state});
} }
}); });
this.setStateChangeXHR(xhr); this.setStateChangeXHR(xhr);
}, },
@ -633,7 +633,7 @@ jQuery.noConflict();
// Support a full reload // Support a full reload
if(xhr.getResponseHeader('X-Reload') && xhr.getResponseHeader('X-ControllerURL')) { if(xhr.getResponseHeader('X-Reload') && xhr.getResponseHeader('X-ControllerURL')) {
document.location.href = $('base').attr('href').replace(/\/*$/, '') document.location.href = $('base').attr('href').replace(/\/*$/, '')
+ '/' + xhr.getResponseHeader('X-ControllerURL'); + '/' + xhr.getResponseHeader('X-ControllerURL');
return; return;
} }
@ -651,7 +651,7 @@ jQuery.noConflict();
if(xhr.getResponseHeader('Content-Type').match(/^((text)|(application))\/json[ \t]*;?/i)) { if(xhr.getResponseHeader('Content-Type').match(/^((text)|(application))\/json[ \t]*;?/i)) {
newFragments = data; newFragments = data;
} else { } else {
// Fall back to replacing the content fragment if HTML is returned // Fall back to replacing the content fragment if HTML is returned
var fragment = document.createDocumentFragment(); var fragment = document.createDocumentFragment();
jQuery.clean( [ data ], document, fragment, [] ); jQuery.clean( [ data ], document, fragment, [] );
@ -732,9 +732,9 @@ jQuery.noConflict();
}, },
/** /**
* *
* *
* Parameters: * Parameters:
* - fragments {Array} * - fragments {Array}
* Returns: jQuery collection * Returns: jQuery collection
*/ */
@ -751,14 +751,14 @@ jQuery.noConflict();
/** /**
* Function: refresh * Function: refresh
* *
* Updates the container based on the current url * Updates the container based on the current url
* *
* Returns: void * Returns: void
*/ */
refresh: function() { refresh: function() {
$(window).trigger('statechange'); $(window).trigger('statechange');
$(this).redraw(); $(this).redraw();
}, },
@ -787,7 +787,7 @@ jQuery.noConflict();
window.sessionStorage.setItem(tabsUrl, JSON.stringify(selectedTabs)); window.sessionStorage.setItem(tabsUrl, JSON.stringify(selectedTabs));
} catch(err) { } catch(err) {
if (err.code === DOMException.QUOTA_EXCEEDED_ERR && window.sessionStorage.length === 0) { if (err.code === DOMException.QUOTA_EXCEEDED_ERR && window.sessionStorage.length === 0) {
// If this fails we ignore the error as the only issue is that it // If this fails we ignore the error as the only issue is that it
// does not remember the tab state. // does not remember the tab state.
// This is a Safari bug which happens when private browsing is enabled. // This is a Safari bug which happens when private browsing is enabled.
return; return;
@ -847,7 +847,7 @@ jQuery.noConflict();
var s = window.sessionStorage; var s = window.sessionStorage;
if(url) { if(url) {
s.removeItem('tabs-' + url); s.removeItem('tabs-' + url);
} else { } else {
for(var i=0;i<s.length;i++) { for(var i=0;i<s.length;i++) {
if(s.key(i).match(/^tabs-/)) s.removeItem(s.key(i)); if(s.key(i).match(/^tabs-/)) s.removeItem(s.key(i));
@ -868,7 +868,7 @@ jQuery.noConflict();
.replace(/#.*/, '') .replace(/#.*/, '')
.replace($('base').attr('href'), ''); .replace($('base').attr('href'), '');
}, },
showLoginDialog: function() { showLoginDialog: function() {
var tempid = $('body').data('member-tempid'), var tempid = $('body').data('member-tempid'),
dialog = $('.leftandmain-logindialog'), dialog = $('.leftandmain-logindialog'),
@ -876,13 +876,13 @@ jQuery.noConflict();
// Force regeneration of any existing dialog // Force regeneration of any existing dialog
if(dialog.length) dialog.remove(); if(dialog.length) dialog.remove();
// Join url params // Join url params
url = $.path.addSearchParams(url, { url = $.path.addSearchParams(url, {
'tempid': tempid, 'tempid': tempid,
'BackURL': window.location.href 'BackURL': window.location.href
}); });
// Show a placeholder for instant feedback. Will be replaced with actual // Show a placeholder for instant feedback. Will be replaced with actual
// form dialog once its loaded. // form dialog once its loaded.
dialog = $('<div class="leftandmain-logindialog"></div>'); dialog = $('<div class="leftandmain-logindialog"></div>');
@ -891,12 +891,12 @@ jQuery.noConflict();
$('body').append(dialog); $('body').append(dialog);
} }
}); });
// Login dialog page // Login dialog page
$('.leftandmain-logindialog').entwine({ $('.leftandmain-logindialog').entwine({
onmatch: function() { onmatch: function() {
this._super(); this._super();
// Create jQuery dialog // Create jQuery dialog
this.ssdialog({ this.ssdialog({
iframeUrl: this.data('url'), iframeUrl: this.data('url'),
@ -912,7 +912,7 @@ jQuery.noConflict();
}, },
close: function() { close: function() {
$('.ui-widget-overlay').removeClass('leftandmain-logindialog-overlay'); $('.ui-widget-overlay').removeClass('leftandmain-logindialog-overlay');
} }
}); });
}, },
onunmatch: function() { onunmatch: function() {
@ -943,7 +943,7 @@ jQuery.noConflict();
this.close(); this.close();
} }
}); });
/** /**
* Add loading overlay to selected regions in the CMS automatically. * Add loading overlay to selected regions in the CMS automatically.
* Not applied to all "*.loading" elements to avoid secondary regions * Not applied to all "*.loading" elements to avoid secondary regions
@ -988,7 +988,7 @@ jQuery.noConflict();
return; return;
} }
var href = this.attr('href'), var href = this.attr('href'),
url = (href && !href.match(/^#/)) ? href : this.data('href'), url = (href && !href.match(/^#/)) ? href : this.data('href'),
data = {pjax: this.data('pjaxTarget')}; data = {pjax: this.data('pjaxTarget')};
@ -1006,17 +1006,17 @@ jQuery.noConflict();
onclick: function(e) { onclick: function(e) {
$(this).removeClass('ui-button-text-only'); $(this).removeClass('ui-button-text-only');
$(this).addClass('ss-ui-button-loading ui-button-text-icons'); $(this).addClass('ss-ui-button-loading ui-button-text-icons');
var loading = $(this).find(".ss-ui-loading-icon"); var loading = $(this).find(".ss-ui-loading-icon");
if(loading.length < 1) { if(loading.length < 1) {
loading = $("<span></span>").addClass('ss-ui-loading-icon ui-button-icon-primary ui-icon'); loading = $("<span></span>").addClass('ss-ui-loading-icon ui-button-icon-primary ui-icon');
$(this).prepend(loading); $(this).prepend(loading);
} }
loading.show(); loading.show();
var href = this.attr('href'), url = href ? href : this.data('href'); var href = this.attr('href'), url = href ? href : this.data('href');
jQuery.ajax({ jQuery.ajax({
@ -1024,16 +1024,16 @@ jQuery.noConflict();
// Ensure that form view is loaded (rather than whole "Content" template) // Ensure that form view is loaded (rather than whole "Content" template)
complete: function(xmlhttp, status) { complete: function(xmlhttp, status) {
var msg = (xmlhttp.getResponseHeader('X-Status')) ? xmlhttp.getResponseHeader('X-Status') : xmlhttp.responseText; var msg = (xmlhttp.getResponseHeader('X-Status')) ? xmlhttp.getResponseHeader('X-Status') : xmlhttp.responseText;
try { try {
if (typeof msg != "undefined" && msg !== null) eval(msg); if (typeof msg != "undefined" && msg !== null) eval(msg);
} }
catch(e) {} catch(e) {}
loading.hide(); loading.hide();
$(".cms-container").refresh(); $(".cms-container").refresh();
$(this).removeClass('ss-ui-button-loading ui-button-text-icons'); $(this).removeClass('ss-ui-button-loading ui-button-text-icons');
$(this).addClass('ui-button-text-only'); $(this).addClass('ui-button-text-only');
}, },
@ -1064,14 +1064,14 @@ jQuery.noConflict();
dialog = $('<div class="ss-ui-dialog" id="' + id + '" />'); dialog = $('<div class="ss-ui-dialog" id="' + id + '" />');
$('body').append(dialog); $('body').append(dialog);
} }
var extraClass = this.data('popupclass')?this.data('popupclass'):''; var extraClass = this.data('popupclass')?this.data('popupclass'):'';
dialog.ssdialog({iframeUrl: this.attr('href'), autoOpen: true, dialogExtraClass: extraClass}); dialog.ssdialog({iframeUrl: this.attr('href'), autoOpen: true, dialogExtraClass: extraClass});
return false; return false;
} }
}); });
/** /**
* Add styling to all contained buttons, and create buttonsets if required. * Add styling to all contained buttons, and create buttonsets if required.
*/ */
@ -1101,20 +1101,20 @@ jQuery.noConflict();
if(window.debug) console.log('redraw', this.attr('class'), this.get(0)); if(window.debug) console.log('redraw', this.attr('class'), this.get(0));
// Remove whitespace to avoid gaps with inline elements // Remove whitespace to avoid gaps with inline elements
this.contents().filter(function() { this.contents().filter(function() {
return (this.nodeType == 3 && !/\S/.test(this.nodeValue)); return (this.nodeType == 3 && !/\S/.test(this.nodeValue));
}).remove(); }).remove();
// Init buttons if required // Init buttons if required
this.find('.ss-ui-button').each(function() { this.find('.ss-ui-button').each(function() {
if(!$(this).data('button')) $(this).button(); if(!$(this).data('button')) $(this).button();
}); });
// Mark up buttonsets // Mark up buttonsets
this.find('.ss-ui-buttonset').buttonset(); this.find('.ss-ui-buttonset').buttonset();
} }
}); });
/** /**
* Duplicates functionality in DateField.js, but due to using entwine we can match * Duplicates functionality in DateField.js, but due to using entwine we can match
* the DOM element on creation, rather than onclick - which allows us to decorate * the DOM element on creation, rather than onclick - which allows us to decorate
@ -1136,23 +1136,23 @@ jQuery.noConflict();
$(this).datepicker(config); $(this).datepicker(config);
// // Unfortunately jQuery UI only allows configuration of icon images, not sprites // // Unfortunately jQuery UI only allows configuration of icon images, not sprites
// this.next('button').button('option', 'icons', {primary : 'ui-icon-calendar'}); // this.next('button').button('option', 'icons', {primary : 'ui-icon-calendar'});
this._super(); this._super();
}, },
onunmatch: function() { onunmatch: function() {
this._super(); this._super();
} }
}); });
/** /**
* Styled dropdown select fields via chosen. Allows things like search and optgroup * Styled dropdown select fields via chosen. Allows things like search and optgroup
* selection support. Rather than manually adding classes to selects we want * selection support. Rather than manually adding classes to selects we want
* styled, we style everything but the ones we tell it not to. * styled, we style everything but the ones we tell it not to.
* *
* For the CMS we also need to tell the parent div that his has a select so * For the CMS we also need to tell the parent div that it has a select so
* we can fix the height cropping. * we can fix the height cropping.
*/ */
$('.cms .field.dropdown select, .cms .field select[multiple], .fieldholder-small select.dropdown').entwine({ $('.cms .field.dropdown select, .cms .field select[multiple], .fieldholder-small select.dropdown').entwine({
onmatch: function() { onmatch: function() {
if(this.is('.no-chzn')) { if(this.is('.no-chzn')) {
@ -1169,20 +1169,20 @@ jQuery.noConflict();
// Apply Chosen // Apply Chosen
applyChosen(this); applyChosen(this);
this._super(); this._super();
}, },
onunmatch: function() { onunmatch: function() {
this._super(); this._super();
} }
}); });
$(".cms-panel-layout").entwine({ $(".cms-panel-layout").entwine({
redraw: function() { redraw: function() {
if(window.debug) console.log('redraw', this.attr('class'), this.get(0)); if(window.debug) console.log('redraw', this.attr('class'), this.get(0));
} }
}); });
/** /**
* Overload the default GridField behaviour (open a new URL in the browser) * Overload the default GridField behaviour (open a new URL in the browser)
* with the CMS-specific ajax loading. * with the CMS-specific ajax loading.
@ -1200,7 +1200,7 @@ jQuery.noConflict();
/** /**
* Generic search form in the CMS, often hooked up to a GridField results display. * Generic search form in the CMS, often hooked up to a GridField results display.
*/ */
$('.cms-search-form').entwine({ $('.cms-search-form').entwine({
onsubmit: function(e) { onsubmit: function(e) {
// Remove empty elements and make the URL prettier // Remove empty elements and make the URL prettier
@ -1258,7 +1258,7 @@ jQuery.noConflict();
}, },
onremove: function() { onremove: function() {
if(window.debug) console.log('saving', this.data('url'), this); if(window.debug) console.log('saving', this.data('url'), this);
// Save the HTML state at the last possible moment. // Save the HTML state at the last possible moment.
// Don't store the DOM to avoid memory leaks. // Don't store the DOM to avoid memory leaks.
if(!this.data('deferredNoCache')) window._panelDeferredCache[this.data('url')] = this.html(); if(!this.data('deferredNoCache')) window._panelDeferredCache[this.data('url')] = this.html();
@ -1317,7 +1317,7 @@ jQuery.noConflict();
if(!this.data('uiTabs')) this.tabs({ if(!this.data('uiTabs')) this.tabs({
active: (activeTab.index() != -1) ? activeTab.index() : 0, active: (activeTab.index() != -1) ? activeTab.index() : 0,
beforeLoad: function(e, ui) { beforeLoad: function(e, ui) {
// Disable automatic ajax loading of tabs without matching DOM elements, // Disable automatic ajax loading of tabs without matching DOM elements,
// determining if the current URL differs from the tab URL is too error prone. // determining if the current URL differs from the tab URL is too error prone.
return false; return false;
}, },
@ -1338,7 +1338,7 @@ jQuery.noConflict();
} }
}); });
}, },
/** /**
* Ensure hash links are prefixed with the current page URL, * Ensure hash links are prefixed with the current page URL,
* otherwise jQuery interprets them as being external. * otherwise jQuery interprets them as being external.
@ -1353,7 +1353,7 @@ jQuery.noConflict();
} }
}); });
}); });
}(jQuery)); }(jQuery));
var statusMessage = function(text, type) { var statusMessage = function(text, type) {

View File

@ -149,7 +149,7 @@ When a string is literal (contains no variable substitutions), the apostrophe or
When a literal string itself contains apostrophes, it is permitted to demarcate the string with quotation marks or "double quotes". When a literal string itself contains apostrophes, it is permitted to demarcate the string with quotation marks or "double quotes".
:::php :::php
$greeting = "She said 'hello'"; $greeting = "They said 'hello'";
This syntax is preferred over escaping apostrophes as it is much easier to read. This syntax is preferred over escaping apostrophes as it is much easier to read.

View File

@ -222,7 +222,7 @@ In order for this to work, the CMS templates declare certain sections as "PJAX f
through a `data-pjax-fragment` attribute. These names correlate to specific through a `data-pjax-fragment` attribute. These names correlate to specific
rendering logic in the PHP controllers, through the `[api:PjaxResponseNegotiator]` class. rendering logic in the PHP controllers, through the `[api:PjaxResponseNegotiator]` class.
Through a custom `X-Pjax` HTTP header, the client can declare which view he's expecting, Through a custom `X-Pjax` HTTP header, the client can declare which view they're expecting,
through identifiers like `CurrentForm` or `Content` (see `[api:LeftAndMain->getResponseNegotiator()]`). through identifiers like `CurrentForm` or `Content` (see `[api:LeftAndMain->getResponseNegotiator()]`).
These identifiers are passed to `loadPanel()` via the `pjax` data option. These identifiers are passed to `loadPanel()` via the `pjax` data option.
The HTTP response is a JSON object literal, with template replacements keyed by their Pjax fragment. The HTTP response is a JSON object literal, with template replacements keyed by their Pjax fragment.

View File

@ -156,4 +156,4 @@ cases.
## Summary ## ## Summary ##
The code presented gives you a fully functioning alternating button, similar to the defaults that come with the the CMS. The code presented gives you a fully functioning alternating button, similar to the defaults that come with the the CMS.
These alternating buttons can be used to give user the advantage of visual feedback upon his actions. These alternating buttons can be used to give user the advantage of visual feedback upon their actions.

View File

@ -35,14 +35,14 @@ Thanks to Rutger de Jong for reporting.
Severity: Moderate Severity: Moderate
Autologin tokens (remember me and reset password) are stored in the database as a plain text. Autologin tokens (remember me and reset password) are stored in the database as a plain text.
If attacker obtained the database he would be able to gain access to accounts that have requested a password change, or have "remember me" enabled. If attacker obtained the database they would be able to gain access to accounts that have requested a password change, or have "remember me" enabled.
### Security: Privilege escalation through profile form ### Security: Privilege escalation through profile form
Severity: Moderate Severity: Moderate
A logged-in CMS user can gain additional privileges by crafting a request A logged-in CMS user can gain additional privileges by crafting a request
to his/her profile form which resets another user's password. to their profile form which resets another user's password.
This method can potentially be used by CSRF attacks as well. This method can potentially be used by CSRF attacks as well.
Thanks to Nathaniel Carew (Sense of Security) for reporting. Thanks to Nathaniel Carew (Sense of Security) for reporting.

View File

@ -607,9 +607,7 @@ when using deprecated functionality (through the new `Deprecation` class).
* 2012-04-12 [e9dc610](https://github.com/silverstripe/sapphire/commit/e9dc610) API-CHANGE: new GridFieldFooter component (Julian Seidenberg) * 2012-04-12 [e9dc610](https://github.com/silverstripe/sapphire/commit/e9dc610) API-CHANGE: new GridFieldFooter component (Julian Seidenberg)
* 2012-04-10 [9888f98](https://github.com/silverstripe/silverstripe-cms/commit/9888f98) ENHANCMENT: Link pages in reports to cms edit (Andrew O'Neil) * 2012-04-10 [9888f98](https://github.com/silverstripe/silverstripe-cms/commit/9888f98) ENHANCMENT: Link pages in reports to cms edit (Andrew O'Neil)
* 2012-04-10 [1516934](https://github.com/silverstripe/silverstripe-cms/commit/1516934) Revert "BUGFIX: SSF-168 fixing rendering issue in Chrome, which displays extra control at the bottom of the window in a report that is of a certain length" (Julian Seidenberg) * 2012-04-10 [1516934](https://github.com/silverstripe/silverstripe-cms/commit/1516934) Revert "BUGFIX: SSF-168 fixing rendering issue in Chrome, which displays extra control at the bottom of the window in a report that is of a certain length" (Julian Seidenberg)
* 2012-04-06 [797d526](https://github.com/silverstripe/sapphire/commit/797d526) For png images with transparency, the imagesaveaplpha() needs to be set to true on the source image in order for * 2012-04-06 [797d526](https://github.com/silverstripe/sapphire/commit/797d526) For png images with transparency, the imagesaveaplpha() needs to be set to true on the source image in order for the alpha to be preserved when using the modifier methods. (jmwohl)
he alpha to be preserved when using the modifier methods. (jmwohl)
* 2012-04-05 [e76913f](https://github.com/silverstripe/sapphire/commit/e76913f) API-CHANGE: adding a default option of null to the $args argument in DataExtension::add_to_class. The args argument isn't used anywhere in the class and adding a third argument to every call to this function is tedious. (Julian Seidenberg) * 2012-04-05 [e76913f](https://github.com/silverstripe/sapphire/commit/e76913f) API-CHANGE: adding a default option of null to the $args argument in DataExtension::add_to_class. The args argument isn't used anywhere in the class and adding a third argument to every call to this function is tedious. (Julian Seidenberg)
* 2012-04-04 [5826b36](https://github.com/silverstripe/sapphire/commit/5826b36) ENHACEMENT: SSF-168 updated the font for titles on print stylesheets (Felipe Skroski) * 2012-04-04 [5826b36](https://github.com/silverstripe/sapphire/commit/5826b36) ENHACEMENT: SSF-168 updated the font for titles on print stylesheets (Felipe Skroski)
* 2012-04-04 [349a04d](https://github.com/silverstripe/silverstripe-cms/commit/349a04d) API-CHANGE: SSF-168 changing the API/code-conventions for excluding specific reports. get_reports method now returns an ArrayList instead of an array of SS_Reports. (Julian Seidenberg) * 2012-04-04 [349a04d](https://github.com/silverstripe/silverstripe-cms/commit/349a04d) API-CHANGE: SSF-168 changing the API/code-conventions for excluding specific reports. get_reports method now returns an ArrayList instead of an array of SS_Reports. (Julian Seidenberg)

View File

@ -22,7 +22,7 @@ Thanks to Rutger de Jong for reporting.
Severity: Moderate Severity: Moderate
A logged-in CMS user can gain additional privileges by crafting a request A logged-in CMS user can gain additional privileges by crafting a request
to his/her profile form which resets another user's password. to their profile form which resets another user's password.
This method can potentially be used by CSRF attacks as well. This method can potentially be used by CSRF attacks as well.
Thanks to Nathaniel Carew (Sense of Security) for reporting. Thanks to Nathaniel Carew (Sense of Security) for reporting.

View File

@ -25,7 +25,7 @@ API changes related to the below security patch:
Severity: Moderate Severity: Moderate
Autologin tokens (remember me and reset password) are stored in the database as a plain text. Autologin tokens (remember me and reset password) are stored in the database as a plain text.
If attacker obtained the database he would be able to gain access to accounts that have requested a password change, or have "remember me" enabled. If attacker obtained the database they would be able to gain access to accounts that have requested a password change, or have "remember me" enabled.
## Changelog ## Changelog

View File

@ -119,7 +119,7 @@ translators.
### I'm seeing lots of duplicated translations, what should I do? ### I'm seeing lots of duplicated translations, what should I do?
For now, please translate all duplications - sometimes they might be intentional, but mostly the developer just didn't For now, please translate all duplications - sometimes they might be intentional, but mostly the developer just didn't
know his phrase was already translated. Please contact us about any duplicates that might be worth merging. know their phrase was already translated. Please contact us about any duplicates that might be worth merging.
### What happened to translate.silverstripe.org? ### What happened to translate.silverstripe.org?

View File

@ -60,11 +60,11 @@ class Member extends DataObject implements TemplateGlobalProvider {
); );
private static $has_one = array(); private static $has_one = array();
private static $has_many = array(); private static $has_many = array();
private static $many_many = array(); private static $many_many = array();
private static $many_many_extraFields = array(); private static $many_many_extraFields = array();
private static $default_sort = '"Surname", "FirstName"'; private static $default_sort = '"Surname", "FirstName"';
@ -72,7 +72,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
private static $indexes = array( private static $indexes = array(
'Email' => true, 'Email' => true,
//Removed due to duplicate null values causing MSSQL problems //Removed due to duplicate null values causing MSSQL problems
//'AutoLoginHash' => Array('type'=>'unique', 'value'=>'AutoLoginHash', 'ignoreNulls'=>true) //'AutoLoginHash' => Array('type'=>'unique', 'value'=>'AutoLoginHash', 'ignoreNulls'=>true)
); );
/** /**
@ -80,7 +80,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
* @var boolean * @var boolean
*/ */
private static $notify_password_change = false; private static $notify_password_change = false;
/** /**
* All searchable database columns * All searchable database columns
* in this object, currently queried * in this object, currently queried
@ -97,7 +97,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
'Surname', 'Surname',
'Email', 'Email',
); );
private static $summary_fields = array( private static $summary_fields = array(
'FirstName', 'FirstName',
'Surname', 'Surname',
@ -122,13 +122,13 @@ class Member extends DataObject implements TemplateGlobalProvider {
'Salt', 'Salt',
'NumVisit' 'NumVisit'
); );
/** /**
* @config * @config
* @var Array See {@link set_title_columns()} * @var Array See {@link set_title_columns()}
*/ */
private static $title_format = null; private static $title_format = null;
/** /**
* The unique field used to identify this member. * The unique field used to identify this member.
* By default, it's "Email", but another common * By default, it's "Email", but another common
@ -138,13 +138,13 @@ class Member extends DataObject implements TemplateGlobalProvider {
* @var string * @var string
*/ */
private static $unique_identifier_field = 'Email'; private static $unique_identifier_field = 'Email';
/** /**
* @config * @config
* {@link PasswordValidator} object for validating user's password * {@link PasswordValidator} object for validating user's password
*/ */
private static $password_validator = null; private static $password_validator = null;
/** /**
* @config * @config
* The number of days that a password should be valid for. * The number of days that a password should be valid for.
@ -155,8 +155,8 @@ class Member extends DataObject implements TemplateGlobalProvider {
/** /**
* @config * @config
* @var Int Number of incorrect logins after which * @var Int Number of incorrect logins after which
* the user is blocked from further attempts for the timespan * the user is blocked from further attempts for the timespan
* defined in {@link $lock_out_delay_mins}. * defined in {@link $lock_out_delay_mins}.
*/ */
private static $lock_out_after_incorrect_logins = 10; private static $lock_out_after_incorrect_logins = 10;
@ -166,7 +166,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
* Only applies if {@link $lock_out_after_incorrect_logins} greater than 0. * Only applies if {@link $lock_out_after_incorrect_logins} greater than 0.
*/ */
private static $lock_out_delay_mins = 15; private static $lock_out_delay_mins = 15;
/** /**
* @config * @config
* @var String If this is set, then a session cookie with the given name will be set on log-in, * @var String If this is set, then a session cookie with the given name will be set on log-in,
@ -177,7 +177,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
/** /**
* Indicates that when a {@link Member} logs in, Member:session_regenerate_id() * Indicates that when a {@link Member} logs in, Member:session_regenerate_id()
* should be called as a security precaution. * should be called as a security precaution.
* *
* This doesn't always work, especially if you're trying to set session cookies * This doesn't always work, especially if you're trying to set session cookies
* across an entire site using the domain parameter to session_set_cookie_params() * across an entire site using the domain parameter to session_set_cookie_params()
* *
@ -218,7 +218,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
parent::populateDefaults(); parent::populateDefaults();
$this->Locale = i18n::get_closest_translation(i18n::get_locale()); $this->Locale = i18n::get_closest_translation(i18n::get_locale());
} }
public function requireDefaultRecords() { public function requireDefaultRecords() {
parent::requireDefaultRecords(); parent::requireDefaultRecords();
// Default groups should've been built by Group->requireDefaultRecords() already // Default groups should've been built by Group->requireDefaultRecords() already
@ -233,7 +233,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
public static function default_admin() { public static function default_admin() {
// Check if set // Check if set
if(!Security::has_default_admin()) return null; if(!Security::has_default_admin()) return null;
// Find or create ADMIN group // Find or create ADMIN group
singleton('Group')->requireDefaultRecords(); singleton('Group')->requireDefaultRecords();
$adminGroup = Permission::get_groups_by_permission('ADMIN')->First(); $adminGroup = Permission::get_groups_by_permission('ADMIN')->First();
@ -264,13 +264,13 @@ class Member extends DataObject implements TemplateGlobalProvider {
* If this is called, then a session cookie will be set to "1" whenever a user * If this is called, then a session cookie will be set to "1" whenever a user
* logs in. This lets 3rd party tools, such as apache's mod_rewrite, detect * logs in. This lets 3rd party tools, such as apache's mod_rewrite, detect
* whether a user is logged in or not and alter behaviour accordingly. * whether a user is logged in or not and alter behaviour accordingly.
* *
* One known use of this is to bypass static caching for logged in users. This is * One known use of this is to bypass static caching for logged in users. This is
* done by putting this into _config.php * done by putting this into _config.php
* <pre> * <pre>
* Member::set_login_marker_cookie("SS_LOGGED_IN"); * Member::set_login_marker_cookie("SS_LOGGED_IN");
* </pre> * </pre>
* *
* And then adding this condition to each of the rewrite rules that make use of * And then adding this condition to each of the rewrite rules that make use of
* the static cache. * the static cache.
* <pre> * <pre>
@ -283,7 +283,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
public static function set_login_marker_cookie($cookieName) { public static function set_login_marker_cookie($cookieName) {
Deprecation::notice('3.2', 'Use the "Member.login_marker_cookie" config setting instead'); Deprecation::notice('3.2', 'Use the "Member.login_marker_cookie" config setting instead');
self::config()->login_marker_cookie = $cookieName; self::config()->login_marker_cookie = $cookieName;
} }
/** /**
* Check if the passed password matches the stored one (if the member is not locked out). * Check if the passed password matches the stored one (if the member is not locked out).
@ -304,7 +304,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
$e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption); $e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption);
if(!$e->check($this->Password, $password, $this->Salt, $this)) { if(!$e->check($this->Password, $password, $this->Salt, $this)) {
$iidentifierField = $iidentifierField =
$result->error(_t ( $result->error(_t (
'Member.ERRORWRONGCRED', 'Member.ERRORWRONGCRED',
'The provided details don\'t seem to be correct. Please try again.' 'The provided details don\'t seem to be correct. Please try again.'
@ -350,7 +350,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
/** /**
* Regenerate the session_id. * Regenerate the session_id.
* This wrapper is here to make it easier to disable calls to session_regenerate_id(), should you need to. * This wrapper is here to make it easier to disable calls to session_regenerate_id(), should you need to.
* They have caused problems in certain * They have caused problems in certain
* quirky problems (such as using the Windmill 0.3.6 proxy). * quirky problems (such as using the Windmill 0.3.6 proxy).
*/ */
@ -359,15 +359,15 @@ class Member extends DataObject implements TemplateGlobalProvider {
// This can be called via CLI during testing. // This can be called via CLI during testing.
if(Director::is_cli()) return; if(Director::is_cli()) return;
$file = ''; $file = '';
$line = ''; $line = '';
// @ is to supress win32 warnings/notices when session wasn't cleaned up properly // @ is to supress win32 warnings/notices when session wasn't cleaned up properly
// There's nothing we can do about this, because it's an operating system function! // There's nothing we can do about this, because it's an operating system function!
if(!headers_sent($file, $line)) @session_regenerate_id(true); if(!headers_sent($file, $line)) @session_regenerate_id(true);
} }
/** /**
* Get the field used for uniquely identifying a member * Get the field used for uniquely identifying a member
* in the database. {@see Member::$unique_identifier_field} * in the database. {@see Member::$unique_identifier_field}
@ -379,7 +379,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
Deprecation::notice('3.2', 'Use the "Member.unique_identifier_field" config setting instead'); Deprecation::notice('3.2', 'Use the "Member.unique_identifier_field" config setting instead');
return Member::config()->unique_identifier_field; return Member::config()->unique_identifier_field;
} }
/** /**
* Set the field used for uniquely identifying a member * Set the field used for uniquely identifying a member
* in the database. {@see Member::$unique_identifier_field} * in the database. {@see Member::$unique_identifier_field}
@ -391,14 +391,14 @@ class Member extends DataObject implements TemplateGlobalProvider {
Deprecation::notice('3.2', 'Use the "Member.unique_identifier_field" config setting instead'); Deprecation::notice('3.2', 'Use the "Member.unique_identifier_field" config setting instead');
Member::config()->unique_identifier_field = $field; Member::config()->unique_identifier_field = $field;
} }
/** /**
* Set a {@link PasswordValidator} object to use to validate member's passwords. * Set a {@link PasswordValidator} object to use to validate member's passwords.
*/ */
public static function set_password_validator($pv) { public static function set_password_validator($pv) {
self::$password_validator = $pv; self::$password_validator = $pv;
} }
/** /**
* Returns the current {@link PasswordValidator} * Returns the current {@link PasswordValidator}
*/ */
@ -416,7 +416,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
Deprecation::notice('3.2', 'Use the "Member.password_expiry_days" config setting instead'); Deprecation::notice('3.2', 'Use the "Member.password_expiry_days" config setting instead');
self::config()->password_expiry_days = $days; self::config()->password_expiry_days = $days;
} }
/** /**
* Configure the security system to lock users out after this many incorrect logins * Configure the security system to lock users out after this many incorrect logins
* *
@ -426,8 +426,8 @@ class Member extends DataObject implements TemplateGlobalProvider {
Deprecation::notice('3.2', 'Use the "Member.lock_out_after_incorrect_logins" config setting instead'); Deprecation::notice('3.2', 'Use the "Member.lock_out_after_incorrect_logins" config setting instead');
self::config()->lock_out_after_incorrect_logins = $numLogins; self::config()->lock_out_after_incorrect_logins = $numLogins;
} }
public function isPasswordExpired() { public function isPasswordExpired() {
if(!$this->PasswordExpiry) return false; if(!$this->PasswordExpiry) return false;
return strtotime(date('Y-m-d')) >= strtotime($this->PasswordExpiry); return strtotime(date('Y-m-d')) >= strtotime($this->PasswordExpiry);
@ -461,10 +461,10 @@ class Member extends DataObject implements TemplateGlobalProvider {
Cookie::set('alc_enc', null); Cookie::set('alc_enc', null);
Cookie::force_expiry('alc_enc'); Cookie::force_expiry('alc_enc');
} }
// Clear the incorrect log-in count // Clear the incorrect log-in count
$this->registerSuccessfulLogin(); $this->registerSuccessfulLogin();
// Don't set column if its not built yet (the login might be precursor to a /dev/build...) // Don't set column if its not built yet (the login might be precursor to a /dev/build...)
if(array_key_exists('LockedOutUntil', DB::fieldList('Member'))) { if(array_key_exists('LockedOutUntil', DB::fieldList('Member'))) {
$this->LockedOutUntil = null; $this->LockedOutUntil = null;
@ -473,7 +473,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
$this->regenerateTempID(); $this->regenerateTempID();
$this->write(); $this->write();
// Audit logging hook // Audit logging hook
$this->extend('memberLoggedIn'); $this->extend('memberLoggedIn');
} }
@ -497,7 +497,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
* Check if the member ID logged in session actually * Check if the member ID logged in session actually
* has a database record of the same ID. If there is * has a database record of the same ID. If there is
* no logged in user, FALSE is returned anyway. * no logged in user, FALSE is returned anyway.
* *
* @return boolean TRUE record found FALSE no record found * @return boolean TRUE record found FALSE no record found
*/ */
public static function logged_in_session_exists() { public static function logged_in_session_exists() {
@ -506,10 +506,10 @@ class Member extends DataObject implements TemplateGlobalProvider {
if($member->exists()) return true; if($member->exists()) return true;
} }
} }
return false; return false;
} }
/** /**
* Log the user in if the "remember login" cookie is set * Log the user in if the "remember login" cookie is set
* *
@ -519,7 +519,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
public static function autoLogin() { public static function autoLogin() {
// Don't bother trying this multiple times // Don't bother trying this multiple times
self::$_already_tried_to_auto_log_in = true; self::$_already_tried_to_auto_log_in = true;
if(strpos(Cookie::get('alc_enc'), ':') && !Session::get("loggedInAs")) { if(strpos(Cookie::get('alc_enc'), ':') && !Session::get("loggedInAs")) {
list($uid, $token) = explode(':', Cookie::get('alc_enc'), 2); list($uid, $token) = explode(':', Cookie::get('alc_enc'), 2);
$SQL_uid = Convert::raw2sql($uid); $SQL_uid = Convert::raw2sql($uid);
@ -541,7 +541,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
if(Member::config()->login_marker_cookie) { if(Member::config()->login_marker_cookie) {
Cookie::set(Member::config()->login_marker_cookie, 1, 0, null, null, false, true); Cookie::set(Member::config()->login_marker_cookie, 1, 0, null, null, false, true);
} }
$generator = new RandomGenerator(); $generator = new RandomGenerator();
$token = $generator->randomToken('sha1'); $token = $generator->randomToken('sha1');
$hash = $member->encryptWithUserSettings($token); $hash = $member->encryptWithUserSettings($token);
@ -550,7 +550,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
$member->NumVisit++; $member->NumVisit++;
$member->write(); $member->write();
// Audit logging hook // Audit logging hook
$member->extend('memberAutoLoggedIn'); $member->extend('memberAutoLoggedIn');
} }
@ -574,12 +574,12 @@ class Member extends DataObject implements TemplateGlobalProvider {
Cookie::set('alc_enc', null); // // Clear the Remember Me cookie Cookie::set('alc_enc', null); // // Clear the Remember Me cookie
Cookie::force_expiry('alc_enc'); Cookie::force_expiry('alc_enc');
// Switch back to live in order to avoid infinite loops when // Switch back to live in order to avoid infinite loops when
// redirecting to the login screen (if this login screen is versioned) // redirecting to the login screen (if this login screen is versioned)
Session::clear('readingMode'); Session::clear('readingMode');
$this->write(); $this->write();
// Audit logging hook // Audit logging hook
$this->extend('memberLoggedOut'); $this->extend('memberLoggedOut');
} }
@ -685,7 +685,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
/** /**
* Returns the fields for the member form - used in the registration/profile module. * Returns the fields for the member form - used in the registration/profile module.
* It should return fields that are editable by the admin and the logged-in user. * It should return fields that are editable by the admin and the logged-in user.
* *
* @return FieldList Returns a {@link FieldList} containing the fields for * @return FieldList Returns a {@link FieldList} containing the fields for
* the member form. * the member form.
@ -746,7 +746,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
return Member::get()->byId($id); return Member::get()->byId($id);
} }
} }
/** /**
* Returns true if the current member is a repeat visitor who has logged in more than once. * Returns true if the current member is a repeat visitor who has logged in more than once.
*/ */
@ -803,14 +803,14 @@ class Member extends DataObject implements TemplateGlobalProvider {
if($this->SetPassword) $this->Password = $this->SetPassword; if($this->SetPassword) $this->Password = $this->SetPassword;
// If a member with the same "unique identifier" already exists with a different ID, don't allow merging. // If a member with the same "unique identifier" already exists with a different ID, don't allow merging.
// Note: This does not a full replacement for safeguards in the controller layer (e.g. in a registration form), // Note: This does not a full replacement for safeguards in the controller layer (e.g. in a registration form),
// but rather a last line of defense against data inconsistencies. // but rather a last line of defense against data inconsistencies.
$identifierField = Member::config()->unique_identifier_field; $identifierField = Member::config()->unique_identifier_field;
if($this->$identifierField) { if($this->$identifierField) {
// Note: Same logic as Member_Validator class // Note: Same logic as Member_Validator class
$idClause = ($this->ID) ? sprintf(" AND \"Member\".\"ID\" <> %d", (int)$this->ID) : ''; $idClause = ($this->ID) ? sprintf(" AND \"Member\".\"ID\" <> %d", (int)$this->ID) : '';
$existingRecord = DataObject::get_one( $existingRecord = DataObject::get_one(
'Member', 'Member',
sprintf( sprintf(
"\"%s\" = '%s' %s", "\"%s\" = '%s' %s",
$identifierField, $identifierField,
@ -820,8 +820,8 @@ class Member extends DataObject implements TemplateGlobalProvider {
); );
if($existingRecord) { if($existingRecord) {
throw new ValidationException(new ValidationResult(false, _t( throw new ValidationException(new ValidationResult(false, _t(
'Member.ValidationIdentifierFailed', 'Member.ValidationIdentifierFailed',
'Can\'t overwrite existing member #{id} with identical identifier ({name} = {value}))', 'Can\'t overwrite existing member #{id} with identical identifier ({name} = {value}))',
'Values in brackets show "fieldname = value", usually denoting an existing email address', 'Values in brackets show "fieldname = value", usually denoting an existing email address',
array( array(
'id' => $existingRecord->ID, 'id' => $existingRecord->ID,
@ -835,9 +835,9 @@ class Member extends DataObject implements TemplateGlobalProvider {
// We don't send emails out on dev/tests sites to prevent accidentally spamming users. // We don't send emails out on dev/tests sites to prevent accidentally spamming users.
// However, if TestMailer is in use this isn't a risk. // However, if TestMailer is in use this isn't a risk.
if( if(
(Director::isLive() || Email::mailer() instanceof TestMailer) (Director::isLive() || Email::mailer() instanceof TestMailer)
&& $this->isChanged('Password') && $this->isChanged('Password')
&& $this->record['Password'] && $this->record['Password']
&& $this->config()->notify_password_change && $this->config()->notify_password_change
) { ) {
$e = Member_ChangePasswordEmail::create(); $e = Member_ChangePasswordEmail::create();
@ -879,10 +879,10 @@ class Member extends DataObject implements TemplateGlobalProvider {
if(!$this->Locale) { if(!$this->Locale) {
$this->Locale = i18n::get_locale(); $this->Locale = i18n::get_locale();
} }
parent::onBeforeWrite(); parent::onBeforeWrite();
} }
public function onAfterWrite() { public function onAfterWrite() {
parent::onAfterWrite(); parent::onAfterWrite();
@ -890,10 +890,10 @@ class Member extends DataObject implements TemplateGlobalProvider {
MemberPassword::log($this); MemberPassword::log($this);
} }
} }
/** /**
* If any admin groups are requested, deny the whole save operation. * If any admin groups are requested, deny the whole save operation.
* *
* @param Array $ids Database IDs of Group records * @param Array $ids Database IDs of Group records
* @return boolean * @return boolean
*/ */
@ -921,7 +921,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
if($groups) foreach($groups as $group) { if($groups) foreach($groups as $group) {
if($this->inGroup($group, $strict)) return true; if($this->inGroup($group, $strict)) return true;
} }
return false; return false;
} }
@ -944,9 +944,9 @@ class Member extends DataObject implements TemplateGlobalProvider {
} else { } else {
user_error('Member::inGroup(): Wrong format for $group parameter', E_USER_ERROR); user_error('Member::inGroup(): Wrong format for $group parameter', E_USER_ERROR);
} }
if(!$groupCheckObj) return false; if(!$groupCheckObj) return false;
$groupCandidateObjs = ($strict) ? $this->getManyManyComponents("Groups") : $this->Groups(); $groupCandidateObjs = ($strict) ? $this->getManyManyComponents("Groups") : $this->Groups();
if($groupCandidateObjs) foreach($groupCandidateObjs as $groupCandidateObj) { if($groupCandidateObjs) foreach($groupCandidateObjs as $groupCandidateObj) {
if($groupCandidateObj->ID == $groupCheckObj->ID) return true; if($groupCandidateObj->ID == $groupCheckObj->ID) return true;
@ -954,32 +954,32 @@ class Member extends DataObject implements TemplateGlobalProvider {
return false; return false;
} }
/** /**
* Adds the member to a group. This will create the group if the given * Adds the member to a group. This will create the group if the given
* group code does not return a valid group object. * group code does not return a valid group object.
* *
* @param string $groupcode * @param string $groupcode
* @param string Title of the group * @param string Title of the group
*/ */
public function addToGroupByCode($groupcode, $title = "") { public function addToGroupByCode($groupcode, $title = "") {
$group = DataObject::get_one('Group', "\"Code\" = '" . Convert::raw2sql($groupcode). "'"); $group = DataObject::get_one('Group', "\"Code\" = '" . Convert::raw2sql($groupcode). "'");
if($group) { if($group) {
$this->Groups()->add($group); $this->Groups()->add($group);
} }
else { else {
if(!$title) $title = $groupcode; if(!$title) $title = $groupcode;
$group = new Group(); $group = new Group();
$group->Code = $groupcode; $group->Code = $groupcode;
$group->Title = $title; $group->Title = $title;
$group->write(); $group->write();
$this->Groups()->add($group); $this->Groups()->add($group);
} }
} }
/** /**
* Removes a member from a group. * Removes a member from a group.
* *
@ -987,12 +987,12 @@ class Member extends DataObject implements TemplateGlobalProvider {
*/ */
public function removeFromGroupByCode($groupcode) { public function removeFromGroupByCode($groupcode) {
$group = Group::get()->filter(array('Code' => $groupcode))->first(); $group = Group::get()->filter(array('Code' => $groupcode))->first();
if($group) { if($group) {
$this->Groups()->remove($group); $this->Groups()->remove($group);
} }
} }
/** /**
* @param Array $columns Column names on the Member record to show in {@link getTitle()}. * @param Array $columns Column names on the Member record to show in {@link getTitle()}.
* @param String $sep Separator * @param String $sep Separator
@ -1007,7 +1007,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
/** /**
* Get the complete name of the member, by default in the format "<Surname>, <FirstName>". * Get the complete name of the member, by default in the format "<Surname>, <FirstName>".
* Falls back to showing either field on its own. * Falls back to showing either field on its own.
* *
* You can overload this getter with {@link set_title_format()} * You can overload this getter with {@link set_title_format()}
* and {@link set_title_sql()}. * and {@link set_title_sql()}.
* *
@ -1041,7 +1041,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
/** /**
* Return a SQL CONCAT() fragment suitable for a SELECT statement. * Return a SQL CONCAT() fragment suitable for a SELECT statement.
* Useful for custom queries which assume a certain member title format. * Useful for custom queries which assume a certain member title format.
* *
* @param String $tableName * @param String $tableName
* @return String SQL * @return String SQL
*/ */
@ -1055,7 +1055,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
foreach($format['columns'] as $column) { foreach($format['columns'] as $column) {
$columnsWithTablename[] = "\"$tableName\".\"$column\""; $columnsWithTablename[] = "\"$tableName\".\"$column\"";
} }
return "(".join(" $op '".$format['sep']."' $op ", $columnsWithTablename).")"; return "(".join(" $op '".$format['sep']."' $op ", $columnsWithTablename).")";
} else { } else {
return "(\"$tableName\".\"Surname\" $op ' ' $op \"$tableName\".\"FirstName\")"; return "(\"$tableName\".\"Surname\" $op ' ' $op \"$tableName\".\"FirstName\")";
@ -1102,7 +1102,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
* Override the default getter for DateFormat so the * Override the default getter for DateFormat so the
* default format for the user's locale is used * default format for the user's locale is used
* if the user has not defined their own. * if the user has not defined their own.
* *
* @return string ISO date format * @return string ISO date format
*/ */
public function getDateFormat() { public function getDateFormat() {
@ -1120,7 +1120,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
* Override the default getter for TimeFormat so the * Override the default getter for TimeFormat so the
* default format for the user's locale is used * default format for the user's locale is used
* if the user has not defined their own. * if the user has not defined their own.
* *
* @return string ISO date format * @return string ISO date format
*/ */
public function getTimeFormat() { public function getTimeFormat() {
@ -1147,7 +1147,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
public function Groups() { public function Groups() {
$groups = Member_GroupSet::create('Group', 'Group_Members', 'GroupID', 'MemberID'); $groups = Member_GroupSet::create('Group', 'Group_Members', 'GroupID', 'MemberID');
$groups = $groups->forForeignID($this->ID); $groups = $groups->forForeignID($this->ID);
$this->extend('updateGroups', $groups); $this->extend('updateGroups', $groups);
return $groups; return $groups;
@ -1159,19 +1159,19 @@ class Member extends DataObject implements TemplateGlobalProvider {
public function DirectGroups() { public function DirectGroups() {
return $this->getManyManyComponents('Groups'); return $this->getManyManyComponents('Groups');
} }
/** /**
* Get a member SQLMap of members in specific groups * Get a member SQLMap of members in specific groups
* *
* If no $groups is passed, all members will be returned * If no $groups is passed, all members will be returned
* *
* @param mixed $groups - takes a SS_List, an array or a single Group.ID * @param mixed $groups - takes a SS_List, an array or a single Group.ID
* @return SQLMap Returns an SQLMap that returns all Member data. * @return SQLMap Returns an SQLMap that returns all Member data.
* @see map() * @see map()
*/ */
public static function map_in_groups($groups = null) { public static function map_in_groups($groups = null) {
$groupIDList = array(); $groupIDList = array();
if($groups instanceof SS_List) { if($groups instanceof SS_List) {
foreach( $groups as $group ) { foreach( $groups as $group ) {
$groupIDList[] = $group->ID; $groupIDList[] = $group->ID;
@ -1181,18 +1181,18 @@ class Member extends DataObject implements TemplateGlobalProvider {
} elseif($groups) { } elseif($groups) {
$groupIDList[] = $groups; $groupIDList[] = $groups;
} }
// No groups, return all Members // No groups, return all Members
if(!$groupIDList) { if(!$groupIDList) {
return Member::get()->sort(array('Surname'=>'ASC', 'FirstName'=>'ASC'))->map(); return Member::get()->sort(array('Surname'=>'ASC', 'FirstName'=>'ASC'))->map();
} }
$membersList = new ArrayList(); $membersList = new ArrayList();
// This is a bit ineffective, but follow the ORM style // This is a bit ineffective, but follow the ORM style
foreach(Group::get()->byIDs($groupIDList) as $group) { foreach(Group::get()->byIDs($groupIDList) as $group) {
$membersList->merge($group->Members()); $membersList->merge($group->Members());
} }
$membersList->removeDuplicates('ID'); $membersList->removeDuplicates('ID');
return $membersList->map(); return $membersList->map();
} }
@ -1211,19 +1211,19 @@ class Member extends DataObject implements TemplateGlobalProvider {
public static function mapInCMSGroups($groups = null) { public static function mapInCMSGroups($groups = null) {
if(!$groups || $groups->Count() == 0) { if(!$groups || $groups->Count() == 0) {
$perms = array('ADMIN', 'CMS_ACCESS_AssetAdmin'); $perms = array('ADMIN', 'CMS_ACCESS_AssetAdmin');
if(class_exists('CMSMain')) { if(class_exists('CMSMain')) {
$cmsPerms = singleton('CMSMain')->providePermissions(); $cmsPerms = singleton('CMSMain')->providePermissions();
} else { } else {
$cmsPerms = singleton('LeftAndMain')->providePermissions(); $cmsPerms = singleton('LeftAndMain')->providePermissions();
} }
if(!empty($cmsPerms)) { if(!empty($cmsPerms)) {
$perms = array_unique(array_merge($perms, array_keys($cmsPerms))); $perms = array_unique(array_merge($perms, array_keys($cmsPerms)));
} }
$SQL_perms = "'" . implode("', '", Convert::raw2sql($perms)) . "'"; $SQL_perms = "'" . implode("', '", Convert::raw2sql($perms)) . "'";
$groups = DataObject::get('Group') $groups = DataObject::get('Group')
->innerJoin( ->innerJoin(
"Permission", "Permission",
@ -1244,7 +1244,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
$filterClause = ($groupIDList) $filterClause = ($groupIDList)
? "\"GroupID\" IN (" . implode( ',', $groupIDList ) . ")" ? "\"GroupID\" IN (" . implode( ',', $groupIDList ) . ")"
: ""; : "";
return Member::get()->where($filterClause)->sort("\"Surname\", \"FirstName\"") return Member::get()->where($filterClause)->sort("\"Surname\", \"FirstName\"")
->innerJoin("Group_Members", "\"MemberID\"=\"Member\".\"ID\"") ->innerJoin("Group_Members", "\"MemberID\"=\"Member\".\"ID\"")
->innerJoin("Group", "\"Group\".\"ID\"=\"GroupID\"") ->innerJoin("Group", "\"Group\".\"ID\"=\"GroupID\"")
@ -1272,7 +1272,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
unset($groupList[$index]); unset($groupList[$index]);
} }
} }
return $groupList; return $groupList;
} }
@ -1292,10 +1292,10 @@ class Member extends DataObject implements TemplateGlobalProvider {
$mainFields = $fields->fieldByName("Root")->fieldByName("Main")->Children; $mainFields = $fields->fieldByName("Root")->fieldByName("Main")->Children;
$password = new ConfirmedPasswordField( $password = new ConfirmedPasswordField(
'Password', 'Password',
null, null,
null, null,
null, null,
true // showOnClick true // showOnClick
); );
$password->setCanBeEmpty(true); $password->setCanBeEmpty(true);
@ -1303,12 +1303,12 @@ class Member extends DataObject implements TemplateGlobalProvider {
$mainFields->replaceField('Password', $password); $mainFields->replaceField('Password', $password);
$mainFields->replaceField('Locale', new DropdownField( $mainFields->replaceField('Locale', new DropdownField(
"Locale", "Locale",
_t('Member.INTERFACELANG', "Interface Language", 'Language of the CMS'), _t('Member.INTERFACELANG', "Interface Language", 'Language of the CMS'),
i18n::get_existing_translations() i18n::get_existing_translations()
)); ));
$mainFields->removeByName($self->config()->hidden_fields); $mainFields->removeByName($self->config()->hidden_fields);
if( ! $self->config()->lock_out_after_incorrect_logins) { if( ! $self->config()->lock_out_after_incorrect_logins) {
$mainFields->removeByName('FailedLoginCount'); $mainFields->removeByName('FailedLoginCount');
} }
@ -1333,7 +1333,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
->setMultiple(true) ->setMultiple(true)
->setSource($groupsMap) ->setSource($groupsMap)
->setAttribute( ->setAttribute(
'data-placeholder', 'data-placeholder',
_t('Member.ADDGROUP', 'Add group', 'Placeholder text for a dropdown') _t('Member.ADDGROUP', 'Add group', 'Placeholder text for a dropdown')
) )
); );
@ -1357,7 +1357,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
$permissionsTab = $fields->fieldByName("Root")->fieldByName('Permissions'); $permissionsTab = $fields->fieldByName("Root")->fieldByName('Permissions');
if($permissionsTab) $permissionsTab->addExtraClass('readonly'); if($permissionsTab) $permissionsTab->addExtraClass('readonly');
$defaultDateFormat = Zend_Locale_Format::getDateFormat(new Zend_Locale($self->Locale)); $defaultDateFormat = Zend_Locale_Format::getDateFormat(new Zend_Locale($self->Locale));
$dateFormatMap = array( $dateFormatMap = array(
'MMM d, yyyy' => Zend_Date::now()->toString('MMM d, yyyy'), 'MMM d, yyyy' => Zend_Date::now()->toString('MMM d, yyyy'),
@ -1375,7 +1375,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
) )
); );
$dateFormatField->setValue($self->DateFormat); $dateFormatField->setValue($self->DateFormat);
$defaultTimeFormat = Zend_Locale_Format::getTimeFormat(new Zend_Locale($self->Locale)); $defaultTimeFormat = Zend_Locale_Format::getTimeFormat(new Zend_Locale($self->Locale));
$timeFormatMap = array( $timeFormatMap = array(
'h:mm a' => Zend_Date::now()->toString('h:mm a'), 'h:mm a' => Zend_Date::now()->toString('h:mm a'),
@ -1392,18 +1392,18 @@ class Member extends DataObject implements TemplateGlobalProvider {
); );
$timeFormatField->setValue($self->TimeFormat); $timeFormatField->setValue($self->TimeFormat);
}); });
return parent::getCMSFields(); return parent::getCMSFields();
} }
/** /**
* *
* @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
* *
*/ */
public function fieldLabels($includerelations = true) { public function fieldLabels($includerelations = true) {
$labels = parent::fieldLabels($includerelations); $labels = parent::fieldLabels($includerelations);
$labels['FirstName'] = _t('Member.FIRSTNAME', 'First Name'); $labels['FirstName'] = _t('Member.FIRSTNAME', 'First Name');
$labels['Surname'] = _t('Member.SURNAME', 'Surname'); $labels['Surname'] = _t('Member.SURNAME', 'Surname');
$labels['Email'] = _t('Member.EMAIL', 'Email'); $labels['Email'] = _t('Member.EMAIL', 'Email');
@ -1421,7 +1421,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
} }
return $labels; return $labels;
} }
/** /**
* Users can view their own record. * Users can view their own record.
* Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions. * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions.
@ -1429,54 +1429,54 @@ class Member extends DataObject implements TemplateGlobalProvider {
*/ */
public function canView($member = null) { public function canView($member = null) {
if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser(); if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
// extended access checks // extended access checks
$results = $this->extend('canView', $member); $results = $this->extend('canView', $member);
if($results && is_array($results)) { if($results && is_array($results)) {
if(!min($results)) return false; if(!min($results)) return false;
else return true; else return true;
} }
// members can usually edit their own record // members can usually edit their own record
if($member && $this->ID == $member->ID) return true; if($member && $this->ID == $member->ID) return true;
if( if(
Permission::checkMember($member, 'ADMIN') Permission::checkMember($member, 'ADMIN')
|| Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin') || Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin')
) { ) {
return true; return true;
} }
return false; return false;
} }
/** /**
* Users can edit their own record. * Users can edit their own record.
* Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions
*/ */
public function canEdit($member = null) { public function canEdit($member = null) {
if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser(); if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
// extended access checks // extended access checks
$results = $this->extend('canEdit', $member); $results = $this->extend('canEdit', $member);
if($results && is_array($results)) { if($results && is_array($results)) {
if(!min($results)) return false; if(!min($results)) return false;
else return true; else return true;
} }
// No member found // No member found
if(!($member && $member->exists())) return false; if(!($member && $member->exists())) return false;
// If the requesting member is not an admin, but has access to manage members, // If the requesting member is not an admin, but has access to manage members,
// he still can't edit other members with ADMIN permission. // they still can't edit other members with ADMIN permission.
// This is a bit weak, strictly speaking he shouldn't be allowed to // This is a bit weak, strictly speaking they shouldn't be allowed to
// perform any action that could change the password on a member // perform any action that could change the password on a member
// with "higher" permissions than himself, but thats hard to determine. // with "higher" permissions than himself, but thats hard to determine.
if(!Permission::checkMember($member, 'ADMIN') && Permission::checkMember($this, 'ADMIN')) return false; if(!Permission::checkMember($member, 'ADMIN') && Permission::checkMember($this, 'ADMIN')) return false;
return $this->canView($member); return $this->canView($member);
} }
/** /**
* Users can edit their own record. * Users can edit their own record.
* Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions
@ -1497,7 +1497,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
// Members are not allowed to remove themselves, // Members are not allowed to remove themselves,
// since it would create inconsistencies in the admin UIs. // since it would create inconsistencies in the admin UIs.
if($this->ID && $member->ID == $this->ID) return false; if($this->ID && $member->ID == $this->ID) return false;
return $this->canEdit($member); return $this->canEdit($member);
} }
@ -1507,7 +1507,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
*/ */
public function validate() { public function validate() {
$valid = parent::validate(); $valid = parent::validate();
if(!$this->ID || $this->isChanged('Password')) { if(!$this->ID || $this->isChanged('Password')) {
if($this->Password && self::$password_validator) { if($this->Password && self::$password_validator) {
$valid->combineAnd(self::$password_validator->validate($this->Password, $this)); $valid->combineAnd(self::$password_validator->validate($this->Password, $this));
@ -1521,26 +1521,26 @@ class Member extends DataObject implements TemplateGlobalProvider {
} }
return $valid; return $valid;
} }
/** /**
* Change password. This will cause rehashing according to * Change password. This will cause rehashing according to
* the `PasswordEncryption` property. * the `PasswordEncryption` property.
* *
* @param String $password Cleartext password * @param String $password Cleartext password
*/ */
public function changePassword($password) { public function changePassword($password) {
$this->Password = $password; $this->Password = $password;
$valid = $this->validate(); $valid = $this->validate();
if($valid->valid()) { if($valid->valid()) {
$this->AutoLoginHash = null; $this->AutoLoginHash = null;
$this->write(); $this->write();
} }
return $valid; return $valid;
} }
/** /**
* Tell this member that someone made a failed attempt at logging in as them. * Tell this member that someone made a failed attempt at logging in as them.
* This can be used to lock the user out temporarily if too many failed attempts are made. * This can be used to lock the user out temporarily if too many failed attempts are made.
@ -1550,7 +1550,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
// Keep a tally of the number of failed log-ins so that we can lock people out // Keep a tally of the number of failed log-ins so that we can lock people out
$this->FailedLoginCount = $this->FailedLoginCount + 1; $this->FailedLoginCount = $this->FailedLoginCount + 1;
$this->write(); $this->write();
if($this->FailedLoginCount >= self::config()->lock_out_after_incorrect_logins) { if($this->FailedLoginCount >= self::config()->lock_out_after_incorrect_logins) {
$lockoutMins = self::config()->lock_out_delay_mins; $lockoutMins = self::config()->lock_out_delay_mins;
$this->LockedOutUntil = date('Y-m-d H:i:s', time() + $lockoutMins*60); $this->LockedOutUntil = date('Y-m-d H:i:s', time() + $lockoutMins*60);
@ -1569,18 +1569,18 @@ class Member extends DataObject implements TemplateGlobalProvider {
$this->write(); $this->write();
} }
} }
/** /**
* Get the HtmlEditorConfig for this user to be used in the CMS. * Get the HtmlEditorConfig for this user to be used in the CMS.
* This is set by the group. If multiple configurations are set, * This is set by the group. If multiple configurations are set,
* the one with the highest priority wins. * the one with the highest priority wins.
* *
* @return string * @return string
*/ */
public function getHtmlEditorConfigForCMS() { public function getHtmlEditorConfigForCMS() {
$currentName = ''; $currentName = '';
$currentPriority = 0; $currentPriority = 0;
foreach($this->Groups() as $group) { foreach($this->Groups() as $group) {
$configName = $group->HtmlEditorConfig; $configName = $group->HtmlEditorConfig;
if($configName) { if($configName) {
@ -1591,7 +1591,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
} }
} }
} }
// If can't find a suitable editor, just default to cms // If can't find a suitable editor, just default to cms
return $currentName ? $currentName : 'cms'; return $currentName ? $currentName : 'cms';
} }
@ -1622,7 +1622,7 @@ class Member_GroupSet extends ManyManyList {
$this->foreignKey = $foreignKey; $this->foreignKey = $foreignKey;
$this->extraFields = $extraFields; $this->extraFields = $extraFields;
} }
/** /**
* Link this group set to a specific member. * Link this group set to a specific member.
*/ */
@ -1640,7 +1640,7 @@ class Member_GroupSet extends ManyManyList {
$groupIDs = DataObject::get("Group")->byIDs($groupIDs)->column("ParentID"); $groupIDs = DataObject::get("Group")->byIDs($groupIDs)->column("ParentID");
$groupIDs = array_filter($groupIDs); $groupIDs = array_filter($groupIDs);
} }
// Add a filter to this DataList // Add a filter to this DataList
if($allGroupIDs) { if($allGroupIDs) {
return "\"Group\".\"ID\" IN (" . implode(',', $allGroupIDs) .")"; return "\"Group\".\"ID\" IN (" . implode(',', $allGroupIDs) .")";
@ -1667,7 +1667,7 @@ class Member_ChangePasswordEmail extends Email {
protected $from = ''; // setting a blank from address uses the site's default administrator email protected $from = ''; // setting a blank from address uses the site's default administrator email
protected $subject = ''; protected $subject = '';
protected $ss_template = 'ChangePasswordEmail'; protected $ss_template = 'ChangePasswordEmail';
public function __construct() { public function __construct() {
parent::__construct(); parent::__construct();
@ -1687,7 +1687,7 @@ class Member_ForgotPasswordEmail extends Email {
protected $from = ''; // setting a blank from address uses the site's default administrator email protected $from = ''; // setting a blank from address uses the site's default administrator email
protected $subject = ''; protected $subject = '';
protected $ss_template = 'ForgotPasswordEmail'; protected $ss_template = 'ForgotPasswordEmail';
public function __construct() { public function __construct() {
parent::__construct(); parent::__construct();
@ -1742,7 +1742,7 @@ class Member_Validator extends RequiredFields {
*/ */
public function php($data) { public function php($data) {
$valid = parent::php($data); $valid = parent::php($data);
$identifierField = Member::config()->unique_identifier_field; $identifierField = Member::config()->unique_identifier_field;
$SQL_identifierField = Convert::raw2sql($data[$identifierField]); $SQL_identifierField = Convert::raw2sql($data[$identifierField]);
$member = DataObject::get_one('Member', "\"$identifierField\" = '{$SQL_identifierField}'"); $member = DataObject::get_one('Member', "\"$identifierField\" = '{$SQL_identifierField}'");

View File

@ -223,7 +223,7 @@ JS;
return $this->controller->redirect(Director::absoluteBaseURL() . Security::config()->default_login_dest); return $this->controller->redirect(Director::absoluteBaseURL() . Security::config()->default_login_dest);
} }
// Redirect the user to the page where he came from // Redirect the user to the page where they came from
$member = Member::currentUser(); $member = Member::currentUser();
if($member) { if($member) {
$firstname = Convert::raw2xml($member->FirstName); $firstname = Convert::raw2xml($member->FirstName);

View File

@ -1,22 +1,22 @@
<?php <?php
class GridFieldEditButtonTest extends SapphireTest { class GridFieldEditButtonTest extends SapphireTest {
/** @var ArrayList */ /** @var ArrayList */
protected $list; protected $list;
/** @var GridField */ /** @var GridField */
protected $gridField; protected $gridField;
/** @var Form */ /** @var Form */
protected $form; protected $form;
/** @var string */ /** @var string */
protected static $fixture_file = 'GridFieldActionTest.yml'; protected static $fixture_file = 'GridFieldActionTest.yml';
/** @var array */ /** @var array */
protected $extraDataObjects = array('GridFieldAction_Delete_Team', 'GridFieldAction_Edit_Team'); protected $extraDataObjects = array('GridFieldAction_Delete_Team', 'GridFieldAction_Edit_Team');
public function setUp() { public function setUp() {
parent::setUp(); parent::setUp();
$this->list = new DataList('GridFieldAction_Edit_Team'); $this->list = new DataList('GridFieldAction_Edit_Team');
@ -24,19 +24,19 @@ class GridFieldEditButtonTest extends SapphireTest {
$this->gridField = new GridField('testfield', 'testfield', $this->list, $config); $this->gridField = new GridField('testfield', 'testfield', $this->list, $config);
$this->form = new Form(new Controller(), 'mockform', new FieldList(array($this->gridField)), new FieldList()); $this->form = new Form(new Controller(), 'mockform', new FieldList(array($this->gridField)), new FieldList());
} }
public function testShowEditLinks() { public function testShowEditLinks() {
if(Member::currentUser()) { Member::currentUser()->logOut(); } if(Member::currentUser()) { Member::currentUser()->logOut(); }
$content = new CSSContentParser($this->gridField->FieldHolder()); $content = new CSSContentParser($this->gridField->FieldHolder());
// Check that there are content // Check that there are content
$this->assertEquals(3, count($content->getBySelector('.ss-gridfield-item'))); $this->assertEquals(3, count($content->getBySelector('.ss-gridfield-item')));
// Make sure that there are edit links, even though the user doesn't have "edit" permissions // Make sure that there are edit links, even though the user doesn't have "edit" permissions
// (he can still view the records) // (they can still view the records)
$this->assertEquals(2, count($content->getBySelector('.edit-link')), $this->assertEquals(2, count($content->getBySelector('.edit-link')),
'Edit links should show when not logged in.'); 'Edit links should show when not logged in.');
} }
public function testShowEditLinksWithAdminPermission() { public function testShowEditLinksWithAdminPermission() {
$this->logInWithPermission('ADMIN'); $this->logInWithPermission('ADMIN');
$content = new CSSContentParser($this->gridField->FieldHolder()); $content = new CSSContentParser($this->gridField->FieldHolder());
@ -50,7 +50,7 @@ class GridFieldAction_Edit_Team extends DataObject implements TestOnly {
'Name' => 'Varchar', 'Name' => 'Varchar',
'City' => 'Varchar' 'City' => 'Varchar'
); );
public function canView($member = null) { public function canView($member = null) {
return true; return true;
} }