diff --git a/src/main/resources/META-INF/xnat/spawner/site-admin-elements.yaml b/src/main/resources/META-INF/xnat/spawner/site-admin-elements.yaml index aba9d999fe9d041dc9fa8bae9086b40a625dba88..7be1a702a4a297299ae199d943851fdea92a4186 100644 --- a/src/main/resources/META-INF/xnat/spawner/site-admin-elements.yaml +++ b/src/main/resources/META-INF/xnat/spawner/site-admin-elements.yaml @@ -45,7 +45,7 @@ passwordExpirationInterval: label: Password Expiration (Interval) after: > <p class="description">Interval of time after which unchanged passwords expire and users have to change them. - Uses <a target="_blank" href="http://www.postgresql.org/docs/9.0/static/functions-datetime.html">PostgreSQL interval notation</a></p> + Uses <a target="_blank" href="http://www.postgresql.org/docs/9.0/static/functions-datetime.html">PostgreSQL interval notation</a>.</p> passwordExpirationDate: kind: input.text @@ -297,21 +297,21 @@ userLoginsSessionControls: label: Session Timeout description: > Interval for timing out user sessions. Uses - <a target="_blank" href="http://www.postgresql.org/docs/9.0/static/functions-datetime.html">PostgreSQL interval notation</a> + <a target="_blank" href="http://www.postgresql.org/docs/9.0/static/functions-datetime.html">PostgreSQL interval notation</a>. aliasTokenTimeout: kind: panel.input.text name: aliasTokenTimeout label: Alias Token Timeout description: > Interval for timing out alias tokens. Uses - <a target="_blank" href="http://www.postgresql.org/docs/9.0/static/functions-datetime.html">PostgreSQL interval notation</a> + <a target="_blank" href="http://www.postgresql.org/docs/9.0/static/functions-datetime.html">PostgreSQL interval notation</a>. aliasTokenTimeoutSchedule: kind: panel.input.text name: aliasTokenTimeoutSchedule label: Alias Token Timeout Schedule description: > - How often to check alias tokens for timeout (0 0 * * * * means it runs every hour). Uses - <a target="_blank" href="http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/scheduling/support/CronSequenceGenerator.html">Cron notation</a> + How often to check alias tokens for timeout (0 0 * * * * means it runs every hour). Uses basic + <a target="_blank" href="http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/scheduling/support/CronSequenceGenerator.html">Cron notation</a> (lists and ranges aren't supported). sessionTimeoutMessage: kind: panel.textarea name: sessionTimeoutMessage @@ -347,28 +347,28 @@ userLoginsSessionControls: label: Failed Logins Lockout Duration description: > Interval of time to lock user accounts that have exceeded the max_failed_logins count. Uses - <a target="_blank" href="http://www.postgresql.org/docs/9.0/static/functions-datetime.html">PostgreSQL interval notation</a> + <a target="_blank" href="http://www.postgresql.org/docs/9.0/static/functions-datetime.html">PostgreSQL interval notation</a>. resetFailedLoginsSchedule: kind: panel.input.text name: resetFailedLoginsSchedule label: Reset Failed Logins Schedule description: > - How often to check if the Failed Logins Lockout Duration time has expired so locked out users can be allowed to log in again (0 0 * * * * means it runs every hour). Uses - <a target="_blank" href="http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/scheduling/support/CronSequenceGenerator.html">Cron notation</a> + How often to check if the Failed Logins Lockout Duration time has expired so locked out users can be allowed to log in again (0 0 * * * * means it runs every hour). Uses basic + <a target="_blank" href="http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/scheduling/support/CronSequenceGenerator.html">Cron notation</a> (lists and ranges aren't supported). userInactivityLockout: kind: panel.input.text name: inactivityBeforeLockout label: User Inactivity Lockout description: > Interval of inactivity before a user account is disabled. Uses - <a target="_blank" href="http://www.postgresql.org/docs/9.0/static/functions-datetime.html">PostgreSQL interval notation</a> + <a target="_blank" href="http://www.postgresql.org/docs/9.0/static/functions-datetime.html">PostgreSQL interval notation</a>. inactivityBeforeLockoutSchedule: kind: panel.input.text name: inactivityBeforeLockoutSchedule label: Inactivity Lockout Schedule description: > - How often to check user accounts for inactivity (0 0 1 * * * means it runs at 1AM every day). Uses - <a target="_blank" href="http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/scheduling/support/CronSequenceGenerator.html">Cron notation</a> + How often to check user accounts for inactivity (0 0 1 * * * means it runs at 1AM every day). Uses basic + <a target="_blank" href="http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/scheduling/support/CronSequenceGenerator.html">Cron notation</a> (lists and ranges aren't supported). passwords: kind: panel.form @@ -394,7 +394,7 @@ passwords: kind: panel.element label: Password Expiration info: > - <p>A <b>password expiration interval</b> specifies the length of time users have before they must change their passwords (Specified in <a target="_blank" href="http://www.postgresql.org/docs/9.0/static/functions-datetime.html">PostgreSQL interval notation</a>)</p><p>Alternatively, a <b>password expiration date</b> can be configured to expire passwords that were last changed before it. This is useful for purging access to obsolete accounts.</p> + <p>A <b>password expiration interval</b> specifies the length of time users have before they must change their passwords (Specified in <a target="_blank" href="http://www.postgresql.org/docs/9.0/static/functions-datetime.html">PostgreSQL interval notation</a>).</p><p>Alternatively, a <b>password expiration date</b> can be configured to expire passwords that were last changed before it. This is useful for purging access to obsolete accounts.</p> contents: info: tag: p @@ -457,7 +457,7 @@ passwords: label: Password History description: > Interval for which users cannot reuse an old password of theirs. Uses - <a target="_blank" href="http://www.postgresql.org/docs/9.0/static/functions-datetime.html">PostgreSQL interval notation</a> + <a target="_blank" href="http://www.postgresql.org/docs/9.0/static/functions-datetime.html">PostgreSQL interval notation</a>. requireSaltedPasswords: kind: panel.input.checkbox id: requireSaltedPasswords @@ -1126,9 +1126,7 @@ dicomScpReceivers: dicomScpManager: tag: "div#dicom-scp-manager" dicomScpManagerScript: - tag: script - element: - src: /scripts/xnat/admin/dicomScpManager.js + tag: script|src="~/scripts/xnat/admin/dicomScpManager.js" fileSystem: kind: panel.form diff --git a/src/main/webapp/WEB-INF/tags/page/xnat.tag b/src/main/webapp/WEB-INF/tags/page/xnat.tag index 228401f46f1ea29f91576ddf604ff3bbc9a234d8..12a3c4b558eceb5c5c3b2bab46e127bd99249b02 100644 --- a/src/main/webapp/WEB-INF/tags/page/xnat.tag +++ b/src/main/webapp/WEB-INF/tags/page/xnat.tag @@ -180,6 +180,7 @@ <script src="${SITE_ROOT}/scripts/lib/dateTimePicker/jquery.datetimepicker.full.min.js"></script> <!-- XNAT JLAPI scripts --> + <script src="${SITE_ROOT}/scripts/xnat/validate.js"></script> <script src="${SITE_ROOT}/scripts/xnat/url.js"></script> <script src="${SITE_ROOT}/scripts/xnat/xhr.js"></script> <script src="${SITE_ROOT}/scripts/xnat/event.js"></script> diff --git a/src/main/webapp/scripts/xnat/admin/dicomScpManager.js b/src/main/webapp/scripts/xnat/admin/dicomScpManager.js index f66d5c8eb9433303358fdb90f02cb1626d220ffd..695feb59d6630ab76166583fcf0d675da765d810 100644 --- a/src/main/webapp/scripts/xnat/admin/dicomScpManager.js +++ b/src/main/webapp/scripts/xnat/admin/dicomScpManager.js @@ -2,6 +2,8 @@ * Manage DICOM SCP Receivers */ +console.log('dicomScpManager.js'); + var XNAT = getObject(XNAT || {}); (function(factory){ diff --git a/src/main/webapp/scripts/xnat/spawner.js b/src/main/webapp/scripts/xnat/spawner.js index 66fa3444633a1df93b642c74a16d7416afb35c70..e1661b05cc6905395798adf714dc948f97937b3c 100644 --- a/src/main/webapp/scripts/xnat/spawner.js +++ b/src/main/webapp/scripts/xnat/spawner.js @@ -95,6 +95,18 @@ var XNAT = getObject(XNAT); try { spawnedElement = spawn(prop.tag || prop.element.tag || 'div', prop.element, prop.content); + + // convert relative URIs for href, src, and action attributes + if (spawnedElement.href) { + spawnedElement.href = setRoot(spawnedElement.getAttribute('href')) + } + if (spawnedElement.src) { + spawnedElement.src = setRoot(spawnedElement.getAttribute('src')) + } + if (spawnedElement.action) { + spawnedElement.action = setRoot(spawnedElement.getAttribute('action')) + } + // jQuery's .append() method is // MUCH more robust and forgiving // than element.appendChild() diff --git a/src/main/webapp/scripts/xnat/validate.js b/src/main/webapp/scripts/xnat/validate.js new file mode 100644 index 0000000000000000000000000000000000000000..e62e7c1c5d204d82f53f4108723da70da7ab190c --- /dev/null +++ b/src/main/webapp/scripts/xnat/validate.js @@ -0,0 +1,704 @@ +/*! + * Form and value validation functions for XNAT + * Some code adapted from http://rickharrison.github.com/validate.js + */ + +var XNAT = getObject(XNAT); + +(function(factory){ + if (typeof define === 'function' && define.amd) { + define(factory); + } + else if (typeof exports === 'object') { + module.exports = factory(); + } + else { + return factory(); + } +}(function(){ + + var undefined, validate; + + XNAT.validation = getObject(XNAT.validation || {}); + + // HELPERS + + function isNull(value){ + return !!(value == null || value === ''); + } + + // copy of jQuery's $.isNumeric() method + function isNumeric( num ) { + return !isArray( num ) && (num - parseFloat( num ) + 1) >= 0; + } + + // TODO: implement display of error messages + var message = { + required: 'The __NAME__ field is required.', + matches: 'The __NAME__ field does not match the __VALUE__ field.', + "default": 'The __NAME__ field is still set to default, please change.', + email: 'The __NAME__ field must contain a valid email address.', + emails: 'The __NAME__ field must contain all valid email addresses.', + minLength: 'The __NAME__ field must be at least __VALUE__ characters in length.', + maxLength: 'The __NAME__ field must not exceed __VALUE__ characters in length.', + exactLength: 'The __NAME__ field must be exactly __VALUE__ characters in length.', + greaterThan: 'The __NAME__ field must contain a number greater than __VALUE__.', + lessThan: 'The __NAME__ field must contain a number less than __VALUE__.', + alpha: 'The __NAME__ field must only contain alphabetical characters.', + alphaNumeric: 'The __NAME__ field must only contain alpha-numeric characters.', + alphaDash: 'The __NAME__ field must only contain alpha-numeric characters, underscores, and dashes.', + numeric: 'The __NAME__ field must contain only numbers.', + number: "The __NAME__ value must be of type 'number'.", + integer: 'The __NAME__ field must contain an integer.', + decimal: 'The __NAME__ field must contain a decimal number.', + natural: 'The __NAME__ field must contain only positive numbers.', + naturalNoZero: 'The __NAME__ field must contain a number greater than zero.', + ip: 'The __NAME__ field must contain a valid IP.', + base64: 'The __NAME__ field must contain a base64 string.', + creditCard: 'The __NAME__ field must contain a valid credit card number.', + fileType: 'The __NAME__ field must contain only __VALUE__ files.', + validUrl: 'The __NAME__ field must contain a valid URL.', + greaterThanDate: 'The __NAME__ field must contain a more recent date than __VALUE__.', + lessThanDate: 'The __NAME__ field must contain an older date than __VALUE__.', + greaterThanOrEqualDate: "The __NAME__ field must contain a date that's at least as recent as __VALUE__.", + lessThanOrEqualDate: "The __NAME__ field must contain a date that's __VALUE__ or older." + }; + + // auto-generate alternate property names from camelCase names + // creates hyphen-ated and under_score aliases + // clutters up the namespace, but... oh, well + forOwn(message, function(name){ + message[name.toLowerCase()] = message[name]; // lowercase names + message[toDashed(name)] = message[name]; // hyphen-ated names + message[toUnderscore(name)] = message[name]; // under_score names + }); + + + var regex = { + required: /[\S\W]+/, // whitespace characters will still validate + notEmpty: /[\S]/, // must contain more than just whitespace characters + rule: /^(.+?)\[(.+)\]$/, // ? + //numeric: /^-?\d*\d{3}[,]*\d[.]*\d+$/, + integer: /^-?[0-9]+$/, // positive or negative whole number + natural: /^[0-9]+$/, // positive whole number + naturalNoZero: /^[1-9][0-9]*$/, // no leading 0 + decimal: /^-?[0-9]*\.?[0-9]+$/, + hexadecimal: /^[0-9a-f]+$/, + email: /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/, + alpha: /^[a-z]+$/i, // ONLY letters + alphaSafe: /^[a-z_]+$/i, // ONLY letters and underscores + alphaDash: /^[a-z_\-]+$/i, // ONLY letters, underscore, and dash + alphaNum: /^[a-z0-9]+$/i, // ONLY letters and numbers + alphaNumSafe: /^[a-z0-9_]+$/i, // ONLY letters, numbers, and underscore + alphaNumDash: /^[a-z0-9_\-]+$/i, // ONLY letters, numbers, underscore, and dash + idSafe: /^[a-z][a-z0-9_\-]+$/i, // safe to use as an ID - alphasafe and must start with a letter + idStrict: /^[a-z][a-z0-9_]+$/i, // 'idSafe' without hyphens + ip: /^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})$/i, + base64: /[^a-zA-Z0-9\/+=]/i, + numericDash: /^[\d\-\s]+$/, + url: /^((http|https):\/\/(\w+:{0,1}\w*@)?(\S+)|)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/, + uri: /^([\/](\w+:{0,1}\w*@)?(\S+)|)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/, + // these date regexes can't check leap years or other incorrect MM/DD combos + dateISO: /^(19|20)\d\d([- /.])(0[1-9]|1[012])\2(0[1-9]|[12][0-9]|3[01])$/, + dateUS: /^(0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])[- /.](19|20)\d\d$/, + dateEU: /^(0[1-9]|[12][0-9]|3[01])[- /.](0[1-9]|1[012])[- /.](19|20)\d\d$/, + // CRON!!!!! (Say it like "KHAN!!!!!") + cronWords: /^@(reboot|yearly|annually|monthly|weekly|daily|midnight|hourly)$/i, + cronSeconds: /^((\*|\?|0|([1-9]|[1-5][0-9]))(\/\d+)?)$/, + cronMinutes: /^((\*|\?|0|([1-9]|[1-5][0-9]))(\/\d+)?)$/, + cronHours: /^((\*|\?|([0-9]|1[0-9]|2[0-3]))(\/\d+)?)$/, + cronDay: /^((\*|\?|([0-9]|[1-2][0-9]|3[0-1]))(\/\d+)?)$/, + cronMonth: /^((\*|\?|([0-9]|1[0-2]))(\/\d+)?)$/, + cronMonths: /^(((\*|\?|([0-9]|1[0-2]))(\/\d+)?)|(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|DEC))$/i, + cronMonthNames: /^(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|DEC)$/i, + cronWeekday: /^((\*|\?|([0-7]))(\/\d+)?)$/, + cronWeekdays: /^(((\*|\?|([0-7]))(\/\d+)?)|(MON|TUE|WED|THU|FRI|SAT|SUN))$/i, + cronWeekdayNames: /^(MON|TUE|WED|THU|FRI|SAT|SUN)$/i, + // cronAlt: /^(\*|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|\*\/([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])) (\*|([0-9]|1[0-9]|2[0-3])|\*\/([0-9]|1[0-9]|2[0-3])) (\*|([1-9]|1[0-9]|2[0-9]|3[0-1])|\*\/([1-9]|1[0-9]|2[0-9]|3[0-1])) (\*|([1-9]|1[0-2])|\*\/([1-9]|1[0-2])) (\*|([0-6])|\*\/([0-6]))$/, + // cron regex lifted from this post: http://stackoverflow.com/questions/235504/validating-crontab-entries-w-php + // cron: /^\s*($|#|\w+\s*=|(\*(?:\/\d+)?|(?:[0-5]?\d)(?:-(?:[0-5]?\d)(?:\/\d+)?)?(?:,(?:[0-5]?\d)(?:-(?:[0-5]?\d)(?:\/\d+)?)?)*)\s+(\*(?:\/\d+)?|(?:[01]?\d|2[0-3])(?:-(?:[01]?\d|2[0-3])(?:\/\d+)?)?(?:,(?:[01]?\d|2[0-3])(?:-(?:[01]?\d|2[0-3])(?:\/\d+)?)?)*)\s+(\*(?:\/\d+)?|(?:0?[1-9]|[12]\d|3[01])(?:-(?:0?[1-9]|[12]\d|3[01])(?:\/\d+)?)?(?:,(?:0?[1-9]|[12]\d|3[01])(?:-(?:0?[1-9]|[12]\d|3[01])(?:\/\d+)?)?)*)\s+(\*(?:\/\d+)?|(?:[1-9]|1[012])(?:-(?:[1-9]|1[012])(?:\/\d+)?)?(?:,(?:[1-9]|1[012])(?:-(?:[1-9]|1[012])(?:\/\d+)?)?)*|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\s+(\*(?:\/\d+)?|(?:[0-6])(?:-(?:[0-6])(?:\/\d+)?)?(?:,(?:[0-6])(?:-(?:[0-6])(?:\/\d+)?)?)*|mon|tue|wed|thu|fri|sat|sun)\s+|(@reboot|@yearly|@annually|@monthly|@weekly|@daily|@midnight|@hourly)\s+)([^\s]+)\s+(.*)$/, + bogus: /bogus/i // filler + }; + // aliases + regex.int = regex.integer; + regex.number = regex.numeric; + regex.float = regex.decimal; + regex.hex = regex.hexadecimal; + regex.alphaNumeric = regex.alphaNum; + regex.alphaNumericSafe = regex.alphaNumSafe; + regex.ipAddr = regex.ipAddress = regex.ip; + regex.fullUrl = regex.url; + regex.date = regex.dateISO; + + // auto-generate alternate property names from camelCase names + // creates hyphen-ated and under_score aliases + // clutters up the namespace, but... oh, well + forOwn(regex, function(name){ + regex[name.toLowerCase()] = regex[name]; // lowercase names + regex[toDashed(name)] = regex[name]; // hyphen-ated names + regex[toUnderscore(name)] = regex[name]; // under_score names + }); + + // export combined 'regex' object back to global namespace + XNAT.validation.regex = extend(regex, XNAT.validation.regex || {}); + + + // define custom test methods for more complex validations + var test = {}; + + test.numeric = function(value){ + console.log('numeric'); + return isNumeric(value); + }; + + test.interval = function(value){ + console.log('interval'); + var parts = value.split(/([0-9]+)/); + var units = /\s+(sec|second|min|minute|hour|day|week|month|year)(s)?\s*/; + var num = true; + var valid = true; + var i = 1; // start i at 1 since parts[0] will be an empty string + var part; + while (parts[i] && valid === true) { + part = (parts[i] + ''); + if (num) { + valid = /\d+/.test(part); + } + else { + valid = units.test(part); + } + num = !num; // flip for next iteration + i++; + } + return valid; + }; + + // match to 6-field cron syntax: + // 0 0 * * * * + test.cronFn = function(value){ + + // TODO: replace all this with regex tests + + /* + var WORDS = ('reboot yearly annually monthly ' + + 'weekly daily midnight hourly').split(/\s+/).map(function(word){ + return '@' + word + }); + var MONTHS = ('JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC').split(/\s+/); + var DAYS = ('MON TUE WED THU FRI SAT SUN').split(/\s+/); + + // is it a special cron keyword? + if (WORDS.indexOf(value) > -1) { + return true + } + + // split passed value into separate fields + var FIELDS = (value+'').trim().split(/\s+/); + + // check for 6 fields + if (FIELDS.length < 6) { + return false + } + + var SECONDS = FIELDS[0].split('/'); + var MINUTES = FIELDS[1].split('/'); + var HOURS = FIELDS[2].split('/'); + var DAY = FIELDS[3]; + var MONTH = FIELDS[4]; + var WEEKDAY = FIELDS[5]; + + var errors = 0; + + function isWild(val){ + return /[*?/]/.test((val+'').trim()); + } + + function isRange(val){ + return /[a-z0-9]-[a-z0-9]/i.test(val) + } + + function isError(val, regex){ + var notWild = !isWild(val); + var notMatch = regex ? !regex.test(val) : false; + if (notWild && notMatch) { + errors++ + } + } + + function checkTime(val, limit, regex){ + + val = [].concat(val); + + var notWild = !isWild(val[0]); + + if (notWild){ + if (+val[0] < 0 || +val[0] > limit) { + isError(val, regex) + } + } + + // seconds interval must be a number + if (val[1] && !/[0-9]/.test(val[1])) { + errors++ + } + + } + + // seconds + checkTime(SECONDS, 59); + + // minutes + checkTime(MINUTES, 59); + + // hours + checkTime(HOURS, 23); + + // day + checkTime(DAY, 31); + + // month + var monthRegex = /JAN|FEB|MAR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC/i; + checkTime(MONTH, 12, monthRegex); + + // day of the week + var weekdayRegex = /MON|TUE|WED|THU|FRI|SAT|SUN/i; + checkTime(WEEKDAY, 7, weekdayRegex); + + return (errors === 0); + + */ + + }; + + // check a comma- or space-separated list of multiple email addresses + test.emails = function(value){ + var errors = 0; + value.split(/[,\s]+/).forEach(function(email){ + if (errors) return false; + email = email.trim(); + if (!regex.email.test(email)) { + errors++ + } + }); + return errors === 0; + }; + + // make sure there's a minimum number of characters + test.minLength = function(value, length){ + if (!regex.naturalNoZero.test(length)) { + return false; + } + return (value.length >= parseInt(length, 10)); + }; + + // don't exceed the maximum number of characters + test.maxLength = function(value, length){ + if (!regex.naturalNoZero.test(length)) { + return false; + } + return (value.length <= parseInt(length, 10)); + }; + + test.exactLength = function(value, length){ + if (!regex.naturalNoZero.test(length)) { + return false; + } + return (value.length === parseInt(length, 10)); + }; + test.isLength = test.exactLength; + + // XNAT.validate('#concurrent-sessions').is('greaterThan', 0).check(); + test.greaterThan = function(value, num){ + if (!regex.decimal.test(value)) { + return false; + } + return (parseFloat(value) > parseFloat(num)); + }; + + test.greaterThanOrEqual = function(value, num){ + if (!regex.decimal.test(value)) { + return false; + } + return (parseFloat(value) >= parseFloat(num)); + }; + test.greaterThanOrEqualTo = test.greaterThanOrEqual; + test.gte = test.greaterThanOrEqual; + + // XNAT.validate('#session-timeout').is('lessThan', 999).check(); + test.lessThan = function(value, num){ + if (!regex.decimal.test(value)) { + return false; + } + return (parseFloat(value) < parseFloat(num)); + }; + + test.lessThanOrEqual = function(value, num){ + if (!regex.decimal.test(value)) { + return false; + } + return (parseFloat(value) <= parseFloat(num)); + }; + test.lessThanOrEqualTo = test.lessThanOrEqual; + test.lte = test.lessThanOrEqual; + + test.equalTo = function(value, test){ + return value+'' === test+'' + }; + test.equals = test.equalTo; + + // XNAT.validate('input.credit-card').is('creditCard').check(); + test.creditCard = function(value){ + // Luhn Check Code from https://gist.github.com/4075533 + // accept only digits, dashes or spaces + if (!regex.numericDash.test(value)) return false; + + // The Luhn Algorithm. It's so pretty. + var nCheck = 0, nDigit = 0, bEven = false; + var strippedField = value.replace(/\D/g, ""); + + for (var n = strippedField.length - 1; n >= 0; n--) { + var cDigit = strippedField.charAt(n); + nDigit = parseInt(cDigit, 10); + if (bEven) { + if ((nDigit *= 2) > 9) nDigit -= 9; + } + + nCheck += nDigit; + bEven = !bEven; + } + + return (nCheck % 10) === 0; + }; + + // Check file extension of submitted file. + // + // XNAT.validate('input.doc[type="file"]').is('fileType', 'doc').check() + // + test.fileType = function(value, type){ + //if (type !== 'file') { + // return true; + //} + var ext = value.substr((value.lastIndexOf('.') + 1)).toLowerCase(), + typeArray = type.split(','), + inArray = false, + i = -1, + len = typeArray.length; + + while (++i < len) { + if (ext == typeArray[i].toLowerCase()) { + inArray = true; + } + } + + return inArray; + }; + + // auto-generate alternate property names from camelCase names + // creates hyphen-ated and under_score aliases + forOwn(test, function(name){ + test[name.toLowerCase()] = test[name]; // lowercase names + test[toDashed(name)] = test[name]; // hyphen-ated names + test[toUnderscore(name)] = test[name]; // under_score names + }); + + // export combined 'test' object back to global namespace + XNAT.validation.test = extend(test, XNAT.validation.test || {}); + + + // HELPERS + + function init(element){ + var obj = { + element$: $.spawn('input.tmp|type=hidden'), + len: 0, + regex: '', + value: '', + values: [], // use to check more than one value + validated: null + }; + obj.element = obj.element$[0]; + if (element) { + obj.element$ = $$(element).removeClass('valid invalid'); + obj.element = obj.element$[0]; + obj.value = obj.element.value || ''; + obj.len = obj.element$.length; + } + return obj; + } + + function getValidDate(date){ + + if (!date.match('today') && !date.match(regex[date])) { + return false; + } + + var validDate = new Date(), + validDateArray; + + if (!date.match('today')) { + validDateArray = date.split(/[\s.-]+/); + validDate.setFullYear(validDateArray[0]); + validDate.setMonth(validDateArray[1] - 1); + validDate.setDate(validDateArray[2]); + } + + return validDate; + + } + + + // CONSTRUCTOR + + function Validator(element){ + extend(this, init(element)); + } + + Validator.fn = Validator.prototype; + + Validator.fn.init = function(element){ + if (element){ + extend(this, init(element)); + } + return this; + }; + + // reset element so it can be validated again + Validator.fn.reset = function(element){ + extend(this, init(element||this.element)); + return this; + }; + + // explicitly set a value to check + // XNAT.validate().val('bar@foo.org').is('email').check(); + // -> true + Validator.fn.val = function(value){ + this.value = value; + return this; + }; + + Validator.fn.trim = function(){ + this.trimValue = true; + this.element$.each(function(){ + this.value = this.value.trim(); + }); + return this; + }; + + // set className to valid/invalid + Validator.fn.setClass = function(){ + var className = this.validated ? 'valid': 'invalid'; + this.element$ + .removeClass('valid invalid') + .addClass(className); + }; + + Validator.fn.is = function(type, args){ + + // check all if there's more than + // one element in the selection + if (this.len > 1) { + this.all(type, args); + return this; + } + + // return early if the validation is already false + // (this is necessary for working with chained methods) + if (this.validated === false) { return this } + + if (this.trimValue) { + this.value = (this.value+'').trim(); + } + + // if there's a test['test'] method, use that + if (typeof test[type] == 'function') { + this.validated = test[type](this.value, args); + } + // if there's a regex defined (above) for 'type', use that + else if (regex[type]) { + this.pattern(regex[type]); + // this.validated = regex[type].test(this.value); + } + // if 'type' is a string, do a comparison + else if (typeof type == 'string') { + this.validated = test.equals(this.value, type); + } + // a 'type' function can also be passed + // (must return boolean true or false) + else if (typeof type == 'function') { + this.validated = type.apply(this, [].concat(args)); + } + // otherwise do a regex test + else { + try { + this.pattern(type); + // this.validated = type.test(this.value); + } + catch(e) { + console.log(e); + } + } + + this.setClass(); + + return this; + }; + + Validator.fn.not = function(type){ + this.is(type); + this.validated = !this.validated; + return this; + }; + + // validate all elements in a collection + // XNAT.validate('input.float').all('float'); + Validator.fn.all = function(type, args){ + // var self = this; + var invalid = 0; + this.elements = this.element$.toArray(); + if (!type) return this; + // if type is specified, check each element + this.element$.each(function(){ + var elValidate = new Validator(this); + elValidate.is(type, args); + //valid = regex[type].test(this.value); + if (elValidate.isValid(false)) { + invalid++ + } + }); + this.validated = invalid === 0; + return this; + }; + + Validator.fn.none = function(type){ + // make sure NONE of the elements match the type + }; + + Validator.fn.pattern = function(regex){ + this.regex = regex; + this.validated = regex.test(this.value); + return this; + }; + + // match the value of another element + // optionally trimming leading and trailing whitespace + Validator.fn.matches = function(target, trim){ + var sourceValue = this.value + ''; + var targetValue = $$(target).val() + ''; + if (trim) { + sourceValue = sourceValue.trim(); + targetValue = targetValue.trim(); + } + this.validated = sourceValue === targetValue; + return this; + }; + + // XNAT.validate('#url').required().check(); + Validator.fn.required = function(){ + this.all(function(){ + if (/checkbox|radio/.test(this.element$.type)) { + return (this.checked === true); + } + return this.value+'' !== ''; + }); + return this; + }; + + // set up shortcut methods (uses test[type]() methods) + // example: + // XNAT.validate('#sessions-concurrent-max').lessThan(1000).check(); + [ 'minLength', 'maxLength', 'exactLength', 'isLength', + 'greaterThan', 'greaterThanOrEqual', 'greaterThanOrEqualTo', 'gte', + 'lessThan', 'lessThanOrEqual', 'lessThanOrEqualTo', 'lte', + 'equalTo', 'equals', 'fileType' ].forEach(function(method) { + + Validator.fn[method] = function (test) { + this.is(method, test); + return this; + } + + }); + + // .valid() must be called LAST + // XNAT.validate('#email').trim().is('email').valid(true); + Validator.fn.valid = function(bool){ + bool = (bool === undefined) ? true : bool; + return bool ? this.validated : !this.validated; + }; + Validator.fn.isValid = Validator.fn.valid; + // + // call *either* .valid() -OR- .check() last + // + // XNAT.validate('input.email').is('email').valid(true); + // + // --OR-- + // + // XNAT.validate().value('foo').check(function(){ return this.value === 'foo' }); + // + // .check() must be called last + // XNAT.validate('#email').check('email'); + // + // type can be regex['type'] string, + // function (must return true or false), + // or custom regex + Validator.fn.check = function(type){ + if (type) { + this.is(type); + } + return this.isValid(true); + }; + + // usage: + // XNAT.validate('#user-email').trim().is('email').check(); + + validate = function(element){ + return new Validator(element); + }; + + + // TODO: move the date methods below to {test} object: test['greaterThanDate']() etc... + validate.check = { + + greater_than_date: function(field, date){ + var enteredDate = getValidDate(field.value), + validDate = getValidDate(date); + if (!validDate || !enteredDate) { + return false; + } + return enteredDate > validDate; + }, + + less_than_date: function(field, date){ + var enteredDate = getValidDate(field.value), + validDate = getValidDate(date); + if (!validDate || !enteredDate) { + return false; + } + return enteredDate < validDate; + }, + + greater_than_or_equal_date: function(field, date){ + var enteredDate = getValidDate(field.value), + validDate = getValidDate(date); + if (!validDate || !enteredDate) { + return false; + } + return enteredDate >= validDate; + }, + + less_than_or_equal_date: function(field, date){ + var enteredDate = getValidDate(field.value), + validDate = getValidDate(date); + if (!validDate || !enteredDate) { + return false; + } + return enteredDate <= validDate; + } + }; + + + // this script has loaded + validate.loaded = true; + + return XNAT.validate = validate; + +})); diff --git a/src/main/webapp/scripts/xnat/xhr.js b/src/main/webapp/scripts/xnat/xhr.js index 4fc01234a90d602b3ce37f6286544ad6e5eb3fd2..7418977d0d7b946dbe300caca57d331900e8a97a 100755 --- a/src/main/webapp/scripts/xnat/xhr.js +++ b/src/main/webapp/scripts/xnat/xhr.js @@ -442,36 +442,47 @@ var XNAT = getObject(XNAT||{}), function setValues(form, dataObj){ // cache and check if form exists var $form = $$(form); + if (!$form.length) return; + // find all input and select elements with a name attribute - $form.find('input[name], select[name]').each(function(){ - var val = ''; - if (Array.isArray(dataObj)) { - val = dataObj.join(', '); + $form.find(':input').each(function(){ + + var $this = $(this); + var val = lookupObjectValue(dataObj, this.name||this.title); + + if (Array.isArray(val)) { + val = val.join(', '); + $this.addClass('array-list') } else { - val = stringable(dataObj) ? dataObj+'' : dataObj[this.name] || ''; + val = stringable(val) ? val : JSON.stringify(val); } + changeValue(this, val); + // special handling for checkboxes if (this.type === 'checkbox') { - this.checked = (realValue(this.value) === true) + this.checked = realValue(val) + } + // special handling for radio buttons + if (this.type === 'radio') { + this.checked = isEqual(this.value, val); + if (this.checked) { + $this.trigger('change'); + } } - // // special handling for radio buttons (???) - // if (this.type === 'radio') { - // this.checked = (realValue(this.value) === (dataObj[this.name]||'')) - // } - }); - // set textarea innerText from a 'value' property - $form.find('textarea[name]').each(function(){ - var $textarea = $(this); - var textValue = (function(){ - var val = dataObj[this.name]; - return stringable(val) ? val+'' : safeStringify(val); - })(); - changeValue($textarea, textValue); - // $textarea.val(textValue).change(); }); + // // set textarea innerText from a 'value' property + // $form.find('textarea[name]').each(function(){ + // var $textarea = $(this); + // var textValue = (function(){ + // var val = dataObj[this.name]; + // return stringable(val) ? val+'' : safeStringify(val); + // })(); + // changeValue($textarea, textValue); + // // $textarea.val(textValue).change(); + // }); return $form; } @@ -489,8 +500,8 @@ var XNAT = getObject(XNAT||{}), callback = diddly; opts = cloneObject(opts); - opts.url = XNAT.url.rootUrl(opts.url || $form.attr('action')); - opts.method = opts.method || _form.method || 'GET'; + opts.url = XNAT.url.rootUrl(opts.url || $form.data('url') || $form.attr('action')); + opts.method = opts.method || $form.data('method') || _form.method || 'GET'; if ($.isFunction(opts.validate)) { validation = opts.validate.call(_form, opts); @@ -511,9 +522,12 @@ var XNAT = getObject(XNAT||{}), // don't pass 'callback' property into the AJAX request delete opts.callback; + var inputs = $form.find(':input').not('button, [type="submit"]').toArray(); + if (/POST|PUT/i.test(opts.method)) { if ($form.hasClass('json') || /json/i.test(opts.contentType||'')){ - opts.data = formToJSON($form, true); + // opts.data = formToJSON($form, true); + opts.data = JSON.stringify(form2js(inputs, ':', false)); opts.processData = false; opts.contentType = 'application/json'; } @@ -530,7 +544,7 @@ var XNAT = getObject(XNAT||{}), opts.success = function(data){ callback.apply($form, arguments); // DON'T TRUST RETURNED DATA - //setValues($form, data); + setValues($form, data); }; } @@ -541,9 +555,10 @@ var XNAT = getObject(XNAT||{}), // $('form.foo').submitJSON(); $.fn.submitJSON = function(opts){ - $(this).addClass('json'); + var $form = $(this); + $form.addClass('json'); return xhr.form(this, extend(true, { - method: this.method || 'POST', + method: $form.data('method') || this.method || 'POST', processData: false, contentType: 'application/json' }, opts)) diff --git a/src/main/webapp/xnat-templates/navigations/AppJS.vm b/src/main/webapp/xnat-templates/navigations/AppJS.vm index 3f4cc3dea360f90b3e1fb74927f14bd666f85944..816fcb3f1475c48853d0f858c2e46beeec67e8b7 100644 --- a/src/main/webapp/xnat-templates/navigations/AppJS.vm +++ b/src/main/webapp/xnat-templates/navigations/AppJS.vm @@ -1,7 +1,7 @@ ## Main JavaScript and CSS files for XNAT #set ($SITE_ROOT = $content.getURI('')) -#set ($versionString = "v=1.7.0a2") +#set ($versionString = "v=1.7.0a2") ## only load once #if (!$appJS) @@ -17,6 +17,7 @@ <script src="${SITE_ROOT}/scripts/xmodal-v1/xmodal-migrate.js"></script> <!-- XNAT JLAPI scripts --> + <script src="${SITE_ROOT}/scripts/xnat/validate.js"></script> <script src="${SITE_ROOT}/scripts/xnat/url.js"></script> <script src="${SITE_ROOT}/scripts/xnat/xhr.js"></script> <script src="${SITE_ROOT}/scripts/xnat/event.js"></script>