url-lib.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. /* Any copyright is dedicated to the Public Domain.
  2. * http://creativecommons.org/publicdomain/zero/1.0/
  3. */
  4. // Polyfill obtained from: https://github.com/Polymer/URL
  5. (function URLConstructorClosure() {
  6. 'use strict';
  7. var relative = Object.create(null);
  8. relative['ftp'] = 21;
  9. relative['file'] = 0;
  10. relative['gopher'] = 70;
  11. relative['http'] = 80;
  12. relative['https'] = 443;
  13. relative['ws'] = 80;
  14. relative['wss'] = 443;
  15. var relativePathDotMapping = Object.create(null);
  16. relativePathDotMapping['%2e'] = '.';
  17. relativePathDotMapping['.%2e'] = '..';
  18. relativePathDotMapping['%2e.'] = '..';
  19. relativePathDotMapping['%2e%2e'] = '..';
  20. function isRelativeScheme(scheme) {
  21. return relative[scheme] !== undefined;
  22. }
  23. function invalid() {
  24. clear.call(this);
  25. this._isInvalid = true;
  26. }
  27. function IDNAToASCII(h) {
  28. if (h === '') {
  29. invalid.call(this);
  30. }
  31. // XXX
  32. return h.toLowerCase();
  33. }
  34. function percentEscape(c) {
  35. var unicode = c.charCodeAt(0);
  36. if (unicode > 0x20 &&
  37. unicode < 0x7F &&
  38. // " # < > ? `
  39. [0x22, 0x23, 0x3C, 0x3E, 0x3F, 0x60].indexOf(unicode) === -1
  40. ) {
  41. return c;
  42. }
  43. return encodeURIComponent(c);
  44. }
  45. function percentEscapeQuery(c) {
  46. // XXX This actually needs to encode c using encoding and then
  47. // convert the bytes one-by-one.
  48. var unicode = c.charCodeAt(0);
  49. if (unicode > 0x20 &&
  50. unicode < 0x7F &&
  51. // " # < > ` (do not escape '?')
  52. [0x22, 0x23, 0x3C, 0x3E, 0x60].indexOf(unicode) === -1
  53. ) {
  54. return c;
  55. }
  56. return encodeURIComponent(c);
  57. }
  58. var EOF, ALPHA = /[a-zA-Z]/,
  59. ALPHANUMERIC = /[a-zA-Z0-9\+\-\.]/;
  60. function parse(input, stateOverride, base) {
  61. function err(message) {
  62. errors.push(message);
  63. }
  64. var state = stateOverride || 'scheme start',
  65. cursor = 0,
  66. buffer = '',
  67. seenAt = false,
  68. seenBracket = false,
  69. errors = [];
  70. loop: while ((input[cursor - 1] !== EOF || cursor === 0) &&
  71. !this._isInvalid) {
  72. var c = input[cursor];
  73. switch (state) {
  74. case 'scheme start':
  75. if (c && ALPHA.test(c)) {
  76. buffer += c.toLowerCase(); // ASCII-safe
  77. state = 'scheme';
  78. } else if (!stateOverride) {
  79. buffer = '';
  80. state = 'no scheme';
  81. continue;
  82. } else {
  83. err('Invalid scheme.');
  84. break loop;
  85. }
  86. break;
  87. case 'scheme':
  88. if (c && ALPHANUMERIC.test(c)) {
  89. buffer += c.toLowerCase(); // ASCII-safe
  90. } else if (c === ':') {
  91. this._scheme = buffer;
  92. buffer = '';
  93. if (stateOverride) {
  94. break loop;
  95. }
  96. if (isRelativeScheme(this._scheme)) {
  97. this._isRelative = true;
  98. }
  99. if (this._scheme === 'file') {
  100. state = 'relative';
  101. } else if (this._isRelative && base &&
  102. base._scheme === this._scheme) {
  103. state = 'relative or authority';
  104. } else if (this._isRelative) {
  105. state = 'authority first slash';
  106. } else {
  107. state = 'scheme data';
  108. }
  109. } else if (!stateOverride) {
  110. buffer = '';
  111. cursor = 0;
  112. state = 'no scheme';
  113. continue;
  114. } else if (c === EOF) {
  115. break loop;
  116. } else {
  117. err('Code point not allowed in scheme: ' + c);
  118. break loop;
  119. }
  120. break;
  121. case 'scheme data':
  122. if (c === '?') {
  123. this._query = '?';
  124. state = 'query';
  125. } else if (c === '#') {
  126. this._fragment = '#';
  127. state = 'fragment';
  128. } else {
  129. // XXX error handling
  130. if (c !== EOF && c !== '\t' && c !== '\n' && c !== '\r') {
  131. this._schemeData += percentEscape(c);
  132. }
  133. }
  134. break;
  135. case 'no scheme':
  136. if (!base || !(isRelativeScheme(base._scheme))) {
  137. err('Missing scheme.');
  138. invalid.call(this);
  139. } else {
  140. state = 'relative';
  141. continue;
  142. }
  143. break;
  144. case 'relative or authority':
  145. if (c === '/' && input[cursor + 1] === '/') {
  146. state = 'authority ignore slashes';
  147. } else {
  148. err('Expected /, got: ' + c);
  149. state = 'relative';
  150. continue;
  151. }
  152. break;
  153. case 'relative':
  154. this._isRelative = true;
  155. if (this._scheme !== 'file') {
  156. this._scheme = base._scheme;
  157. }
  158. if (c === EOF) {
  159. this._host = base._host;
  160. this._port = base._port;
  161. this._path = base._path.slice();
  162. this._query = base._query;
  163. this._username = base._username;
  164. this._password = base._password;
  165. break loop;
  166. } else if (c === '/' || c === '\\') {
  167. if (c === '\\') {
  168. err('\\ is an invalid code point.');
  169. }
  170. state = 'relative slash';
  171. } else if (c === '?') {
  172. this._host = base._host;
  173. this._port = base._port;
  174. this._path = base._path.slice();
  175. this._query = '?';
  176. this._username = base._username;
  177. this._password = base._password;
  178. state = 'query';
  179. } else if (c === '#') {
  180. this._host = base._host;
  181. this._port = base._port;
  182. this._path = base._path.slice();
  183. this._query = base._query;
  184. this._fragment = '#';
  185. this._username = base._username;
  186. this._password = base._password;
  187. state = 'fragment';
  188. } else {
  189. var nextC = input[cursor + 1];
  190. var nextNextC = input[cursor + 2];
  191. if (this._scheme !== 'file' || !ALPHA.test(c) ||
  192. (nextC !== ':' && nextC !== '|') ||
  193. (nextNextC !== EOF && nextNextC !== '/' && nextNextC !== '\\' &&
  194. nextNextC !== '?' && nextNextC !== '#')) {
  195. this._host = base._host;
  196. this._port = base._port;
  197. this._username = base._username;
  198. this._password = base._password;
  199. this._path = base._path.slice();
  200. this._path.pop();
  201. }
  202. state = 'relative path';
  203. continue;
  204. }
  205. break;
  206. case 'relative slash':
  207. if (c === '/' || c === '\\') {
  208. if (c === '\\') {
  209. err('\\ is an invalid code point.');
  210. }
  211. if (this._scheme === 'file') {
  212. state = 'file host';
  213. } else {
  214. state = 'authority ignore slashes';
  215. }
  216. } else {
  217. if (this._scheme !== 'file') {
  218. this._host = base._host;
  219. this._port = base._port;
  220. this._username = base._username;
  221. this._password = base._password;
  222. }
  223. state = 'relative path';
  224. continue;
  225. }
  226. break;
  227. case 'authority first slash':
  228. if (c === '/') {
  229. state = 'authority second slash';
  230. } else {
  231. err('Expected \'/\', got: ' + c);
  232. state = 'authority ignore slashes';
  233. continue;
  234. }
  235. break;
  236. case 'authority second slash':
  237. state = 'authority ignore slashes';
  238. if (c !== '/') {
  239. err('Expected \'/\', got: ' + c);
  240. continue;
  241. }
  242. break;
  243. case 'authority ignore slashes':
  244. if (c !== '/' && c !== '\\') {
  245. state = 'authority';
  246. continue;
  247. } else {
  248. err('Expected authority, got: ' + c);
  249. }
  250. break;
  251. case 'authority':
  252. if (c === '@') {
  253. if (seenAt) {
  254. err('@ already seen.');
  255. buffer += '%40';
  256. }
  257. seenAt = true;
  258. for (var i = 0; i < buffer.length; i++) {
  259. var cp = buffer[i];
  260. if (cp === '\t' || cp === '\n' || cp === '\r') {
  261. err('Invalid whitespace in authority.');
  262. continue;
  263. }
  264. // XXX check URL code points
  265. if (cp === ':' && this._password === null) {
  266. this._password = '';
  267. continue;
  268. }
  269. var tempC = percentEscape(cp);
  270. if (this._password !== null) {
  271. this._password += tempC;
  272. } else {
  273. this._username += tempC;
  274. }
  275. }
  276. buffer = '';
  277. } else if (c === EOF || c === '/' || c === '\\' ||
  278. c === '?' || c === '#') {
  279. cursor -= buffer.length;
  280. buffer = '';
  281. state = 'host';
  282. continue;
  283. } else {
  284. buffer += c;
  285. }
  286. break;
  287. case 'file host':
  288. if (c === EOF || c === '/' || c === '\\' || c === '?' || c === '#') {
  289. if (buffer.length === 2 && ALPHA.test(buffer[0]) &&
  290. (buffer[1] === ':' || buffer[1] === '|')) {
  291. state = 'relative path';
  292. } else if (buffer.length === 0) {
  293. state = 'relative path start';
  294. } else {
  295. this._host = IDNAToASCII.call(this, buffer);
  296. buffer = '';
  297. state = 'relative path start';
  298. }
  299. continue;
  300. } else if (c === '\t' || c === '\n' || c === '\r') {
  301. err('Invalid whitespace in file host.');
  302. } else {
  303. buffer += c;
  304. }
  305. break;
  306. case 'host':
  307. case 'hostname':
  308. if (c === ':' && !seenBracket) {
  309. // XXX host parsing
  310. this._host = IDNAToASCII.call(this, buffer);
  311. buffer = '';
  312. state = 'port';
  313. if (stateOverride === 'hostname') {
  314. break loop;
  315. }
  316. } else if (c === EOF || c === '/' ||
  317. c === '\\' || c === '?' || c === '#') {
  318. this._host = IDNAToASCII.call(this, buffer);
  319. buffer = '';
  320. state = 'relative path start';
  321. if (stateOverride) {
  322. break loop;
  323. }
  324. continue;
  325. } else if (c !== '\t' && c !== '\n' && c !== '\r') {
  326. if (c === '[') {
  327. seenBracket = true;
  328. } else if (c === ']') {
  329. seenBracket = false;
  330. }
  331. buffer += c;
  332. } else {
  333. err('Invalid code point in host/hostname: ' + c);
  334. }
  335. break;
  336. case 'port':
  337. if (/[0-9]/.test(c)) {
  338. buffer += c;
  339. } else if (c === EOF || c === '/' || c === '\\' ||
  340. c === '?' || c === '#' || stateOverride) {
  341. if (buffer !== '') {
  342. var temp = parseInt(buffer, 10);
  343. if (temp !== relative[this._scheme]) {
  344. this._port = temp + '';
  345. }
  346. buffer = '';
  347. }
  348. if (stateOverride) {
  349. break loop;
  350. }
  351. state = 'relative path start';
  352. continue;
  353. } else if (c === '\t' || c === '\n' || c === '\r') {
  354. err('Invalid code point in port: ' + c);
  355. } else {
  356. invalid.call(this);
  357. }
  358. break;
  359. case 'relative path start':
  360. if (c === '\\') {
  361. err('\'\\\' not allowed in path.');
  362. }
  363. state = 'relative path';
  364. if (c !== '/' && c !== '\\') {
  365. continue;
  366. }
  367. break;
  368. case 'relative path':
  369. if (c === EOF || c === '/' || c === '\\' ||
  370. (!stateOverride && (c === '?' || c === '#'))) {
  371. if (c === '\\') {
  372. err('\\ not allowed in relative path.');
  373. }
  374. var tmp;
  375. if ((tmp = relativePathDotMapping[buffer.toLowerCase()])) {
  376. buffer = tmp;
  377. }
  378. if (buffer === '..') {
  379. this._path.pop();
  380. if (c !== '/' && c !== '\\') {
  381. this._path.push('');
  382. }
  383. } else if (buffer === '.' && c !== '/' && c !== '\\') {
  384. this._path.push('');
  385. } else if (buffer !== '.') {
  386. if (this._scheme === 'file' && this._path.length === 0 &&
  387. buffer.length === 2 && ALPHA.test(buffer[0]) &&
  388. buffer[1] === '|') {
  389. buffer = buffer[0] + ':';
  390. }
  391. this._path.push(buffer);
  392. }
  393. buffer = '';
  394. if (c === '?') {
  395. this._query = '?';
  396. state = 'query';
  397. } else if (c === '#') {
  398. this._fragment = '#';
  399. state = 'fragment';
  400. }
  401. } else if (c !== '\t' && c !== '\n' && c !== '\r') {
  402. buffer += percentEscape(c);
  403. }
  404. break;
  405. case 'query':
  406. if (!stateOverride && c === '#') {
  407. this._fragment = '#';
  408. state = 'fragment';
  409. } else if (c !== EOF && c !== '\t' && c !== '\n' && c !== '\r') {
  410. this._query += percentEscapeQuery(c);
  411. }
  412. break;
  413. case 'fragment':
  414. if (c !== EOF && c !== '\t' && c !== '\n' && c !== '\r') {
  415. this._fragment += c;
  416. }
  417. break;
  418. }
  419. cursor++;
  420. }
  421. }
  422. function clear() {
  423. this._scheme = '';
  424. this._schemeData = '';
  425. this._username = '';
  426. this._password = null;
  427. this._host = '';
  428. this._port = '';
  429. this._path = [];
  430. this._query = '';
  431. this._fragment = '';
  432. this._isInvalid = false;
  433. this._isRelative = false;
  434. }
  435. // Does not process domain names or IP addresses.
  436. // Does not handle encoding for the query parameter.
  437. function JURL(url, base /* , encoding */) {
  438. if (base !== undefined && !(base instanceof JURL)) {
  439. base = new JURL(String(base));
  440. }
  441. this._url = url;
  442. clear.call(this);
  443. var input = url.replace(/^[ \t\r\n\f]+|[ \t\r\n\f]+$/g, '');
  444. // encoding = encoding || 'utf-8'
  445. parse.call(this, input, null, base);
  446. }
  447. JURL.prototype = {
  448. toString() {
  449. return this.href;
  450. },
  451. get href() {
  452. if (this._isInvalid) {
  453. return this._url;
  454. }
  455. var authority = '';
  456. if (this._username !== '' || this._password !== null) {
  457. authority = this._username +
  458. (this._password !== null ? ':' + this._password : '') + '@';
  459. }
  460. return this.protocol +
  461. (this._isRelative ? '//' + authority + this.host : '') +
  462. this.pathname + this._query + this._fragment;
  463. },
  464. // The named parameter should be different from the setter's function name.
  465. // Otherwise Safari 5 will throw an error (see issue 8541)
  466. set href(value) {
  467. clear.call(this);
  468. parse.call(this, value);
  469. },
  470. get protocol() {
  471. return this._scheme + ':';
  472. },
  473. set protocol(value) {
  474. if (this._isInvalid) {
  475. return;
  476. }
  477. parse.call(this, value + ':', 'scheme start');
  478. },
  479. get host() {
  480. return this._isInvalid ? '' : this._port ?
  481. this._host + ':' + this._port : this._host;
  482. },
  483. set host(value) {
  484. if (this._isInvalid || !this._isRelative) {
  485. return;
  486. }
  487. parse.call(this, value, 'host');
  488. },
  489. get hostname() {
  490. return this._host;
  491. },
  492. set hostname(value) {
  493. if (this._isInvalid || !this._isRelative) {
  494. return;
  495. }
  496. parse.call(this, value, 'hostname');
  497. },
  498. get port() {
  499. return this._port;
  500. },
  501. set port(value) {
  502. if (this._isInvalid || !this._isRelative) {
  503. return;
  504. }
  505. parse.call(this, value, 'port');
  506. },
  507. get pathname() {
  508. return this._isInvalid ? '' : this._isRelative ?
  509. '/' + this._path.join('/') : this._schemeData;
  510. },
  511. set pathname(value) {
  512. if (this._isInvalid || !this._isRelative) {
  513. return;
  514. }
  515. this._path = [];
  516. parse.call(this, value, 'relative path start');
  517. },
  518. get search() {
  519. return this._isInvalid || !this._query || this._query === '?' ?
  520. '' : this._query;
  521. },
  522. set search(value) {
  523. if (this._isInvalid || !this._isRelative) {
  524. return;
  525. }
  526. this._query = '?';
  527. if (value[0] === '?') {
  528. value = value.slice(1);
  529. }
  530. parse.call(this, value, 'query');
  531. },
  532. get hash() {
  533. return this._isInvalid || !this._fragment || this._fragment === '#' ?
  534. '' : this._fragment;
  535. },
  536. set hash(value) {
  537. if (this._isInvalid) {
  538. return;
  539. }
  540. this._fragment = '#';
  541. if (value[0] === '#') {
  542. value = value.slice(1);
  543. }
  544. parse.call(this, value, 'fragment');
  545. },
  546. get origin() {
  547. var host;
  548. if (this._isInvalid || !this._scheme) {
  549. return '';
  550. }
  551. // javascript: Gecko returns String(""), WebKit/Blink String("null")
  552. // Gecko throws error for "data://"
  553. // data: Gecko returns "", Blink returns "data://", WebKit returns "null"
  554. // Gecko returns String("") for file: mailto:
  555. // WebKit/Blink returns String("SCHEME://") for file: mailto:
  556. switch (this._scheme) {
  557. case 'data':
  558. case 'file':
  559. case 'javascript':
  560. case 'mailto':
  561. return 'null';
  562. case 'blob':
  563. // Special case of blob: -- returns valid origin of _schemeData.
  564. try {
  565. return new JURL(this._schemeData).origin || 'null';
  566. } catch (_) {
  567. // Invalid _schemeData origin -- ignoring errors.
  568. }
  569. return 'null';
  570. }
  571. host = this.host;
  572. if (!host) {
  573. return '';
  574. }
  575. return this._scheme + '://' + host;
  576. },
  577. };
  578. exports.URL = JURL;
  579. })();