ImportMap.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. /*
  2. * Copyright (c) 2024, Jamie Mansfield <jmansfield@cadixdev.org>
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include <LibJS/Console.h>
  7. #include <LibJS/Runtime/ConsoleObject.h>
  8. #include <LibWeb/DOMURL/DOMURL.h>
  9. #include <LibWeb/HTML/Scripting/Fetching.h>
  10. #include <LibWeb/HTML/Scripting/ImportMap.h>
  11. #include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
  12. #include <LibWeb/Infra/JSON.h>
  13. namespace Web::HTML {
  14. // https://html.spec.whatwg.org/multipage/webappapis.html#parse-an-import-map-string
  15. WebIDL::ExceptionOr<ImportMap> parse_import_map_string(JS::Realm& realm, ByteString const& input, URL::URL base_url)
  16. {
  17. HTML::TemporaryExecutionContext execution_context { realm };
  18. // 1. Let parsed be the result of parsing a JSON string to an Infra value given input.
  19. auto parsed = TRY(Infra::parse_json_string_to_javascript_value(realm, input));
  20. // 2. If parsed is not an ordered map, then throw a TypeError indicating that the top-level value needs to be a JSON object.
  21. if (!parsed.is_object())
  22. return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, String::formatted("The top-level value of an importmap needs to be a JSON object.").release_value_but_fixme_should_propagate_errors() };
  23. auto& parsed_object = parsed.as_object();
  24. // 3. Let sortedAndNormalizedImports be an empty ordered map.
  25. ModuleSpecifierMap sorted_and_normalised_imports;
  26. // 4. If parsed["imports"] exists, then:
  27. if (TRY(parsed_object.has_property("imports"))) {
  28. auto imports = TRY(parsed_object.get("imports"));
  29. // If parsed["imports"] is not an ordered map, then throw a TypeError indicating that the value for the "imports" top-level key needs to be a JSON object.
  30. if (!imports.is_object())
  31. return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, String::formatted("The 'imports' top-level value of an importmap needs to be a JSON object.").release_value_but_fixme_should_propagate_errors() };
  32. // Set sortedAndNormalizedImports to the result of sorting and normalizing a module specifier map given parsed["imports"] and baseURL.
  33. sorted_and_normalised_imports = TRY(sort_and_normalise_module_specifier_map(realm, imports.as_object(), base_url));
  34. }
  35. // 5. Let sortedAndNormalizedScopes be an empty ordered map.
  36. HashMap<URL::URL, ModuleSpecifierMap> sorted_and_normalised_scopes;
  37. // 6. If parsed["scopes"] exists, then:
  38. if (TRY(parsed_object.has_property("scopes"))) {
  39. auto scopes = TRY(parsed_object.get("scopes"));
  40. // If parsed["scopes"] is not an ordered map, then throw a TypeError indicating that the value for the "scopes" top-level key needs to be a JSON object.
  41. if (!scopes.is_object())
  42. return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, String::formatted("The 'scopes' top-level value of an importmap needs to be a JSON object.").release_value_but_fixme_should_propagate_errors() };
  43. // Set sortedAndNormalizedScopes to the result of sorting and normalizing scopes given parsed["scopes"] and baseURL.
  44. sorted_and_normalised_scopes = TRY(sort_and_normalise_scopes(realm, scopes.as_object(), base_url));
  45. }
  46. // 7. Let normalizedIntegrity be an empty ordered map.
  47. ModuleIntegrityMap normalised_integrity;
  48. // 8. If parsed["integrity"] exists, then:
  49. if (TRY(parsed_object.has_property("integrity"))) {
  50. auto integrity = TRY(parsed_object.get("integrity"));
  51. // 1. If parsed["integrity"] is not an ordered map, then throw a TypeError indicating that the value for the "integrity" top-level key needs to be a JSON object.
  52. if (!integrity.is_object())
  53. return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, String::formatted("The 'integrity' top-level value of an importmap needs to be a JSON object.").release_value_but_fixme_should_propagate_errors() };
  54. // 2. Set normalizedIntegrity to the result of normalizing a module integrity map given parsed["integrity"] and baseURL.
  55. normalised_integrity = TRY(normalize_module_integrity_map(realm, integrity.as_object(), base_url));
  56. }
  57. // 9. If parsed's keys contains any items besides "imports", "scopes", or "integrity", then the user agent should report a warning to the console indicating that an invalid top-level key was present in the import map.
  58. for (auto& key : parsed_object.shape().property_table().keys()) {
  59. if (key.as_string().is_one_of("imports", "scopes", "integrity"))
  60. continue;
  61. auto& console = realm.intrinsics().console_object()->console();
  62. console.output_debug_message(JS::Console::LogLevel::Warn,
  63. TRY_OR_THROW_OOM(realm.vm(), String::formatted("An invalid top-level key ({}) was present in the import map", key.as_string())));
  64. }
  65. // 10. Return an import map whose imports are sortedAndNormalizedImports, whose scopes are sortedAndNormalizedScopes, and whose integrity are normalizedIntegrity.
  66. ImportMap import_map;
  67. import_map.set_imports(sorted_and_normalised_imports);
  68. import_map.set_scopes(sorted_and_normalised_scopes);
  69. import_map.set_integrity(normalised_integrity);
  70. return import_map;
  71. }
  72. // https://html.spec.whatwg.org/multipage/webappapis.html#normalizing-a-specifier-key
  73. WebIDL::ExceptionOr<Optional<DeprecatedFlyString>> normalise_specifier_key(JS::Realm& realm, DeprecatedFlyString specifier_key, URL::URL base_url)
  74. {
  75. // 1. If specifierKey is the empty string, then:
  76. if (specifier_key.is_empty()) {
  77. // 1. The user agent may report a warning to the console indicating that specifier keys may not be the empty string.
  78. auto& console = realm.intrinsics().console_object()->console();
  79. console.output_debug_message(JS::Console::LogLevel::Warn,
  80. TRY_OR_THROW_OOM(realm.vm(), String::formatted("Specifier keys may not be empty")));
  81. // 2. Return null.
  82. return Optional<DeprecatedFlyString> {};
  83. }
  84. // 2. Let url be the result of resolving a URL-like module specifier, given specifierKey and baseURL.
  85. auto url = resolve_url_like_module_specifier(specifier_key, base_url);
  86. // 3. If url is not null, then return the serialization of url.
  87. if (url.has_value())
  88. return url->serialize();
  89. // 4. Return specifierKey.
  90. return specifier_key;
  91. }
  92. // https://html.spec.whatwg.org/multipage/webappapis.html#sorting-and-normalizing-a-module-specifier-map
  93. WebIDL::ExceptionOr<ModuleSpecifierMap> sort_and_normalise_module_specifier_map(JS::Realm& realm, JS::Object& original_map, URL::URL base_url)
  94. {
  95. // 1. Let normalized be an empty ordered map.
  96. ModuleSpecifierMap normalised;
  97. // 2. For each specifierKey → value of originalMap:
  98. for (auto& specifier_key : original_map.shape().property_table().keys()) {
  99. auto value = TRY(original_map.get(specifier_key.as_string()));
  100. // 1. Let normalizedSpecifierKey be the result of normalizing a specifier key given specifierKey and baseURL.
  101. auto normalised_specifier_key = TRY(normalise_specifier_key(realm, specifier_key.as_string(), base_url));
  102. // 2. If normalizedSpecifierKey is null, then continue.
  103. if (!normalised_specifier_key.has_value())
  104. continue;
  105. // 3. If value is not a string, then:
  106. if (!value.is_string()) {
  107. // 1. The user agent may report a warning to the console indicating that addresses need to be strings.
  108. auto& console = realm.intrinsics().console_object()->console();
  109. console.output_debug_message(JS::Console::LogLevel::Warn,
  110. TRY_OR_THROW_OOM(realm.vm(), String::formatted("Addresses need to be strings")));
  111. // 2. Set normalized[normalizedSpecifierKey] to null.
  112. normalised.set(normalised_specifier_key.value(), {});
  113. // 3. Continue.
  114. continue;
  115. }
  116. // 4. Let addressURL be the result of resolving a URL-like module specifier given value and baseURL.
  117. auto address_url = resolve_url_like_module_specifier(value.as_string().byte_string(), base_url);
  118. // 5. If addressURL is null, then:
  119. if (!address_url.has_value()) {
  120. // 1. The user agent may report a warning to the console indicating that the address was invalid.
  121. auto& console = realm.intrinsics().console_object()->console();
  122. console.output_debug_message(JS::Console::LogLevel::Warn,
  123. TRY_OR_THROW_OOM(realm.vm(), String::formatted("Address was invalid")));
  124. // 2. Set normalized[normalizedSpecifierKey] to null.
  125. normalised.set(normalised_specifier_key.value(), {});
  126. // 3. Continue.
  127. continue;
  128. }
  129. // 6. If specifierKey ends with U+002F (/), and the serialization of addressURL does not end with U+002F (/), then:
  130. if (specifier_key.as_string().ends_with("/"sv) && !address_url->serialize().ends_with("/"sv)) {
  131. // 1. The user agent may report a warning to the console indicating that an invalid address was given for the specifier key specifierKey; since specifierKey ends with a slash, the address needs to as well.
  132. auto& console = realm.intrinsics().console_object()->console();
  133. console.output_debug_message(JS::Console::LogLevel::Warn,
  134. TRY_OR_THROW_OOM(realm.vm(), String::formatted("An invalid address was given for the specifier key ({}); since specifierKey ends with a slash, the address needs to as well", specifier_key.as_string())));
  135. // 2. Set normalized[normalizedSpecifierKey] to null.
  136. normalised.set(normalised_specifier_key.value(), {});
  137. // 3. Continue.
  138. continue;
  139. }
  140. // 7. Set normalized[normalizedSpecifierKey] to addressURL.
  141. normalised.set(normalised_specifier_key.value(), address_url.value());
  142. }
  143. // 3. Return the result of sorting in descending order normalized, with an entry a being less than an entry b if a's key is code unit less than b's key.
  144. return normalised;
  145. }
  146. // https://html.spec.whatwg.org/multipage/webappapis.html#sorting-and-normalizing-scopes
  147. WebIDL::ExceptionOr<HashMap<URL::URL, ModuleSpecifierMap>> sort_and_normalise_scopes(JS::Realm& realm, JS::Object& original_map, URL::URL base_url)
  148. {
  149. // 1. Let normalized be an empty ordered map.
  150. HashMap<URL::URL, ModuleSpecifierMap> normalised;
  151. // 2. For each scopePrefix → potentialSpecifierMap of originalMap:
  152. for (auto& scope_prefix : original_map.shape().property_table().keys()) {
  153. auto potential_specifier_map = TRY(original_map.get(scope_prefix.as_string()));
  154. // 1. If potentialSpecifierMap is not an ordered map, then throw a TypeError indicating that the value of the scope with prefix scopePrefix needs to be a JSON object.
  155. if (!potential_specifier_map.is_object())
  156. return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, String::formatted("The value of the scope with the prefix '{}' needs to be a JSON object.", scope_prefix.as_string()).release_value_but_fixme_should_propagate_errors() };
  157. // 2. Let scopePrefixURL be the result of URL parsing scopePrefix with baseURL.
  158. auto scope_prefix_url = DOMURL::parse(scope_prefix.as_string(), base_url);
  159. // 3. If scopePrefixURL is failure, then:
  160. if (!scope_prefix_url.is_valid()) {
  161. // 1. The user agent may report a warning to the console that the scope prefix URL was not parseable.
  162. auto& console = realm.intrinsics().console_object()->console();
  163. console.output_debug_message(JS::Console::LogLevel::Warn,
  164. TRY_OR_THROW_OOM(realm.vm(), String::formatted("The scope prefix URL ({}) was not parseable", scope_prefix.as_string())));
  165. // 2. Continue.
  166. continue;
  167. }
  168. // 4. Let normalizedScopePrefix be the serialization of scopePrefixURL.
  169. auto normalised_scope_prefix = scope_prefix_url.serialize();
  170. // 5. Set normalized[normalizedScopePrefix] to the result of sorting and normalizing a module specifier map given potentialSpecifierMap and baseURL.
  171. normalised.set(normalised_scope_prefix, TRY(sort_and_normalise_module_specifier_map(realm, potential_specifier_map.as_object(), base_url)));
  172. }
  173. // 3. Return the result of sorting in descending order normalized, with an entry a being less than an entry b if a's key is code unit less than b's key.
  174. return normalised;
  175. }
  176. // https://html.spec.whatwg.org/multipage/webappapis.html#normalizing-a-module-integrity-map
  177. WebIDL::ExceptionOr<ModuleIntegrityMap> normalize_module_integrity_map(JS::Realm& realm, JS::Object& original_map, URL::URL base_url)
  178. {
  179. // 1. Let normalized be an empty ordered map.
  180. ModuleIntegrityMap normalised;
  181. // 2. For each key → value of originalMap:
  182. for (auto& key : original_map.shape().property_table().keys()) {
  183. auto value = TRY(original_map.get(key.as_string()));
  184. // 1. Let resolvedURL be the result of resolving a URL-like module specifier given key and baseURL.
  185. auto resolved_url = resolve_url_like_module_specifier(key.as_string(), base_url);
  186. // 2. If resolvedURL is null, then:
  187. if (!resolved_url.has_value()) {
  188. // 1. The user agent may report a warning to the console indicating that the key failed to resolve.
  189. auto& console = realm.intrinsics().console_object()->console();
  190. console.output_debug_message(JS::Console::LogLevel::Warn,
  191. TRY_OR_THROW_OOM(realm.vm(), String::formatted("Failed to resolve key ({})", key.as_string())));
  192. // 2. Continue.
  193. continue;
  194. }
  195. // 3. If value is not a string, then:
  196. if (!value.is_string()) {
  197. // 1. The user agent may report a warning to the console indicating that integrity metadata values need to be strings.
  198. auto& console = realm.intrinsics().console_object()->console();
  199. console.output_debug_message(JS::Console::LogLevel::Warn,
  200. TRY_OR_THROW_OOM(realm.vm(), String::formatted("Integrity metadata value for '{}' needs to be a string", key.as_string())));
  201. // 2. Continue.
  202. continue;
  203. }
  204. // 4. Set normalized[resolvedURL] to value.
  205. normalised.set(resolved_url.release_value(), value.as_string().byte_string());
  206. }
  207. // 3. Return normalized.
  208. return normalised;
  209. }
  210. }