ruffle/web/packages/core/src/ruffle-object.ts

285 lines
9.1 KiB
TypeScript
Raw Normal View History

import {
isFallbackElement,
isYoutubeFlashSource,
workaroundYoutubeMixedContent,
RufflePlayer,
web: Fix Ruffle configuration not working properly for Polyfill elements Previously, the Ruffle configuration options weren't working properly for polyfilled elements. A polyfilled element can have specific configuration options which overwrite the more general Ruffle configuration settings. But the code handling those specific configuration options has previously set some of them to default values if they haven't been provided, therefore overwriting the more general configuration settings. This has been fixed. A getPolyfillOptions function has been created and returns a URLLoadOptions object, containing only the options that have been set for the respective element. Helper functions have been adapted to not return any default values anymore. getPolyfillOptions is now used in all places where polyfilled options need to be retrieved (therefore reducing duplicated code). Documentation has been added to clarify that these options must not contain any default values, since those would overwrite other configuration settings with a lower priority. The extension and demo code has been changed to clarify that no default values are contained in the element configuration options. The RuffleEmbed::attributeChangedCallback method has previously loaded an SWF file only with the parameters and base options. This has been fixed as well since it now also uses getPolyfillOptions. When using RuffleObject::connectedCallback to load an SWF file, setting an element config option to "" hasn't worked for most config options before either. This has been fixed as well by using the new getPolyfillOptions function. The default WindowMode value of the default Ruffle web config has been set to Window (as it is in the desktop version and according to the documentation). It has previously been set to Opaque (which causes the same functionality).
2023-09-22 19:06:05 +00:00
getPolyfillOptions,
} from "./ruffle-player";
import { FLASH_ACTIVEX_CLASSID } from "./flash-identifiers";
import { registerElement } from "./register-element";
import { RuffleEmbed } from "./ruffle-embed";
import { isSwf } from "./swf-utils";
2019-08-22 01:02:43 +00:00
/**
* Find and return the first value in obj with the given key.
* Many Flash params were case insensitive, so we use this when checking for them.
*
* @param obj Object to check
* @param key Key to find
* @param defaultValue Value if not found
2020-11-17 22:53:17 +00:00
* @returns Value if found, else [[defaultValue]]
*/
function findCaseInsensitive(
2023-02-20 14:19:18 +00:00
obj: Record<string, string>,
key: string,
2023-07-20 11:19:39 +00:00
defaultValue: string | null,
): string | null {
key = key.toLowerCase();
2023-02-20 14:19:18 +00:00
for (const [k, value] of Object.entries(obj)) {
if (k.toLowerCase() === key) {
return value;
}
}
return defaultValue;
}
/**
* Returns all flash params ([[HTMLParamElement]]) that are for the given object.
*
* @param elem Element to check.
2020-11-17 22:53:17 +00:00
* @returns A record of every parameter.
*/
function paramsOf(elem: Element): Record<string, string> {
const params: Record<string, string> = {};
for (const param of elem.children) {
if (param instanceof HTMLParamElement) {
const key = param.attributes.getNamedItem("name")?.value;
const value = param.attributes.getNamedItem("value")?.value;
if (key && value) {
params[key] = value;
}
}
}
return params;
}
/**
* A polyfill html element.
*
* This specific class tries to polyfill existing `<object>` tags,
* and should not be used. Prefer [[RufflePlayer]] instead.
*
* @internal
*/
export class RuffleObject extends RufflePlayer {
private params: Record<string, string> = {};
/**
* Constructs a new Ruffle flash player for insertion onto the page.
*
* This specific class tries to polyfill existing `<object>` tags,
* and should not be used. Prefer [[RufflePlayer]] instead.
*/
constructor() {
super();
2019-08-22 01:02:43 +00:00
}
/**
* @ignore
2020-11-17 22:53:17 +00:00
* @internal
*/
2023-02-20 14:19:18 +00:00
override connectedCallback(): void {
super.connectedCallback();
this.params = paramsOf(this);
let url = null;
if (this.attributes.getNamedItem("data")) {
url = this.attributes.getNamedItem("data")?.value;
2023-02-20 14:19:18 +00:00
} else if (this.params["movie"]) {
url = this.params["movie"];
}
if (url) {
web: Fix Ruffle configuration not working properly for Polyfill elements Previously, the Ruffle configuration options weren't working properly for polyfilled elements. A polyfilled element can have specific configuration options which overwrite the more general Ruffle configuration settings. But the code handling those specific configuration options has previously set some of them to default values if they haven't been provided, therefore overwriting the more general configuration settings. This has been fixed. A getPolyfillOptions function has been created and returns a URLLoadOptions object, containing only the options that have been set for the respective element. Helper functions have been adapted to not return any default values anymore. getPolyfillOptions is now used in all places where polyfilled options need to be retrieved (therefore reducing duplicated code). Documentation has been added to clarify that these options must not contain any default values, since those would overwrite other configuration settings with a lower priority. The extension and demo code has been changed to clarify that no default values are contained in the element configuration options. The RuffleEmbed::attributeChangedCallback method has previously loaded an SWF file only with the parameters and base options. This has been fixed as well since it now also uses getPolyfillOptions. When using RuffleObject::connectedCallback to load an SWF file, setting an element config option to "" hasn't worked for most config options before either. This has been fixed as well by using the new getPolyfillOptions function. The default WindowMode value of the default Ruffle web config has been set to Window (as it is in the desktop version and according to the documentation). It has previously been set to Opaque (which causes the same functionality).
2023-09-22 19:06:05 +00:00
// Get the configuration options that have been overwritten for this movie.
const attributeCheckOptions = [
"allowNetworking",
"base",
"bgcolor",
"flashvars",
];
const getOptionString = (optionName: string) =>
findCaseInsensitive(
this.params,
optionName,
attributeCheckOptions.includes(optionName)
? this.getAttribute(optionName)
: null,
);
const options = getPolyfillOptions(url, getOptionString);
// Kick off the SWF download.
this.load(options, true);
2019-08-22 01:02:43 +00:00
}
}
2023-02-20 14:19:18 +00:00
protected override debugPlayerInfo(): string {
let result = "Player type: Object\n";
let url = null;
if (this.attributes.getNamedItem("data")) {
url = this.attributes.getNamedItem("data")?.value;
2023-02-20 14:19:18 +00:00
} else if (this.params["movie"]) {
url = this.params["movie"];
}
result += `SWF URL: ${url}\n`;
Object.keys(this.params).forEach((key) => {
result += `Param ${key}: ${this.params[key]}\n`;
});
Object.keys(this.attributes).forEach((key) => {
result += `Attribute ${key}: ${
this.attributes.getNamedItem(key)?.value
}\n`;
});
return result;
}
/**
* Polyfill of HTMLObjectElement.
*
* @ignore
2020-11-17 22:53:17 +00:00
* @internal
*/
get data(): string | null {
return this.getAttribute("data");
2019-09-14 19:40:19 +00:00
}
/**
* Polyfill of HTMLObjectElement.
*
* @ignore
2020-11-17 22:53:17 +00:00
* @internal
*/
set data(href: string | null) {
2022-05-05 09:16:29 +00:00
if (href) {
const attr = document.createAttribute("data");
attr.value = href;
this.attributes.setNamedItem(attr);
} else {
this.attributes.removeNamedItem("data");
}
2019-09-14 19:40:19 +00:00
}
/**
* Checks if the given element may be polyfilled with this one.
*
* @param elem Element to check.
* @returns True if the element looks like a Flash object.
*/
static isInterdictable(elem: Element): boolean {
// Don't polyfill if the element is inside a specific node.
if (isFallbackElement(elem)) {
return false;
}
// Don't polyfill if there's already a <ruffle-object> or a <ruffle-embed> inside the <object>.
if (
elem.getElementsByTagName("ruffle-object").length > 0 ||
elem.getElementsByTagName("ruffle-embed").length > 0
) {
return false;
}
const data = elem.attributes.getNamedItem("data")?.value.toLowerCase();
const type = elem.attributes.getNamedItem("type")?.value ?? null;
const params = paramsOf(elem);
// Check for SWF file.
let filename;
if (data) {
// Don't polyfill when the file is a YouTube Flash source.
if (isYoutubeFlashSource(data)) {
// Workaround YouTube mixed content; this isn't what browsers do automatically, but while we're here, we may as well.
workaroundYoutubeMixedContent(elem, "data");
return false;
}
filename = data;
2023-02-20 14:19:18 +00:00
} else if (params && params["movie"]) {
// Don't polyfill when the file is a YouTube Flash source.
2023-02-20 14:19:18 +00:00
if (isYoutubeFlashSource(params["movie"])) {
// Workaround YouTube mixed content; this isn't what browsers do automatically, but while we're here, we may as well.
const movieElem = elem.querySelector("param[name='movie']");
if (movieElem) {
workaroundYoutubeMixedContent(movieElem, "value");
// The data attribute needs to be set for the re-fetch to happen.
// It also needs to be set on Firefox for the YouTube object rewrite to work, regardless of mixed content.
const movieSrc = movieElem.getAttribute("value");
if (movieSrc) {
elem.setAttribute("data", movieSrc);
}
}
return false;
}
filename = params["movie"];
} else {
// Don't polyfill when no file is specified.
return false;
}
// Check ActiveX class ID.
const classid = elem.attributes
.getNamedItem("classid")
?.value.toLowerCase();
if (classid === FLASH_ACTIVEX_CLASSID.toLowerCase()) {
// classid is an old-IE style embed that would not work on modern browsers.
// Often there will be an <embed> inside the <object> that would take precedence.
// Only polyfill this <object> if it doesn't contain a polyfillable <embed> or
// another <object> that would be supported on modern browsers.
return (
!Array.from(elem.getElementsByTagName("object")).some(
2023-07-20 11:19:39 +00:00
RuffleObject.isInterdictable,
) &&
!Array.from(elem.getElementsByTagName("embed")).some(
2023-07-20 11:19:39 +00:00
RuffleEmbed.isInterdictable,
)
);
2022-05-05 09:16:29 +00:00
} else if (classid) {
// Non-Flash classid.
return false;
}
return isSwf(filename, type);
2019-08-22 01:02:43 +00:00
}
/**
* Creates a RuffleObject that will polyfill and replace the given element.
*
* @param elem Element to replace.
2020-11-17 22:53:17 +00:00
* @returns Created RuffleObject.
*/
static fromNativeObjectElement(elem: Element): RuffleObject {
const externalName = registerElement("ruffle-object", RuffleObject);
const ruffleObj: RuffleObject = <RuffleObject>(
document.createElement(externalName)
);
// Avoid copying embeds-inside-objects to avoid double polyfilling.
for (const embedElem of Array.from(
2023-07-20 11:19:39 +00:00
elem.getElementsByTagName("embed"),
)) {
if (RuffleEmbed.isInterdictable(embedElem)) {
embedElem.remove();
}
}
// Avoid copying objects-inside-objects to avoid double polyfilling.
// This may happen when Internet Explorer's conditional comments are used.
for (const objectElem of Array.from(
2023-07-20 11:19:39 +00:00
elem.getElementsByTagName("object"),
)) {
if (RuffleObject.isInterdictable(objectElem)) {
objectElem.remove();
}
}
ruffleObj.copyElement(elem);
2019-08-22 01:02:43 +00:00
return ruffleObj;
2019-08-22 01:02:43 +00:00
}
}