various fixes and enhancements
[AnyEvent-HTTPD.git] / lib / BS / HTTPD / Appgets.pm
blob73b930d4df98920ef35c581da91af92d223e8ae4
1 package BS::HTTPD::Appgets;
2 use feature ':5.10';
3 use strict;
4 no warnings;
5 use CGI qw/escapeHTML/;
7 require Exporter;
9 our @ISA = qw/Exporter/;
11 our @EXPORT = qw/o link abutton set_request set_httpd js
12 js_ajaxobj_func capture form entry submit/;
14 =head1 NAME
16 BS::HTTPD::Appgets - Some utility functions for web applications
18 =head1 EXPORTS
20 This module mostly exports these functions:
22 =over 4
24 =cut
26 our $REQ;
28 =item B<set_request ($ref)>
30 This function sets the current request the output is appended
31 to for the response.
33 Use it eg. like this:
35 $httpd->reg_cb (
36 _ => sub {
37 my ($httpd, $req) = @_;
38 set_request ($req);
40 o "<html><body><h1>test</h1></body></html>";
42 $req->respond;
46 =cut
48 sub set_request { $REQ = $_[0] }
50 =item B<capture ($block)>
52 C<capture> temporarily redirects the output done in C<$block> and returns it.
54 This function should be called with a block as argument like this:
56 my $r = capture {
57 o ("<html><body>Hi</body></html>")
60 The return value will be simply the concationated output as it would be sent to the
61 callback or appended to the reference given to C<set_output>.
63 =cut
65 sub capture(&@) {
66 my ($blk) = @_;
67 my $old = $REQ;
68 my $out;
69 $REQ = \$out;
70 $blk->();
71 $REQ = $old;
72 return $out;
75 our $curform;
77 =item B<o (@strs)>
79 This function will append all arguments it gets and
80 append that to the current output context, which is either
81 set by the C<capture> function or C<set_request>.
83 If it is called outside a C<capture> function it will just forward
84 everything to the C<o> method of C<set_request>.
86 =cut
88 sub o {
89 if (ref $REQ ne 'SCALAR') {
90 $REQ->o (join '', @_);
91 } else {
92 $$REQ .= join '', @_;
96 =item B<form ($block, $callback)>
98 This function will generate a html form for you, which you can fill
99 with your own input elements. The C<$callback> will be called when the next
100 request is handled and if the form was submitted. It will be executed before any
101 of your content callbacks are run.
102 The C<form> function has a special prototype which allows this syntax:
104 my $new_element;
105 form {
106 entry (\$new_element);
107 o '<input type="submit" value="append"/>'
108 } sub {
109 push @list, $new_element;
112 This function is just a convenience wrapper around the C<form> method
113 of the L<BS::HTTPD> object.
115 =cut
117 sub form(&;@) {
118 my ($blk, $formcb) = @_;
119 $curform = { next_field_idx => 1 };
120 my $f = capture { $blk->() };
121 my $thisform = $curform;
122 $curform = undef;
123 my $set_refs = sub {
124 my ($req) = @_;
126 for (keys %{$thisform->{flds}}) {
127 ${$thisform->{flds}->{$_}} = $req->parm ("field$_");
130 $formcb->($req);
132 o ($REQ->form ($f, $set_refs));
135 =item B<entry ($ref)>
137 This function will output a text input form field via the C<o> function
138 which will set the scalar reference to the value of the text field
139 when the form is submitted.
141 See also the C<form> function above for an example.
143 =cut
145 sub entry {
146 my ($ref) = @_;
147 my $idx = $curform->{next_field_idx}++;
148 $curform->{flds}->{$idx} = $ref;
149 o "<input type=\"text\" name=\"field$idx\" value=\"".escapeHTML ($$ref)."\" />";
152 =item B<submit ($label)>
154 This function will output a submit button with the label C<$label>.
156 =cut
158 sub submit {
159 my ($lbl) = @_;
160 o "<input type=\"submit\" value=\"".escapeHTML ($lbl)."\" />";
163 =item B<js (@strs)>
165 This function will output the C<@strs> appended enclosed in a HTML
166 script tag for javascript.
168 See also the C<o> function.
170 =cut
172 sub js {
173 o ("<script type=\"text/javascript\">\n");
174 o (@_);
175 o ("</script>\n");
178 =item B<js_ajaxobj_func ($funcname)>
180 This function will output javascript compatibility cruft code
181 to get a XMLHttpRequest object. The javascript function C<$funcname>
182 will be declared and can be called in javascript code with the
183 content callback as first argument:
185 js_ajaxobj_func 'newxhreq';
187 js (<<'JS');
188 function response_cb (xh, textcontent) {
192 var xh = newxhreq (response_cb);
193 xh.open ("GET", "/", true)
194 xh.send (null);
197 The first argument of the C<response_cb> is the XMLHttpRequest object
198 and the second the responseText of the finished request.
200 =cut
202 sub js_ajaxobj_func {
203 my ($funcname) = @_;
204 js (<<AJAXFUNC);
205 function $funcname (content_cb) {
206 var xh;
208 if( !window.XMLHttpRequest ) XMLHttpRequest = function()
210 try{ return new ActiveXObject("Msxml2.XMLHTTP.6.0") }catch(e){}
211 try{ return new ActiveXObject("Msxml2.XMLHTTP.3.0") }catch(e){}
212 try{ return new ActiveXObject("Msxml2.XMLHTTP") }catch(e){}
213 try{ return new ActiveXObject("Microsoft.XMLHTTP") }catch(e){}
214 throw new Error("Could not find an XMLHttpRequest alternative.")
217 xh = new XMLHttpRequest ();
219 xh.onreadystatechange = function () {
220 if (xh.readyState == 4 && xh.status == 200) {
221 content_cb (xh, xh.responseText);
224 return xh;
226 AJAXFUNC
229 =item B<link ($label, $callback, $newurl)>
231 This does exactly the same as the C<link> method of L<BS::HTTPD::Request>
232 just uses the current request as object and prints out the link via the C<o>
233 function.
235 =cut
237 sub link {
238 o ($REQ->link (@_))
241 =back
243 =head1 VARIABLES
245 =over 4
247 =item B<$BS::HTTPD::Appgets::JSON_JS>
249 This variable contains the javascript source of the JSON serializer
250 and deserializer described in L<http://www.JSON.org/js.html>.
252 You can use this in your application by for example output it via the C<js> function
253 like this:
255 js ($BS::HTTPD::Appgets::JSON_JS);
257 =back
259 =cut
261 our $JSON_JS = <<'JSON_JS_CODE';
263 json2.js
264 2008-02-14
266 Public Domain
268 No warranty expressed or implied. Use at your own risk.
270 See http://www.JSON.org/js.html
272 This file creates a global JSON object containing two methods:
274 JSON.stringify(value, whitelist)
275 value any JavaScript value, usually an object or array.
277 whitelist an optional array parameter that determines how object
278 values are stringified.
280 This method produces a JSON text from a JavaScript value.
281 There are three possible ways to stringify an object, depending
282 on the optional whitelist parameter.
284 If an object has a toJSON method, then the toJSON() method will be
285 called. The value returned from the toJSON method will be
286 stringified.
288 Otherwise, if the optional whitelist parameter is an array, then
289 the elements of the array will be used to select members of the
290 object for stringification.
292 Otherwise, if there is no whitelist parameter, then all of the
293 members of the object will be stringified.
295 Values that do not have JSON representaions, such as undefined or
296 functions, will not be serialized. Such values in objects will be
297 dropped; in arrays will be replaced with null.
298 JSON.stringify(undefined) returns undefined. Dates will be
299 stringified as quoted ISO dates.
301 Example:
303 var text = JSON.stringify(['e', {pluribus: 'unum'}]);
304 // text is '["e",{"pluribus":"unum"}]'
306 JSON.parse(text, filter)
307 This method parses a JSON text to produce an object or
308 array. It can throw a SyntaxError exception.
310 The optional filter parameter is a function that can filter and
311 transform the results. It receives each of the keys and values, and
312 its return value is used instead of the original value. If it
313 returns what it received, then structure is not modified. If it
314 returns undefined then the member is deleted.
316 Example:
318 // Parse the text. If a key contains the string 'date' then
319 // convert the value to a date.
321 myData = JSON.parse(text, function (key, value) {
322 return key.indexOf('date') >= 0 ? new Date(value) : value;
325 This is a reference implementation. You are free to copy, modify, or
326 redistribute.
328 Use your own copy. It is extremely unwise to load third party
329 code into your pages.
332 /*jslint evil: true */
334 /*global JSON */
336 /*members "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
337 charCodeAt, floor, getUTCDate, getUTCFullYear, getUTCHours,
338 getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, length,
339 parse, propertyIsEnumerable, prototype, push, replace, stringify, test,
340 toJSON, toString
343 if (!this.JSON) {
345 JSON = function () {
347 function f(n) { // Format integers to have at least two digits.
348 return n < 10 ? '0' + n : n;
351 Date.prototype.toJSON = function () {
353 // Eventually, this method will be based on the date.toISOString method.
355 return this.getUTCFullYear() + '-' +
356 f(this.getUTCMonth() + 1) + '-' +
357 f(this.getUTCDate()) + 'T' +
358 f(this.getUTCHours()) + ':' +
359 f(this.getUTCMinutes()) + ':' +
360 f(this.getUTCSeconds()) + 'Z';
364 var m = { // table of character substitutions
365 '\b': '\\b',
366 '\t': '\\t',
367 '\n': '\\n',
368 '\f': '\\f',
369 '\r': '\\r',
370 '"' : '\\"',
371 '\\': '\\\\'
374 function stringify(value, whitelist) {
375 var a, // The array holding the partial texts.
376 i, // The loop counter.
377 k, // The member key.
378 l, // Length.
379 r = /["\\\x00-\x1f\x7f-\x9f]/g,
380 v; // The member value.
382 switch (typeof value) {
383 case 'string':
385 // If the string contains no control characters, no quote characters, and no
386 // backslash characters, then we can safely slap some quotes around it.
387 // Otherwise we must also replace the offending characters with safe sequences.
389 return r.test(value) ?
390 '"' + value.replace(r, function (a) {
391 var c = m[a];
392 if (c) {
393 return c;
395 c = a.charCodeAt();
396 return '\\u00' + Math.floor(c / 16).toString(16) +
397 (c % 16).toString(16);
398 }) + '"' :
399 '"' + value + '"';
401 case 'number':
403 // JSON numbers must be finite. Encode non-finite numbers as null.
405 return isFinite(value) ? String(value) : 'null';
407 case 'boolean':
408 case 'null':
409 return String(value);
411 case 'object':
413 // Due to a specification blunder in ECMAScript,
414 // typeof null is 'object', so watch out for that case.
416 if (!value) {
417 return 'null';
420 // If the object has a toJSON method, call it, and stringify the result.
422 if (typeof value.toJSON === 'function') {
423 return stringify(value.toJSON());
425 a = [];
426 if (typeof value.length === 'number' &&
427 !(value.propertyIsEnumerable('length'))) {
429 // The object is an array. Stringify every element. Use null as a placeholder
430 // for non-JSON values.
432 l = value.length;
433 for (i = 0; i < l; i += 1) {
434 a.push(stringify(value[i], whitelist) || 'null');
437 // Join all of the elements together and wrap them in brackets.
439 return '[' + a.join(',') + ']';
441 if (whitelist) {
443 // If a whitelist (array of keys) is provided, use it to select the components
444 // of the object.
446 l = whitelist.length;
447 for (i = 0; i < l; i += 1) {
448 k = whitelist[i];
449 if (typeof k === 'string') {
450 v = stringify(value[k], whitelist);
451 if (v) {
452 a.push(stringify(k) + ':' + v);
456 } else {
458 // Otherwise, iterate through all of the keys in the object.
460 for (k in value) {
461 if (typeof k === 'string') {
462 v = stringify(value[k], whitelist);
463 if (v) {
464 a.push(stringify(k) + ':' + v);
470 // Join all of the member texts together and wrap them in braces.
472 return '{' + a.join(',') + '}';
476 return {
477 stringify: stringify,
478 parse: function (text, filter) {
479 var j;
481 function walk(k, v) {
482 var i, n;
483 if (v && typeof v === 'object') {
484 for (i in v) {
485 if (Object.prototype.hasOwnProperty.apply(v, [i])) {
486 n = walk(i, v[i]);
487 if (n !== undefined) {
488 v[i] = n;
489 } else {
490 delete v[i];
495 return filter(k, v);
499 // Parsing happens in three stages. In the first stage, we run the text against
500 // regular expressions that look for non-JSON patterns. We are especially
501 // concerned with '()' and 'new' because they can cause invocation, and '='
502 // because it can cause mutation. But just to be safe, we want to reject all
503 // unexpected forms.
505 // We split the first stage into 4 regexp operations in order to work around
506 // crippling inefficiencies in IE's and Safari's regexp engines. First we
507 // replace all backslash pairs with '@' (a non-JSON character). Second, we
508 // replace all simple value tokens with ']' characters. Third, we delete all
509 // open brackets that follow a colon or comma or that begin the text. Finally,
510 // we look to see that the remaining characters are only whitespace or ']' or
511 // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
513 if (/^[\],:{}\s]*$/.test(text.replace(/\\./g, '@').
514 replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
515 replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
517 // In the second stage we use the eval function to compile the text into a
518 // JavaScript structure. The '{' operator is subject to a syntactic ambiguity
519 // in JavaScript: it can begin a block or an object literal. We wrap the text
520 // in parens to eliminate the ambiguity.
522 j = eval('(' + text + ')');
524 // In the optional third stage, we recursively walk the new structure, passing
525 // each name/value pair to a filter function for possible transformation.
527 return typeof filter === 'function' ? walk('', j) : j;
530 // If the text is not JSON parseable, then a SyntaxError is thrown.
532 throw new SyntaxError('parseJSON');
535 }();
537 JSON_JS_CODE