1 /// A CAPTCHA generator for D services.
2 /// Written in the D programming language.
3 
4 module dcaptcha.dcaptcha;
5 
6 import std.algorithm;
7 import std.conv;
8 import std.exception;
9 import std.random;
10 import std.range;
11 import std.string;
12 
13 import ae.utils.array;
14 
15 import dcaptcha.markov;
16 
17 struct CaptchaSpec
18 {
19 	bool allowEasy = true;
20 	bool allowHard = false;
21 	bool allowStatic = false;
22 }
23 
24 struct Challenge
25 {
26 	string question, code;
27 	string[] answers;
28 	string hint; /// HTML!
29 }
30 
31 /**
32 	Goals:
33 	- Answers should not be obvious:
34 	  - Keywords shouldn't give away the answer, e.g. `unittest { ... }`
35 	    is obviously an unit test block.
36 	  - `return 2+2` is obvious to non-programmers.
37 	- Answers should vary considerably:
38 	  - A question which has the answer "0" much of the time is easy
39 	    to defeat simply by giving the answer "0" all of the time.
40 	- Questions should not be Google-able:
41 	  - Search engines ignore most punctuation and math operators.
42 	  - Keywords and string literals should vary or be generic enough.
43 **/
44 
45 Challenge getCaptcha(CaptchaSpec spec = CaptchaSpec.init)
46 {
47 	string[] identifiers =
48 		26
49 		.iota
50 		.map!(l => [cast(char)('a'+l)].assumeUnique())
51 		.filter!(s => s != "l")
52 		.array();
53 	identifiers ~= "foo, bar, baz".split(", ");
54 	//identifiers ~= "qux, quux, corge, grault, garply, waldo, fred, plugh, xyzzy, thud".split(", ");
55 
56 	enum hardFactor = 100;
57 	int lowerFactor = spec.allowEasy ? 1 : hardFactor;
58 	int upperFactor = spec.allowHard ? hardFactor : 1;
59 
60 	assert(spec.allowEasy || spec.allowHard, "No difficulty selected");
61 
62 	int specUniform(int lowerBound, int upperBound) { return uniform(lowerBound*lowerFactor, upperBound*upperFactor); }
63 
64 	string[] mathOperators = "+ - / * %".split();
65 
66 	Challenge challenge;
67 	with (challenge)
68 		[
69 			// Identify syntax
70 			{
71 				if (!spec.allowStatic || !spec.allowHard)
72 					return false;
73 				question = "What is the name of the D language syntax feature illustrated in the following fragment of D code?";
74 				hint = `You can find the answer in the<br><a href="http://dlang.org/spec.html">Language Reference section of dlang.org</a>.`;
75 				//	`You can find the answers in the <a href="http://dlang.org/lex.html">Lexical</a> and `
76 				//	`<a href="http://dlang.org/grammar.html">Grammar</a> language reference sections on dlang.org.`;
77 				return
78 				[
79 					// lambda
80 					{
81 						code =
82 							q{
83 								(A, B) => A @ B
84 							}.formatExample()
85 							.replace("A", identifiers.pluck)
86 							.replace("B", identifiers.pluck)
87 							.replace("@", mathOperators.pluck)
88 						;
89 						answers = cartesianJoin(["lambda", "lambda function", "anonymous function"], ["", " literal"]);
90 						return true;
91 					},
92 					// static destructor
93 					{
94 						string bye = ["Bye", "Goodbye", "Shutting down", "Exiting"].sample ~ ["", ".", "...", "!"].sample;
95 						code =
96 							q{
97 								static ~this()
98 								{
99 									writeln("BYE");
100 								}
101 							}.formatExample()
102 							.replace("BYE", bye)
103 						;
104 						answers = ["static destructor", "module destructor", "thread destructor"];
105 						return true;
106 					},
107 					// nested comments
108 					{
109 						code =
110 							q{
111 								/+ A = B @ C; /+ A = X; +/ +/
112 							}.formatExample()
113 							.replace("A", identifiers.pluck)
114 							.replace("B", identifiers.pluck)
115 							.replace("C", identifiers.pluck)
116 							.replace("X", uniform(10, 100).text)
117 							.replace("@", mathOperators.pluck)
118 						;
119 						answers = cartesianJoin(["nested ", "nesting "], ["", "block "], ["comment", "comments"]);
120 						return true;
121 					},
122 					// anonymous nested classes
123 					{
124 						code =
125 							q{
126 								auto A = new class O {};
127 							}.formatExample()
128 							.replace("A", identifiers.pluck)
129 							.replace("O", identifiers.pluck.toUpper)
130 						;
131 						answers = cartesianJoin(["anonymous "], ["", "nested "], ["class", "classes"]);
132 						return true;
133 					},
134 					// delimited (heredoc) strings
135 					{
136 						auto delimiter = ["EOF", "DELIM", "STR", "QUOT", "MARK"].sample;
137 						code =
138 							q{
139 								string A = TEXT;
140 							}.formatExample()
141 							.replace("F", identifiers.pluck)
142 							.replace("TEXT", `q"` ~ delimiter ~ "\n" ~ MarkovChain!2.query().join(" ").wrap(38).strip() ~ "\n" ~ delimiter ~ `"`)
143 						;
144 						answers = cartesianJoin(["", "multiline ", "multi-line "], ["delimited", "heredoc"], ["", " string", " strings"]);
145 						return true;
146 					},
147 					// hex strings (deprecated)
148 					/+
149 					{
150 						string hex;
151 						do
152 							hex =
153 								uniform(3, 5)
154 								.iota
155 								.map!(i => "xX".sample)
156 								.map!(f =>
157 									[1, 2, 4].sample
158 									.iota
159 									.map!(j =>
160 										format("%02" ~ f, uniform(0, 0x100))
161 									)
162 									.join("")
163 								)
164 								.join(" ");
165 						while (hex.length > 20);
166 						code =
167 							q{
168 								string A = x"CC";
169 							}.formatExample()
170 							.replace("A", identifiers.pluck)
171 							.replace("CC", hex)
172 						;
173 						answers = cartesianJoin(["hex", "hex ", "hexadecimal "], ["string", "strings"], ["", " literal", " literals"]);
174 						return true;
175 					},+/
176 					// associative arrays
177 					{
178 						string[] types = ["int", "string"];
179 						code =
180 							q{
181 								T[U] A;
182 							}.formatExample()
183 							.replace("A", identifiers.pluck)
184 							.replace("T", types.sample)
185 							.replace("U", types.sample)
186 						;
187 						answers = ["AA", "associative array", "hashmap"];
188 						return true;
189 					},
190 					// array slicing
191 					{
192 						code =
193 							q{
194 								A = B[X..Y];
195 							}.formatExample()
196 							.replace("A", identifiers.pluck)
197 							.replace("B", identifiers.pluck)
198 							.replace("X", uniform(0, 5).text)
199 							.replace("Y", uniform(5, 10).text)
200 						;
201 						answers = cartesianJoin(["", "array "], ["slice", "slicing"]);
202 						return true;
203 					},
204 				].randomCover().map!(f => f()).any();
205 			},
206 			// Calculate function result
207 			// (use syntax that only programmers should be familiar with)
208 			{
209 				question = "What will be the return value of the following function?";
210 				hint = `You can run D code online on <a href="https://run.dlang.io/">run.dlang.io</a>.`;
211 				return
212 				[
213 					// Modulo operator (%)
214 					{
215 						int x, y;
216 						do
217 						{
218 							x = specUniform(10, 50);
219 							y = specUniform(x/4, x/2);
220 						}
221 						while (x % y == 0);
222 
223 						code =
224 							q{
225 								int F()
226 								{
227 									int A = X;
228 									A %= Y;
229 									return A;
230 								}
231 							}.formatExample()
232 							.replace("F", identifiers.pluck)
233 							.replace("A", identifiers.pluck)
234 							.replace("X", x.text)
235 							.replace("Y", y.text)
236 						;
237 						answers = [(x % y).text];
238 						return true;
239 					},
240 					// Integer division, increment
241 					{
242 						int y = specUniform(2, 5);
243 						int x = uniform(5, 50*upperFactor / y) * y + specUniform(1, y);
244 						int sign = uniform(0, 2) ? -1 : 1;
245 						code =
246 							q{
247 								int F()
248 								{
249 									int A = X, B = Y;
250 									B@@;
251 									A /= B;
252 									return A;
253 								}
254 							}.formatExample()
255 							.replace("F", identifiers.pluck)
256 							.replace("A", identifiers.pluck)
257 							.replace("B", identifiers.pluck)
258 							.replace("X", x.text)
259 							.replace("Y", (y - sign).text)
260 							.replace("@", sign > 0 ? "+" : "-")
261 						;
262 						answers = [(x / y).text];
263 						return true;
264 					},
265 					// Ternary operator + division/modulo
266 					{
267 						int x = specUniform(10, 50);
268 						int y = specUniform(2, 4);
269 						int a = specUniform(10, 50);
270 						int b = uniform(2*lowerFactor, a/3);
271 						int d = specUniform(5, 10);
272 						int c = uniform(2, 50*upperFactor / d) * d + uniform(1, d);
273 						code =
274 							q{
275 								int F()
276 								{
277 									return X % Y
278 										? A / B
279 										: C % D;
280 								}
281 							}.formatExample()
282 							.replace("F", identifiers.pluck)
283 							.replace("X", x.text)
284 							.replace("Y", y.text)
285 							.replace("A", a.text)
286 							.replace("B", b.text)
287 							.replace("C", c.text)
288 							.replace("D", d.text)
289 						;
290 						answers = [(x % y ? a / b : c % d).text];
291 						return true;
292 					},
293 					// Formatting, hexadecimal numbers
294 					{
295 						int n = specUniform(20, 100);
296 						n &= ~7;
297 						int w = uniform(2, 8);
298 						string id = identifiers.pluck;
299 						code =
300 							q{
301 								string F()
302 								{
303 									return format("A=%0WX", N);
304 								}
305 							}.formatExample()
306 							.replace("F", identifiers.pluck)
307 							.replace("A", id)
308 							.replace("N", n.text)
309 							.replace("W", w.text)
310 						;
311 						answers = [format("%s=%0*X", id, w, n)];
312 						answers ~= answers.map!(s => `"`~s~`"`).array();
313 						return true;
314 					},
315 					// iota+reduce - max
316 					{
317 						int x = specUniform(10, 100);
318 						code =
319 							q{
320 								int F()
321 								{
322 									return iota(X).reduce!max;
323 								}
324 							}.formatExample()
325 							.replace("F", identifiers.pluck)
326 							.replace("X", x.text)
327 						;
328 						answers = [(x - 1).text];
329 						return true;
330 					},
331 					// iota+reduce - sum
332 					{
333 						if (!spec.allowHard) return false;
334 						int x = specUniform(3, 10);
335 						code =
336 							q{
337 								int F()
338 								{
339 									return iota(X).reduce!"a+b";
340 								}
341 							}.formatExample()
342 							.replace("F", identifiers.pluck)
343 							.replace("X", x.text)
344 						;
345 						answers = [(iota(x).reduce!"a+b").text];
346 						return true;
347 					},
348 				].randomCover().map!(f => f()).any();
349 			},
350 		].randomCover().map!(f => f()).any().enforce("Can't find suitable CAPTCHA");
351 	return challenge;
352 }
353 
354 private string[] cartesianJoin(PARTS...)(PARTS parts)
355 {
356 	return cartesianProduct(parts).map!(t => join([t.expand])).array();
357 }
358 
359 private string formatExample(string s)
360 {
361 	return s
362 		.outdent()
363 		.strip()
364 		.replace("\t", "  ")
365 	;
366 }