From 1709e7640990f20d5b91f01367b1fe5369c75849 Mon Sep 17 00:00:00 2001 From: Mike Welsh Date: Wed, 22 Jul 2020 18:22:23 -0700 Subject: [PATCH] avm1: Implement LoadVars --- core/src/avm1/activation.rs | 46 +++- core/src/avm1/globals.rs | 10 + core/src/avm1/globals/load_vars.rs | 383 +++++++++++++++++++++++++++++ core/src/loader.rs | 116 ++++++++- 4 files changed, 543 insertions(+), 12 deletions(-) create mode 100644 core/src/avm1/globals/load_vars.rs diff --git a/core/src/avm1/activation.rs b/core/src/avm1/activation.rs index bd96ddb82..db50421c4 100644 --- a/core/src/avm1/activation.rs +++ b/core/src/avm1/activation.rs @@ -2488,23 +2488,22 @@ impl<'a, 'gc: 'a> Activation<'a, 'gc> { } } - /// Convert the current locals pool into a set of form values. + /// Convert the enumerable properties of an object into a set of form values. /// /// This is necessary to support form submission from Flash via a couple of /// legacy methods, such as the `ActionGetURL2` opcode or `getURL` function. /// /// WARNING: This does not support user defined virtual properties! - pub fn locals_into_form_values( + pub fn object_into_form_values( &mut self, context: &mut UpdateContext<'_, 'gc, '_>, + object: Object<'gc>, ) -> HashMap { let mut form_values = HashMap::new(); - let scope = self.scope_cell(); - let locals = scope.read().locals_cell(); - let keys = locals.get_keys(self); + let keys = object.get_keys(self); for k in keys { - let v = locals.get(&k, self, context); + let v = object.get(&k, self, context); //TODO: What happens if an error occurs inside a virtual property? form_values.insert( @@ -2520,17 +2519,18 @@ impl<'a, 'gc: 'a> Activation<'a, 'gc> { form_values } - /// Construct request options for a fetch operation that may send locals as + /// Construct request options for a fetch operation that may sends object properties as /// form data in the request body or URL. - pub fn locals_into_request_options<'b>( + pub fn object_into_request_options<'b>( &mut self, context: &mut UpdateContext<'_, 'gc, '_>, + object: Object<'gc>, url: Cow<'b, str>, method: Option, ) -> (Cow<'b, str>, RequestOptions) { match method { Some(method) => { - let vars = self.locals_into_form_values(context); + let vars = self.object_into_form_values(context, object); let qstring = form_urlencoded::Serializer::new(String::new()) .extend_pairs(vars.iter()) .finish(); @@ -2557,6 +2557,34 @@ impl<'a, 'gc: 'a> Activation<'a, 'gc> { } } + /// Convert the current locals pool into a set of form values. + /// + /// This is necessary to support form submission from Flash via a couple of + /// legacy methods, such as the `ActionGetURL2` opcode or `getURL` function. + /// + /// WARNING: This does not support user defined virtual properties! + pub fn locals_into_form_values( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> HashMap { + let scope = self.scope_cell(); + let locals = scope.read().locals_cell(); + self.object_into_form_values(context, locals) + } + + /// Construct request options for a fetch operation that may send locals as + /// form data in the request body or URL. + pub fn locals_into_request_options<'b>( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + url: Cow<'b, str>, + method: Option, + ) -> (Cow<'b, str>, RequestOptions) { + let scope = self.scope_cell(); + let locals = scope.read().locals_cell(); + self.object_into_request_options(context, locals, url, method) + } + /// Resolves a target value to a display object, relative to a starting display object. /// /// This is used by any action/function with a parameter that can be either diff --git a/core/src/avm1/globals.rs b/core/src/avm1/globals.rs index f695328f5..2fb494523 100644 --- a/core/src/avm1/globals.rs +++ b/core/src/avm1/globals.rs @@ -20,6 +20,7 @@ pub(crate) mod display_object; pub(crate) mod error; mod function; mod key; +mod load_vars; mod math; mod matrix; pub(crate) mod mouse; @@ -250,6 +251,8 @@ pub fn create_globals<'gc>( let number_proto: Object<'gc> = number::create_proto(gc_context, object_proto, function_proto); let boolean_proto: Object<'gc> = boolean::create_proto(gc_context, object_proto, function_proto); + let load_vars_proto: Object<'gc> = + load_vars::create_proto(gc_context, object_proto, function_proto); let matrix_proto: Object<'gc> = matrix::create_proto(gc_context, object_proto, function_proto); let point_proto: Object<'gc> = point::create_proto(gc_context, object_proto, function_proto); let rectangle_proto: Object<'gc> = @@ -288,6 +291,12 @@ pub fn create_globals<'gc>( Some(function_proto), Some(function_proto), ); + let load_vars = FunctionObject::function( + gc_context, + Executable::Native(load_vars::constructor), + Some(function_proto), + Some(load_vars_proto), + ); let movie_clip = FunctionObject::function( gc_context, Executable::Native(movie_clip::constructor), @@ -371,6 +380,7 @@ pub fn create_globals<'gc>( globals.define_value(gc_context, "Error", error.into(), EnumSet::empty()); globals.define_value(gc_context, "Object", object.into(), EnumSet::empty()); globals.define_value(gc_context, "Function", function.into(), EnumSet::empty()); + globals.define_value(gc_context, "LoadVars", load_vars.into(), EnumSet::empty()); globals.define_value(gc_context, "MovieClip", movie_clip.into(), EnumSet::empty()); globals.define_value( gc_context, diff --git a/core/src/avm1/globals/load_vars.rs b/core/src/avm1/globals/load_vars.rs new file mode 100644 index 000000000..c2def0f07 --- /dev/null +++ b/core/src/avm1/globals/load_vars.rs @@ -0,0 +1,383 @@ +//! AVM1 LoadVars object +//! TODO: bytesLoaded, bytesTotal, contentType, addRequestHeader + +use crate::avm1::activation::Activation; +use crate::avm1::error::Error; +use crate::avm1::property::Attribute; +use crate::avm1::{AvmString, Object, ScriptObject, TObject, UpdateContext, Value}; +use crate::backend::navigator::{NavigationMethod, RequestOptions}; +use gc_arena::MutationContext; +use std::borrow::Cow; + +/// Implements `LoadVars` +pub fn constructor<'gc>( + _activation: &mut Activation<'_, 'gc>, + _context: &mut UpdateContext<'_, 'gc, '_>, + _this: Object<'gc>, + _args: &[Value<'gc>], +) -> Result, Error<'gc>> { + // No-op constructor + Ok(Value::Undefined) +} + +pub fn create_proto<'gc>( + gc_context: MutationContext<'gc, '_>, + proto: Object<'gc>, + fn_proto: Object<'gc>, +) -> Object<'gc> { + use Attribute::*; + + let mut object = ScriptObject::object(gc_context, Some(proto)); + + object.force_set_function( + "load", + load, + gc_context, + DontDelete | DontEnum | ReadOnly, + Some(fn_proto), + ); + + object.force_set_function( + "send", + send, + gc_context, + DontDelete | DontEnum | ReadOnly, + Some(fn_proto), + ); + + object.force_set_function( + "sendAndLoad", + send_and_load, + gc_context, + DontDelete | DontEnum | ReadOnly, + Some(fn_proto), + ); + + object.force_set_function( + "decode", + decode, + gc_context, + DontDelete | DontEnum | ReadOnly, + Some(fn_proto), + ); + + object.force_set_function( + "getBytesLoaded", + get_bytes_loaded, + gc_context, + DontDelete | DontEnum | ReadOnly, + Some(fn_proto), + ); + + object.force_set_function( + "getBytesTotal", + get_bytes_total, + gc_context, + DontDelete | DontEnum | ReadOnly, + Some(fn_proto), + ); + + object.force_set_function( + "toString", + to_string, + gc_context, + DontDelete | DontEnum | ReadOnly, + Some(fn_proto), + ); + + object.define_value( + gc_context, + "contentType", + "application/x-www-form-url-encoded".into(), + DontDelete | DontEnum | ReadOnly, + ); + + object.force_set_function( + "onLoad", + on_load, + gc_context, + DontDelete | DontEnum | ReadOnly, + Some(fn_proto), + ); + + object.force_set_function( + "onData", + on_data, + gc_context, + DontDelete | DontEnum | ReadOnly, + Some(fn_proto), + ); + + object.force_set_function( + "addRequestHeader", + add_request_header, + gc_context, + DontDelete | DontEnum | ReadOnly, + Some(fn_proto), + ); + + object.into() +} + +fn add_request_header<'gc>( + _activation: &mut Activation<'_, 'gc>, + _context: &mut UpdateContext<'_, 'gc, '_>, + _this: Object<'gc>, + _args: &[Value<'gc>], +) -> Result, Error<'gc>> { + log::warn!("LoadVars.addRequestHeader: Unimplemented"); + Ok(Value::Undefined) +} + +fn decode<'gc>( + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + // Spec says added in SWF 7, but not version gated. + // Decode the query string into properties on this object. + if let Some(data) = args.get(0) { + let data = data.coerce_to_string(activation, context)?; + for (k, v) in url::form_urlencoded::parse(data.as_bytes()) { + this.set( + &k, + crate::avm1::AvmString::new(context.gc_context, v.into_owned()).into(), + activation, + context, + )?; + } + } + + Ok(Value::Undefined) +} + +fn get_bytes_loaded<'gc>( + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + _args: &[Value<'gc>], +) -> Result, Error<'gc>> { + // Forwards to undocumented property on the object. + this.get("_bytesLoaded", activation, context) +} + +fn get_bytes_total<'gc>( + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + _args: &[Value<'gc>], +) -> Result, Error<'gc>> { + // Forwards to undocumented property on the object. + this.get("_bytesTotal", activation, context) +} + +fn load<'gc>( + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + let url = match args.get(0) { + Some(val) => val.coerce_to_string(activation, context)?, + None => return Ok(false.into()), + }; + + spawn_load_var_fetch(activation, context, this, &url, None)?; + + Ok(true.into()) +} + +fn on_data<'gc>( + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + // Default implementation forwards to decode and onLoad. + let success = match args.get(0) { + None | Some(Value::Undefined) | Some(Value::Null) => false, + Some(val) => { + this.call_method(&"decode", &[val.clone()], activation, context)?; + this.set("loaded", true.into(), activation, context)?; + true + } + }; + + this.call_method(&"onLoad", &[success.into()], activation, context)?; + + Ok(Value::Undefined) +} + +fn on_load<'gc>( + _activation: &mut Activation<'_, 'gc>, + _context: &mut UpdateContext<'_, 'gc, '_>, + _this: Object<'gc>, + _args: &[Value<'gc>], +) -> Result, Error<'gc>> { + // Default implementation: no-op? + Ok(Value::Undefined) +} + +fn send<'gc>( + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + // `send` navigates the browser to a URL with the given query parameter. + let url = match args.get(0) { + Some(url) => url.coerce_to_string(activation, context)?, + None => return Ok(false.into()), + }; + + let window = match args.get(1) { + Some(v) => Some(v.coerce_to_string(activation, context)?), + None => None, + }; + + let method_name = args + .get(1) + .unwrap_or(&Value::Undefined) + .coerce_to_string(activation, context)?; + let method = NavigationMethod::from_method_str(&method_name).unwrap_or(NavigationMethod::POST); + + use std::collections::HashMap; + + let mut form_values = HashMap::new(); + let keys = this.get_keys(activation); + + for k in keys { + let v = this.get(&k, activation, context); + + form_values.insert( + k, + v.ok() + .unwrap_or_else(|| Value::Undefined) + .coerce_to_string(activation, context) + .unwrap_or_else(|_| "undefined".into()) + .to_string(), + ); + } + + if let Some(window) = window { + context.navigator.navigate_to_url( + url.to_string(), + Some(window.to_string()), + Some((method, form_values)), + ); + } + + Ok(true.into()) +} + +fn send_and_load<'gc>( + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + let url_val = args.get(0).cloned().unwrap_or(Value::Undefined); + let url = url_val.coerce_to_string(activation, context)?; + let target = match args.get(1) { + Some(&Value::Object(o)) => o, + _ => return Ok(false.into()), + }; + + let method_name = args + .get(2) + .unwrap_or(&Value::Undefined) + .coerce_to_string(activation, context)?; + let method = NavigationMethod::from_method_str(&method_name).unwrap_or(NavigationMethod::POST); + + spawn_load_var_fetch(activation, context, target, &url, Some((this, method)))?; + + Ok(true.into()) +} + +fn to_string<'gc>( + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + _args: &[Value<'gc>], +) -> Result, Error<'gc>> { + use std::collections::HashMap; + + let mut form_values = HashMap::new(); + let keys = this.get_keys(activation); + + for k in keys { + let v = this.get(&k, activation, context); + + //TODO: What happens if an error occurs inside a virtual property? + form_values.insert( + k, + v.ok() + .unwrap_or_else(|| Value::Undefined) + .coerce_to_string(activation, context) + .unwrap_or_else(|_| "undefined".into()) + .to_string(), + ); + } + + let query_string = url::form_urlencoded::Serializer::new(String::new()) + .extend_pairs(form_values.iter()) + .finish(); + + Ok(crate::avm1::AvmString::new(context.gc_context, query_string).into()) +} + +fn spawn_load_var_fetch<'gc>( + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + loader_object: Object<'gc>, + url: &AvmString, + send_object: Option<(Object<'gc>, NavigationMethod)>, +) -> Result, Error<'gc>> { + let (url, request_options) = if let Some((send_object, method)) = send_object { + // Send properties from `send_object`. + activation.object_into_request_options( + context, + send_object, + Cow::Borrowed(&url), + Some(method), + ) + } else { + // Not sending any parameters. + (Cow::Borrowed(url.as_str()), RequestOptions::get()) + }; + + let fetch = context.navigator.fetch(&url, request_options); + let process = context.load_manager.load_form_into_load_vars( + context.player.clone().unwrap(), + loader_object, + fetch, + ); + + // Create hidden properties on object. + if !loader_object.has_property(activation, context, "_bytesLoaded") { + loader_object.define_value( + context.gc_context, + "_bytesLoaded", + 0.into(), + Attribute::DontDelete | Attribute::DontEnum, + ); + } else { + loader_object.set("_bytesLoaded", 0.into(), activation, context)?; + } + + if !loader_object.has_property(activation, context, "loaded") { + loader_object.define_value( + context.gc_context, + "loaded", + false.into(), + Attribute::DontDelete | Attribute::DontEnum, + ); + } else { + loader_object.set("loaded", false.into(), activation, context)?; + } + + context.navigator.spawn_future(process); + + Ok(true.into()) +} diff --git a/core/src/loader.rs b/core/src/loader.rs index d0aed7607..1384d40ce 100644 --- a/core/src/loader.rs +++ b/core/src/loader.rs @@ -3,7 +3,7 @@ use crate::avm1::activation::{Activation, ActivationIdentifier}; use crate::avm1::{AvmString, Object, TObject, Value}; use crate::backend::navigator::OwnedFuture; -use crate::context::{ActionQueue, ActionType}; +use crate::context::{ActionQueue, ActionType, UpdateContext}; use crate::display_object::{DisplayObject, MorphShape, TDisplayObject}; use crate::player::{Player, NEWEST_PLAYER_VERSION}; use crate::tag_utils::SwfMovie; @@ -28,6 +28,9 @@ pub enum Error { #[error("Non-form loader spawned as form loader")] NotFormLoader, + #[error("Non-load vars loader spawned as load vars loader")] + NotLoadVarsLoader, + #[error("Non-XML loader spawned as XML loader")] NotXmlLoader, @@ -46,6 +49,16 @@ pub enum Error { Avm1Error(String), } +pub type FormLoadHandler<'gc> = fn( + &mut Activation<'_, 'gc>, + &mut UpdateContext<'_, 'gc, '_>, + Object<'gc>, + data: &[u8], +) -> Result<(), Error>; + +pub type FormErrorHandler<'gc> = + fn(&mut Activation<'_, 'gc>, &mut UpdateContext<'_, 'gc, '_>, Object<'gc>) -> Result<(), Error>; + impl From> for Error { fn from(error: crate::avm1::error::Error<'_>) -> Self { Error::Avm1Error(error.to_string()) @@ -163,6 +176,27 @@ impl<'gc> LoadManager<'gc> { loader.form_loader(player, fetch) } + /// Kick off a form data load into an AVM1 object. + /// + /// Returns the loader's async process, which you will need to spawn. + pub fn load_form_into_load_vars( + &mut self, + player: Weak>, + target_object: Object<'gc>, + fetch: OwnedFuture, Error>, + ) -> OwnedFuture<(), Error> { + let loader = Loader::LoadVars { + self_handle: None, + target_object, + }; + let handle = self.add_loader(loader); + + let loader = self.get_loader_mut(handle).unwrap(); + loader.introduce_loader_handle(handle); + + loader.load_vars_loader(player, fetch) + } + /// Kick off an XML data load into an XML node. /// /// Returns the loader's async process, which you will need to spawn. @@ -227,6 +261,15 @@ pub enum Loader<'gc> { target_object: Object<'gc>, }, + /// Loader that is loading form data into an AVM1 LoadVars object. + LoadVars { + /// The handle to refer to this loader instance. + self_handle: Option, + + /// The target AVM1 object to load form data into. + target_object: Object<'gc>, + }, + /// Loader that is loading XML data into an XML tree. XML { /// The handle to refer to this loader instance. @@ -257,6 +300,7 @@ unsafe impl<'gc> Collect for Loader<'gc> { target_broadcaster.trace(cc); } Loader::Form { target_object, .. } => target_object.trace(cc), + Loader::LoadVars { target_object, .. } => target_object.trace(cc), Loader::XML { target_node, .. } => target_node.trace(cc), } } @@ -271,6 +315,7 @@ impl<'gc> Loader<'gc> { match self { Loader::Movie { self_handle, .. } => *self_handle = Some(handle), Loader::Form { self_handle, .. } => *self_handle = Some(handle), + Loader::LoadVars { self_handle, .. } => *self_handle = Some(handle), Loader::XML { self_handle, .. } => *self_handle = Some(handle), } } @@ -467,12 +512,13 @@ impl<'gc> Loader<'gc> { Box::pin(async move { let data = fetch.await?; + // Fire the load handler. player.lock().unwrap().update(|avm1, _avm2, uc| { let loader = uc.load_manager.get_loader(handle); let that = match loader { - Some(Loader::Form { target_object, .. }) => *target_object, + Some(&Loader::Form { target_object, .. }) => target_object, None => return Err(Error::Cancelled), - _ => return Err(Error::NotMovieLoader), + _ => return Err(Error::NotFormLoader), }; let mut activation = Activation::from_nothing( @@ -498,6 +544,70 @@ impl<'gc> Loader<'gc> { }) } + /// Creates a future for a LoadVars load call. + pub fn load_vars_loader( + &mut self, + player: Weak>, + fetch: OwnedFuture, Error>, + ) -> OwnedFuture<(), Error> { + let handle = match self { + Loader::LoadVars { self_handle, .. } => { + self_handle.expect("Loader not self-introduced") + } + _ => return Box::pin(async { Err(Error::NotLoadVarsLoader) }), + }; + + let player = player + .upgrade() + .expect("Could not upgrade weak reference to player"); + + Box::pin(async move { + let data = fetch.await; + + // Fire the load handler. + player.lock().unwrap().update(|avm1, _avm2, uc| { + let loader = uc.load_manager.get_loader(handle); + let that = match loader { + Some(&Loader::LoadVars { target_object, .. }) => target_object, + None => return Err(Error::Cancelled), + _ => return Err(Error::NotLoadVarsLoader), + }; + + let mut activation = Activation::from_nothing( + avm1, + ActivationIdentifier::root("[Form Loader]"), + uc.swf.version(), + avm1.global_object_cell(), + uc.gc_context, + *uc.levels.get(&0).unwrap(), + ); + + match data { + Ok(data) => { + // Fire the onData method with the loaded string. + let string_data = + AvmString::new(uc.gc_context, String::from_utf8_lossy(&data)); + let _ = + that.call_method("onData", &[string_data.into()], &mut activation, uc); + } + Err(_) => { + // TODO: Log "Error opening URL" trace similar to the Flash Player? + // Simulate 404 HTTP status. This should probably be fired elsewhere + // because a failed local load doesn't fire a 404. + let _ = + that.call_method("onHTTPStatus", &[404.into()], &mut activation, uc); + + // Fire the onData method with no data to indicate an unsuccessful load. + let _ = + that.call_method("onData", &[Value::Undefined], &mut activation, uc); + } + } + + Ok(()) + }) + }) + } + /// Event handler morally equivalent to `onLoad` on a movie clip. /// /// Returns `true` if the loader has completed and should be removed.