diff --git a/src/frontend/faq.html b/src/frontend/faq.html
new file mode 100644
index 0000000000..aa1c1a550f
--- /dev/null
+++ b/src/frontend/faq.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+ Internet Identity
+
+
+
+
diff --git a/src/frontend/src/index.ts b/src/frontend/src/index.ts
index 03a89f30ac..1d381a086f 100644
--- a/src/frontend/src/index.ts
+++ b/src/frontend/src/index.ts
@@ -101,14 +101,6 @@ const init = async () => {
// https://github.com/dfinity/internet-identity#build-features
showWarningIfNecessary();
- // Redirect to the FAQ
- // The canister should already be handling this with a 301 when serving "/faq", this is just a safety
- // measure.
- if (window.location.pathname === "/faq") {
- const faqUrl = "https://identitysupport.dfinity.org/hc/en-us";
- window.location.replace(faqUrl);
- }
-
const okOrReason = await checkRequiredFeatures(url);
if (okOrReason !== true) {
return compatibilityNotice(okOrReason);
diff --git a/src/internet_identity/src/assets.rs b/src/internet_identity/src/assets.rs
index 7ca4885079..775c6fa08b 100644
--- a/src/internet_identity/src/assets.rs
+++ b/src/internet_identity/src/assets.rs
@@ -261,7 +261,17 @@ fn collect_assets_from_dir(dir: &Dir) -> Vec<(String, Vec, ContentEncoding,
),
};
- assets.push((file_to_asset_path(asset), content, encoding, content_type));
+ let urlpaths = filepath_to_urlpaths(asset.path().to_str().unwrap().to_string());
+ for urlpath in urlpaths {
+ // XXX: we clone the content for each asset instead of doing something smarter
+ // for simplicity & because the only assets that currently may be duplicated are
+ // small HTML files.
+ //
+ // XXX: the behavior is undefined for assets with overlapping URL paths (e.g. "foo.html" &
+ // "foo/index.html"). This assumes that the bundler creating the assets directory
+ // creates sensible assets.
+ assets.push((urlpath, content.clone(), encoding, content_type));
+ }
}
assets
}
@@ -283,33 +293,127 @@ fn file_extension<'a>(asset: &'a File) -> &'a str {
.1
}
-/// Returns the asset path for a given file:
-/// * make relative path absolute
-/// * map **/index.html to **/
-/// * map **/.html to **/foo
-/// * map **/.js.gz to **/.js
-fn file_to_asset_path(asset: &File) -> String {
- // make path absolute
- let mut file_path = "/".to_string() + asset.path().to_str().unwrap();
-
- if file_path.ends_with("index.html") {
- // drop index.html filename (i.e. maps **/index.html to **/)
- file_path = file_path
- .chars()
- .take(file_path.len() - "index.html".len())
- .collect()
- } else if file_path.ends_with(".html") {
- // drop .html file endings (i.e. maps **/.html to **/foo)
- file_path = file_path
- .chars()
- .take(file_path.len() - ".html".len())
- .collect()
- } else if file_path.ends_with(".gz") {
- // drop .gz for .foo.gz files (i.e. maps **/.js.gz to **/.js)
- file_path = file_path
- .chars()
- .take(file_path.len() - ".gz".len())
- .collect()
+/// Returns the URL paths for a given asset filepath. For instance:
+///
+/// * "index.html" -> "/", "/index.html"
+/// * "foo/bar.html" -> "/foo/bar", "/foo/bar/", "foo/bar/index.html"
+///
+/// NOTE: The behavior is undefined if the argument is NOT relative, i.e. if
+/// the filepath has a leading slash.
+///
+/// NOTE: The returned paths will always start with a slash.
+fn filepath_to_urlpaths(file_path: String) -> Vec {
+ // Create paths, WITHOUT leading slash (leading lash is prepended later)
+ fn inner(elements: Vec<&str>, last: &str) -> Vec {
+ if elements.is_empty() && last == "index.html" {
+ // The special case of the root index.html, which we serve
+ // on both "/" and "/index.html"
+ vec!["".to_string(), "index.html".to_string()]
+ } else if last == "index.html" {
+ // An index.html in a subpath
+ let page = elements.join("/").to_string();
+ vec![
+ format!("{page}"),
+ format!("{page}/"),
+ format!("{page}/index.html"),
+ ]
+ } else if let Some(page) = last.strip_suffix(".html") {
+ // A (non-index) HTML page
+ let mut elements = elements.to_vec();
+ elements.push(page);
+ let page = elements.join("/").to_string();
+ vec![
+ format!("{page}"),
+ format!("{page}/"),
+ format!("{page}/index.html"),
+ ]
+ } else if let Some(file) = last.strip_suffix(".gz") {
+ // A gzipped asset; remove suffix and retry
+ // XXX: this recursion is safe (i.e. not infinite) because
+ // we always reduce the argument (remove ".gz")
+ inner(elements, file)
+ } else {
+ // The default cases for any asset
+ // XXX: here we could create an iterator and `intersperse`
+ // the element but this feature is unstable at the time
+ // of writing: https://github.com/rust-lang/rust/issues/79524
+ let mut elements = elements.clone();
+ elements.push(last);
+ let asset = elements.join("/").to_string();
+ vec![asset]
+ }
+ }
+
+ let paths = match file_path.split('/').collect::>().split_last() {
+ None => {
+ // The argument was an empty string
+ // We can't really do much about this, so we fail explicitly
+ panic!("Expected non-empty filepath for asset");
+ }
+ Some((last, elements)) => inner(elements.to_vec(), last),
+ };
+
+ // Prefix everything with "/"
+ paths.into_iter().map(|path| format!("/{path}")).collect()
+}
+
+#[test]
+fn test_filepath_urlpaths() {
+ fn assert_gen_paths(inp: String, exp: Vec) {
+ let mut exp = exp.clone();
+ exp.sort();
+
+ let mut actual = filepath_to_urlpaths(inp);
+ actual.sort();
+ assert_eq!(exp, actual);
}
- file_path
+
+ assert_gen_paths(
+ "index.html".to_string(),
+ vec!["/".to_string(), "/index.html".to_string()],
+ );
+
+ assert_gen_paths(
+ "foo.html".to_string(),
+ vec![
+ "/foo".to_string(),
+ "/foo/".to_string(),
+ "/foo/index.html".to_string(),
+ ],
+ );
+
+ assert_gen_paths(
+ "foo/index.html".to_string(),
+ vec![
+ "/foo".to_string(),
+ "/foo/".to_string(),
+ "/foo/index.html".to_string(),
+ ],
+ );
+
+ assert_gen_paths("index.css".to_string(), vec!["/index.css".to_string()]);
+ assert_gen_paths("foo.bar.gz".to_string(), vec!["/foo.bar".to_string()]);
+
+ assert_gen_paths(
+ "sub/foo.bar.gz".to_string(),
+ vec!["/sub/foo.bar".to_string()],
+ );
+
+ assert_gen_paths(
+ "foo.html.gz".to_string(),
+ vec![
+ "/foo".to_string(),
+ "/foo/".to_string(),
+ "/foo/index.html".to_string(),
+ ],
+ );
+
+ assert_gen_paths(
+ "sub/foo.html.gz".to_string(),
+ vec![
+ "/sub/foo".to_string(),
+ "/sub/foo/".to_string(),
+ "/sub/foo/index.html".to_string(),
+ ],
+ );
}
diff --git a/vite.config.ts b/vite.config.ts
index 8c1183dd60..de172b9ad7 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -43,6 +43,7 @@ export default defineConfig(({ mode }: UserConfig): UserConfig => {
rollupOptions: {
// Bundle only english words in bip39.
external: /.*\/wordlists\/(?!english).*\.json/,
+ input: ["src/frontend/index.html", "src/frontend/faq.html"],
output: {
entryFileNames: `[name].js`,
// II canister only supports resources that contains a single dot in their filenames. qr-creator.js.gz = ok. qr-creator.min.js.gz not ok. qr-creator.es6.min.js.gz no ok.