| 1 | try { var _ = require("underscore"); } catch (err) {} //for server |
| 2 | |
| 3 | let Validator = { }; |
| 4 | |
| 5 | // Cell in evaluation.questions array |
| 6 | Validator.Question = { |
| 7 | "index": "section", //"2.2.1", "3.2", "1" ...etc |
| 8 | "wording": "string", |
| 9 | "options": "stringArray", //only for quiz |
| 10 | "fixed": "boolean", |
| 11 | "answer": "string", //both this and next are mutually exclusive |
| 12 | "choice": "integerArray", |
| 13 | "active": "boolean", |
| 14 | "points": "number", |
| 15 | }; |
| 16 | |
| 17 | Validator.Input = { |
| 18 | "index": "section", |
| 19 | "input": "stringOrIntegerArray", |
| 20 | }; |
| 21 | |
| 22 | // One student response to an exam |
| 23 | Validator.Paper = { |
| 24 | "number": "code", |
| 25 | // (array of) strings for open questions, arrays of integers for quizzes: |
| 26 | "inputs": Validator.Input, |
| 27 | "startTime": "positiveInteger", |
| 28 | "endTime": "positiveInteger", |
| 29 | "discoTime": "positiveInteger", |
| 30 | "discoCount": "positiveInteger", |
| 31 | "totalDisco": "positiveInteger", |
| 32 | "password": "password", |
| 33 | }; |
| 34 | |
| 35 | Validator.Evaluation = { |
| 36 | "_id": "bson", |
| 37 | "cid": "bson", |
| 38 | "name": "code", |
| 39 | "mode": "alphanumeric", //"open" or "exam", but alphanumeric is good enough |
| 40 | "active": "boolean", |
| 41 | "fixed": "boolean", |
| 42 | "display": "alphanumeric", //"one" or "all" |
| 43 | "time": "integer", |
| 44 | "introduction": "string", |
| 45 | "coefficient": "number", |
| 46 | "questions": Validator.Question, |
| 47 | "papers": Validator.Paper, |
| 48 | }; |
| 49 | |
| 50 | Validator.User = { |
| 51 | "_id": "bson", |
| 52 | "email": "email", |
| 53 | "name": "name", |
| 54 | "initials": "unchecked", //not a user input |
| 55 | "loginToken": "unchecked", |
| 56 | "sessionTokens": "unchecked", |
| 57 | "token": "alphanumeric", //exception: for the purpose of user registration |
| 58 | }; |
| 59 | |
| 60 | Validator.Student = { |
| 61 | "number": "code", |
| 62 | "name": "name", |
| 63 | "group": "positiveInteger", |
| 64 | }; |
| 65 | |
| 66 | Validator.Course = { |
| 67 | "_id": "bson", |
| 68 | "uid": "bson", |
| 69 | "code": "code", |
| 70 | "description": "string", |
| 71 | "password": "hash", |
| 72 | "students": Validator.Student, |
| 73 | }; |
| 74 | |
| 75 | Object.assign(Validator, |
| 76 | { |
| 77 | // Recurse into sub-documents |
| 78 | checkObject_aux: function(obj, model) |
| 79 | { |
| 80 | for (let key of Object.keys(obj)) |
| 81 | { |
| 82 | if (!model[key]) |
| 83 | return "Unknown field"; |
| 84 | if (model[key] == "unchecked") //not a user input (ignored) |
| 85 | continue; |
| 86 | if (_.isObject(model[key])) |
| 87 | { |
| 88 | // TODO: next loop seems too heavy... (only a concern if big class import?) |
| 89 | for (let item of obj[key]) |
| 90 | { |
| 91 | let error = Validator.checkObject_aux(item, model[key]); |
| 92 | if (error.length > 0) |
| 93 | return error; |
| 94 | } |
| 95 | } |
| 96 | else |
| 97 | { |
| 98 | let error = Validator[ "check_" + model[key] ](obj[key]); |
| 99 | if (error.length > 0) |
| 100 | return key + ": " + error; |
| 101 | } |
| 102 | } |
| 103 | return ""; |
| 104 | }, |
| 105 | |
| 106 | // Always check top-level object |
| 107 | checkObject: function(obj, name) |
| 108 | { |
| 109 | return Validator.checkObject_aux(obj, Validator[name]); |
| 110 | }, |
| 111 | |
| 112 | "check_string": function(arg) |
| 113 | { |
| 114 | return ""; //strings are unchecked, but sanitized |
| 115 | }, |
| 116 | |
| 117 | "check_section": function(arg) |
| 118 | { |
| 119 | if (!_.isString(arg)) |
| 120 | return "not a string"; |
| 121 | if (!/^[0-9.]+$/.test(arg)) |
| 122 | return "digits and dot only"; |
| 123 | return ""; |
| 124 | }, |
| 125 | |
| 126 | "check_stringArray": function(arg) |
| 127 | { |
| 128 | return !_.isArray(arg) ? "not an array" : ""; |
| 129 | }, |
| 130 | |
| 131 | "check_alphanumeric": function(arg) |
| 132 | { |
| 133 | return arg.match(/^[\w]{1,32}$/) === null ? "[1,32] alphanumerics" : ""; |
| 134 | }, |
| 135 | |
| 136 | "check_bson": function(arg) |
| 137 | { |
| 138 | return arg.match(/^[a-z0-9]{24}$/) === null ? "not a BSON id" : ""; |
| 139 | }, |
| 140 | |
| 141 | "check_name": function(arg) |
| 142 | { |
| 143 | if (!_.isString(arg)) |
| 144 | return "not a string"; |
| 145 | if (!/^[a-zA-Z\u00C0-\u024F -]{1,32}$/.test(arg)) |
| 146 | return "[1,32] letters + hyphen/space"; |
| 147 | return ""; |
| 148 | }, |
| 149 | |
| 150 | "check_email": function(arg) |
| 151 | { |
| 152 | if (!_.isString(arg)) |
| 153 | return "not a string"; |
| 154 | if (arg.length > 64) |
| 155 | return "string too long: max. 64 characters"; |
| 156 | // Regexp used in "type='email'" inputs ( http://emailregex.com/ ) |
| 157 | if (!/^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/.test(arg)) |
| 158 | return "X@Y, alphanumerics and . _ - +(X)"; |
| 159 | return ""; |
| 160 | }, |
| 161 | |
| 162 | "check_code": function(arg) |
| 163 | { |
| 164 | if (!_.isString(arg)) |
| 165 | return "not a string"; |
| 166 | if (!/^[\w.-]{1,16}$/.test(arg)) |
| 167 | return "[1,16] alphanumerics and . _ -"; |
| 168 | return ""; |
| 169 | }, |
| 170 | |
| 171 | "check_number": function(arg) |
| 172 | { |
| 173 | if (!_.isNumber(arg)) |
| 174 | arg = parseFloat(arg); |
| 175 | if (isNaN(arg)) |
| 176 | return "not a number"; |
| 177 | return ""; |
| 178 | }, |
| 179 | |
| 180 | "check_integer": function(arg) |
| 181 | { |
| 182 | if (!_.isNumber(arg)) |
| 183 | arg = parseInt(arg); |
| 184 | if (isNaN(arg) || arg % 1 != 0) |
| 185 | return "not an integer"; |
| 186 | return ""; |
| 187 | }, |
| 188 | |
| 189 | "check_positiveInteger": function(arg) |
| 190 | { |
| 191 | return Validator["check_integer"](arg) || (arg<0 ? "not positive" : ""); |
| 192 | }, |
| 193 | |
| 194 | "check_boolean": function(arg) |
| 195 | { |
| 196 | if (!_.isBoolean(arg)) |
| 197 | return "not a boolean"; |
| 198 | return ""; |
| 199 | }, |
| 200 | |
| 201 | "check_password": function(arg) |
| 202 | { |
| 203 | if (!_.isString(arg)) |
| 204 | return "not a string"; |
| 205 | if (!/^[\x21-\x7E]{1,16}$/.test(arg)) |
| 206 | return "[1,16] ASCII characters with code in [33,126]"; |
| 207 | return ""; |
| 208 | }, |
| 209 | |
| 210 | // Sha-1 hash: length 40, hexadecimal |
| 211 | "check_hash": function(arg) |
| 212 | { |
| 213 | if (!_.isString(arg)) |
| 214 | return "not a string"; |
| 215 | if (!/^[a-f0-9]{40}$/.test(arg)) |
| 216 | return "not a sha-1 hash"; |
| 217 | return ""; |
| 218 | }, |
| 219 | |
| 220 | "check_integerArray": function(arg) |
| 221 | { |
| 222 | if (!_.isArray(arg)) |
| 223 | return "not an array"; |
| 224 | for (let i=0; i<arg.length; i++) |
| 225 | { |
| 226 | let error = Validator["check_integer"](arg[i]); |
| 227 | if (error.length > 0) |
| 228 | return error; |
| 229 | } |
| 230 | return ""; |
| 231 | }, |
| 232 | |
| 233 | "check_stringOrIntegerArray": function(arg) |
| 234 | { |
| 235 | if (!_.isString(arg)) |
| 236 | return Validator["check_integerArray"](arg); |
| 237 | return ""; |
| 238 | }, |
| 239 | }); |
| 240 | |
| 241 | try { module.exports = Validator.checkObject; } catch (err) {} //for server |