diff --git a/core/src/avm2/globals/flash/net/FileReference.as b/core/src/avm2/globals/flash/net/FileReference.as index 48230ddbe..eb1849963 100644 --- a/core/src/avm2/globals/flash/net/FileReference.as +++ b/core/src/avm2/globals/flash/net/FileReference.as @@ -64,9 +64,7 @@ package flash.net stub_method("flash.net.FileReference", "requestPermission"); } - public function save(data:*, defaultFileName:String = null):void { - stub_method("flash.net.FileReference", "save"); - } + public native function save(data:*, defaultFileName:String = null):void; public function upload(request:URLRequest, uploadDataFieldName:String = "Filedata", testUpload:Boolean = false):void { stub_method("flash.net.FileReference", "upload"); diff --git a/core/src/avm2/globals/flash/net/file_reference.rs b/core/src/avm2/globals/flash/net/file_reference.rs index a515f868d..a6addd9ac 100644 --- a/core/src/avm2/globals/flash/net/file_reference.rs +++ b/core/src/avm2/globals/flash/net/file_reference.rs @@ -1,5 +1,5 @@ use crate::avm2::bytearray::ByteArrayStorage; -use crate::avm2::error::{make_error_2037, make_error_2097}; +use crate::avm2::error::{argument_error, error, make_error_2037, make_error_2097}; pub use crate::avm2::object::file_reference_allocator; use crate::avm2::object::{ByteArrayObject, FileReference}; use crate::avm2::{Activation, Avm2, Error, EventObject, Object, TObject, Value}; @@ -164,3 +164,57 @@ pub fn load<'gc>( Ok(Value::Undefined) } + +pub fn save<'gc>( + activation: &mut Activation<'_, 'gc>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + let this = this.as_file_reference().unwrap(); + let data = args[0]; + + let data = match data { + Value::Null | Value::Undefined => { + // For some reason this isn't a proper error. + return Err(Error::AvmError(argument_error(activation, "data", 0)?)); + } + Value::Object(obj) => { + if let Some(bytearray) = obj.as_bytearray() { + bytearray.bytes().to_vec() + } else if let Some(xml) = obj.as_xml_object() { + xml.as_xml_string(activation).to_string().into_bytes() + } else { + data.coerce_to_string(activation)?.to_string().into_bytes() + } + } + _ => data.coerce_to_string(activation)?.to_string().into_bytes(), + }; + + let file_name = if let Value::String(name) = args[1] { + name.to_string() + } else { + "".into() + }; + + // Create and spawn dialog + let dialog = activation.context.ui.display_file_save_dialog( + file_name.to_owned(), + format!("Select location to save the file {}", file_name), + ); + + match dialog { + Some(dialog) => { + let process = activation.context.load_manager.save_file_dialog( + activation.context.player.clone(), + this, + dialog, + data, + ); + + activation.context.navigator.spawn_future(process); + } + None => return Err(Error::AvmError(error(activation, "Error #2174: Only one download, upload, load or save operation can be active at a time on each FileReference.", 2174)?)), + } + + Ok(Value::Undefined) +} diff --git a/core/src/loader.rs b/core/src/loader.rs index e050cf2ad..4946cd883 100644 --- a/core/src/loader.rs +++ b/core/src/loader.rs @@ -185,6 +185,9 @@ pub enum Error { #[error("Non-file dialog loader spawned as file dialog loader")] NotFileDialogLoader, + #[error("Non-file save dialog loader spawned as file save dialog loader")] + NotFileSaveDialogLoader, + #[error("Non-file download dialog loader spawned as file download dialog loader")] NotFileDownloadDialogLoader, @@ -250,6 +253,7 @@ impl<'gc> LoadManager<'gc> { | Loader::NetStream { self_handle, .. } | Loader::FileDialog { self_handle, .. } | Loader::FileDialogAvm2 { self_handle, .. } + | Loader::SaveFileDialog { self_handle, .. } | Loader::DownloadFileDialog { self_handle, .. } | Loader::UploadFile { self_handle, .. } | Loader::MovieUnloader { self_handle, .. } => *self_handle = Some(handle), @@ -522,6 +526,24 @@ impl<'gc> LoadManager<'gc> { loader.file_dialog_loader(player, dialog) } + /// Display a dialog allowing a user to save a file + #[must_use] + pub fn save_file_dialog( + &mut self, + player: Weak>, + target_object: FileReferenceObject<'gc>, + dialog: DialogResultFuture, + data: Vec, + ) -> OwnedFuture<(), Error> { + let loader = Loader::SaveFileDialog { + self_handle: None, + target_object, + }; + let handle = self.add_loader(loader); + let loader = self.get_loader_mut(handle).unwrap(); + loader.file_save_dialog_loader(player, dialog, data) + } + /// Display a dialog allowing a user to download a file /// /// Returns a future that will be resolved when a file is selected and the download has completed @@ -736,6 +758,16 @@ pub enum Loader<'gc> { target_object: FileReferenceObject<'gc>, }, + /// Loader that is saving a file to disk from an AVM2 scope. + SaveFileDialog { + /// The handle to refer to this loader instance. + #[collect(require_static)] + self_handle: Option, + + /// The target AVM2 object to select a save location for. + target_object: FileReferenceObject<'gc>, + }, + /// Loader that is downloading a file from an AVM1 object scope. DownloadFileDialog { /// The handle to refer to this loader instance. @@ -2533,6 +2565,115 @@ impl<'gc> Loader<'gc> { }) } + /// Loader to handle saving a file to disk. + pub fn file_save_dialog_loader( + &mut self, + player: Weak>, + dialog: DialogResultFuture, + data: Vec, + ) -> OwnedFuture<(), Error> { + let handle = match self { + Loader::SaveFileDialog { self_handle, .. } => { + self_handle.expect("Loader not self-introduced") + } + _ => return Box::pin(async { Err(Error::NotFileSaveDialogLoader) }), + }; + + let player = player + .upgrade() + .expect("Could not upgrade weak reference to player"); + + Box::pin(async move { + let dialog_result = dialog.await; + + // Dialog is done, allow opening new dialogs + player.lock().unwrap().ui_mut().close_file_dialog(); + + // Fire the load handler. + player.lock().unwrap().update(|uc| -> Result<(), Error> { + let loader = uc.load_manager.get_loader(handle); + let target_object = match loader { + Some(&Loader::SaveFileDialog { target_object, .. }) => target_object, + None => return Err(Error::Cancelled), + _ => return Err(Error::NotFileSaveDialogLoader), + }; + + match dialog_result { + Ok(mut dialog_result) => { + if !dialog_result.is_cancelled() { + dialog_result.write(&data); + dialog_result.refresh(); + target_object.init_from_dialog_result(dialog_result); + + let mut activation = Avm2Activation::from_nothing(uc.reborrow()); + + let select_event = Avm2EventObject::bare_default_event( + &mut activation.context, + "select", + ); + Avm2::dispatch_event( + &mut activation.context, + select_event, + target_object.into(), + ); + + let open_event = Avm2EventObject::bare_default_event( + &mut activation.context, + "open", + ); + Avm2::dispatch_event( + &mut activation.context, + open_event, + target_object.into(), + ); + + let size = data.len() as u64; + let progress_evt = Avm2EventObject::progress_event( + &mut activation, + "progress", + size, + size, + false, + false, + ); + Avm2::dispatch_event( + &mut activation.context, + progress_evt, + target_object.into(), + ); + + let complete_event = Avm2EventObject::bare_default_event( + &mut activation.context, + "complete", + ); + Avm2::dispatch_event( + &mut activation.context, + complete_event, + target_object.into(), + ); + } else { + let mut activation = Avm2Activation::from_nothing(uc.reborrow()); + let cancel_event = Avm2EventObject::bare_default_event( + &mut activation.context, + "cancel", + ); + Avm2::dispatch_event( + &mut activation.context, + cancel_event, + target_object.into(), + ); + } + } + Err(err) => { + tracing::warn!("Save dialog had an error {:?}", err); + } + } + + Ok(()) + }) + }) + } + /// Loader to handle a file download dialog /// /// Fetches the data from `url`, saves the data to the selected destination and processes callbacks