diff options
Diffstat (limited to 'plugins/shotwell-publishing/PiwigoPublishing.vala')
-rw-r--r-- | plugins/shotwell-publishing/PiwigoPublishing.vala | 1736 |
1 files changed, 1736 insertions, 0 deletions
diff --git a/plugins/shotwell-publishing/PiwigoPublishing.vala b/plugins/shotwell-publishing/PiwigoPublishing.vala new file mode 100644 index 0000000..8deada4 --- /dev/null +++ b/plugins/shotwell-publishing/PiwigoPublishing.vala @@ -0,0 +1,1736 @@ +/* Copyright 2009-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +public class PiwigoService : Object, Spit.Pluggable, Spit.Publishing.Service { + private const string ICON_FILENAME = "piwigo.png"; + + private static Gdk.Pixbuf[] icon_pixbuf_set = null; + + public PiwigoService(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 "org.yorba.shotwell.publishing.piwigo"; + } + + public unowned string get_pluggable_name() { + return "Piwigo"; + } + + public void get_info(ref Spit.PluggableInfo info) { + info.authors = "Bruno Girin"; + info.copyright = _("Copyright 2009-2014 Yorba Foundation"); + info.translators = Resources.TRANSLATORS; + info.version = _VERSION; + info.website_name = Resources.WEBSITE_NAME; + info.website_url = Resources.WEBSITE_URL; + info.is_license_wordwrapped = false; + info.license = Resources.LICENSE; + info.icons = icon_pixbuf_set; + } + + public void activation(bool enabled) { + } + + public Spit.Publishing.Publisher create_publisher(Spit.Publishing.PluginHost host) { + return new Publishing.Piwigo.PiwigoPublisher(this, host); + } + + public Spit.Publishing.Publisher.MediaType get_supported_media() { + return (Spit.Publishing.Publisher.MediaType.PHOTO); + } +} + +namespace Publishing.Piwigo { + +internal const string SERVICE_NAME = "Piwigo"; +internal const string PIWIGO_WS = "ws.php"; +internal const int ORIGINAL_SIZE = -1; + +internal class Category { + public int id; + public string name; + public string comment; + public string display_name; + public string uppercats; + public static const int NO_ID = -1; + + public Category(int id, string name, string uppercats, string? comment = "") { + this.id = id; + this.name = name; + this.uppercats = uppercats; + this.comment = comment; + } + + public Category.local(string name, int parent_id, string? comment = "") { + this.id = NO_ID; + this.name = name; + // for new categories abuse the uppercats value for + // the id of the new parent! + this.uppercats = parent_id.to_string(); + this.comment = comment; + } + + public bool is_local() { + return this.id == NO_ID; + } +} + +internal class PermissionLevel { + public int id; + public string name; + + public PermissionLevel(int id, string name) { + this.id = id; + this.name = name; + } +} + +internal class SizeEntry { + public int id; + public string name; + + public SizeEntry(int id, string name) { + this.id = id; + this.name = name; + } +} + +internal class PublishingParameters { + public Category category = null; + public PermissionLevel perm_level = null; + public SizeEntry photo_size = null; + public bool title_as_comment = false; + public bool no_upload_tags = false; + + public PublishingParameters() { + } +} + +public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { + private Spit.Publishing.Service service; + private Spit.Publishing.PluginHost host; + private bool running = false; + private bool strip_metadata = false; + private Session session; + private Category[] categories = null; + private PublishingParameters parameters = null; + private Spit.Publishing.ProgressCallback progress_reporter = null; + + public PiwigoPublisher(Spit.Publishing.Service service, + Spit.Publishing.PluginHost host) { + debug("PiwigoPublisher instantiated."); + this.service = service; + this.host = host; + session = new Session(); + } + + // Publisher interface implementation + + public Spit.Publishing.Service get_service() { + return service; + } + + public Spit.Publishing.PluginHost get_host() { + return host; + } + + public bool is_running() { + return running; + } + + public void start() { + if (is_running()) + return; + + debug("PiwigoPublisher: starting interaction."); + + running = true; + + if (session.is_authenticated()) { + debug("PiwigoPublisher: session is authenticated."); + do_fetch_categories(); + } else { + debug("PiwigoPublisher: session is not authenticated."); + string? persistent_url = get_persistent_url(); + string? persistent_username = get_persistent_username(); + string? persistent_password = get_persistent_password(); + if (persistent_url != null && persistent_username != null && persistent_password != null) + do_network_login(persistent_url, persistent_username, + persistent_password, get_remember_password()); + else + do_show_authentication_pane(); + } + } + + public void stop() { + running = false; + } + + // Session and persistent data + + public string? get_persistent_url() { + return host.get_config_string("url", null); + } + + private void set_persistent_url(string url) { + host.set_config_string("url", url); + } + + public string? get_persistent_username() { + return host.get_config_string("username", null); + } + + private void set_persistent_username(string username) { + host.set_config_string("username", username); + } + + public string? get_persistent_password() { + return host.get_config_string("password", null); + } + + private void set_persistent_password(string? password) { + host.set_config_string("password", password); + } + + public bool get_remember_password() { + return host.get_config_bool("remember-password", false); + } + + private void set_remember_password(bool remember_password) { + host.set_config_bool("remember-password", remember_password); + } + + public int get_last_category() { + return host.get_config_int("last-category", -1); + } + + private void set_last_category(int last_category) { + host.set_config_int("last-category", last_category); + } + + public int get_last_permission_level() { + return host.get_config_int("last-permission-level", -1); + } + + private void set_last_permission_level(int last_permission_level) { + host.set_config_int("last-permission-level", last_permission_level); + } + + public int get_last_photo_size() { + return host.get_config_int("last-photo-size", -1); + } + + private void set_last_photo_size(int last_photo_size) { + host.set_config_int("last-photo-size", last_photo_size); + } + + private bool get_last_title_as_comment() { + return host.get_config_bool("last-title-as-comment", false); + } + + private void set_last_title_as_comment(bool title_as_comment) { + host.set_config_bool("last-title-as-comment", title_as_comment); + } + + private bool get_last_no_upload_tags() { + return host.get_config_bool("last-no-upload-tags", false); + } + + private void set_last_no_upload_tags(bool no_upload_tags) { + host.set_config_bool("last-no-upload-tags", no_upload_tags); + } + + private bool get_metadata_removal_choice() { + return host.get_config_bool("strip_metadata", false); + } + + private void set_metadata_removal_choice(bool strip_metadata) { + host.set_config_bool("strip_metadata", strip_metadata); + } + + // Actions and events implementation + + /** + * Action that shows the authentication pane. + * + * This action method shows the authentication pane. It is shown at the + * very beginning of the interaction when no persistent parameters are found + * or after a failed login attempt using persisted parameters. It can be + * given a mode flag to specify whether it should be displayed in initial + * mode or in any of the error modes that it supports. + * + * @param mode the mode for the authentication pane + */ + private void do_show_authentication_pane(AuthenticationPane.Mode mode = AuthenticationPane.Mode.INTRO) { + debug("ACTION: installing authentication pane"); + + host.set_service_locked(false); + AuthenticationPane authentication_pane = + new AuthenticationPane(this, mode); + authentication_pane.login.connect(on_authentication_pane_login_clicked); + host.install_dialog_pane(authentication_pane, Spit.Publishing.PluginHost.ButtonMode.CLOSE); + host.set_dialog_default_widget(authentication_pane.get_default_widget()); + } + + /** + * Event triggered when the login button in the authentication panel is + * clicked. + * + * This event is triggered when the login button in the authentication + * panel is clicked. It then triggers a network login interaction. + * + * @param url the URL of the Piwigo service as entered in the dialog + * @param username the name of the Piwigo user as entered in the dialog + * @param password the password of the Piwigo as entered in the dialog + */ + private void on_authentication_pane_login_clicked( + string url, string username, string password, bool remember_password + ) { + debug("EVENT: on_authentication_pane_login_clicked"); + if (!running) + return; + + do_network_login(url, username, password, remember_password); + } + + /** + * Action to perform a network login to a Piwigo service. + * + * This action performs a network login a Piwigo service specified by a + * URL and using the given user name and password as credentials. + * + * @param url the URL of the Piwigo service; this URL will be normalised + * before being used + * @param username the name of the Piwigo user used to login + * @param password the password of the Piwigo user used to login + */ + private void do_network_login(string url, string username, string password, bool remember_password) { + debug("ACTION: logging in"); + host.set_service_locked(true); + host.install_login_wait_pane(); + + set_remember_password(remember_password); + if (remember_password) + set_persistent_password(password); + else + set_persistent_password(null); + + SessionLoginTransaction login_trans = new SessionLoginTransaction( + session, normalise_url(url), username, password); + login_trans.network_error.connect(on_login_network_error); + login_trans.completed.connect(on_login_network_complete); + + try { + login_trans.execute(); + } catch (Spit.Publishing.PublishingError err) { + debug("ERROR: do_network_login"); + do_show_error(err); + } + } + + public static string normalise_url(string url) { + string norm_url = url; + + if(!norm_url.has_suffix(".php")) { + if(!norm_url.has_suffix("/")) { + norm_url = norm_url + "/"; + } + norm_url = norm_url + PIWIGO_WS; + } + + if(!norm_url.has_prefix("http://") && !norm_url.has_prefix("https://")) { + norm_url = "http://" + norm_url; + } + + return norm_url; + } + + /** + * Event triggered when the network login action is complete and successful. + * + * This event is triggered on successful completion of a network login. + * Calling this event implies that the URL, user name and password provided + * in the authentication pane are valid and that the transaction should + * contain a Set-Cookie header that includes the value pwg_id for that + * user. As a result, this event will also authenticate the session and + * persist all values so that they can be re-used during the next publishing + * interaction. + * + * @param txn the received REST transaction + */ + private void on_login_network_complete(Publishing.RESTSupport.Transaction txn) { + debug("EVENT: on_login_network_complete"); + txn.completed.disconnect(on_login_network_complete); + txn.network_error.disconnect(on_login_network_error); + + try { + Publishing.RESTSupport.XmlDocument.parse_string( + txn.get_response(), Transaction.validate_xml); + } catch (Spit.Publishing.PublishingError err) { + // Get error code first + try { + Publishing.RESTSupport.XmlDocument.parse_string( + txn.get_response(), Transaction.get_error_code); + } catch (Spit.Publishing.PublishingError code) { + int code_int = int.parse(code.message); + if (code_int == 999) { + debug("ERROR: on_login_network_complete, code 999"); + do_show_authentication_pane(AuthenticationPane.Mode.FAILED_RETRY_USER); + } else { + debug("ERROR: on_login_network_complete"); + do_show_error(err); + } + } + return; + } + // Get session ID and authenticate the session + string endpoint_url = txn.get_endpoint_url(); + debug("Setting endpoint URL to %s", endpoint_url); + string pwg_id = get_pwg_id_from_transaction(txn); + debug("Setting session pwg_id to %s", pwg_id); + session = new Session(); + session.set_pwg_id(pwg_id); + + do_fetch_session_status(endpoint_url, pwg_id); + } + + /** + * Event triggered when a network login action fails due to a network error. + * + * This event triggered as a result of a network error during the login + * transaction. As a result, it assumes that the service URL entered in the + * authentication dialog is incorrect and re-presents the authentication + * dialog with FAILED_RETRY_URL mode. + * + * @param bad_txn the received REST transaction + * @param err the received error + */ + private void on_login_network_error( + Publishing.RESTSupport.Transaction bad_txn, + Spit.Publishing.PublishingError err + ) { + debug("EVENT: on_login_network_error"); + bad_txn.completed.disconnect(on_login_network_complete); + bad_txn.network_error.disconnect(on_login_network_error); + + if (session.is_authenticated()) // ignore these events if the session is already auth'd + return; + + do_show_authentication_pane(AuthenticationPane.Mode.FAILED_RETRY_URL); + } + + /** + * Action to fetch the session status for a known Piwigo user. + * + * This action fetches the session status for a Piwigo user for whom the + * pwg_id is known. If triggered after a network login, it should just + * confirm that the session is OK. It can also be triggered as the first + * action of the interaction for users for who the pwg_id was previously + * persisted. In this case, it will log the user in and confirm the + * identity. + */ + private void do_fetch_session_status(string url = "", string pwg_id = "") { + debug("ACTION: fetching session status"); + host.set_service_locked(true); + host.install_account_fetch_wait_pane(); + + if (!session.is_authenticated()) { + SessionGetStatusTransaction status_txn = new SessionGetStatusTransaction.unauthenticated(session, url, pwg_id); + status_txn.network_error.connect(on_session_get_status_error); + status_txn.completed.connect(on_session_get_status_complete); + + try { + status_txn.execute(); + } catch (Spit.Publishing.PublishingError err) { + debug("ERROR: do_fetch_session_status, not authenticated"); + do_show_error(err); + } + } else { + SessionGetStatusTransaction status_txn = new SessionGetStatusTransaction(session); + status_txn.network_error.connect(on_session_get_status_error); + status_txn.completed.connect(on_session_get_status_complete); + + try { + status_txn.execute(); + } catch (Spit.Publishing.PublishingError err) { + debug("ERROR: do_fetch_session_status, authenticated"); + do_show_error(err); + } + } + } + + /** + * Event triggered when the get session status action completes successfully. + * + * This event being triggered confirms that the session is valid and can becyclonic enema + * used. If the session is not fully authenticated yet, this event finalises + * session authentication. It then triggers the fetch categories action. + */ + private void on_session_get_status_complete(Publishing.RESTSupport.Transaction txn) { + debug("EVENT: on_session_get_status_complete"); + txn.completed.disconnect(on_session_get_status_complete); + txn.network_error.disconnect(on_session_get_status_error); + + if (!session.is_authenticated()) { + string endpoint_url = txn.get_endpoint_url(); + string pwg_id = session.get_pwg_id(); + debug("Fetching session status for pwg_id %s", pwg_id); + // Parse the response + try { + Publishing.RESTSupport.XmlDocument doc = + Publishing.RESTSupport.XmlDocument.parse_string( + txn.get_response(), Transaction.validate_xml); + Xml.Node* root = doc.get_root_node(); + Xml.Node* username_node; + try { + username_node = doc.get_named_child(root, "username"); + string username = username_node->get_content(); + debug("Returned username is %s", username); + session.authenticate(endpoint_url, username, pwg_id); + set_persistent_url(session.get_pwg_url()); + set_persistent_username(session.get_username()); + do_fetch_categories(); + } catch (Spit.Publishing.PublishingError err2) { + debug("ERROR: on_session_get_status_complete, inner"); + do_show_error(err2); + return; + } + } catch (Spit.Publishing.PublishingError err) { + debug("ERROR: on_session_get_status_complete, outer"); + do_show_error(err); + return; + } + } else { + // This should never happen as the session should not be + // authenticated at that point so this call is a safeguard + // against the interaction not happening properly. + do_fetch_categories(); + } + } + + /** + * Event triggered when the get session status fails due to a network error. + */ + private void on_session_get_status_error( + Publishing.RESTSupport.Transaction bad_txn, + Spit.Publishing.PublishingError err + ) { + debug("EVENT: on_session_get_status_error"); + bad_txn.completed.disconnect(on_session_get_status_complete); + bad_txn.network_error.disconnect(on_session_get_status_error); + on_network_error(bad_txn, err); + } + + /** + * Action that fetches all available categories from the Piwigo service. + * + * This action fetches all categories from the Piwigo service in order + * to populate the publishing pane presented to the user. + */ + private void do_fetch_categories() { + debug("ACTION: fetching categories"); + host.set_service_locked(true); + host.install_account_fetch_wait_pane(); + + CategoriesGetListTransaction cat_trans = new CategoriesGetListTransaction(session); + cat_trans.network_error.connect(on_category_fetch_error); + cat_trans.completed.connect(on_category_fetch_complete); + + try { + cat_trans.execute(); + } catch (Spit.Publishing.PublishingError err) { + debug("ERROR: do_fetch_categories"); + do_show_error(err); + } + } + + /** + * Event triggered when the fetch categories action completes successfully. + * + * This event retrieves all categories from the received transaction and + * populates the categories list. It then triggers the display of the + * publishing options pane. + */ + private void on_category_fetch_complete(Publishing.RESTSupport.Transaction txn) { + debug("EVENT: on_category_fetch_complete"); + txn.completed.disconnect(on_category_fetch_complete); + txn.network_error.disconnect(on_category_fetch_error); + debug("PiwigoConnector: list of categories: %s", txn.get_response()); + // Empty the categories + if (categories != null) { + categories = null; + } + // Parse the response + try { + Publishing.RESTSupport.XmlDocument doc = + Publishing.RESTSupport.XmlDocument.parse_string( + txn.get_response(), Transaction.validate_xml); + Xml.Node* root = doc.get_root_node(); + Xml.Node* categories_node = root->first_element_child(); + Xml.Node* category_node_iter = categories_node->children; + Xml.Node* name_node; + Xml.Node* uppercats_node; + string name = ""; + string id_string = ""; + string uppercats = ""; + for ( ; category_node_iter != null; category_node_iter = category_node_iter->next) { + name_node = doc.get_named_child(category_node_iter, "name"); + name = name_node->get_content(); + uppercats_node = doc.get_named_child(category_node_iter, "uppercats"); + uppercats = (string)uppercats_node->get_content(); + id_string = category_node_iter->get_prop("id"); + if (categories == null) { + categories = new Category[0]; + } + categories += new Category(int.parse(id_string), name, uppercats); + } + // compute the display name for the categories + // currently done by an unnecessary triple loop + // one could make a loop that goes over the categories + // and creates a list of back references cat_id -> index + // but since cat_ids are not guaranteed to be continuous + // that needs a perl hash ;-) + for(int i = 0; i < categories.length; i++) { + string[] upcatids = categories[i].uppercats.split(","); + var builder = new StringBuilder(); + for (int j=0; j < upcatids.length; j++) { + builder.append ("/ "); + // search for the upper category + for (int k=0; k < categories.length; k++) { + if (upcatids[j] == categories[k].id.to_string()) { + builder.append (categories[k].name); + break; + } + } + builder.append (" "); + } + categories[i].display_name = builder.str; + } + } catch (Spit.Publishing.PublishingError err) { + debug("ERROR: on_category_fetch_complete"); + do_show_error(err); + return; + } + + do_show_publishing_options_pane(); + } + + /** + * Event triggered when the fetch categories transaction fails due to a + * network error. + */ + private void on_category_fetch_error( + Publishing.RESTSupport.Transaction bad_txn, + Spit.Publishing.PublishingError err + ) { + debug("EVENT: on_category_fetch_error"); + bad_txn.completed.disconnect(on_category_fetch_complete); + bad_txn.network_error.disconnect(on_category_fetch_error); + on_network_error(bad_txn, err); + } + + /** + * Action that shows the publishing options pane. + * + * This action method shows the publishing options pane. + */ + private void do_show_publishing_options_pane() { + debug("ACTION: installing publishing options pane"); + + host.set_service_locked(false); + PublishingOptionsPane opts_pane = new PublishingOptionsPane( + this, categories, get_last_category(), get_last_permission_level(), get_last_photo_size(), + get_last_title_as_comment(), get_last_no_upload_tags(), get_metadata_removal_choice()); + opts_pane.logout.connect(on_publishing_options_pane_logout_clicked); + opts_pane.publish.connect(on_publishing_options_pane_publish_clicked); + host.install_dialog_pane(opts_pane, Spit.Publishing.PluginHost.ButtonMode.CLOSE); + host.set_dialog_default_widget(opts_pane.get_default_widget()); + } + + /** + * Event triggered when the user clicks logout in the publishing options pane. + */ + private void on_publishing_options_pane_logout_clicked() { + debug("EVENT: on_publishing_options_pane_logout_clicked"); + SessionLogoutTransaction logout_trans = new SessionLogoutTransaction(session); + logout_trans.network_error.connect(on_logout_network_error); + logout_trans.completed.connect(on_logout_network_complete); + + try { + logout_trans.execute(); + } catch (Spit.Publishing.PublishingError err) { + debug("ERROR: on_publishing_options_pane_logout_clicked"); + do_show_error(err); + } + } + + /** + * Event triggered when the logout action completes successfully. + * + * This event de-authenticates the session and shows the authentication + * pane again. + */ + private void on_logout_network_complete(Publishing.RESTSupport.Transaction txn) { + debug("EVENT: on_logout_network_complete"); + txn.completed.disconnect(on_logout_network_complete); + txn.network_error.disconnect(on_logout_network_error); + + session.deauthenticate(); + + do_show_authentication_pane(AuthenticationPane.Mode.INTRO); + } + + /** + * Event triggered when the logout action fails due to a network error. + */ + private void on_logout_network_error( + Publishing.RESTSupport.Transaction bad_txn, + Spit.Publishing.PublishingError err + ) { + debug("EVENT: on_logout_network_error"); + bad_txn.completed.disconnect(on_logout_network_complete); + bad_txn.network_error.disconnect(on_logout_network_error); + on_network_error(bad_txn, err); + } + + /** + * Event triggered when the user clicks publish in the publishing options pane. + * + * This event first saves the parameters so that they can re-used later. + * If the publishing parameters indicate that the user wants to create a new + * category, the create category action is called. Otherwise, the upload + * action is called. + * + * @param parameters the publishing parameters + */ + private void on_publishing_options_pane_publish_clicked(PublishingParameters parameters, + bool strip_metadata) { + debug("EVENT: on_publishing_options_pane_publish_clicked"); + this.parameters = parameters; + this.strip_metadata = strip_metadata; + + if (parameters.category.is_local()) { + do_create_category(parameters.category); + } else { + do_upload(this.strip_metadata); + } + } + + /** + * Action that creates a new category in the Piwigo library. + * + * This actions runs a REST transaction to create a new category in the + * Piwigo library. It displays a wait pane with an information message + * while the transaction is running. This action should only be called with + * a local cateogory, i.e. one that does not exist on the server and does + * not yet have an ID. + * + * @param category the new category to create on the server + */ + private void do_create_category(Category category) { + debug("ACTION: creating a new category: %s".printf(category.name)); + assert(category.is_local()); + + host.set_service_locked(true); + host.install_static_message_pane(_("Creating album %s...").printf(category.name)); + + CategoriesAddTransaction creation_trans = new CategoriesAddTransaction( + session, category.name.strip(), int.parse(category.uppercats), category.comment); + creation_trans.network_error.connect(on_category_add_error); + creation_trans.completed.connect(on_category_add_complete); + + try { + creation_trans.execute(); + } catch (Spit.Publishing.PublishingError err) { + debug("ERROR: do_create_category"); + do_show_error(err); + } + } + + /** + * Event triggered when the add category action completes successfully. + * + * This event parses the ID assigned to new category out of the received + * transaction and assigns that ID to the category currently held in + * the publishing parameters. It then calls the upload action. + */ + private void on_category_add_complete(Publishing.RESTSupport.Transaction txn) { + debug("EVENT: on_category_add_complete"); + txn.completed.disconnect(on_category_add_complete); + txn.network_error.disconnect(on_category_add_error); + + // Parse the response + try { + Publishing.RESTSupport.XmlDocument doc = + Publishing.RESTSupport.XmlDocument.parse_string( + txn.get_response(), Transaction.validate_xml); + Xml.Node* rsp = doc.get_root_node(); + Xml.Node* id_node; + id_node = doc.get_named_child(rsp, "id"); + string id_string = id_node->get_content(); + int id = int.parse(id_string); + parameters.category.id = id; + do_upload(strip_metadata); + } catch (Spit.Publishing.PublishingError err) { + debug("ERROR: on_category_add_complete"); + do_show_error(err); + } + } + + /** + * Event triggered when the add category action fails due to a network error. + */ + private void on_category_add_error( + Publishing.RESTSupport.Transaction bad_txn, + Spit.Publishing.PublishingError err + ) { + debug("EVENT: on_category_add_error"); + bad_txn.completed.disconnect(on_category_add_complete); + bad_txn.network_error.disconnect(on_category_add_error); + on_network_error(bad_txn, err); + } + + /** + * Upload action: the big one, the one we've been waiting for! + */ + private void do_upload(bool strip_metadata) { + this.strip_metadata = strip_metadata; + debug("ACTION: uploading pictures"); + + host.set_service_locked(true); + // Save last category, permission level and size for next use + set_last_category(parameters.category.id); + set_last_permission_level(parameters.perm_level.id); + set_last_photo_size(parameters.photo_size.id); + set_last_title_as_comment(parameters.title_as_comment); + set_last_no_upload_tags(parameters.no_upload_tags); + set_metadata_removal_choice(strip_metadata); + + progress_reporter = host.serialize_publishables(parameters.photo_size.id, this.strip_metadata); + Spit.Publishing.Publishable[] publishables = host.get_publishables(); + + Uploader uploader = new Uploader(session, publishables, parameters); + uploader.upload_complete.connect(on_upload_complete); + uploader.upload_error.connect(on_upload_error); + uploader.upload(on_upload_status_updated); + } + + /** + * Event triggered when the batch uploader reports that at least one of the + * network transactions encapsulating uploads has completed successfully + */ + private void on_upload_complete(Publishing.RESTSupport.BatchUploader uploader, int num_published) { + debug("EVENT: on_upload_complete"); + uploader.upload_complete.disconnect(on_upload_complete); + uploader.upload_error.disconnect(on_upload_error); + + // TODO: should a message be displayed to the user if num_published is zero? + + do_show_success_pane(); + } + + /** + * Event triggered when the batch uploader reports that at least one of the + * network transactions encapsulating uploads has caused a network error + */ + private void on_upload_error( + Publishing.RESTSupport.BatchUploader uploader, + Spit.Publishing.PublishingError err + ) { + debug("EVENT: on_upload_error"); + uploader.upload_complete.disconnect(on_upload_complete); + uploader.upload_error.disconnect(on_upload_error); + + do_show_error(err); + } + + /** + * Event triggered when upload progresses and the status needs to be updated. + */ + 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); + } + + /** + * Action to display the success pane in the publishing dialog. + */ + private void do_show_success_pane() { + debug("ACTION: installing success pane"); + + host.set_service_locked(false); + host.install_success_pane(); + } + + /** + * Helper event to handle network errors. + */ + private void on_network_error( + Publishing.RESTSupport.Transaction bad_txn, + Spit.Publishing.PublishingError err + ) { + debug("EVENT: on_network_error"); + do_show_error(err); + } + + /** + * Action to display an error to the user. + */ + private void do_show_error(Spit.Publishing.PublishingError e) { + debug("ACTION: do_show_error"); + string error_type = "UNKNOWN"; + if (e is Spit.Publishing.PublishingError.NO_ANSWER) { + do_show_authentication_pane(AuthenticationPane.Mode.FAILED_RETRY_URL); + return; + } else if(e is Spit.Publishing.PublishingError.COMMUNICATION_FAILED) { + error_type = "COMMUNICATION_FAILED"; + } else if(e is Spit.Publishing.PublishingError.PROTOCOL_ERROR) { + error_type = "PROTOCOL_ERROR"; + } else if(e is Spit.Publishing.PublishingError.SERVICE_ERROR) { + error_type = "SERVICE_ERROR"; + } else if(e is Spit.Publishing.PublishingError.MALFORMED_RESPONSE) { + error_type = "MALFORMED_RESPONSE"; + } else if(e is Spit.Publishing.PublishingError.LOCAL_FILE_ERROR) { + error_type = "LOCAL_FILE_ERROR"; + } else if(e is Spit.Publishing.PublishingError.EXPIRED_SESSION) { + error_type = "EXPIRED_SESSION"; + } + + debug("Unhandled error: type=%s; message='%s'".printf(error_type, e.message)); + do_show_error_message(_("An error message occurred when publishing to Piwigo. Please try again.")); + } + + /** + * Action to display an error message to the user. + */ + private void do_show_error_message(string message) { + debug("ACTION: do_show_error_message"); + host.install_static_message_pane(message, + Spit.Publishing.PluginHost.ButtonMode.CLOSE); + } + + // Helper methods + + /** + * Retrieves session ID from a REST Transaction received + * + * This helper method extracts the pwg_id out of the Set-Cookie header if + * present in the received transaction. + * + * @param txn the received transaction + * @return the value of pwg_id if present or null if not found + */ + private new string? get_pwg_id_from_transaction(Publishing.RESTSupport.Transaction txn) { + string cookie = txn.get_response_headers().get_list("Set-Cookie"); + string pwg_id = null; + debug("Full cookie string: %s".printf(cookie)); + if (!is_string_empty(cookie)) { + string[] cookie_segments = cookie.split(";"); + debug("Split full string into %d individual segments".printf(cookie_segments.length)); + foreach(string cookie_segment in cookie_segments) { + debug("Individual cookie segment: %s".printf(cookie_segment)); + string[] cookie_sub_segments = cookie_segment.split(","); + debug("Split segment into %d individual sub-segments".printf(cookie_sub_segments.length)); + foreach(string cookie_sub_segment in cookie_sub_segments) { + debug("Individual cookie sub-segment: %s".printf(cookie_sub_segment)); + string[] cookie_kv = cookie_sub_segment.split("="); + debug("Split sub-segment into %d chunks".printf(cookie_kv.length)); + if (cookie_kv.length > 1 && cookie_kv[0].strip() == "pwg_id") { + debug("Found pwg_id: %s".printf(cookie_kv[1].strip())); + pwg_id = cookie_kv[1].strip(); + } + } + } + } + + return pwg_id; + } +} + +// The uploader + +internal class Uploader : Publishing.RESTSupport.BatchUploader { + private PublishingParameters parameters; + + 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) { + return new ImagesAddTransaction((Session) get_session(), parameters, + publishable); + } +} + +// UI elements + +/** + * The authentication pane used when asking service URL, user name and password + * from the user. + */ +internal class AuthenticationPane : Spit.Publishing.DialogPane, Object { + public enum Mode { + INTRO, + FAILED_RETRY_URL, + FAILED_RETRY_USER + } + private static string INTRO_MESSAGE = _("Enter the URL of your Piwigo photo library as well as the username and password associated with your Piwigo account for that library."); + private static string FAILED_RETRY_URL_MESSAGE = _("Shotwell cannot contact your Piwigo photo library. Please verify the URL you entered"); + private static string FAILED_RETRY_USER_MESSAGE = _("Username and/or password invalid. Please try again"); + + private Gtk.Box pane_widget = null; + private Gtk.Builder builder; + private Gtk.Entry url_entry; + private Gtk.Entry username_entry; + private Gtk.Entry password_entry; + private Gtk.CheckButton remember_password_checkbutton; + private Gtk.Button login_button; + + public signal void login(string url, string user, string password, bool remember_password); + + public AuthenticationPane(PiwigoPublisher publisher, Mode mode = Mode.INTRO) { + this.pane_widget = new Gtk.Box(Gtk.Orientation.VERTICAL, 0); + + File ui_file = publisher.get_host().get_module_file().get_parent(). + get_child("piwigo_authentication_pane.glade"); + + try { + builder = new Gtk.Builder(); + builder.add_from_file(ui_file.get_path()); + builder.connect_signals(null); + Gtk.Alignment align = builder.get_object("alignment") as Gtk.Alignment; + + Gtk.Label message_label = builder.get_object("message_label") as Gtk.Label; + switch (mode) { + case Mode.INTRO: + message_label.set_text(INTRO_MESSAGE); + break; + + case Mode.FAILED_RETRY_URL: + message_label.set_markup("<b>%s</b>\n\n%s".printf(_( + "Invalid URL"), FAILED_RETRY_URL_MESSAGE)); + break; + + case Mode.FAILED_RETRY_USER: + message_label.set_markup("<b>%s</b>\n\n%s".printf(_( + "Invalid User Name or Password"), FAILED_RETRY_USER_MESSAGE)); + break; + } + + url_entry = builder.get_object ("url_entry") as Gtk.Entry; + string? persistent_url = publisher.get_persistent_url(); + if (persistent_url != null) { + url_entry.set_text(persistent_url); + } + username_entry = builder.get_object ("username_entry") as Gtk.Entry; + string? persistent_username = publisher.get_persistent_username(); + if (persistent_username != null) { + username_entry.set_text(persistent_username); + } + password_entry = builder.get_object ("password_entry") as Gtk.Entry; + string? persistent_password = publisher.get_persistent_password(); + if (persistent_password != null) { + password_entry.set_text(persistent_password); + } + remember_password_checkbutton = + builder.get_object ("remember_password_checkbutton") as Gtk.CheckButton; + remember_password_checkbutton.set_active(publisher.get_remember_password()); + + login_button = builder.get_object("login_button") as Gtk.Button; + + username_entry.changed.connect(on_user_changed); + url_entry.changed.connect(on_url_changed); + password_entry.changed.connect(on_password_changed); + login_button.clicked.connect(on_login_button_clicked); + + align.reparent(pane_widget); + publisher.get_host().set_dialog_default_widget(login_button); + } catch (Error e) { + warning("Could not load UI: %s", e.message); + } + } + + public Gtk.Widget get_default_widget() { + return login_button; + } + + private void on_login_button_clicked() { + login(url_entry.get_text(), username_entry.get_text(), + password_entry.get_text(), remember_password_checkbutton.get_active()); + } + + private void on_url_changed() { + update_login_button_sensitivity(); + } + + private void on_user_changed() { + update_login_button_sensitivity(); + } + + private void on_password_changed() { + update_login_button_sensitivity(); + } + + private void update_login_button_sensitivity() { + login_button.set_sensitive( + !is_string_empty(url_entry.get_text()) && + !is_string_empty(username_entry.get_text()) && + !is_string_empty(password_entry.get_text()) + ); + } + + 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() { + url_entry.grab_focus(); + password_entry.set_activates_default(true); + login_button.can_default = true; + update_login_button_sensitivity(); + } + + public void on_pane_uninstalled() { + } +} + +/** + * The publishing options pane. + */ +internal class PublishingOptionsPane : Spit.Publishing.DialogPane, Object { + + private static string DEFAULT_CATEGORY_NAME = _("Shotwell Connect"); + + private Gtk.Box pane_widget = null; + private Gtk.Builder builder; + private Gtk.RadioButton use_existing_radio; + private Gtk.RadioButton create_new_radio; + private Gtk.ComboBoxText existing_categories_combo; + private Gtk.Entry new_category_entry; + private Gtk.Label within_existing_label; + private Gtk.ComboBoxText within_existing_combo; + private Gtk.ComboBoxText perms_combo; + private Gtk.ComboBoxText size_combo; + private Gtk.CheckButton strip_metadata_check = null; + private Gtk.CheckButton title_as_comment_check = null; + private Gtk.CheckButton no_upload_tags_check = null; + private Gtk.Button logout_button; + private Gtk.Button publish_button; + private Gtk.TextView album_comment; + private Gtk.Label album_comment_label; + + private Category[] existing_categories; + private PermissionLevel[] perm_levels; + private SizeEntry[] photo_sizes; + + private int last_category; + private int last_permission_level; + private int last_photo_size; + private bool last_title_as_comment; + private bool last_no_upload_tags; + + public signal void publish(PublishingParameters parameters, bool strip_metadata); + public signal void logout(); + + public PublishingOptionsPane( + PiwigoPublisher publisher, Category[] categories, + int last_category, int last_permission_level, int last_photo_size, + bool last_title_as_comment, bool last_no_upload_tags, bool strip_metadata_enabled + ) { + this.pane_widget = new Gtk.Box(Gtk.Orientation.VERTICAL, 0); + this.last_category = last_category; + this.last_permission_level = last_permission_level; + this.last_photo_size = last_photo_size; + this.last_title_as_comment = last_title_as_comment; + this.last_no_upload_tags = last_no_upload_tags; + + File ui_file = publisher.get_host().get_module_file().get_parent(). + get_child("piwigo_publishing_options_pane.glade"); + + try { + builder = new Gtk.Builder(); + builder.add_from_file(ui_file.get_path()); + builder.connect_signals(null); + Gtk.Alignment align = builder.get_object("alignment") as Gtk.Alignment; + + use_existing_radio = builder.get_object("use_existing_radio") as Gtk.RadioButton; + create_new_radio = builder.get_object("create_new_radio") as Gtk.RadioButton; + existing_categories_combo = builder.get_object("existing_categories_combo") as Gtk.ComboBoxText; + new_category_entry = builder.get_object ("new_category_entry") as Gtk.Entry; + within_existing_label = builder.get_object ("within_existing_label") as Gtk.Label; + within_existing_combo = builder.get_object ("within_existing_combo") as Gtk.ComboBoxText; + + album_comment = builder.get_object ("album_comment") as Gtk.TextView; + album_comment.buffer = new Gtk.TextBuffer(null); + album_comment_label = builder.get_object ("album_comment_label") as Gtk.Label; + + perms_combo = builder.get_object("perms_combo") as Gtk.ComboBoxText; + size_combo = builder.get_object("size_combo") as Gtk.ComboBoxText; + + strip_metadata_check = builder.get_object("strip_metadata_check") as Gtk.CheckButton; + strip_metadata_check.set_active(strip_metadata_enabled); + + title_as_comment_check = builder.get_object("title_as_comment_check") as Gtk.CheckButton; + title_as_comment_check.set_active(last_title_as_comment); + + no_upload_tags_check = builder.get_object("no_upload_tags_check") as Gtk.CheckButton; + no_upload_tags_check.set_active(last_no_upload_tags); + + logout_button = builder.get_object("logout_button") as Gtk.Button; + logout_button.clicked.connect(on_logout_button_clicked); + + publish_button = builder.get_object("publish_button") as Gtk.Button; + publish_button.clicked.connect(on_publish_button_clicked); + + use_existing_radio.clicked.connect(on_use_existing_radio_clicked); + create_new_radio.clicked.connect(on_create_new_radio_clicked); + new_category_entry.changed.connect(on_new_category_entry_changed); + within_existing_combo.changed.connect(on_existing_combo_changed); + + align.reparent(pane_widget); + pane_widget.set_child_packing(align, true, true, 0, Gtk.PackType.START); + } catch (Error e) { + warning("Could not load UI: %s", e.message); + } + + this.existing_categories = categories; + this.perm_levels = create_perm_levels(); + this.photo_sizes = create_sizes(); + this.album_comment.buffer.set_text(get_common_comment_if_possible(publisher)); + } + + public Gtk.Widget get_default_widget() { + return publish_button; + } + + private PermissionLevel[] create_perm_levels() { + PermissionLevel[] result = new PermissionLevel[0]; + + result += new PermissionLevel(0, _("Everyone")); + result += new PermissionLevel(1, _("Admins, Family, Friends, Contacts")); + result += new PermissionLevel(2, _("Admins, Family, Friends")); + result += new PermissionLevel(4, _("Admins, Family")); + result += new PermissionLevel(8, _("Admins")); + + return result; + } + + private SizeEntry[] create_sizes() { + SizeEntry[] result = new SizeEntry[0]; + + result += new SizeEntry(500, _("500 x 375 pixels")); + result += new SizeEntry(1024, _("1024 x 768 pixels")); + result += new SizeEntry(2048, _("2048 x 1536 pixels")); + result += new SizeEntry(4096, _("4096 x 3072 pixels")); + result += new SizeEntry(ORIGINAL_SIZE, _("Original size")); + + return result; + } + + private void on_logout_button_clicked() { + logout(); + } + + private void on_publish_button_clicked() { + PublishingParameters params = new PublishingParameters(); + params.perm_level = perm_levels[perms_combo.get_active()]; + params.photo_size = photo_sizes[size_combo.get_active()]; + params.title_as_comment = title_as_comment_check.get_active(); + params.no_upload_tags = no_upload_tags_check.get_active(); + if (create_new_radio.get_active()) { + string uploadcomment = album_comment.buffer.text.strip(); + int a = within_existing_combo.get_active(); + if (a == 0) { + params.category = new Category.local(new_category_entry.get_text(), 0, uploadcomment); + } else { + // the list in existing_categories and in the within_existing_combo are shifted + // by 1, since we add the root + a--; + params.category = new Category.local(new_category_entry.get_text(), + existing_categories[a].id, uploadcomment); + } + } else { + params.category = existing_categories[existing_categories_combo.get_active()]; + } + publish(params, strip_metadata_check.get_active()); + } + + // UI interaction + private void on_use_existing_radio_clicked() { + existing_categories_combo.set_sensitive(true); + new_category_entry.set_sensitive(false); + within_existing_label.set_sensitive(false); + within_existing_combo.set_sensitive(false); + existing_categories_combo.grab_focus(); + album_comment_label.set_sensitive(false); + album_comment.set_sensitive(false); + update_publish_button_sensitivity(); + } + + private void on_create_new_radio_clicked() { + new_category_entry.set_sensitive(true); + within_existing_label.set_sensitive(true); + within_existing_combo.set_sensitive(true); + album_comment_label.set_sensitive(true); + album_comment.set_sensitive(true); + existing_categories_combo.set_sensitive(false); + new_category_entry.grab_focus(); + update_publish_button_sensitivity(); + } + + private void on_new_category_entry_changed() { + update_publish_button_sensitivity(); + } + + private void on_existing_combo_changed() { + update_publish_button_sensitivity(); + } + + private void update_publish_button_sensitivity() { + string category_name = new_category_entry.get_text().strip(); + int a = within_existing_combo.get_active(); + string search_name; + if (a <= 0) { + search_name = "/ " + category_name; + } else { + a--; + search_name = existing_categories[a].display_name + "/ " + category_name; + } + publish_button.set_sensitive( + !( + create_new_radio.get_active() && + ( + is_string_empty(category_name) || + category_already_exists(search_name) + ) + ) + ); + } + + 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() { + create_categories_combo(); + create_within_categories_combo(); + create_permissions_combo(); + create_size_combo(); + + publish_button.can_default = true; + update_publish_button_sensitivity(); + } + + private string get_common_comment_if_possible(PiwigoPublisher publisher) { + // we have to determine whether all the publishing items + // belong to the same event + Spit.Publishing.Publishable[] publishables = publisher.get_host().get_publishables(); + string common = ""; + bool isfirst = true; + if (publishables != null) { + foreach (Spit.Publishing.Publishable pub in publishables) { + string cur = pub.get_param_string( + Spit.Publishing.Publishable.PARAM_STRING_EVENTCOMMENT); + if (isfirst) { + common = cur; + isfirst = false; + } else { + if (cur != common) { + common = ""; + break; + } + } + } + } + debug("PiwigoConnector: found common event comment %s\n", common); + return common; + } + + private void create_categories_combo() { + foreach (Category cat in existing_categories) { + existing_categories_combo.append_text(cat.display_name); + } + if (existing_categories.length == 0) { + // if no existing categories, disable the option to choose one + existing_categories_combo.set_sensitive(false); + use_existing_radio.set_sensitive(false); + create_new_radio.set_active(true); + album_comment.set_sensitive(true); + album_comment_label.set_sensitive(true); + new_category_entry.grab_focus(); + } else { + int last_category_index = find_category_index(last_category); + if (last_category_index < 0) { + existing_categories_combo.set_active(0); + } else { + existing_categories_combo.set_active(last_category_index); + } + new_category_entry.set_sensitive(false); + album_comment.set_sensitive(false); + album_comment_label.set_sensitive(false); + } + if (!category_already_exists(DEFAULT_CATEGORY_NAME)) + new_category_entry.set_text(DEFAULT_CATEGORY_NAME); + } + + private void create_within_categories_combo() { + // root menu + within_existing_combo.append_text("/ "); + foreach (Category cat in existing_categories) { + within_existing_combo.append_text(cat.display_name); + } + // by default select root album as target + within_existing_label.set_sensitive(false); + within_existing_combo.set_active(0); + within_existing_combo.set_sensitive(false); + } + + private void create_permissions_combo() { + foreach (PermissionLevel perm in perm_levels) { + perms_combo.append_text(perm.name); + } + int last_permission_level_index = find_permission_level_index(last_permission_level); + if (last_permission_level_index < 0) { + perms_combo.set_active(0); + } else { + perms_combo.set_active(last_permission_level_index); + } + } + + private void create_size_combo() { + foreach (SizeEntry size in photo_sizes) { + size_combo.append_text(size.name); + } + int last_size_index = find_size_index(last_photo_size); + if (last_size_index < 0) { + size_combo.set_active(find_size_index(ORIGINAL_SIZE)); + } else { + size_combo.set_active(last_size_index); + } + } + + public void on_pane_uninstalled() { + } + + private int find_category_index(int category_id) { + int result = -1; + for(int i = 0; i < existing_categories.length; i++) { + if (existing_categories[i].id == category_id) { + result = i; + break; + } + } + return result; + } + + private int find_permission_level_index(int permission_level_id) { + int result = -1; + for(int i = 0; i < perm_levels.length; i++) { + if (perm_levels[i].id == permission_level_id) { + result = i; + break; + } + } + return result; + } + + private int find_size_index(int size_id) { + int result = -1; + for(int i = 0; i < photo_sizes.length; i++) { + if (photo_sizes[i].id == size_id) { + result = i; + break; + } + } + return result; + } + + private bool category_already_exists(string category_name) { + bool result = false; + foreach(Category category in existing_categories) { + if (category.display_name.strip() == category_name) { + result = true; + break; + } + } + return result; + } +} + +// REST support classes + +/** + * Session class that keeps track of the authentication status and of the + * user token pwg_id. + */ +internal class Session : Publishing.RESTSupport.Session { + private string? pwg_url = null; + private string? pwg_id = null; + private string? username = null; + + public Session() { + base(""); + } + + public override bool is_authenticated() { + return (pwg_id != null && pwg_url != null && username != null); + } + + public void authenticate(string url, string username, string id) { + this.pwg_url = url; + this.username = username; + this.pwg_id = id; + } + + public void deauthenticate() { + pwg_url = null; + pwg_id = null; + username = null; + } + + public string get_username() { + return username; + } + + public string get_pwg_url() { + return pwg_url; + } + + public string get_pwg_id() { + return pwg_id; + } + + public void set_pwg_id(string id) { + pwg_id = id; + } +} + +/** + * Generic REST transaction class. + * + * This class implements the generic logic for all REST transactions used + * by the Piwigo publishing plugin. In particular, it ensures that if the + * session has been authenticated, the pwg_id token is included in the + * transaction header. + */ +internal class Transaction : Publishing.RESTSupport.Transaction { + public Transaction(Session session) { + base(session); + if (session.is_authenticated()) { + add_header("Cookie", "pwg_id=".concat(session.get_pwg_id())); + } + } + + public Transaction.authenticated(Session session) { + base.with_endpoint_url(session, session.get_pwg_url()); + add_header("Cookie", "pwg_id=".concat(session.get_pwg_id())); + } + + public static string? validate_xml(Publishing.RESTSupport.XmlDocument doc) { + Xml.Node* root = doc.get_root_node(); + string? status = root->get_prop("stat"); + + // treat malformed root as an error condition + if (status == null) + return "No status property in root node"; + + if (status == "ok") + return null; + + Xml.Node* errcode; + try { + errcode = doc.get_named_child(root, "err"); + } catch (Spit.Publishing.PublishingError err) { + return "No error code specified"; + } + + return "%s (error code %s)".printf(errcode->get_prop("msg"), errcode->get_prop("code")); + } + + public static new string? get_error_code(Publishing.RESTSupport.XmlDocument doc) { + Xml.Node* root = doc.get_root_node(); + Xml.Node* errcode; + try { + errcode = doc.get_named_child(root, "err"); + } catch (Spit.Publishing.PublishingError err) { + return "0"; + } + return errcode->get_prop("code"); + } +} + +/** + * Transaction used to implement the network login interaction. + */ +internal class SessionLoginTransaction : Transaction { + public SessionLoginTransaction(Session session, string url, string username, string password) { + base.with_endpoint_url(session, url); + + add_argument("method", "pwg.session.login"); + add_argument("username", username); + add_argument("password", password); + } +} + +/** + * Transaction used to implement the get status interaction. + */ +internal class SessionGetStatusTransaction : Transaction { + public SessionGetStatusTransaction.unauthenticated(Session session, string url, string pwg_id) { + base.with_endpoint_url(session, url); + add_header("Cookie", "pwg_id=".concat(session.get_pwg_id())); + + add_argument("method", "pwg.session.getStatus"); + } + + public SessionGetStatusTransaction(Session session) { + base.authenticated(session); + + add_argument("method", "pwg.session.getStatus"); + } +} + +/** + * Transaction used to implement the fetch categories interaction. + */ +private class CategoriesGetListTransaction : Transaction { + public CategoriesGetListTransaction(Session session) { + base.authenticated(session); + + add_argument("method", "pwg.categories.getList"); + add_argument("recursive", "true"); + } +} + +private class SessionLogoutTransaction : Transaction { + public SessionLogoutTransaction(Session session) { + base.authenticated(session); + + add_argument("method", "pwg.session.logout"); + } +} + +private class CategoriesAddTransaction : Transaction { + public CategoriesAddTransaction(Session session, string category, int parent_id = 0, string? comment = "") { + base.authenticated(session); + + add_argument("method", "pwg.categories.add"); + add_argument("name", category); + + if (parent_id != 0) { + add_argument("parent", parent_id.to_string()); + } + + if (comment != "") { + add_argument("comment", comment); + } + } +} + +private class ImagesAddTransaction : Publishing.RESTSupport.UploadTransaction { + private PublishingParameters parameters = null; + + public ImagesAddTransaction(Session session, PublishingParameters parameters, Spit.Publishing.Publishable publishable) { + base.with_endpoint_url(session, publishable, session.get_pwg_url()); + if (session.is_authenticated()) { + add_header("Cookie", "pwg_id=".concat(session.get_pwg_id())); + } + this.parameters = parameters; + + string[] keywords = publishable.get_publishing_keywords(); + string tags = ""; + if (keywords != null) { + foreach (string tag in keywords) { + if (!is_string_empty(tags)) { + tags += ","; + } + tags += tag; + } + } + + debug("PiwigoConnector: Uploading photo %s to category id %d with perm level %d", + publishable.get_serialized_file().get_basename(), + parameters.category.id, parameters.perm_level.id); + string name = publishable.get_publishing_name(); + string comment = publishable.get_param_string( + Spit.Publishing.Publishable.PARAM_STRING_COMMENT); + if (is_string_empty(name)) { + name = publishable.get_param_string( + Spit.Publishing.Publishable.PARAM_STRING_BASENAME); + add_argument("name", name); + if (!is_string_empty(comment)) { + add_argument("comment", comment); + } + } else { + // name is set + if (!is_string_empty(comment)) { + add_argument("name", name); + add_argument("comment", comment); + } else { + // name is set, comment is unset + // for backward compatibility with people having used + // the title as comment field, keep this option + if (parameters.title_as_comment) { + add_argument("comment", name); + } else { + add_argument("name", name); + } + } + } + add_argument("method", "pwg.images.addSimple"); + add_argument("category", parameters.category.id.to_string()); + add_argument("level", parameters.perm_level.id.to_string()); + if (!parameters.no_upload_tags) + if (!is_string_empty(tags)) + add_argument("tags", tags); + // TODO: update the Publishable interface so that it gives access to + // the image's meta-data where the author (artist) is kept + /*if (!is_string_empty(author)) + add_argument("author", author);*/ + + // TODO: implement description in APIGlue + /*if (!is_string_empty(publishable.get_publishing_description())) + add_argument("comment", publishable.get_publishing_description());*/ + + GLib.HashTable<string, string> disposition_table = + new GLib.HashTable<string, string>(GLib.str_hash, GLib.str_equal); + disposition_table.insert("filename", Soup.URI.encode( + publishable.get_param_string( + Spit.Publishing.Publishable.PARAM_STRING_BASENAME), null)); + disposition_table.insert("name", "image"); + + set_binary_disposition_table(disposition_table); + } +} + +} // namespace + |