Unverified Commit a7db279c authored by Hong Minhee's avatar Hong Minhee
Browse files

Relative URL handling w/ auto baseUrl inference

- Automatically infer baseUrl from object's @id when not provided
- Use document URL as fallback baseUrl when fetching remote objects
- Add comprehensive tests for relative URL resolution scenarios
- Update property fetching to use documentUrl for baseUrl inference
- Remove manual baseUrl parameter from property getter methods

This eliminates the need for manual baseUrl specification in most cases
while maintaining full compatibility with existing code.

https://github.com/fedify-dev/fedify/issues/411
parent 3b3c0462
Loading
Loading
Loading
Loading
+3 −2
Original line number Diff line number Diff line
@@ -11,8 +11,9 @@ To be released.
### @fedify/fedify

 -  Fixed handling of ActivityPub objects containing relative URLs.  The
    Activity Vocabulary classes now properly resolve relative URLs when
    a `baseUrl` option is provided to `fromJsonLd()` method, improving
    Activity Vocabulary classes now automatically resolve relative URLs by
    inferring the base URL from the object's `@id` or document URL, eliminating
    the need for manual `baseUrl` specification in most cases.  This improves
    interoperability with ActivityPub servers that emit relative URLs in
    properties like `icon.url` and `image.url`.  [[#411], [#443] by Jiwon Kwon]

+1275 −1057

File changed.

Preview size limit exceeded, changes collapsed.

+18 −4
Original line number Diff line number Diff line
@@ -351,6 +351,9 @@ export async function* generateDecoder(
        // deno-lint-ignore no-explicit-any
        (expanded[0] ?? {}) as (Record<string, any[]> & { "@id"?: string });
    }
    if (options.baseUrl == null && values["@id"] != null) {
      options = { ...options, baseUrl: new URL(values["@id"]) };
    }
  `;
  const subtypes = getSubtypes(typeUri, types, true);
  yield `
@@ -434,15 +437,26 @@ export async function* generateDecoder(
    }
    if (property.range.length == 1) {
      yield `${variable}.push(${
        getDecoder(property.range[0], types, "v", "options")
        getDecoder(
          property.range[0],
          types,
          "v",
          "options",
          `(values["@id"] == null ? options.baseUrl : new URL(values["@id"]))`,
        )
      })`;
    } else {
      yield `
      const decoded =
      `;
      for (const code of getDecoders(property.range, types, "v", "options")) {
        yield code;
      }
      const decoders = getDecoders(
        property.range,
        types,
        "v",
        "options",
        `(values["@id"] == null ? options.baseUrl : new URL(values["@id"]))`,
      );
      for (const code of decoders) yield code;
      yield `
      ;
      if (typeof decoded === "undefined") continue;
+5 −10
Original line number Diff line number Diff line
@@ -64,7 +64,6 @@ async function* generateProperty(
        contextLoader?: DocumentLoader,
        suppressError?: boolean,
        tracerProvider?: TracerProvider,
        baseUrl?: URL
      } = {},
    ): Promise<${getTypeNames(property.range, types)} | null> {
      const documentLoader =
@@ -77,7 +76,6 @@ async function* generateProperty(
        ${JSON.stringify(metadata.name)},
        ${JSON.stringify(metadata.version)},
      );
      const baseUrl = options.baseUrl;
      return await tracer.startActiveSpan("activitypub.lookup_object", async (span) => {
        let fetchResult: RemoteDocument;
        try {
@@ -97,7 +95,8 @@ async function* generateProperty(
          }
          throw error;
        }
        const { document } = fetchResult;
        const { document, documentUrl } = fetchResult;
        const baseUrl = new URL(documentUrl);
        try {
          const obj = await this.#${property.singularName}_fromJsonLd(
            document,
@@ -194,7 +193,6 @@ async function* generateProperty(
          contextLoader?: DocumentLoader,
          suppressError?: boolean,
          tracerProvider?: TracerProvider,
          baseUrl?: URL
        } = {}
      ): Promise<${getTypeNames(property.range, types)} | null> {
        if (this._warning != null) {
@@ -226,7 +224,7 @@ async function* generateProperty(
            ${JSON.stringify(property.compactName)}];
          const obj = Array.isArray(prop) ? prop[0] : prop;
          if (obj != null && typeof obj === "object" && "@context" in obj) {
            return await this.#${property.singularName}_fromJsonLd(obj, {...options, baseUrl: options.baseUrl});
            return await this.#${property.singularName}_fromJsonLd(obj, options);
          }
        }
        `;
@@ -263,7 +261,6 @@ async function* generateProperty(
          contextLoader?: DocumentLoader,
          suppressError?: boolean,
          tracerProvider?: TracerProvider,
          baseUrl?: URL
        } = {}
      ): AsyncIterable<${getTypeNames(property.range, types)}> {
        if (this._warning != null) {
@@ -277,8 +274,7 @@ async function* generateProperty(
          const v = vs[i];
          if (v instanceof URL) {
            const fetched =
              await this.#fetch${pascalCase(property.singularName)}(
                v, {...options, baseUrl: options.baseUrl});
              await this.#fetch${pascalCase(property.singularName)}(v, options);
            if (fetched == null) continue;
            vs[i] = fetched;
            this._cachedJsonLd = undefined;
@@ -298,8 +294,7 @@ async function* generateProperty(
              ${JSON.stringify(property.compactName)}];
            const obj = Array.isArray(prop) ? prop[i] : prop;
            if (obj != null && typeof obj === "object" && "@context" in obj) {
              yield await this.#${property.singularName}_fromJsonLd(obj, {...options, baseUrl: options.baseUrl
              });
              yield await this.#${property.singularName}_fromJsonLd(obj, options);
              continue;
            }
          }
+13 −3
Original line number Diff line number Diff line
@@ -577,16 +577,19 @@ export function getDecoder(
  types: Record<string, TypeSchema>,
  variable: string,
  optionsVariable: string,
  baseUrlVariable: string,
): string {
  if (typeUri in scalarTypes) {
    return scalarTypes[typeUri].decoder(
      variable,
      `${optionsVariable}?.baseUrl`,
      baseUrlVariable,
    );
  }
  if (typeUri in types) {
    return `await ${types[typeUri].name}.fromJsonLd(
      ${variable}, ${optionsVariable})`;
      ${variable},
      { ...${optionsVariable}, baseUrl: ${baseUrlVariable} }
    )`;
  }
  throw new Error(`Unknown type: ${typeUri}`);
}
@@ -614,11 +617,18 @@ export function* getDecoders(
  types: Record<string, TypeSchema>,
  variable: string,
  optionsVariable: string,
  baseUrlVariable: string,
): Iterable<string> {
  for (const typeUri of typeUris) {
    yield getDataCheck(typeUri, types, variable);
    yield " ? ";
    yield getDecoder(typeUri, types, variable, optionsVariable);
    yield getDecoder(
      typeUri,
      types,
      variable,
      optionsVariable,
      baseUrlVariable,
    );
    yield " : ";
  }
  yield "undefined";
Loading