PageRenderTime 44ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 0ms

/src/static/js/corsclient.js

https://gitlab.com/Blueprint-Marketing/test-cors.org
JavaScript | 659 lines | 428 code | 116 blank | 115 comment | 63 complexity | 3f5820c331de2f65ee77438e1473050c MD5 | raw file
  1. (function(window, undefined) {
  2. /**
  3. * The url to send the request to
  4. * (if using "local" mode).
  5. */
  6. var SERVER_URL = '$SERVER/server';
  7. /**
  8. * The prefix to identify server fields.
  9. */
  10. var SERVER_PREFIX_ = 'server_';
  11. /**
  12. * Helper function to html escape a string.
  13. */
  14. var htmlEscape = function(str) {
  15. return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  16. };
  17. /**
  18. * Logs status messages to the results log section of the page.
  19. * @constructor
  20. */
  21. var Logger = function(opt_id) {
  22. this.elem_ = $('#' + (opt_id || 'tabresultlog'));
  23. };
  24. /**
  25. * Log a status message to the results log.
  26. * Does not HTML escape the message.
  27. */
  28. Logger.prototype.log = function(msg) {
  29. msg = msg + '<br>';
  30. if (this.inCode_) {
  31. this.buffer_.push(msg);
  32. } else {
  33. this.elem_.append(msg);
  34. }
  35. };
  36. Logger.prototype.startCode = function() {
  37. this.buffer_ = [];
  38. this.inCode_ = true;
  39. };
  40. Logger.prototype.endCode = function() {
  41. var msg = this.buffer_.join('');
  42. msg = '<pre>' + msg + '</pre>';
  43. this.buffer_ = null;
  44. this.inCode_ = false;
  45. this.log(msg);
  46. };
  47. Logger.prototype.reset = function() {
  48. this.elem_.empty();
  49. };
  50. Logger.prototype.logEvent = function(msg, opt_color) {
  51. var color = opt_color || 'yellow';
  52. this.log(
  53. 'Fired XHR event: <span class="log-' + color + '">' + msg + '</span>');
  54. }
  55. Logger.prototype.logXhr = function(xhr) {
  56. this.log('<br>XHR status: ' + xhr.status);
  57. if ('statusText' in xhr) {
  58. // Firefox doesn't allow access to statusText when there's an error.
  59. this.log('XHR status text: ' + htmlEscape(xhr.statusText));
  60. }
  61. if ('getAllResponseHeaders' in xhr) {
  62. var headers = xhr.getAllResponseHeaders();
  63. if (headers) {
  64. this.log('XHR exposed response headers: ' +
  65. '<pre class="headers">' + htmlEscape(headers) + '</pre>');
  66. }
  67. }
  68. var text = xhr.responseText;
  69. if (text) {
  70. try {
  71. // Log the details of the body.
  72. // If this is a request to the local test server, the response body will
  73. // be a JSON object containing the request and response HTTP details.
  74. var response = JSON.parse(text);
  75. this.log('');
  76. for (var i = 0; i < response.length; i++) {
  77. var r = response[i];
  78. if (r['requestType'] == 'preflight') {
  79. this.logPreflight(r);
  80. } else {
  81. this.logCors(r);
  82. }
  83. }
  84. } catch (e) {
  85. // Response was not JSON
  86. // Don't log the body.
  87. }
  88. }
  89. }
  90. Logger.prototype.logHttp = function(label, r) {
  91. this.log(htmlEscape(label));
  92. this.startCode();
  93. var msg = '';
  94. if (r['httpMethod']) {
  95. msg += htmlEscape(r['httpMethod']) + ' ';
  96. }
  97. if (r['url']) {
  98. msg += htmlEscape(r['url']);
  99. }
  100. if (msg.length > 0) {
  101. this.log(msg);
  102. }
  103. var headers = r['headers'];
  104. if (headers) {
  105. for (var name in headers) {
  106. if (!headers.hasOwnProperty(name)) {
  107. continue;
  108. }
  109. this.log(htmlEscape(name) + ': ' + htmlEscape(headers[name]));
  110. }
  111. }
  112. this.endCode();
  113. this.log('');
  114. }
  115. Logger.prototype.logPreflight = function(r) {
  116. this.logHttp('Preflight Request', r['request']);
  117. this.logHttp('Preflight Response', r['response']);
  118. }
  119. Logger.prototype.logCors = function(r) {
  120. this.logHttp('CORS Request', r['request']);
  121. this.logHttp('CORS Response', r['response']);
  122. }
  123. var logger = new Logger();
  124. /**
  125. * Like a logger, but for the code section.
  126. */
  127. var Codder = function() {
  128. };
  129. Codder.prototype.setUrl = function(url) {
  130. $('#code_url').text(url);
  131. };
  132. Codder.prototype.setMethod = function(method) {
  133. $('#code_method').text(method);
  134. };
  135. Codder.prototype.addExtra = function(code) {
  136. if ($('#code_extras').children().length === 0) {
  137. code = '\r\n' + code;
  138. }
  139. $('#code_extras').append(code);
  140. };
  141. Codder.prototype.setCredentials = function() {
  142. this.addExtra('xhr.withCredentials = true;');
  143. };
  144. Codder.prototype.addHeader = function(key, value) {
  145. this.addExtra('xhr.setRequestHeader(\'' + htmlEscape(key) + '\', \'' + htmlEscape(value) + '\');');
  146. };
  147. /**
  148. * Reset the code section by clearing out the variables.
  149. */
  150. Codder.prototype.reset = function() {
  151. $('#code_url').empty();
  152. $('#code_method').empty();
  153. $('#code_extras').empty();
  154. };
  155. var codder = new Codder();
  156. /**
  157. * Helper functions to parse key/value pairs in a query string.
  158. */
  159. var Query = {};
  160. /**
  161. * Parses the values of the query string into an object.
  162. * e.g. a=1&b=2 => {a: '1', b: '2'}
  163. */
  164. Query.parse = function(query) {
  165. var queryObj = {};
  166. if (!query) {
  167. return queryObj;
  168. }
  169. pairs = query.split('&');
  170. for (var i = 0; i < pairs.length; i++) {
  171. var pair = pairs[i];
  172. parts = pair.split('=');
  173. if (parts.length != 2) {
  174. continue;
  175. }
  176. var key = decodeURIComponent(parts[0]);
  177. var val = decodeURIComponent(parts[1]) || null;
  178. queryObj[key] = val;
  179. }
  180. return queryObj;
  181. };
  182. /**
  183. * Serializes an object to a query string.
  184. * e.g. {a: '1', b: '2'} => a=1&b=2
  185. */
  186. Query.serialize = function(queryObj) {
  187. var queryArray = [];
  188. for (var key in queryObj) {
  189. if (!queryObj.hasOwnProperty(key)) {
  190. continue;
  191. }
  192. var val = queryObj[key];
  193. if (val === null || val === undefined || val === '') {
  194. // Skip the value if it does not exist.
  195. // Note that boolean 'false' is considered significant and will be
  196. // preserved.
  197. continue;
  198. }
  199. if (queryArray.length > 0) {
  200. queryArray.push('&');
  201. }
  202. queryArray.push(encodeURIComponent(key));
  203. queryArray.push('=');
  204. queryArray.push(encodeURIComponent(val));
  205. }
  206. return queryArray.join('');
  207. };
  208. /**
  209. * Reads/Writes data values from the url.
  210. */
  211. var Url = function() {
  212. this.query_ = {};
  213. };
  214. Url.PREFIX_ = '#?';
  215. /**
  216. * Read the data from the url hash.
  217. */
  218. Url.prototype.read = function(opt_hash) {
  219. this.query_ = {};
  220. var hash = opt_hash || window.location.hash;
  221. if (!hash) {
  222. return;
  223. }
  224. if (hash.indexOf(Url.PREFIX_) === 0) {
  225. hash = hash.substring(2);
  226. }
  227. if (!hash) {
  228. return;
  229. }
  230. this.query_ = Query.parse(hash);
  231. };
  232. /**
  233. * Write the data back to the url.
  234. */
  235. Url.prototype.write = function() {
  236. var hash = Query.serialize(this.query_);
  237. // TODO: Use history API here.
  238. window.location.hash = Url.PREFIX_ + hash;
  239. };
  240. Url.prototype.get = function(key) {
  241. return this.query_[key];
  242. };
  243. Url.prototype.set = function(key, val) {
  244. this.query_[key] = val;
  245. };
  246. Url.prototype.each = function(func) {
  247. for (var key in this.query_) {
  248. if (!this.query_.hasOwnProperty(key)) {
  249. continue;
  250. }
  251. var val = this.query_[key];
  252. func.call(window, key, val);
  253. }
  254. };
  255. /**
  256. * Represents a single field on the page.
  257. * A field contains data from the UI (like a text field or a checkbox) that is
  258. * used by the app and is preserved in the url.
  259. */
  260. var Field = function(id, url) {
  261. this.id_ = id;
  262. this.elem_ = $('#' + id);
  263. this.url_ = url;
  264. this.val_ = null;
  265. };
  266. Field.prototype.getId = function() {
  267. return this.id_;
  268. }
  269. Field.prototype.get = function() {
  270. return this.val_;
  271. }
  272. Field.prototype.set = function(val) {
  273. this.val_ = val;
  274. };
  275. Field.prototype.fromUrl = function() {
  276. this.val_ = this.url_.get(this.id_);
  277. };
  278. Field.prototype.toUrl = function() {
  279. this.url_.set(this.id_, this.val_);
  280. };
  281. /**
  282. * A form text box.
  283. */
  284. var TextField = function() {
  285. this.base_ = Field;
  286. this.base_.apply(this, arguments);
  287. };
  288. TextField.prototype = new Field;
  289. TextField.prototype.fromUi = function() {
  290. this.val_ = this.elem_.val();
  291. };
  292. TextField.prototype.toUi = function() {
  293. if (this.val_) {
  294. // Only set the value if it exists.
  295. // This preserves any default value in the field.
  296. this.elem_.val(this.val_);
  297. }
  298. };
  299. /**
  300. * A form checkbox field.
  301. */
  302. var CheckboxField = function() {
  303. this.base_ = Field;
  304. this.base_.apply(this, arguments);
  305. };
  306. CheckboxField.prototype = new Field;
  307. CheckboxField.prototype.fromUrl = function() {
  308. var val = this.url_.get(this.id_);
  309. if (val !== null && typeof val !== 'undefined') {
  310. // Only set the value if it exists in the query string
  311. // Otherwise the default value from the HTML is used.
  312. val = (val === 'true');
  313. }
  314. this.val_ = val;
  315. };
  316. CheckboxField.prototype.fromUi = function() {
  317. this.val_ = this.elem_.is(':checked');
  318. };
  319. CheckboxField.prototype.toUi = function() {
  320. if (this.val_ !== null) {
  321. this.elem_.prop('checked', this.val_);
  322. }
  323. };
  324. /**
  325. * A Bootstrap tab bar.
  326. */
  327. var TabField = function() {
  328. this.base_ = Field;
  329. this.base_.apply(this, arguments);
  330. };
  331. TabField.prototype = new Field;
  332. TabField.prototype.fromUi = function() {
  333. // The value is the id of the alink inside the selected item's li.
  334. this.val_ = this.elem_.children().filter('.active').children().attr('id');
  335. };
  336. TabField.prototype.toUi = function() {
  337. // Default value of this field is 'remote'.
  338. this.val_ = this.val_ || 'remote';
  339. $('#' + this.val_).tab('show');
  340. };
  341. /**
  342. * Manages all the fields on this page.
  343. */
  344. var FieldsController = function() {
  345. this.items_ = {};
  346. };
  347. FieldsController.prototype.add = function(field) {
  348. this.items_[field.getId()] = field;
  349. }
  350. FieldsController.prototype.each = function(func) {
  351. $.each(this.items_, func);
  352. };
  353. FieldsController.prototype.getValue = function(id) {
  354. return this.items_[id].get();
  355. };
  356. /**
  357. * Retrieve a unique ID to bust the preflight cache
  358. * (Or used the fixed 'preflightcache' value if we want to honor the preflight
  359. * cache).
  360. */
  361. var getId = function(controller) {
  362. // If maxAge has a value, it means we want the preflight response to be
  363. // cached. However, preflights are cached by request url. In order for
  364. // request url to be an exact match, we set it to a fixed id.
  365. if (controller.getValue('server_max_age')) {
  366. return 'preflightcache';
  367. }
  368. return Math.floor(Math.random()*10000000);
  369. };
  370. /**
  371. * Retrieve the url to make the request to.
  372. */
  373. var getServerUrl = function(controller) {
  374. if (controller.getValue('server_tabs') == 'remote') {
  375. // If running in "remote" mode, use the url supplied by the user.
  376. return controller.getValue('server_url');
  377. }
  378. var queryObj = {};
  379. queryObj['id'] = getId(controller);
  380. controller.each(function(index, value) {
  381. var id = value.getId();
  382. if (id.indexOf(SERVER_PREFIX_) === 0) {
  383. if (id === 'server_tabs' || id === 'server_url') {
  384. // Skip any server fields that aren't used by the local server.
  385. return;
  386. }
  387. queryObj[value.getId().substring(SERVER_PREFIX_.length)] = value.get();
  388. }
  389. });
  390. return SERVER_URL + '?' + Query.serialize(queryObj);
  391. };
  392. /**
  393. * Returns a new XMLHttpRequest object (or null if CORS is not supported).
  394. */
  395. var createCORSRequest = function(method, url) {
  396. var xhr = new XMLHttpRequest();
  397. if ("withCredentials" in xhr) {
  398. // Most browsers.
  399. xhr.open(method, url, true);
  400. } else if (typeof XDomainRequest != "undefined") {
  401. // IE8 & IE9
  402. xhr = new XDomainRequest();
  403. xhr.open(method, url);
  404. } else {
  405. // CORS not supported.
  406. xhr = null;
  407. }
  408. return xhr;
  409. };
  410. /**
  411. * Parses a text blob representing a set of HTTP headers. Expected format:
  412. * HeaderName1: HeaderValue1\r\n
  413. * HeaderName2: HeaderValue2\r\n
  414. */
  415. var parseHeaders = function(headerStr) {
  416. var headers = {};
  417. if (!headerStr) {
  418. return headers;
  419. }
  420. var headerPairs = headerStr.split('\n');
  421. for (var i = 0; i < headerPairs.length; i++) {
  422. var headerPair = headerPairs[i];
  423. // Can't use split() here because it does the wrong thing
  424. // if the header value has the string ": " in it.
  425. var index = headerPair.indexOf(': ');
  426. if (index > 0) {
  427. var key = $.trim(headerPair.substring(0, index));
  428. var val = $.trim(headerPair.substring(index + 2));
  429. headers[key] = val;
  430. }
  431. }
  432. return headers;
  433. }
  434. /**
  435. * Sends the CORS request to the server, and logs key events.
  436. */
  437. var sendRequest = function(controller, url) {
  438. // Reset the logs for a new run.
  439. logger.reset();
  440. codder.reset();
  441. // Load the value from the form and write them to the url.
  442. controller.each(function(index, value) {
  443. value.fromUi();
  444. value.toUrl();
  445. });
  446. url.write();
  447. // Link to this test.
  448. logger.log('<a href="#" onclick="javascript:prompt(\'Here\\\'s a link to this test\', \'' + htmlEscape(window.location.href) + '\');return false;">Link to this test</a><br>');
  449. // Create the XHR object and make the request.
  450. var httpMethod = controller.getValue('client_method');
  451. var serverUrl = getServerUrl(controller);
  452. var xhr = createCORSRequest(httpMethod, serverUrl);
  453. var msg = 'Sending ' + htmlEscape(httpMethod) + ' request to ' +
  454. '<code>' + htmlEscape(serverUrl) + '</code><br>';
  455. codder.setUrl(serverUrl);
  456. codder.setMethod(httpMethod);
  457. if (controller.getValue('client_credentials')) {
  458. xhr.withCredentials = true;
  459. msg += ', with credentials';
  460. codder.setCredentials();
  461. }
  462. var headersMsg = '';
  463. var requestHeaders = parseHeaders(controller.getValue('client_headers'));
  464. $.each(requestHeaders, function(key, val) {
  465. xhr.setRequestHeader(key, val);
  466. codder.addHeader(key, val);
  467. if (headersMsg.length == 0) {
  468. headersMsg = ', with custom headers: ';
  469. } else {
  470. headersMsg += ', ';
  471. }
  472. headersMsg += htmlEscape(key);
  473. });
  474. msg += headersMsg;
  475. xhr.onreadystatechange = function() {
  476. logger.logEvent('readystatechange');
  477. };
  478. xhr.onloadstart = function() {
  479. logger.logEvent('loadstart');
  480. };
  481. xhr.onprogress = function() {
  482. logger.logEvent('progress');
  483. };
  484. xhr.onabort = function() {
  485. logger.logEvent('abort', 'red');
  486. };
  487. xhr.onerror = function() {
  488. logger.logEvent('error', 'red');
  489. logger.logXhr(xhr);
  490. };
  491. xhr.onload = function() {
  492. logger.logEvent('load', 'green');
  493. logger.logXhr(xhr);
  494. };
  495. xhr.ontimeout = function() {
  496. logger.logEvent('timeout', 'red');
  497. };
  498. xhr.onloadend = function() {
  499. logger.logEvent('loadend');
  500. };
  501. logger.log(msg);
  502. xhr.send();
  503. };
  504. $(function() {
  505. // Set up the help menus.
  506. var help_divs = $('.control-group').filter('div[id]').each(function() {
  507. var id = $(this).attr('id');
  508. var placement = 'left';
  509. if (id.indexOf('server_') == 0) {
  510. placement = 'right';
  511. }
  512. $(this).popover({
  513. placement: placement,
  514. trigger: 'hover'});
  515. });
  516. // Set up the shared url object.
  517. var url = new Url();
  518. url.read();
  519. // Initialize the fields.
  520. var controller = new FieldsController();
  521. $('.control-label').each(function() {
  522. var id = $(this).attr('for');
  523. var field = null;
  524. if ($('#' + id).attr('type') === 'checkbox') {
  525. field = new CheckboxField(id, url);
  526. } else {
  527. field = new TextField(id, url);
  528. }
  529. controller.add(field);
  530. });
  531. controller.add(new TabField('server_tabs', url));
  532. // Wire up an event handler on the button.
  533. $('#btnSendRequest').click(function() {
  534. sendRequest(controller, url);
  535. });
  536. $('#result_tabs a:first').tab('show');
  537. // Read the values from the url and write it to the UI.
  538. controller.each(function(index, value) {
  539. value.fromUrl();
  540. value.toUi();
  541. });
  542. });
  543. })(window);