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