/* Copyright 2012-2013 Joe Sapp nixphoeni@gentoo.org * * This software is licensed under the GNU LGPL (version 2.1 or later). * See the COPYING file in this distribution. */ static const string G3_VERSION = "0.1"; static const string G3_LICENSE = """ The Gallery3Publishing module is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. The Gallery3Publishing module is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with The Gallery3Publishing module; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA """; static const string WEBSITE_URL = "https://github.com/sappjw/shotwell-gallery3"; // This module's Spit.Module private class ShotwellPublishingGallery3 : Object, Spit.Module { private Spit.Pluggable[] pluggables = new Spit.Pluggable[0]; public ShotwellPublishingGallery3(GLib.File module_file) { GLib.File resource_directory = module_file.get_parent(); pluggables += new Gallery3Service(resource_directory); } public unowned string get_module_name() { return _("Gallery3 publishing module"); } public unowned string get_version() { return G3_VERSION; } public unowned string get_id() { return "org.yorba.shotwell.sharing.gallery3"; } public unowned Spit.Pluggable[]? get_pluggables() { return pluggables; } } // The Pluggable public class Gallery3Service : Object, Spit.Pluggable, Spit.Publishing.Service { private const string ICON_FILENAME = "gallery3.png"; private static Gdk.Pixbuf[] icon_pixbuf_set = null; public Gallery3Service(GLib.File resource_directory) { if (icon_pixbuf_set == null) icon_pixbuf_set = Resources.load_icon_set( resource_directory.get_child(ICON_FILENAME)); } public int get_pluggable_interface(int min_host_interface, int max_host_interface) { return Spit.negotiate_interfaces(min_host_interface, max_host_interface, Spit.Publishing.CURRENT_INTERFACE); } public unowned string get_id() { return "publishing-gallery3"; } public unowned string get_pluggable_name() { return "Gallery3"; } public void get_info(ref Spit.PluggableInfo info) { info.authors = "Joe Sapp"; info.copyright = "2012-2013 Joe Sapp"; info.translators = Resources.TRANSLATORS; info.version = G3_VERSION; info.website_url = WEBSITE_URL; info.is_license_wordwrapped = false; info.license = G3_LICENSE; info.icons = icon_pixbuf_set; } public void activation(bool enabled) { } public Spit.Publishing.Publisher create_publisher( Spit.Publishing.PluginHost host) { return new Publishing.Gallery3.GalleryPublisher(this, host); } public Spit.Publishing.Publisher.MediaType get_supported_media() { return (Spit.Publishing.Publisher.MediaType.PHOTO | Spit.Publishing.Publisher.MediaType.VIDEO); } } namespace Publishing.Gallery3 { private const string SERVICE_NAME = "Gallery3"; private const string SERVICE_WELCOME_MESSAGE = _("You are not currently logged into your Gallery.\n\nYou must have already signed up for a Gallery3 account to complete the login process."); private const string DEFAULT_ALBUM_DIR = _("Shotwell"); private const string DEFAULT_ALBUM_TITLE = _("Shotwell default directory"); private const string REST_PATH = "/index.php/rest"; private class Album { // Properties public string name { get; private set; default = ""; } public string title { get; private set; default = ""; } public string summary { get; private set; default = ""; } public string parentname { get; private set; default = ""; } public string url { get; private set; default = ""; } public string path { get; private set; default = ""; } public bool editable { get; private set; default = false; } // Each element is a collection public Album(Json.Object collection) { unowned Json.Object entity = collection.get_object_member("entity"); title = entity.get_string_member("title"); name = entity.get_string_member("name"); parentname = entity.get_string_member("parent"); url = collection.get_string_member("url"); editable = entity.get_boolean_member("can_edit"); // Get the path from the last two elements of the URL. // This should always be "/item/#" where "#" is a number. path = strip_session_url(url); } } private class BaseGalleryTransaction : Publishing.RESTSupport.Transaction { protected Json.Parser parser; // BaseGalleryTransaction constructor public BaseGalleryTransaction(Session session, string endpoint_url, string item_path = "", Publishing.RESTSupport.HttpMethod method = Publishing.RESTSupport.HttpMethod.POST) { // TODO: eventually we can remove this if ((item_path != "") && (item_path[0] != '/')) { warning("Bad item path, this is a bug!"); error(item_path); } base.with_endpoint_url(session, endpoint_url + REST_PATH + item_path, method); this.parser = new Json.Parser(); } protected unowned Json.Node get_root_node() throws Spit.Publishing.PublishingError { string json_object; unowned Json.Node root_node; json_object = get_response(); if ((null == json_object) || (0 == json_object.length)) throw new Spit.Publishing.PublishingError.MALFORMED_RESPONSE( "No response data from %s", get_endpoint_url()); try { this.parser.load_from_data(json_object); } catch (GLib.Error e) { // If this didn't work, reset the "executed" state warning("ERROR: didn't load JSON data"); set_is_executed(false); throw new Spit.Publishing.PublishingError.PROTOCOL_ERROR(e.message); } root_node = this.parser.get_root(); if (root_node.is_null()) throw new Spit.Publishing.PublishingError.MALFORMED_RESPONSE( "Root node is null, doesn't appear to be JSON data"); return root_node; } } private class KeyFetchTransaction : BaseGalleryTransaction { private string key = ""; // KeyFetchTransaction constructor // // url: Base gallery URL public KeyFetchTransaction(Session session, string url, string username, string password) { base(session, url); add_argument("user", username); add_argument("password", password); } public string get_key() { if (key != "") return key; key = get_response(); // The returned data isn't actually a JSON object... if (null == key || "" == key || 0 == key.length) { warning("No response data from \"%s\"", get_endpoint_url()); return ""; } // Eliminate quotes surrounding key key = key[1:-1]; return key; } } private class GalleryRequestTransaction : BaseGalleryTransaction { // GalleryRequestTransaction constructor // // item: Item URL component public GalleryRequestTransaction(Session session, string item, Publishing.RESTSupport.HttpMethod method = Publishing.RESTSupport.HttpMethod.GET) { if (!session.is_authenticated()) { error("Not authenticated"); } else { base(session, session.url, item, method); add_header("X-Gallery-Request-Key", session.key); add_header("X-Gallery-Request-Method", "GET"); } } } private class GetAlbumURLsTransaction : GalleryRequestTransaction { public GetAlbumURLsTransaction(Session session) { base(session, "/item/1"); add_argument("type", "album"); add_argument("scope", "all"); } public string [] get_album_urls() { unowned Json.Node root_node; unowned Json.Array all_members; try { root_node = get_root_node(); } catch (Spit.Publishing.PublishingError e) { error("Could not get root node"); } all_members = root_node.get_object().get_array_member("members"); string [] member_urls = null; for (uint i = 0; i <= all_members.get_length() - 1; i++) member_urls += all_members.get_string_element(i); return member_urls; } } private class GetAlbumsTransaction : GalleryRequestTransaction { // Properties // Original list of album URLs public string [] album_urls { get; private set; default = null; } // How many URLs have been sent? public uint urls_sent { get; private set; default = 0; } // Are there (possibly) more URLs to send? public bool more_urls { get; private set; default = false; } public GetAlbumsTransaction(Session session, string [] _album_urls, uint start = 0) { base(session, "/items"); add_argument("scope", "all"); // Save original list of URLs album_urls = _album_urls; // Wrap each URL in double quotes and separate by a comma, but // we should try to keep the length of the URL under 255 // characters. We need to do this to avoid problems with URLs // that are too long on some web servers (and, really, if there // are alot of albums, this can get large quickly). // The Gallery3 API should probably allow this in a POST // transaction... string url_list = "["; string [] my_album_urls = null; string? endpoint_url = session.get_endpoint_url(); int url_length = (null != endpoint_url) ? endpoint_url.length : 0; url_length += 18; // for: ?scope=all&urls=[] // We have to allow at least one URL at a time if (start <= album_urls.length - 1) { urls_sent = start; do { my_album_urls += "\"" + album_urls[urls_sent] + "\""; // Add 3 for: "", url_length += album_urls[urls_sent].length + 3; urls_sent++; } while ((urls_sent <= album_urls.length - 1) && (url_length + album_urls[urls_sent].length + 3 <= 255)); url_list += string.joinv(",", my_album_urls); more_urls = (urls_sent <= (album_urls.length - 1)); } url_list += "]"; add_argument("urls", url_list); } public Album [] get_albums() throws Spit.Publishing.PublishingError { Album [] albums = null; Album tmp_album; unowned Json.Node root_node = get_root_node(); unowned Json.Array members = root_node.get_array(); // Only add editable items for (uint i = 0; i <= members.get_length() - 1; i++) { tmp_album = new Album(members.get_object_element(i)); if (tmp_album.editable) albums += tmp_album; else warning(@"Album \"$(tmp_album.title)\" is not editable"); } return albums; } } // Class to create or get a tag URL. // Tag URLs are placed in the "item_tags" object and relate an item and // its tags. private class GalleryGetTagTransaction : BaseGalleryTransaction { public GalleryGetTagTransaction(Session session, string tag_name) { if (!session.is_authenticated()) { error("Not authenticated"); } else { Json.Generator entity = new Json.Generator(); Json.Node root_node = new Json.Node(Json.NodeType.OBJECT); Json.Object obj = new Json.Object(); base(session, session.url, "/tags", Publishing.RESTSupport.HttpMethod.POST); add_header("X-Gallery-Request-Key", session.key); add_header("X-Gallery-Request-Method", "POST"); obj.set_string_member("name", tag_name); root_node.set_object(obj); entity.set_root(root_node); size_t entity_length; string entity_value = entity.to_data(out entity_length); debug("created entity: %s", entity_value); add_argument("entity", entity_value); } } public string tag_url() { unowned Json.Node root_node; string url; try { root_node = get_root_node(); } catch (Spit.Publishing.PublishingError e) { error("Could not get root node"); } url = root_node.get_object().get_string_member("url"); return url; } } // Get the item_tags URL for a given item private class GalleryGetItemTagsURLsTransaction : GalleryRequestTransaction { private string item_tags_path = ""; public GalleryGetItemTagsURLsTransaction(Session session, string item_url) { base(session, item_url); } public string get_item_tags_path() { unowned Json.Node root_node; unowned Json.Object relationships, tags; if ("" == item_tags_path) { try { root_node = get_root_node(); } catch (Spit.Publishing.PublishingError e) { error("Could not get root node"); } relationships = root_node.get_object().get_object_member("relationships"); tags = relationships.get_object_member("tags"); item_tags_path = tags.get_string_member("url"); // Remove the session URL from the beginning of this URL item_tags_path = strip_session_url(item_tags_path); } return item_tags_path; } } // Set a tag relationship with an item private class GallerySetTagRelationshipTransaction : BaseGalleryTransaction { public GallerySetTagRelationshipTransaction(Session session, string item_tags_path, string tag_url, string item_url) { if (!session.is_authenticated()) { error("Not authenticated"); } else { Json.Generator entity = new Json.Generator(); Json.Node root_node = new Json.Node(Json.NodeType.OBJECT); Json.Object obj = new Json.Object(); base(session, session.url, item_tags_path, Publishing.RESTSupport.HttpMethod.POST); add_header("X-Gallery-Request-Key", session.key); add_header("X-Gallery-Request-Method", "POST"); obj.set_string_member("tag", tag_url); obj.set_string_member("item", item_url); root_node.set_object(obj); entity.set_root(root_node); size_t entity_length; string entity_value = entity.to_data(out entity_length); debug("created entity: %s", entity_value); add_argument("entity", entity_value); } } } private class GalleryAlbumCreateTransaction : BaseGalleryTransaction { // Properties public PublishingParameters parameters { get; private set; } // Private variables private string? session_url; // GalleryAlbumCreateTransaction constructor // // parameters: New album parameters public GalleryAlbumCreateTransaction(Session session, PublishingParameters parameters) { if (!session.is_authenticated()) { error("Not authenticated"); } else { Json.Generator entity = new Json.Generator(); Json.Node root_node = new Json.Node(Json.NodeType.OBJECT); Json.Object obj = new Json.Object(); base(session, session.url, "/item/1", Publishing.RESTSupport.HttpMethod.POST); add_header("X-Gallery-Request-Key", session.key); add_header("X-Gallery-Request-Method", "POST"); this.session_url = session.url; this.parameters = parameters; obj.set_string_member("name", parameters.album_name); obj.set_string_member("type", "album"); obj.set_string_member("title", parameters.album_title); root_node.set_object(obj); entity.set_root(root_node); string entity_value = entity.to_data(null); debug("created entity: %s", entity_value); add_argument("entity", entity_value); } } public string get_new_album_path() { unowned Json.Node root_node; string new_path; try { root_node = get_root_node(); } catch (Spit.Publishing.PublishingError e) { error("Could not get root node"); } new_path = root_node.get_object().get_string_member("url"); new_path = strip_session_url(new_path); return new_path; } } private class GalleryUploadTransaction : Publishing.RESTSupport.UploadTransaction { private Session session; private Json.Generator generator; private PublishingParameters parameters; private string item_url; private string item_path; private string item_tags_path; public GalleryUploadTransaction(Session session, PublishingParameters parameters, Spit.Publishing.Publishable publishable) { // TODO: eventually we can remove this if (parameters.album_path[0] != '/') { warning("Bad upload item path, this is a bug!"); error(parameters.album_path); } base.with_endpoint_url(session, publishable, session.url + REST_PATH + parameters.album_path); this.parameters = parameters; this.session = session; add_header("X-Gallery-Request-Key", session.key); add_header("X-Gallery-Request-Method", "POST"); GLib.HashTable disposition_table = new GLib.HashTable(GLib.str_hash, GLib.str_equal); string? title = publishable.get_publishing_name(); string filename = publishable.get_param_string( Spit.Publishing.Publishable.PARAM_STRING_BASENAME); if (title == null || title == "") //TODO: remove extension? title = filename; disposition_table.insert("filename", @"$(filename)"); disposition_table.insert("name", "file"); set_binary_disposition_table(disposition_table); // Do the JSON stuff generator = new Json.Generator(); string desc = publishable.get_param_string( Spit.Publishing.Publishable.PARAM_STRING_COMMENT); string type = (publishable.get_media_type() == Spit.Publishing.Publisher.MediaType.VIDEO) ? "movie" : "photo"; Json.Node root_node = new Json.Node(Json.NodeType.OBJECT); Json.Object obj = new Json.Object(); obj.set_string_member("name", filename); obj.set_string_member("type", type); obj.set_string_member("title", title); obj.set_string_member("description", desc); root_node.set_object(obj); generator.set_root(root_node); add_argument("entity", generator.to_data(null)); } private string get_new_item_url() { string json_object; string new_url; unowned Json.Node root_node; Json.Parser parser = new Json.Parser(); json_object = get_response(); if ((null == json_object) || (0 == json_object.length)) { warning("No response data from %s", get_endpoint_url()); return ""; } debug("json_object: %s", json_object); try { parser.load_from_data(json_object); } catch (GLib.Error e) { // If this didn't work, reset the "executed" state // TODO: can we recover from this? warning("ERROR: didn't load JSON data"); set_is_executed(false); error(e.message); } root_node = parser.get_root(); if (root_node.is_null()) { warning("Root node is null, doesn't appear to be JSON data"); return ""; } new_url = root_node.get_object().get_string_member("url"); return new_url; } private void do_set_tag_relationship(string tag_url) throws Spit.Publishing.PublishingError { GallerySetTagRelationshipTransaction tag_txn = new GallerySetTagRelationshipTransaction( (Session) get_parent_session(), item_tags_path, tag_url, item_url); tag_txn.execute(); debug("Response from setting tag relationship: %s", tag_txn.get_response()); } private string get_new_item_tags_path() { GalleryGetItemTagsURLsTransaction tag_urls_txn = new GalleryGetItemTagsURLsTransaction( (Session) get_parent_session(), item_path); try { tag_urls_txn.execute(); } catch (Spit.Publishing.PublishingError err) { debug("Problem getting the item_tags URL: %s", err.message); return ""; } return tag_urls_txn.get_item_tags_path(); } private string get_tag_url(string tag) { GalleryGetTagTransaction tag_txn = new GalleryGetTagTransaction( (Session) get_parent_session(), tag); try { tag_txn.execute(); } catch (Spit.Publishing.PublishingError err) { debug("Problem getting the tags URL: %s", err.message); return ""; } return tag_txn.tag_url(); } private void on_upload_completed() throws Spit.Publishing.PublishingError { debug("EVENT: upload completed"); if (!parameters.strip_metadata) { string[] keywords; debug("EVENT: evaluating tags"); keywords = base.publishable.get_publishing_keywords(); // If this publishable has no tags, continue if (null == keywords) { debug("No tags"); return; } // Get URLs from the file we just finished uploading item_url = get_new_item_url(); item_path = strip_session_url(item_url); item_tags_path = get_new_item_tags_path(); debug("new item path is %s", item_path); debug("item_tags path is %s", item_tags_path); // Verify these aren't empty if (("" == item_path) || ("" == item_tags_path)) { throw new Spit.Publishing.PublishingError.COMMUNICATION_FAILED( "Could not obtain URL of uploaded item or its " + "\"item_tags\" relationship URL"); } // Do the tagging here foreach (string tag in keywords) { debug(@"Found tag: $(tag)"); string new_tag_url = get_tag_url(tag); try { do_set_tag_relationship(new_tag_url); } catch (Spit.Publishing.PublishingError err) { debug("Problem setting the relationship between tag " + "and item: %s", err.message); throw err; } } } } public override void execute() throws Spit.Publishing.PublishingError { base.execute(); // Run tagging operations here on_upload_completed(); } } public class GalleryPublisher : Spit.Publishing.Publisher, GLib.Object { private const string BAD_FILE_MSG = _("\n\nThe file \"%s\" may not be supported by or may be too large for this instance of Gallery3."); private const string BAD_MOVIE_MSG = _("\nNote that Gallery3 only supports the video types that Flowplayer does."); private weak Spit.Publishing.PluginHost host = null; private Spit.Publishing.ProgressCallback progress_reporter = null; private weak Spit.Publishing.Service service = null; private Session session = null; private bool running = false; private Album[] albums = null; private string key = null; private PublishingOptionsPane publishing_options_pane = null; public GalleryPublisher(Spit.Publishing.Service service, Spit.Publishing.PluginHost host) { this.service = service; this.host = host; this.session = new Session(); } public bool is_running() { return running; } public Spit.Publishing.Service get_service() { return service; } public void start() { if (is_running()) return; if (host == null) error("GalleryPublisher: start( ): can't start; this " + "publisher is not restartable."); debug("GalleryPublisher: starting interaction."); running = true; key = get_api_key(); if ((null == key) || ("" == key)) do_show_service_welcome_pane(); else { string url = get_gallery_url(); string username = get_gallery_username(); if ((null == username) || (null == key) || (null == url)) do_show_service_welcome_pane(); else { debug("ACTION: attempting network login for user " + "'%s' at URL '%s' from saved credentials.", username, url); host.install_account_fetch_wait_pane(); session.authenticate(url, username, key); // Initiate an album transaction do_fetch_album_urls(); } } } public void stop() { debug("GalleryPublisher: stop( ) invoked."); running = false; } // Config getters/setters // API key internal string? get_api_key() { return host.get_config_string("api-key", null); } internal void set_api_key(string key) { host.set_config_string("api-key", key); } // URL internal string? get_gallery_url() { return host.get_config_string("url", null); } internal void set_gallery_url(string url) { host.set_config_string("url", url); } // Username internal string? get_gallery_username() { return host.get_config_string("username", null); } internal void set_gallery_username(string username) { host.set_config_string("username", username); } internal bool? get_persistent_strip_metadata() { return host.get_config_bool("strip-metadata", false); } internal void set_persistent_strip_metadata(bool strip_metadata) { host.set_config_bool("strip-metadata", strip_metadata); } internal int? get_scaling_constraint_id() { return host.get_config_int("scaling-constraint-id", 0); } internal void set_scaling_constraint_id(int constraint) { host.set_config_int("scaling-constraint-id", constraint); } internal int? get_scaling_pixels() { return host.get_config_int("scaling-pixels", 1024); } internal void set_scaling_pixels(int pixels) { host.set_config_int("scaling-pixels", pixels); } // Pane installation functions private void do_show_service_welcome_pane() { debug("ACTION: showing service welcome pane."); host.install_welcome_pane(SERVICE_WELCOME_MESSAGE, on_service_welcome_login); } private void do_show_credentials_pane(CredentialsPane.Mode mode) { debug("ACTION: showing credentials capture pane in %s mode.", mode.to_string()); session.deauthenticate(); CredentialsPane creds_pane = new CredentialsPane(host, mode, get_gallery_url(), get_gallery_username(), get_api_key()); creds_pane.go_back.connect(on_credentials_go_back); creds_pane.login.connect(on_credentials_login); host.install_dialog_pane(creds_pane); } private void do_network_login(string url, string username, string password) { debug("ACTION: attempting network login for user '%s' at URL " + "'%s'.", username, url); host.install_login_wait_pane(); KeyFetchTransaction fetch_trans = new KeyFetchTransaction(session, url, username, password); fetch_trans.network_error.connect(on_key_fetch_error); fetch_trans.completed.connect(on_key_fetch_complete); try { fetch_trans.execute(); } catch (Spit.Publishing.PublishingError err) { debug("Caught an error attempting to login"); // 403 errors may be recoverable, so don't post the error to // our host immediately; instead, try to recover from it on_key_fetch_error(fetch_trans, err); } } private void do_fetch_album_urls() { host.install_account_fetch_wait_pane(); GetAlbumURLsTransaction album_trans = new GetAlbumURLsTransaction(session); album_trans.network_error.connect(on_album_urls_fetch_error); album_trans.completed.connect(on_album_urls_fetch_complete); try { album_trans.execute(); } catch (Spit.Publishing.PublishingError err) { debug("Caught an error attempting to fetch albums"); // 403 errors may be recoverable, so don't post the error to // our host immediately; instead, try to recover from it on_album_urls_fetch_error(album_trans, err); } } private void do_fetch_albums(string [] album_urls, uint start = 0) { GetAlbumsTransaction album_trans = new GetAlbumsTransaction(session, album_urls, start); album_trans.network_error.connect(on_album_fetch_error); album_trans.completed.connect(on_album_fetch_complete); try { album_trans.execute(); } catch (Spit.Publishing.PublishingError err) { // 403 errors may be recoverable, so don't post the error to // our host immediately; instead, try to recover from it on_album_fetch_error(album_trans, err); } } private void do_show_publishing_options_pane(string url, string username) { debug("ACTION: showing publishing options pane"); Gtk.Builder builder = new Gtk.Builder(); try { builder.add_from_file( host.get_module_file().get_parent().get_child( "gallery3_publishing_options_pane.glade").get_path()); } catch (Error e) { warning("Could not parse UI file! Error: %s.", e.message); host.post_error( new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR( _("A file required for publishing is unavailable. Publishing to %s can't continue.") .printf(SERVICE_NAME) ) ); return; } publishing_options_pane = new PublishingOptionsPane(host, url, username, albums, builder, get_persistent_strip_metadata(), get_scaling_constraint_id(), get_scaling_pixels()); publishing_options_pane.publish.connect( on_publishing_options_pane_publish); publishing_options_pane.logout.connect( on_publishing_options_pane_logout); host.install_dialog_pane(publishing_options_pane); } private void do_create_album(PublishingParameters parameters) { debug("ACTION: creating album"); GalleryAlbumCreateTransaction album_trans = new GalleryAlbumCreateTransaction(session, parameters); album_trans.network_error.connect(on_album_create_error); album_trans.completed.connect(on_album_create_complete); try { album_trans.execute(); } catch (Spit.Publishing.PublishingError err) { // 403 errors may be recoverable, so don't post the error to // our host immediately; instead, try to recover from it on_album_create_error(album_trans, err); } } private void do_publish(PublishingParameters parameters) { debug("ACTION: publishing items"); set_persistent_strip_metadata(parameters.strip_metadata); set_scaling_constraint_id( (parameters.photo_major_axis_size <= 0) ? 0 : 1); set_scaling_pixels(parameters.photo_major_axis_size); host.set_service_locked(true); progress_reporter = host.serialize_publishables(parameters.photo_major_axis_size, parameters.strip_metadata); // Serialization is a long and potentially cancellable // operation, so before we use the publishables, make sure that // the publishing interaction is still running. If it isn't, the // publishing environment may be partially torn down so do a // short-circuit return. if (!is_running()) return; Uploader uploader = new Uploader(session, host.get_publishables(), parameters); uploader.upload_complete.connect(on_publish_complete); uploader.upload_error.connect(on_publish_error); uploader.upload(on_upload_status_updated); } private void do_show_success_pane() { debug("ACTION: showing success pane."); host.set_service_locked(false); host.install_success_pane(); } // Callbacks private void on_service_welcome_login() { if (!is_running()) return; debug("EVENT: user clicked 'Login' in welcome pane."); do_show_credentials_pane(CredentialsPane.Mode.INTRO); } private void on_credentials_login(string url, string username, string password) { if (!is_running()) return; debug("EVENT: user '%s' clicked 'Login' in credentials pane.", username); set_gallery_url(url); set_gallery_username(username); do_network_login(url, username, password); } private void on_credentials_go_back() { if (!is_running()) return; debug("EVENT: user is attempting to go back."); do_show_service_welcome_pane(); } private void on_key_fetch_error( Publishing.RESTSupport.Transaction bad_txn, Spit.Publishing.PublishingError err) { bad_txn.completed.disconnect(on_key_fetch_complete); bad_txn.network_error.disconnect(on_key_fetch_error); if (!is_running()) return; // ignore these events if the session is already auth'd if (session.is_authenticated()) return; debug("EVENT: network transaction to fetch key for login " + "failed; response = '%s'.", bad_txn.get_response()); // HTTP error 403 is invalid authentication -- if we get this // error during key fetch then we can just show the login screen // again with a retry message; if we get any error other than // 403 though, we can't recover from it, so just post the error // to the user if (bad_txn.get_status_code() == 403) { // TODO: can we give more detail on the problem? do_show_credentials_pane(CredentialsPane.Mode.FAILED_RETRY); } else if (bad_txn.get_status_code() == 400) { // This might not be a Gallery URL // TODO: can we give more detail on the problem? do_show_credentials_pane(CredentialsPane.Mode.NOT_GALLERY_URL); } else { host.post_error(err); } } private void on_key_fetch_complete( Publishing.RESTSupport.Transaction txn) { txn.completed.disconnect(on_key_fetch_complete); txn.network_error.disconnect(on_key_fetch_error); if (!is_running()) return; // ignore these events if the session is already auth'd if (session.is_authenticated()) return; key = (txn as KeyFetchTransaction).get_key(); if (key == null) error("key doesn\'t exist"); else { string url = get_gallery_url(); string username = get_gallery_username(); debug("EVENT: network transaction to fetch key completed " + "successfully."); set_api_key(key); session.authenticate(url, username, key); // Initiate an album transaction do_fetch_album_urls(); } } private void on_album_urls_fetch_error( Publishing.RESTSupport.Transaction bad_txn, Spit.Publishing.PublishingError err) { bad_txn.completed.disconnect(on_album_urls_fetch_complete); bad_txn.network_error.disconnect(on_album_urls_fetch_error); if (!is_running()) return; // ignore these events if the session is not auth'd if (!session.is_authenticated()) return; debug("EVENT: network transaction to fetch album URLs " + "failed; response = \'%s\'.", bad_txn.get_response()); // HTTP error 403 is invalid authentication -- if we get this // error during key fetch then we can just show the login screen // again with a retry message; if we get any error other than // 403 though, we can't recover from it, so just post the error // to the user if (bad_txn.get_status_code() == 403) { // TODO: can we give more detail on the problem? do_show_credentials_pane(CredentialsPane.Mode.FAILED_RETRY); } else if (bad_txn.get_status_code() == 400) { // This might not be a Gallery URL // TODO: can we give more detail on the problem? do_show_credentials_pane(CredentialsPane.Mode.NOT_GALLERY_URL); } else { host.post_error(err); } } private void on_album_urls_fetch_complete( Publishing.RESTSupport.Transaction txn) { txn.completed.disconnect(on_album_urls_fetch_complete); txn.network_error.disconnect(on_album_urls_fetch_error); if (!is_running()) return; // ignore these events if the session is not auth'd if (!session.is_authenticated()) return; debug("EVENT: retrieving all album URLs."); string [] album_urls = (txn as GetAlbumURLsTransaction).get_album_urls(); if (null == album_urls) { string url = session.url; string username = session.username; do_show_publishing_options_pane(url, username); } else do_fetch_albums(album_urls); } private void on_album_fetch_error( Publishing.RESTSupport.Transaction bad_txn, Spit.Publishing.PublishingError err) { bad_txn.completed.disconnect(on_album_fetch_complete); bad_txn.network_error.disconnect(on_album_fetch_error); if (!is_running()) return; // ignore these events if the session is not auth'd if (!session.is_authenticated()) return; debug("EVENT: network transaction to fetch albums " + "failed; response = \'%s\'.", bad_txn.get_response()); // HTTP error 403 is invalid authentication -- if we get this // error during key fetch then we can just show the login screen // again with a retry message; if we get any error other than // 403 though, we can't recover from it, so just post the error // to the user if (bad_txn.get_status_code() == 403) { // TODO: can we give more detail on the problem? do_show_credentials_pane(CredentialsPane.Mode.FAILED_RETRY); } else if (bad_txn.get_status_code() == 400) { // This might not be a Gallery URL // TODO: can we give more detail on the problem? do_show_credentials_pane(CredentialsPane.Mode.NOT_GALLERY_URL); } else { host.post_error(err); } } private void on_album_fetch_complete( Publishing.RESTSupport.Transaction txn) { txn.completed.disconnect(on_album_fetch_complete); txn.network_error.disconnect(on_album_fetch_error); Album[] new_albums = null; if (!is_running()) return; // ignore these events if the session is not auth'd if (!session.is_authenticated()) return; debug("EVENT: user is attempting to populate the album list."); try { new_albums = (txn as GetAlbumsTransaction).get_albums(); } catch (Spit.Publishing.PublishingError err) { on_album_fetch_error(txn, err); } // Append new albums to existing for (int i = 0; i <= new_albums.length - 1; i++) albums += new_albums[i]; if ((txn as GetAlbumsTransaction).more_urls) { do_fetch_albums((txn as GetAlbumsTransaction).album_urls, (txn as GetAlbumsTransaction).urls_sent); } else { string url = session.url; string username = session.username; do_show_publishing_options_pane(url, username); } } private void on_album_create_error( Publishing.RESTSupport.Transaction bad_txn, Spit.Publishing.PublishingError err) { bad_txn.completed.disconnect(on_album_create_complete); bad_txn.network_error.disconnect(on_album_create_error); if (!is_running()) return; // ignore these events if the session is not auth'd if (!session.is_authenticated()) return; debug("EVENT: network transaction to create an album " + "failed; response = \'%s\'.", bad_txn.get_response()); // HTTP error 403 is invalid authentication -- if we get this // error during key fetch then we can just show the login screen // again with a retry message; if we get any error other than // 403 though, we can't recover from it, so just post the error // to the user if (bad_txn.get_status_code() == 403) { // TODO: can we give more detail on the problem? do_show_credentials_pane(CredentialsPane.Mode.FAILED_RETRY); } else if (bad_txn.get_status_code() == 400) { // This might not be a Gallery URL // TODO: can we give more detail on the problem? do_show_credentials_pane(CredentialsPane.Mode.NOT_GALLERY_URL); } else { host.post_error(err); } } private void on_album_create_complete( Publishing.RESTSupport.Transaction txn) { txn.completed.disconnect(on_album_create_complete); txn.network_error.disconnect(on_album_create_error); if (!is_running()) return; // ignore these events if the session is not auth'd if (!session.is_authenticated()) return; PublishingParameters new_params = (txn as GalleryAlbumCreateTransaction).parameters; new_params.album_path = (txn as GalleryAlbumCreateTransaction).get_new_album_path(); debug("EVENT: user has created an album at \"%s\".", new_params.album_path); do_publish(new_params); } private void on_publish_error( Publishing.RESTSupport.BatchUploader _uploader, Spit.Publishing.PublishingError err) { if (!is_running()) return; Uploader uploader = _uploader as Uploader; GLib.Error g3_err = err.copy(); debug("EVENT: uploader reports upload error = '%s' " + "for file '%s' (code %d)", err.message, uploader.current_publishable_name, uploader.status_code); uploader.upload_complete.disconnect(on_publish_complete); uploader.upload_error.disconnect(on_publish_error); // Is this a 400 error? Then it may be a bad file. if (uploader.status_code == 400) { g3_err.message += BAD_FILE_MSG.printf(uploader.current_publishable_name); // Add an additional message if this appears to be a video // file. if (uploader.current_publishable_type == Spit.Publishing.Publisher.MediaType.VIDEO) g3_err.message += BAD_MOVIE_MSG; } host.post_error(g3_err); } private void on_upload_status_updated(int file_number, double completed_fraction) { if (!is_running()) return; debug("EVENT: uploader reports upload %.2f percent complete.", 100.0 * completed_fraction); assert(progress_reporter != null); progress_reporter(file_number, completed_fraction); } private void on_publish_complete( Publishing.RESTSupport.BatchUploader uploader, int num_published) { uploader.upload_complete.disconnect(on_publish_complete); uploader.upload_error.disconnect(on_publish_error); if (!is_running()) return; // ignore these events if the session is not auth'd if (!session.is_authenticated()) return; debug("EVENT: publishing complete; %d items published", num_published); do_show_success_pane(); } private void on_publishing_options_pane_logout() { publishing_options_pane.publish.disconnect( on_publishing_options_pane_publish); publishing_options_pane.logout.disconnect( on_publishing_options_pane_logout); if (!is_running()) return; debug("EVENT: user is attempting to log out."); session.deauthenticate(); do_show_service_welcome_pane(); } private void on_publishing_options_pane_publish(PublishingParameters parameters) { publishing_options_pane.publish.disconnect( on_publishing_options_pane_publish); publishing_options_pane.logout.disconnect( on_publishing_options_pane_logout); if (!is_running()) return; debug("EVENT: user is attempting to publish something."); if (parameters.is_to_new_album()) { debug("EVENT: must create new album \"%s\" first.", parameters.album_name); do_create_album(parameters); } else { do_publish(parameters); } } } internal class PublishingOptionsPane : Spit.Publishing.DialogPane, GLib.Object { private const string DEFAULT_ALBUM_NAME = ""; private const string LAST_ALBUM_CONFIG_KEY = "last-album"; private Gtk.Builder builder = null; private Gtk.Grid pane_widget = null; private Gtk.Label title_label = null; private Gtk.RadioButton use_existing_radio = null; private Gtk.ComboBoxText existing_albums_combo = null; private Gtk.RadioButton create_new_radio = null; private Gtk.Entry new_album_entry = null; private Gtk.ComboBoxText scaling_combo = null; private Gtk.Entry pixels = null; private Gtk.CheckButton strip_metadata_check = null; private Gtk.Button publish_button = null; private Gtk.Button logout_button = null; private Album[] albums; private weak Spit.Publishing.PluginHost host; public signal void publish(PublishingParameters parameters); public signal void logout(); public PublishingOptionsPane(Spit.Publishing.PluginHost host, string url, string username, Album[] albums, Gtk.Builder builder, bool strip_metadata, int scaling_id, int scaling_pixels) { this.albums = albums; this.host = host; this.builder = builder; assert(null != builder); assert(builder.get_objects().length() > 0); // pull in all widgets from builder pane_widget = builder.get_object("pane_widget") as Gtk.Grid; title_label = builder.get_object("title_label") as Gtk.Label; use_existing_radio = builder.get_object("publish_to_existing_radio") as Gtk.RadioButton; existing_albums_combo = builder.get_object("existing_albums_combo") as Gtk.ComboBoxText; scaling_combo = builder.get_object("scaling_constraint_combo") as Gtk.ComboBoxText; pixels = builder.get_object("major_axis_pixels") as Gtk.Entry; create_new_radio = builder.get_object("publish_new_radio") as Gtk.RadioButton; new_album_entry = builder.get_object("new_album_name") as Gtk.Entry; strip_metadata_check = this.builder.get_object("strip_metadata_check") as Gtk.CheckButton; publish_button = builder.get_object("publish_button") as Gtk.Button; logout_button = builder.get_object("logout_button") as Gtk.Button; // populate any widgets whose contents are // programmatically-generated title_label.set_label( _("Publishing to %s as %s.").printf(url, username)); strip_metadata_check.set_active(strip_metadata); scaling_combo.set_active(scaling_id); pixels.set_text(@"$(scaling_pixels)"); // connect all signals use_existing_radio.clicked.connect(on_use_existing_radio_clicked); create_new_radio.clicked.connect(on_create_new_radio_clicked); new_album_entry.changed.connect(on_new_album_entry_changed); scaling_combo.changed.connect(on_scaling_constraint_changed); pixels.changed.connect(on_pixels_changed); logout_button.clicked.connect(on_logout_clicked); publish_button.clicked.connect(on_publish_clicked); } private void on_publish_clicked() { string album_name; int photo_major_axis_size = (scaling_combo.get_active() == 1) ? int.parse(pixels.get_text()) : -1; PublishingParameters param; if (create_new_radio.get_active()) { album_name = new_album_entry.get_text(); host.set_config_string(LAST_ALBUM_CONFIG_KEY, album_name); param = new PublishingParameters.to_new_album(album_name); debug("Trying to publish to \"%s\"", album_name); } else { album_name = albums[existing_albums_combo.get_active()].title; host.set_config_string(LAST_ALBUM_CONFIG_KEY, album_name); string album_path = albums[existing_albums_combo.get_active()].path; param = new PublishingParameters.to_existing_album(album_path); } param.photo_major_axis_size = photo_major_axis_size; param.strip_metadata = strip_metadata_check.get_active(); publish(param); } private void on_use_existing_radio_clicked() { existing_albums_combo.set_sensitive(true); new_album_entry.set_sensitive(false); existing_albums_combo.grab_focus(); update_publish_button_sensitivity(); } private void on_create_new_radio_clicked() { new_album_entry.set_sensitive(true); existing_albums_combo.set_sensitive(false); new_album_entry.grab_focus(); update_publish_button_sensitivity(); } private void on_logout_clicked() { logout(); } private void update_publish_button_sensitivity() { string album_name = new_album_entry.get_text(); publish_button.set_sensitive(!(album_name.strip() == "" && create_new_radio.get_active())); } private void on_new_album_entry_changed() { update_publish_button_sensitivity(); } private void update_pixel_entry_sensitivity() { pixels.set_sensitive(scaling_combo.get_active() == 1); } private void on_scaling_constraint_changed() { update_pixel_entry_sensitivity(); } private void on_pixels_changed() { string orig_text = pixels.get_text(); char last_char = orig_text[orig_text.length - 1]; if (orig_text.length > 0) { if (!last_char.isdigit()) pixels.set_text(orig_text.substring(0, orig_text.length - 1)); } } public void installed() { int default_album_id = -1; string last_album = host.get_config_string(LAST_ALBUM_CONFIG_KEY, ""); for (int i = 0; i <= albums.length - 1; i++) { existing_albums_combo.append_text(albums[i].title); if ((albums[i].title == last_album) || ((DEFAULT_ALBUM_NAME == albums[i].title) && (-1 == default_album_id))) default_album_id = i; } if (albums.length == 0) { existing_albums_combo.set_sensitive(false); use_existing_radio.set_sensitive(false); create_new_radio.set_active(true); new_album_entry.grab_focus(); new_album_entry.set_text(DEFAULT_ALBUM_NAME); } else { if (default_album_id >= 0) { use_existing_radio.set_active(true); existing_albums_combo.set_active(default_album_id); new_album_entry.set_sensitive(false); } else { create_new_radio.set_active(true); existing_albums_combo.set_active(0); new_album_entry.set_text(DEFAULT_ALBUM_NAME); new_album_entry.grab_focus(); } } update_publish_button_sensitivity(); update_pixel_entry_sensitivity(); } public Gtk.Widget get_widget() { return pane_widget; } public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry() { return Spit.Publishing.DialogPane.GeometryOptions.NONE; } public void on_pane_installed() { installed(); } public void on_pane_uninstalled() { } } internal class PublishingParameters { // Private variables for properties private string _album_title = ""; // Properties public string album_title { get { assert(is_to_new_album()); return _album_title; } private set { _album_title = value; } } public string album_name { get; private set; default = ""; } public string album_path { get; set; default = ""; } public string entity_title { get; private set; default = ""; } public int photo_major_axis_size { get; set; default = -1; } public bool strip_metadata { get; set; default = false; } private PublishingParameters() { } public PublishingParameters.to_new_album(string album_title) { this.album_name = album_title.delimit(" ", '-'); //this.album_name = this.album_name.delimit("\"\'", ''); this.album_title = album_title; } public PublishingParameters.to_existing_album(string album_path) { this.album_path = album_path; } public bool is_to_new_album() { return (album_name != ""); } } internal class CredentialsPane : Spit.Publishing.DialogPane, GLib.Object { public enum Mode { INTRO, FAILED_RETRY, NOT_GALLERY_URL; public string to_string() { switch (this) { case Mode.INTRO: return "INTRO"; case Mode.FAILED_RETRY: return "FAILED_RETRY"; case Mode.NOT_GALLERY_URL: return "NOT_GALLERY_URL"; default: error("unrecognized CredentialsPane.Mode enumeration value"); } } } private CredentialsGrid frame = null; private Gtk.Widget grid_widget = null; public signal void go_back(); public signal void login(string url, string uname, string password, string key); public CredentialsPane(Spit.Publishing.PluginHost host, Mode mode = Mode.INTRO, string? url = null, string? username = null, string? key = null) { Gtk.Builder builder = new Gtk.Builder(); try { builder.add_from_file( host.get_module_file().get_parent().get_child( "gallery3_authentication_pane.glade").get_path()); } catch (Error e) { warning("Could not parse UI file! Error: %s.", e.message); host.post_error( new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR( _("A file required for publishing is unavailable. Publishing to %s can't continue.") .printf(SERVICE_NAME) ) ); return; } frame = new CredentialsGrid(host, mode, url, username, key, builder); grid_widget = frame.pane_widget as Gtk.Widget; } protected void notify_go_back() { go_back(); } protected void notify_login(string url, string uname, string password, string key) { login(url, uname, password, key); } public Gtk.Widget get_widget() { assert(null != grid_widget); return grid_widget; } public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry() { return Spit.Publishing.DialogPane.GeometryOptions.NONE; } public void on_pane_installed() { frame.go_back.connect(notify_go_back); frame.login.connect(notify_login); frame.installed(); } public void on_pane_uninstalled() { frame.go_back.disconnect(notify_go_back); frame.login.disconnect(notify_login); } } internal class CredentialsGrid : GLib.Object { private const string INTRO_MESSAGE = _("Enter the URL for your Gallery3 site and the username and password (or API key) for your Gallery3 account."); private const string FAILED_RETRY_MESSAGE = _("The username and password or API key were incorrect. To try again, re-enter your username and password below."); private const string NOT_GALLERY_URL_MESSAGE = _("The URL entered does not appear to be the main directory of a Gallery3 instance. Please make sure you typed it correctly and it does not have any trailing components (e.g., index.php)."); public Gtk.Grid pane_widget { get; private set; default = null; } private weak Spit.Publishing.PluginHost host = null; private Gtk.Builder builder = null; private Gtk.Label intro_message_label = null; private Gtk.Entry url_entry = null; private Gtk.Entry username_entry = null; private Gtk.Entry password_entry = null; private Gtk.Entry key_entry = null; private Gtk.Button login_button = null; private Gtk.Button go_back_button = null; private string? url = null; private string? username = null; private string? key = null; public signal void go_back(); public signal void login(string url, string username, string password, string key); public CredentialsGrid(Spit.Publishing.PluginHost host, CredentialsPane.Mode mode = CredentialsPane.Mode.INTRO, string? url = null, string? username = null, string? key = null, Gtk.Builder builder) { this.host = host; this.url = url; this.key = key; this.username = username; this.builder = builder; assert(builder != null); assert(builder.get_objects().length() > 0); // pull in all widgets from builder pane_widget = builder.get_object("gallery3_auth_pane_widget") as Gtk.Grid; intro_message_label = builder.get_object("intro_message_label") as Gtk.Label; url_entry = builder.get_object("url_entry") as Gtk.Entry; username_entry = builder.get_object("username_entry") as Gtk.Entry; key_entry = builder.get_object("key_entry") as Gtk.Entry; password_entry = builder.get_object("password_entry") as Gtk.Entry; go_back_button = builder.get_object("go_back_button") as Gtk.Button; login_button = builder.get_object("login_button") as Gtk.Button; // Intro message switch (mode) { case CredentialsPane.Mode.INTRO: intro_message_label.set_markup(INTRO_MESSAGE); break; case CredentialsPane.Mode.FAILED_RETRY: intro_message_label.set_markup("%s\n\n%s".printf(_( "Unrecognized User"), FAILED_RETRY_MESSAGE)); break; case CredentialsPane.Mode.NOT_GALLERY_URL: intro_message_label.set_markup("%s\n\n%s".printf( _(SERVICE_NAME + " Site Not Found"), NOT_GALLERY_URL_MESSAGE)); break; default: error("Invalid CredentialsPane mode"); } // Gallery URL if (url != null) { url_entry.set_text(url); username_entry.grab_focus(); } url_entry.changed.connect(on_url_or_username_changed); // User name if (username != null) { username_entry.set_text(username); password_entry.grab_focus(); } username_entry.changed.connect(on_url_or_username_changed); // Key if (key != null) { key_entry.set_text(key); key_entry.grab_focus(); } key_entry.changed.connect(on_url_or_username_changed); // Buttons go_back_button.clicked.connect(on_go_back_button_clicked); login_button.clicked.connect(on_login_button_clicked); login_button.set_sensitive((url != null) && (username != null)); } private void on_login_button_clicked() { login(url_entry.get_text(), username_entry.get_text(), password_entry.get_text(), key_entry.get_text()); } private void on_go_back_button_clicked() { go_back(); } private void on_url_or_username_changed() { login_button.set_sensitive( ((url_entry.get_text() != "") && (username_entry.get_text() != "")) || (key_entry.get_text() != "")); } public void installed() { host.set_service_locked(false); // TODO: following line necessary? host.set_dialog_default_widget(login_button); } } internal class Session : Publishing.RESTSupport.Session { // Properties public string? url { get; private set; default = null; } public string? username { get; private set; default = null; } public string? key { get; private set; default = null; } public Session() { } public override bool is_authenticated() { return (null != key); } public void authenticate(string gallery_url, string username, string key) { this.url = gallery_url; this.username = username; this.key = key; notify_authenticated(); } public void deauthenticate() { url = null; username = null; key = null; } } internal class Uploader : Publishing.RESTSupport.BatchUploader { private PublishingParameters parameters; private string _current_publishable_name; private Spit.Publishing.Publisher.MediaType _current_media_type; private Publishing.RESTSupport.Transaction? _current_transaction; /* Properties */ public string current_publishable_name { get { return _current_publishable_name; } } public uint status_code { get { return _current_transaction.get_status_code(); } } public Spit.Publishing.Publisher.MediaType current_publishable_type { get { return _current_media_type; } } public Uploader(Session session, Spit.Publishing.Publishable[] publishables, PublishingParameters parameters) { base(session, publishables); this.parameters = parameters; } protected override Publishing.RESTSupport.Transaction create_transaction(Spit.Publishing.Publishable publishable) { Spit.Publishing.Publishable p = get_current_publishable(); _current_publishable_name = p.get_param_string(Spit.Publishing.Publishable.PARAM_STRING_BASENAME); _current_media_type = p.get_media_type(); _current_transaction = new GalleryUploadTransaction((Session) get_session(), parameters, p); return _current_transaction; } } private string strip_session_url(string url) { // Remove the session URL from the beginning of this URL debug("Searching for \"%s\" in \"%s\"", REST_PATH, url); int item_loc = url.last_index_of(REST_PATH); if (-1 == item_loc) error("Did not find \"%s\" in the base of the new item " + "URL \"%s\"", REST_PATH, url); return url.substring(item_loc + REST_PATH.length); } } // vi:ts=4:sw=4:et