~ubuntu-branches/ubuntu/quantal/shotwell/quantal

« back to all changes in this revision

Viewing changes to .pc/06_uoa.patch/plugins/shotwell-publishing-extras/TumblrPublishing.vala

  • Committer: Package Import Robot
  • Author(s): Ken VanDine, Alberto Mardegan
  • Date: 2012-09-24 11:10:48 UTC
  • Revision ID: package-import@ubuntu.com-20120924111048-dic3zkytvn0iaz36
Tags: 0.13.0-0ubuntu2
[ Alberto Mardegan ]
* debian/patches/06_uoa.patch (LP: #1046461)
  - Support multiple accounts per service
  - Attempt automatic login
  - Remove logout buttons

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/* Copyright 2012 BJA Electronics
 
2
 * Author: Jeroen Arnoldus (b.j.arnoldus@bja-electronics.nl)
 
3
 *
 
4
 * This software is licensed under the GNU Lesser General Public License
 
5
 * (version 2.1 or later).  See the COPYING file in this distribution. 
 
6
 */
 
7
 
 
8
 
 
9
using Publishing.Extras;
 
10
 
 
11
 
 
12
extern string hmac_sha1(string key, string message);
 
13
public class TumblrService : Object, Spit.Pluggable, Spit.Publishing.Service {
 
14
   private const string ICON_FILENAME = "tumblr.png";
 
15
 
 
16
    private static Gdk.Pixbuf[] icon_pixbuf_set = null;
 
17
    
 
18
    public TumblrService(GLib.File resource_directory) {
 
19
        if (icon_pixbuf_set == null)
 
20
            icon_pixbuf_set = Resources.load_icon_set(resource_directory.get_child(ICON_FILENAME));
 
21
    }
 
22
 
 
23
    public int get_pluggable_interface(int min_host_interface, int max_host_interface) {
 
24
        return Spit.negotiate_interfaces(min_host_interface, max_host_interface,
 
25
            Spit.Publishing.CURRENT_INTERFACE);
 
26
    }
 
27
    
 
28
    public unowned string get_id() {
 
29
        return "org.yorba.shotwell.publishing.tumblr";
 
30
    }
 
31
    
 
32
    public unowned string get_pluggable_name() {
 
33
        return "Tumblr";
 
34
    }
 
35
    
 
36
    public void get_info(ref Spit.PluggableInfo info) {
 
37
        info.authors = "Jeroen Arnoldus";
 
38
        info.copyright = _t("Copyright 2012 BJA Electronics");
 
39
        info.translators = Resources.TRANSLATORS;
 
40
        info.version = _VERSION;
 
41
        info.website_name = Resources.WEBSITE_NAME;
 
42
        info.website_url = Resources.WEBSITE_URL;
 
43
        info.is_license_wordwrapped = false;
 
44
        info.license = Resources.LICENSE;
 
45
        info.icons = icon_pixbuf_set;
 
46
    }
 
47
 
 
48
    public void activation(bool enabled) {
 
49
    }
 
50
 
 
51
    public Spit.Publishing.Publisher create_publisher(Spit.Publishing.PluginHost host) {
 
52
        return new Publishing.Tumblr.TumblrPublisher(this, host);
 
53
    }
 
54
    
 
55
    public Spit.Publishing.Publisher.MediaType get_supported_media() {
 
56
        return (Spit.Publishing.Publisher.MediaType.PHOTO |
 
57
            Spit.Publishing.Publisher.MediaType.VIDEO);
 
58
    }
 
59
}
 
60
 
 
61
namespace Publishing.Tumblr {
 
62
 
 
63
internal const string SERVICE_NAME = "Tumblr";
 
64
internal const string ENDPOINT_URL = "http://www.tumblr.com/";
 
65
internal const string API_KEY = "NdXvXQuKVccOsCOj0H4k9HUJcbcjDBYSo2AkaHzXFECHGNuP9k";
 
66
internal const string API_SECRET = "BN0Uoig0MwbeD27OgA0IwYlp3Uvonyfsrl9pf1cnnMj1QoEUvi";
 
67
internal const string ENCODE_RFC_3986_EXTRA = "!*'();:@&=+$,/?%#[] \\";
 
68
internal const int ORIGINAL_SIZE = -1;
 
69
 
 
70
 
 
71
 
 
72
private class BlogEntry {
 
73
   public string blog;
 
74
   public string url;
 
75
   public BlogEntry(string creator_blog, string creator_url) {
 
76
     blog = creator_blog;
 
77
     url = creator_url;
 
78
   }
 
79
}
 
80
 
 
81
private class SizeEntry {
 
82
   public string title;
 
83
   public int size;
 
84
 
 
85
   public SizeEntry(string creator_title, int creator_size) {
 
86
     title = creator_title;
 
87
     size = creator_size;
 
88
   }
 
89
}
 
90
 
 
91
public class TumblrPublisher : Spit.Publishing.Publisher, GLib.Object {
 
92
    private Spit.Publishing.Service service;
 
93
    private Spit.Publishing.PluginHost host;
 
94
    private Spit.Publishing.ProgressCallback progress_reporter = null;
 
95
    private bool running = false;
 
96
    private bool was_started = false;
 
97
    private Session session = null;
 
98
    private PublishingOptionsPane publishing_options_pane = null;
 
99
    private SizeEntry[] sizes = null;
 
100
    private BlogEntry[] blogs = null;
 
101
        private string username = "";
 
102
 
 
103
    
 
104
    private SizeEntry[] create_sizes() {
 
105
        SizeEntry[] result = new SizeEntry[0];
 
106
 
 
107
        result += new SizeEntry(_t("500 x 375 pixels"), 500);
 
108
        result += new SizeEntry(_t("1024 x 768 pixels"), 1024);
 
109
        result += new SizeEntry(_t("1280 x 853 pixels"), 1280);
 
110
//Larger images make no sense for Tumblr
 
111
//        result += new SizeEntry(_("2048 x 1536 pixels"), 2048);
 
112
//        result += new SizeEntry(_("4096 x 3072 pixels"), 4096);
 
113
//        result += new SizeEntry(_("Original size"), ORIGINAL_SIZE);
 
114
 
 
115
        return result;
 
116
    }
 
117
 
 
118
    private BlogEntry[] create_blogs() {
 
119
        BlogEntry[] result = new BlogEntry[0];
 
120
 
 
121
 
 
122
        return result;
 
123
    }
 
124
 
 
125
    public TumblrPublisher(Spit.Publishing.Service service,
 
126
        Spit.Publishing.PluginHost host) {
 
127
        debug("TumblrPublisher instantiated.");
 
128
        this.service = service;
 
129
        this.host = host;
 
130
        this.session = new Session();
 
131
                this.sizes = this.create_sizes();
 
132
                this.blogs = this.create_blogs();
 
133
        session.authenticated.connect(on_session_authenticated);
 
134
    }
 
135
    
 
136
    ~TumblrPublisher() {
 
137
        session.authenticated.disconnect(on_session_authenticated);
 
138
    }
 
139
    
 
140
    private void invalidate_persistent_session() {
 
141
        set_persistent_access_phase_token("");
 
142
        set_persistent_access_phase_token_secret("");
 
143
    }
 
144
    // Publisher interface implementation
 
145
    
 
146
    public Spit.Publishing.Service get_service() {
 
147
        return service;
 
148
    }
 
149
    
 
150
    public Spit.Publishing.PluginHost get_host() {
 
151
        return host;
 
152
    }
 
153
 
 
154
    public bool is_running() {
 
155
        return running;
 
156
    }
 
157
 
 
158
    private bool is_persistent_session_valid() {
 
159
        string? access_phase_token = get_persistent_access_phase_token();
 
160
        string? access_phase_token_secret = get_persistent_access_phase_token_secret();
 
161
 
 
162
        bool valid = ((access_phase_token != null) && (access_phase_token_secret != null));
 
163
 
 
164
        if (valid)
 
165
            debug("existing Tumblr session found in configuration database; using it.");
 
166
        else
 
167
            debug("no persisted Tumblr session exists.");
 
168
 
 
169
        return valid;
 
170
    }
 
171
 
 
172
 
 
173
 
 
174
 
 
175
    public string? get_persistent_access_phase_token() {
 
176
        return host.get_config_string("token", null);
 
177
    }
 
178
    
 
179
    private void set_persistent_access_phase_token(string? token) {
 
180
        host.set_config_string("token", token);
 
181
    } 
 
182
    
 
183
    public string? get_persistent_access_phase_token_secret() {
 
184
        return host.get_config_string("token_secret", null);
 
185
    }
 
186
    
 
187
    private void set_persistent_access_phase_token_secret(string? token_secret) {
 
188
        host.set_config_string("token_secret", token_secret);
 
189
    } 
 
190
 
 
191
    internal int get_persistent_default_size() {
 
192
        return host.get_config_int("default_size", 1);
 
193
    }
 
194
    
 
195
    internal void set_persistent_default_size(int size) {
 
196
        host.set_config_int("default_size", size);
 
197
    }
 
198
 
 
199
    internal int get_persistent_default_blog() {
 
200
        return host.get_config_int("default_blog", 0);
 
201
    }
 
202
    
 
203
    internal void set_persistent_default_blog(int blog) {
 
204
        host.set_config_int("default_blog", blog);
 
205
    }
 
206
 
 
207
    // Actions and events implementation
 
208
    
 
209
    /**
 
210
     * Action that shows the authentication pane.
 
211
     *
 
212
     * This action method shows the authentication pane. It is shown at the
 
213
     * very beginning of the interaction when no persistent parameters are found
 
214
     * or after a failed login attempt using persisted parameters. It can be
 
215
     * given a mode flag to specify whether it should be displayed in initial
 
216
     * mode or in any of the error modes that it supports.
 
217
     *
 
218
     * @param mode the mode for the authentication pane
 
219
     */
 
220
    private void do_show_authentication_pane(AuthenticationPane.Mode mode = AuthenticationPane.Mode.INTRO) {
 
221
        debug("ACTION: installing authentication pane");
 
222
 
 
223
        host.set_service_locked(false);
 
224
        AuthenticationPane authentication_pane =
 
225
            new AuthenticationPane(this, mode);
 
226
        authentication_pane.login.connect(on_authentication_pane_login_clicked);
 
227
        host.install_dialog_pane(authentication_pane, Spit.Publishing.PluginHost.ButtonMode.CLOSE);
 
228
        host.set_dialog_default_widget(authentication_pane.get_default_widget()); 
 
229
    } 
 
230
 
 
231
    /**
 
232
     * Event triggered when the login button in the authentication panel is
 
233
     * clicked.
 
234
     *
 
235
     * This event is triggered when the login button in the authentication
 
236
     * panel is clicked. It then triggers a network login interaction.
 
237
     *
 
238
     * @param username the name of the Tumblr user as entered in the dialog
 
239
     * @param password the password of the Tumblr as entered in the dialog
 
240
     */
 
241
    private void on_authentication_pane_login_clicked( string username, string password ) {
 
242
        debug("EVENT: on_authentication_pane_login_clicked");
 
243
        if (!running)
 
244
            return;
 
245
 
 
246
        do_network_login(username, password); 
 
247
    }
 
248
    
 
249
    /**
 
250
     * Action to perform a network login to a Tumblr blog.
 
251
     *
 
252
     * This action performs a network login a Tumblr blog specified the given user name and password as credentials.
 
253
     *
 
254
     * @param username the name of the Tumblr user used to login
 
255
     * @param password the password of the Tumblr user used to login
 
256
     */
 
257
    private void do_network_login(string username, string password) {
 
258
        debug("ACTION: logging in");
 
259
        host.set_service_locked(true);
 
260
        host.install_login_wait_pane();
 
261
        
 
262
 
 
263
        AccessTokenFetchTransaction txn = new AccessTokenFetchTransaction(session,username,password);
 
264
        txn.completed.connect(on_auth_request_txn_completed);
 
265
        txn.network_error.connect(on_auth_request_txn_error);
 
266
       
 
267
        try {
 
268
            txn.execute();
 
269
        } catch (Spit.Publishing.PublishingError err) {
 
270
            host.post_error(err);
 
271
        }
 
272
    }
 
273
      
 
274
 
 
275
    private void on_auth_request_txn_completed(Publishing.RESTSupport.Transaction txn) {
 
276
        txn.completed.disconnect(on_auth_request_txn_completed);
 
277
        txn.network_error.disconnect(on_auth_request_txn_error);
 
278
 
 
279
        if (!is_running())
 
280
            return;
 
281
 
 
282
        debug("EVENT: OAuth authentication request transaction completed; response = '%s'",
 
283
            txn.get_response());
 
284
 
 
285
        do_parse_token_info_from_auth_request(txn.get_response());
 
286
    }
 
287
 
 
288
    private void on_auth_request_txn_error(Publishing.RESTSupport.Transaction txn,
 
289
        Spit.Publishing.PublishingError err) {
 
290
        txn.completed.disconnect(on_auth_request_txn_completed);
 
291
        txn.network_error.disconnect(on_auth_request_txn_error);
 
292
 
 
293
        if (!is_running())
 
294
            return;
 
295
 
 
296
        debug("EVENT: OAuth authentication request transaction caused a network error");
 
297
        host.post_error(err);
 
298
    }
 
299
 
 
300
 
 
301
    private void do_parse_token_info_from_auth_request(string response) {
 
302
        debug("ACTION: parsing authorization request response '%s' into token and secret", response);
 
303
        
 
304
        string? oauth_token = null;
 
305
        string? oauth_token_secret = null;
 
306
        
 
307
        string[] key_value_pairs = response.split("&");
 
308
        foreach (string pair in key_value_pairs) {
 
309
            string[] split_pair = pair.split("=");
 
310
            
 
311
            if (split_pair.length != 2)
 
312
                host.post_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE(
 
313
                    _t("'%s' isn't a valid response to an OAuth authentication request")));
 
314
 
 
315
            if (split_pair[0] == "oauth_token")
 
316
                oauth_token = split_pair[1];
 
317
            else if (split_pair[0] == "oauth_token_secret")
 
318
                oauth_token_secret = split_pair[1];
 
319
        }
 
320
        
 
321
        if (oauth_token == null || oauth_token_secret == null)
 
322
            host.post_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE(
 
323
                _t("'%s' isn't a valid response to an OAuth authentication request")));
 
324
        
 
325
        session.set_access_phase_credentials(oauth_token, oauth_token_secret);
 
326
    }
 
327
 
 
328
 
 
329
 
 
330
    private void on_session_authenticated() {
 
331
        if (!is_running())
 
332
            return;
 
333
 
 
334
        debug("EVENT: a fully authenticated session has become available");             
 
335
        set_persistent_access_phase_token(session.get_access_phase_token());
 
336
        set_persistent_access_phase_token_secret(session.get_access_phase_token_secret());
 
337
                do_get_blogs();
 
338
 
 
339
}
 
340
 
 
341
    private void do_get_blogs() {
 
342
        debug("ACTION: obtain all blogs of the tumblr user");
 
343
        UserInfoFetchTransaction txn = new UserInfoFetchTransaction(session);
 
344
        txn.completed.connect(on_info_request_txn_completed);
 
345
        txn.network_error.connect(on_info_request_txn_error);
 
346
       
 
347
        try {
 
348
            txn.execute();
 
349
        } catch (Spit.Publishing.PublishingError err) {
 
350
            host.post_error(err);
 
351
        }
 
352
 
 
353
 
 
354
    }
 
355
 
 
356
 
 
357
    private void on_info_request_txn_completed(Publishing.RESTSupport.Transaction txn) {
 
358
        txn.completed.disconnect(on_info_request_txn_completed);
 
359
        txn.network_error.disconnect(on_info_request_txn_error);
 
360
 
 
361
        if (!is_running())
 
362
            return;
 
363
 
 
364
        debug("EVENT: user info request transaction completed; response = '%s'",
 
365
            txn.get_response());
 
366
        do_parse_token_info_from_user_request(txn.get_response());
 
367
        do_show_publishing_options_pane();
 
368
    }
 
369
 
 
370
 
 
371
    private void do_parse_token_info_from_user_request(string response) {
 
372
        debug("ACTION: parsing info request response '%s' into list of available blogs", response);
 
373
        try {
 
374
                        var parser = new Json.Parser();
 
375
                        parser.load_from_data (response, -1);
 
376
                        var root_object = parser.get_root().get_object();
 
377
                        this.username = root_object.get_object_member("response").get_object_member("user").get_string_member ("name");
 
378
                        debug("Got user name: %s",username);
 
379
                    foreach (var blognode in root_object.get_object_member("response").get_object_member("user").get_array_member("blogs").get_elements ()) {
 
380
                        var blog = blognode.get_object ();
 
381
                                string name = blog.get_string_member ("name");
 
382
                                string url = blog.get_string_member ("url").replace("http://","").replace("/","");
 
383
                        debug("Got blog name: %s and url: %s", name, url);
 
384
                        this.blogs += new BlogEntry(name,url);
 
385
                    }
 
386
                } catch (Error err) {
 
387
                host.post_error(err);
 
388
        }
 
389
    }
 
390
 
 
391
    private void on_info_request_txn_error(Publishing.RESTSupport.Transaction txn,
 
392
        Spit.Publishing.PublishingError err) {
 
393
        txn.completed.disconnect(on_info_request_txn_completed);
 
394
        txn.network_error.disconnect(on_info_request_txn_error);
 
395
 
 
396
        if (!is_running())
 
397
            return;
 
398
 
 
399
        session.deauthenticate();
 
400
        invalidate_persistent_session();
 
401
        debug("EVENT: user info request transaction caused a network error");
 
402
        host.post_error(err);
 
403
    }
 
404
 
 
405
    private void do_show_publishing_options_pane() {
 
406
        debug("ACTION: displaying publishing options pane");
 
407
        host.set_service_locked(false);
 
408
        PublishingOptionsPane publishing_options_pane =
 
409
            new PublishingOptionsPane(this, host.get_publishable_media_type(), this.sizes, this.blogs, this.username);
 
410
        publishing_options_pane.publish.connect(on_publishing_options_pane_publish);
 
411
        publishing_options_pane.logout.connect(on_publishing_options_pane_logout);
 
412
        host.install_dialog_pane(publishing_options_pane);
 
413
    }
 
414
 
 
415
 
 
416
 
 
417
    private void on_publishing_options_pane_publish() {
 
418
        publishing_options_pane.publish.disconnect(on_publishing_options_pane_publish);
 
419
        publishing_options_pane.logout.disconnect(on_publishing_options_pane_logout);
 
420
        
 
421
        if (!is_running())
 
422
            return;
 
423
 
 
424
        debug("EVENT: user clicked the 'Publish' button in the publishing options pane");
 
425
        do_publish();
 
426
    }
 
427
 
 
428
    private void on_publishing_options_pane_logout() {
 
429
        publishing_options_pane.publish.disconnect(on_publishing_options_pane_publish);
 
430
        publishing_options_pane.logout.disconnect(on_publishing_options_pane_logout);
 
431
 
 
432
        if (!is_running())
 
433
            return;
 
434
 
 
435
        debug("EVENT: user clicked the 'Logout' button in the publishing options pane");
 
436
 
 
437
        do_logout();
 
438
    }
 
439
 
 
440
    public static int tumblr_date_time_compare_func(Spit.Publishing.Publishable a, 
 
441
        Spit.Publishing.Publishable b) {
 
442
        return a.get_exposure_date_time().compare(b.get_exposure_date_time());
 
443
    }
 
444
 
 
445
    private void do_publish() {
 
446
        debug("ACTION: uploading media items to remote server.");
 
447
 
 
448
        host.set_service_locked(true);
 
449
 
 
450
        progress_reporter = host.serialize_publishables(sizes[get_persistent_default_size()].size);
 
451
 
 
452
        // Serialization is a long and potentially cancellable operation, so before we use
 
453
        // the publishables, make sure that the publishing interaction is still running. If it
 
454
        // isn't the publishing environment may be partially torn down so do a short-circuit
 
455
        // return
 
456
        if (!is_running())
 
457
            return;
 
458
 
 
459
        // Sort publishables in reverse-chronological order.
 
460
        Spit.Publishing.Publishable[] publishables = host.get_publishables();
 
461
        Gee.ArrayList<Spit.Publishing.Publishable> sorted_list =
 
462
            new Gee.ArrayList<Spit.Publishing.Publishable>();
 
463
        foreach (Spit.Publishing.Publishable p in publishables) {
 
464
                debug("ACTION: add publishable");
 
465
            sorted_list.add(p);
 
466
        }
 
467
        sorted_list.sort((CompareFunc) tumblr_date_time_compare_func);
 
468
                string blog_url = this.blogs[get_persistent_default_blog()].url;
 
469
    
 
470
        Uploader uploader = new Uploader(session, sorted_list.to_array(),blog_url);
 
471
        uploader.upload_complete.connect(on_upload_complete);
 
472
        uploader.upload_error.connect(on_upload_error);
 
473
        uploader.upload(on_upload_status_updated);
 
474
    }
 
475
 
 
476
    private void do_show_success_pane() {
 
477
        debug("ACTION: showing success pane.");
 
478
 
 
479
        host.set_service_locked(false);
 
480
        host.install_success_pane();
 
481
    }
 
482
 
 
483
 
 
484
    private void on_upload_status_updated(int file_number, double completed_fraction) {
 
485
        if (!is_running())
 
486
            return;
 
487
 
 
488
        debug("EVENT: uploader reports upload %.2f percent complete.", 100.0 * completed_fraction);
 
489
 
 
490
        assert(progress_reporter != null);
 
491
 
 
492
        progress_reporter(file_number, completed_fraction);
 
493
    }
 
494
 
 
495
    private void on_upload_complete(Publishing.RESTSupport.BatchUploader uploader,
 
496
        int num_published) {
 
497
        if (!is_running())
 
498
            return;
 
499
 
 
500
        debug("EVENT: uploader reports upload complete; %d items published.", num_published);
 
501
 
 
502
        uploader.upload_complete.disconnect(on_upload_complete);
 
503
        uploader.upload_error.disconnect(on_upload_error);
 
504
 
 
505
        do_show_success_pane();
 
506
    }
 
507
 
 
508
    private void on_upload_error(Publishing.RESTSupport.BatchUploader uploader,
 
509
        Spit.Publishing.PublishingError err) {
 
510
        if (!is_running())
 
511
            return;
 
512
 
 
513
        debug("EVENT: uploader reports upload error = '%s'.", err.message);
 
514
 
 
515
        uploader.upload_complete.disconnect(on_upload_complete);
 
516
        uploader.upload_error.disconnect(on_upload_error);
 
517
 
 
518
        host.post_error(err);
 
519
    }
 
520
 
 
521
 
 
522
    private void do_logout() {
 
523
        debug("ACTION: logging user out, deauthenticating session, and erasing stored credentials");
 
524
 
 
525
        session.deauthenticate();
 
526
        invalidate_persistent_session();
 
527
 
 
528
        running = false;
 
529
 
 
530
        attempt_start();
 
531
    } 
 
532
 
 
533
    public void attempt_start() {
 
534
        if (is_running())
 
535
            return;
 
536
        
 
537
        debug("TumblrPublisher: starting interaction.");
 
538
        
 
539
        running = true;
 
540
        if (is_persistent_session_valid()) {
 
541
            debug("attempt start: a persistent session is available; using it");
 
542
 
 
543
            session.authenticate_from_persistent_credentials(get_persistent_access_phase_token(),
 
544
                get_persistent_access_phase_token_secret());
 
545
        } else {
 
546
            debug("attempt start: no persistent session available; showing login welcome pane");
 
547
 
 
548
            do_show_authentication_pane();
 
549
        }
 
550
    }
 
551
 
 
552
    public void start() {
 
553
        if (is_running())
 
554
            return;
 
555
        
 
556
        if (was_started)
 
557
            error(_t("TumblrPublisher: start( ): can't start; this publisher is not restartable."));
 
558
        
 
559
        debug("TumblrPublisher: starting interaction.");
 
560
        
 
561
        attempt_start();
 
562
    }
 
563
    
 
564
    public void stop() {
 
565
        debug("TumblrPublisher: stop( ) invoked.");
 
566
 
 
567
//        if (session != null)
 
568
//            session.stop_transactions();
 
569
 
 
570
        running = false;
 
571
    }
 
572
 
 
573
 
 
574
// UI elements
 
575
 
 
576
/**
 
577
 * The authentication pane used when asking service URL, user name and password
 
578
 * from the user.
 
579
 */
 
580
internal class AuthenticationPane : Spit.Publishing.DialogPane, Object {
 
581
    public enum Mode {
 
582
        INTRO,
 
583
        FAILED_RETRY_USER
 
584
    }
 
585
    private static string INTRO_MESSAGE = _t("Enter the username and password associated with your Tumblr account.");
 
586
    private static string FAILED_RETRY_USER_MESSAGE = _t("Username and/or password invalid. Please try again");
 
587
 
 
588
    private Gtk.Box pane_widget = null;
 
589
    private Gtk.Builder builder;
 
590
    private Gtk.Entry username_entry;
 
591
    private Gtk.Entry password_entry;
 
592
    private Gtk.Button login_button;
 
593
 
 
594
    public signal void login(string user, string password);
 
595
 
 
596
    public AuthenticationPane(TumblrPublisher publisher, Mode mode = Mode.INTRO) {
 
597
        this.pane_widget = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
 
598
 
 
599
        File ui_file = publisher.get_host().get_module_file().get_parent().
 
600
            get_child("tumblr_authentication_pane.glade");
 
601
        
 
602
        try {
 
603
            builder = new Gtk.Builder();
 
604
            builder.add_from_file(ui_file.get_path());
 
605
            builder.connect_signals(null);
 
606
            Gtk.Alignment align = builder.get_object("alignment") as Gtk.Alignment;
 
607
            
 
608
            Gtk.Label message_label = builder.get_object("message_label") as Gtk.Label;
 
609
            switch (mode) {
 
610
                case Mode.INTRO:
 
611
                    message_label.set_text(INTRO_MESSAGE);
 
612
                    break;
 
613
 
 
614
                case Mode.FAILED_RETRY_USER:
 
615
                    message_label.set_markup("<b>%s</b>\n\n%s".printf(_t(
 
616
                        "Invalid User Name or Password"), FAILED_RETRY_USER_MESSAGE));
 
617
                    break;
 
618
            }
 
619
 
 
620
            username_entry = builder.get_object ("username_entry") as Gtk.Entry;
 
621
 
 
622
            password_entry = builder.get_object ("password_entry") as Gtk.Entry;
 
623
    
 
624
 
 
625
 
 
626
            login_button = builder.get_object("login_button") as Gtk.Button;
 
627
 
 
628
            username_entry.changed.connect(on_user_changed);
 
629
            password_entry.changed.connect(on_password_changed);
 
630
            login_button.clicked.connect(on_login_button_clicked);
 
631
 
 
632
            align.reparent(pane_widget);
 
633
            publisher.get_host().set_dialog_default_widget(login_button);
 
634
        } catch (Error e) {
 
635
            warning(_t("Could not load UI: %s"), e.message);
 
636
        }
 
637
    }
 
638
    
 
639
    public Gtk.Widget get_default_widget() {
 
640
        return login_button;
 
641
    }
 
642
 
 
643
    private void on_login_button_clicked() {
 
644
        login(username_entry.get_text(),
 
645
            password_entry.get_text());
 
646
    }
 
647
 
 
648
 
 
649
    private void on_user_changed() {
 
650
        update_login_button_sensitivity();
 
651
    }
 
652
 
 
653
    private void on_password_changed() {
 
654
        update_login_button_sensitivity();
 
655
    }
 
656
    
 
657
    private void update_login_button_sensitivity() {
 
658
        login_button.set_sensitive(
 
659
            !is_string_empty(username_entry.get_text()) &&
 
660
            !is_string_empty(password_entry.get_text())
 
661
        );
 
662
    }
 
663
    
 
664
    public Gtk.Widget get_widget() {
 
665
        return pane_widget;
 
666
    }
 
667
    
 
668
    public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry() {
 
669
        return Spit.Publishing.DialogPane.GeometryOptions.NONE;
 
670
    }
 
671
    
 
672
    public void on_pane_installed() {
 
673
        username_entry.grab_focus();
 
674
        password_entry.set_activates_default(true);
 
675
        login_button.can_default = true;
 
676
        update_login_button_sensitivity();
 
677
    }
 
678
    
 
679
    public void on_pane_uninstalled() {
 
680
    }
 
681
}
 
682
 
 
683
 
 
684
/**
 
685
 * The publishing options pane.
 
686
 */
 
687
 
 
688
 
 
689
internal class PublishingOptionsPane : Spit.Publishing.DialogPane, GLib.Object {
 
690
 
 
691
 
 
692
 
 
693
    private Gtk.Builder builder;
 
694
    private Gtk.Box pane_widget = null;
 
695
    private Gtk.Label upload_info_label = null;
 
696
    private Gtk.Label size_label = null;
 
697
    private Gtk.Label blog_label = null;
 
698
    private Gtk.Button logout_button = null;
 
699
    private Gtk.Button publish_button = null;
 
700
    private Gtk.ComboBoxText size_combo = null;
 
701
    private Gtk.ComboBoxText blog_combo = null;
 
702
    private SizeEntry[] sizes = null;
 
703
    private BlogEntry[] blogs = null;
 
704
        private string username = "";
 
705
    private TumblrPublisher publisher = null;
 
706
    private Spit.Publishing.Publisher.MediaType media_type;
 
707
 
 
708
    public signal void publish();
 
709
    public signal void logout();
 
710
 
 
711
    public PublishingOptionsPane(TumblrPublisher publisher, Spit.Publishing.Publisher.MediaType media_type, SizeEntry[] sizes, BlogEntry[] blogs, string username) {
 
712
 
 
713
        this.pane_widget = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
 
714
                this.username = username;
 
715
                this.publisher = publisher;
 
716
                this.media_type = media_type;
 
717
                this.sizes = sizes;
 
718
                this.blogs=blogs;
 
719
        File ui_file = publisher.get_host().get_module_file().get_parent().
 
720
            get_child("tumblr_publishing_options_pane.glade");
 
721
        
 
722
        try {
 
723
                        builder = new Gtk.Builder();
 
724
                        builder.add_from_file(ui_file.get_path());
 
725
                        builder.connect_signals(null);
 
726
 
 
727
                        // pull in the necessary widgets from the glade file
 
728
                        pane_widget = (Gtk.Box) this.builder.get_object("tumblr_pane");
 
729
                        upload_info_label = (Gtk.Label) this.builder.get_object("upload_info_label");
 
730
                        logout_button = (Gtk.Button) this.builder.get_object("logout_button");
 
731
                        publish_button = (Gtk.Button) this.builder.get_object("publish_button");
 
732
                        size_combo = (Gtk.ComboBoxText) this.builder.get_object("size_combo");
 
733
                        size_label = (Gtk.Label) this.builder.get_object("size_label");
 
734
                        blog_combo = (Gtk.ComboBoxText) this.builder.get_object("blog_combo");
 
735
                        blog_label = (Gtk.Label) this.builder.get_object("blog_label");
 
736
 
 
737
 
 
738
                        string upload_label_text = _t("You are logged into Tumblr as %s.\n\n").printf(this.username);
 
739
                        upload_info_label.set_label(upload_label_text);
 
740
 
 
741
                        populate_blog_combo();
 
742
                        blog_combo.changed.connect(on_blog_changed);
 
743
 
 
744
                        if ((media_type != Spit.Publishing.Publisher.MediaType.VIDEO)) {
 
745
                                populate_size_combo();
 
746
                                size_combo.changed.connect(on_size_changed);
 
747
                        } else {
 
748
                        // publishing -only- video - don't let the user manipulate the photo size choices.
 
749
                                size_combo.set_sensitive(false);
 
750
                                size_label.set_sensitive(false);
 
751
                        }
 
752
 
 
753
                        logout_button.clicked.connect(on_logout_clicked);
 
754
                        publish_button.clicked.connect(on_publish_clicked);
 
755
        } catch (Error e) {
 
756
                        warning(_t("Could not load UI: %s"), e.message);
 
757
        }
 
758
    }
 
759
 
 
760
 
 
761
 
 
762
 
 
763
 
 
764
    private void on_logout_clicked() {
 
765
        logout();
 
766
    }
 
767
 
 
768
    private void on_publish_clicked() {
 
769
 
 
770
 
 
771
        publish();
 
772
    }
 
773
 
 
774
 
 
775
    private void populate_blog_combo() {
 
776
        if (blogs != null) {
 
777
          foreach (BlogEntry b in blogs)
 
778
            blog_combo.append_text(b.blog);
 
779
          blog_combo.set_active(publisher.get_persistent_default_blog());
 
780
                }
 
781
    }
 
782
 
 
783
    private void on_blog_changed() {
 
784
        publisher.set_persistent_default_blog(blog_combo.get_active());
 
785
    }
 
786
 
 
787
    private void populate_size_combo() {
 
788
        if (sizes != null) {
 
789
          foreach (SizeEntry e in sizes)
 
790
            size_combo.append_text(e.title);
 
791
          size_combo.set_active(publisher.get_persistent_default_size());
 
792
                }
 
793
    }
 
794
 
 
795
    private void on_size_changed() {
 
796
        publisher.set_persistent_default_size(size_combo.get_active());
 
797
    }
 
798
 
 
799
 
 
800
    protected void notify_publish() {
 
801
        publish();
 
802
    }
 
803
    
 
804
    protected void notify_logout() {
 
805
        logout();
 
806
    }
 
807
 
 
808
    public Gtk.Widget get_widget() {
 
809
        return pane_widget;
 
810
    }
 
811
    
 
812
    public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry() {
 
813
        return Spit.Publishing.DialogPane.GeometryOptions.NONE;
 
814
    }
 
815
    
 
816
    public void on_pane_installed() {        
 
817
        publish.connect(notify_publish);
 
818
        logout.connect(notify_logout);
 
819
    }
 
820
    
 
821
    public void on_pane_uninstalled() {
 
822
        publish.disconnect(notify_publish);
 
823
        logout.disconnect(notify_logout);
 
824
    }
 
825
}
 
826
 
 
827
 
 
828
// REST support classes
 
829
internal class Transaction : Publishing.RESTSupport.Transaction {
 
830
    public Transaction(Session session, Publishing.RESTSupport.HttpMethod method =
 
831
        Publishing.RESTSupport.HttpMethod.POST) {
 
832
        base(session, method);
 
833
        
 
834
    }
 
835
 
 
836
    public Transaction.with_uri(Session session, string uri,
 
837
        Publishing.RESTSupport.HttpMethod method = Publishing.RESTSupport.HttpMethod.POST) {
 
838
        base.with_endpoint_url(session, uri, method);
 
839
 
 
840
        add_argument("oauth_nonce", session.get_oauth_nonce());
 
841
        add_argument("oauth_signature_method", "HMAC-SHA1");
 
842
        add_argument("oauth_version", "1.0");
 
843
        add_argument("oauth_timestamp", session.get_oauth_timestamp());
 
844
        add_argument("oauth_consumer_key", API_KEY);
 
845
                if (session.get_access_phase_token() != null) {
 
846
            add_argument("oauth_token", session.get_access_phase_token());
 
847
        }
 
848
    } 
 
849
 
 
850
    public override void execute() throws Spit.Publishing.PublishingError {
 
851
        ((Session) get_parent_session()).sign_transaction(this);
 
852
        
 
853
        base.execute();
 
854
    }
 
855
 
 
856
}
 
857
 
 
858
 
 
859
internal class AccessTokenFetchTransaction : Transaction {
 
860
    public AccessTokenFetchTransaction(Session session, string username, string password) {
 
861
        base.with_uri(session, "https://www.tumblr.com/oauth/access_token",
 
862
            Publishing.RESTSupport.HttpMethod.POST);
 
863
        add_argument("x_auth_username", Soup.URI.encode(username, ENCODE_RFC_3986_EXTRA));
 
864
        add_argument("x_auth_password", password);
 
865
        add_argument("x_auth_mode", "client_auth");
 
866
    }
 
867
}
 
868
 
 
869
internal class UserInfoFetchTransaction : Transaction {
 
870
    public UserInfoFetchTransaction(Session session) {
 
871
        base.with_uri(session, "http://api.tumblr.com/v2/user/info",
 
872
            Publishing.RESTSupport.HttpMethod.POST);
 
873
    }
 
874
}
 
875
 
 
876
 
 
877
internal class UploadTransaction : Publishing.RESTSupport.UploadTransaction {
 
878
    private Session session;
 
879
    private Publishing.RESTSupport.Argument[] auth_header_fields;
 
880
 
 
881
 
 
882
//Workaround for Soup.URI.encode() to support binary data (i.e. string with \0) 
 
883
    private string encode( uint8[] data ){
 
884
        var s = new StringBuilder();
 
885
        char[] bytes = new char[2];
 
886
        bytes[1] = 0;
 
887
        foreach( var byte in data )
 
888
        {
 
889
           if(byte == 0) {
 
890
                s.append( "%00" );
 
891
           } else { 
 
892
                bytes[0] = (char)byte;
 
893
                s.append( Soup.URI.encode((string) bytes, ENCODE_RFC_3986_EXTRA) );
 
894
           }
 
895
        }
 
896
        return s.str;
 
897
    }
 
898
 
 
899
 
 
900
    public UploadTransaction(Session session,Spit.Publishing.Publishable publishable, string blog_url)  {
 
901
                debug("Init upload transaction");
 
902
        base.with_endpoint_url(session, publishable,"http://api.tumblr.com/v2/blog/%s/post".printf(blog_url) );
 
903
        this.session = session;
 
904
 
 
905
    }
 
906
    
 
907
 
 
908
  
 
909
    public void add_authorization_header_field(string key, string value) {
 
910
        auth_header_fields += new Publishing.RESTSupport.Argument(key, value);
 
911
    }
 
912
    
 
913
    public Publishing.RESTSupport.Argument[] get_authorization_header_fields() {
 
914
        return auth_header_fields;
 
915
    }
 
916
    
 
917
    public string get_authorization_header_string() {
 
918
        string result = "OAuth ";
 
919
        
 
920
        for (int i = 0; i < auth_header_fields.length; i++) {
 
921
            result += auth_header_fields[i].key;
 
922
            result += "=";
 
923
            result += ("\"" + auth_header_fields[i].value + "\"");
 
924
            
 
925
            if (i < auth_header_fields.length - 1)
 
926
                result += ", ";
 
927
        }
 
928
        
 
929
        return result;
 
930
    }
 
931
    
 
932
    public override void execute() throws Spit.Publishing.PublishingError {
 
933
        add_authorization_header_field("oauth_nonce", session.get_oauth_nonce());
 
934
        add_authorization_header_field("oauth_signature_method", "HMAC-SHA1");
 
935
        add_authorization_header_field("oauth_version", "1.0");
 
936
        add_authorization_header_field("oauth_timestamp", session.get_oauth_timestamp());
 
937
        add_authorization_header_field("oauth_consumer_key", API_KEY);
 
938
        add_authorization_header_field("oauth_token", session.get_access_phase_token());
 
939
 
 
940
 
 
941
        string payload;
 
942
        size_t payload_length;
 
943
        try {
 
944
            FileUtils.get_contents(base.publishable.get_serialized_file().get_path(), out payload,
 
945
                            out payload_length);
 
946
 
 
947
                        string reqdata = this.encode(payload.data[0:payload_length]);
 
948
 
 
949
 
 
950
 
 
951
                        add_argument("data[0]", reqdata);
 
952
                        add_argument("type", "photo");
 
953
                        string[] keywords = base.publishable.get_publishing_keywords();
 
954
                        string tags = "";
 
955
                        if (keywords != null) {
 
956
                                foreach (string tag in keywords) {
 
957
                                if (!is_string_empty(tags)) {
 
958
                                        tags += ",";
 
959
                                }
 
960
                                tags += tag;
 
961
                                }
 
962
                        }
 
963
                        add_argument("tags", Soup.URI.encode(tags, ENCODE_RFC_3986_EXTRA));
 
964
 
 
965
        } catch (FileError e) {
 
966
            throw new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR(
 
967
                _t("A temporary file needed for publishing is unavailable"));
 
968
 
 
969
                }
 
970
 
 
971
 
 
972
        session.sign_transaction(this);
 
973
        
 
974
        string authorization_header = get_authorization_header_string();
 
975
        
 
976
        debug("executing upload transaction: authorization header string = '%s'",
 
977
            authorization_header);
 
978
        add_header("Authorization", authorization_header);
 
979
        
 
980
        Publishing.RESTSupport.Argument[] request_arguments = get_arguments();
 
981
        assert(request_arguments.length > 0);
 
982
 
 
983
                string request_data = "";
 
984
                for (int i = 0; i < request_arguments.length; i++) {
 
985
                        request_data += (request_arguments[i].key + "=" + request_arguments[i].value);
 
986
                        if (i < request_arguments.length - 1)
 
987
                            request_data += "&";
 
988
                 }
 
989
        Soup.Message outbound_message = new Soup.Message( "POST", get_endpoint_url());
 
990
                outbound_message.set_request("application/x-www-form-urlencoded", Soup.MemoryUse.COPY, request_data.data);
 
991
 
 
992
        // TODO: there must be a better way to iterate over a map
 
993
        Gee.MapIterator<string, string> i = base.message_headers.map_iterator();
 
994
        bool cont = i.first();
 
995
        while(cont) {
 
996
            outbound_message.request_headers.append(i.get_key(), i.get_value());
 
997
            cont = i.next();
 
998
        }
 
999
        set_message(outbound_message);
 
1000
    
 
1001
        set_is_executed(true);
 
1002
 
 
1003
        send();
 
1004
    }
 
1005
}
 
1006
 
 
1007
 
 
1008
 
 
1009
internal class Uploader : Publishing.RESTSupport.BatchUploader {
 
1010
        private string blog_url = "";
 
1011
    public Uploader(Session session, Spit.Publishing.Publishable[] publishables, string blog_url) {
 
1012
        base(session, publishables);
 
1013
                this.blog_url=blog_url;
 
1014
 
 
1015
    }
 
1016
    
 
1017
 
 
1018
    protected override Publishing.RESTSupport.Transaction create_transaction(
 
1019
        Spit.Publishing.Publishable publishable) {
 
1020
                debug("Create upload transaction");
 
1021
        return new UploadTransaction((Session) get_session(), get_current_publishable(), this.blog_url);
 
1022
 
 
1023
    }
 
1024
}
 
1025
 
 
1026
/**
 
1027
 * Session class that keeps track of the authentication status and of the
 
1028
 * user token tumblr.
 
1029
 */
 
1030
internal class Session : Publishing.RESTSupport.Session {
 
1031
    private string? access_phase_token = null;
 
1032
    private string? access_phase_token_secret = null;
 
1033
 
 
1034
 
 
1035
    public Session() {
 
1036
        base(ENDPOINT_URL);
 
1037
    }
 
1038
 
 
1039
    public override bool is_authenticated() {
 
1040
        return (access_phase_token != null && access_phase_token_secret != null);
 
1041
    }
 
1042
 
 
1043
    public void authenticate_from_persistent_credentials(string token, string secret) {
 
1044
        this.access_phase_token = token;
 
1045
        this.access_phase_token_secret = secret;
 
1046
 
 
1047
        
 
1048
        authenticated();
 
1049
    }
 
1050
    
 
1051
    public void deauthenticate() {
 
1052
        access_phase_token = null;
 
1053
        access_phase_token_secret = null;
 
1054
    } 
 
1055
    
 
1056
    public void sign_transaction(Publishing.RESTSupport.Transaction txn) {
 
1057
        string http_method = txn.get_method().to_string();
 
1058
        
 
1059
        debug("signing transaction with parameters:");
 
1060
        debug("HTTP method = " + http_method);
 
1061
                string? signing_key = null;
 
1062
        if (access_phase_token_secret != null) {
 
1063
            debug("access phase token secret available; using it as signing key");
 
1064
 
 
1065
            signing_key = API_SECRET + "&" + this.get_access_phase_token_secret();
 
1066
        } else {
 
1067
            debug("Access phase token secret not available; using API " +
 
1068
                "key as signing key");
 
1069
 
 
1070
            signing_key = API_SECRET + "&";
 
1071
        }
 
1072
 
 
1073
 
 
1074
        Publishing.RESTSupport.Argument[] base_string_arguments = txn.get_arguments();
 
1075
        
 
1076
        UploadTransaction? upload_txn = txn as UploadTransaction;
 
1077
        if (upload_txn != null) {
 
1078
            debug("this transaction is an UploadTransaction; including Authorization header " +
 
1079
                "fields in signature base string");
 
1080
            
 
1081
            Publishing.RESTSupport.Argument[] auth_header_args =
 
1082
                upload_txn.get_authorization_header_fields();
 
1083
 
 
1084
            foreach (Publishing.RESTSupport.Argument arg in auth_header_args)
 
1085
                base_string_arguments += arg;
 
1086
        }
 
1087
        
 
1088
        Publishing.RESTSupport.Argument[] sorted_args =
 
1089
            Publishing.RESTSupport.Argument.sort(base_string_arguments);
 
1090
        
 
1091
        string arguments_string = "";
 
1092
        for (int i = 0; i < sorted_args.length; i++) {
 
1093
            arguments_string += (sorted_args[i].key + "=" + sorted_args[i].value);
 
1094
            if (i < sorted_args.length - 1)
 
1095
                arguments_string += "&";
 
1096
        }
 
1097
 
 
1098
 
 
1099
        string signature_base_string = http_method + "&" + Soup.URI.encode(
 
1100
            txn.get_endpoint_url(), ENCODE_RFC_3986_EXTRA) + "&" +
 
1101
            Soup.URI.encode(arguments_string, ENCODE_RFC_3986_EXTRA);
 
1102
 
 
1103
        debug("signature base string = '%s'", signature_base_string);
 
1104
        debug("signing key = '%s'", signing_key);
 
1105
 
 
1106
        // compute the signature
 
1107
        string signature = hmac_sha1(signing_key, signature_base_string);
 
1108
        debug("signature = '%s'", signature);
 
1109
        signature = Soup.URI.encode(signature, ENCODE_RFC_3986_EXTRA);
 
1110
 
 
1111
        debug("signature after RFC encode = '%s'", signature);
 
1112
 
 
1113
        if (upload_txn != null)
 
1114
            upload_txn.add_authorization_header_field("oauth_signature", signature);
 
1115
        else
 
1116
            txn.add_argument("oauth_signature", signature);
 
1117
 
 
1118
 
 
1119
    }
 
1120
    
 
1121
    public void set_access_phase_credentials(string token, string secret) {
 
1122
        this.access_phase_token = token;
 
1123
        this.access_phase_token_secret = secret;
 
1124
 
 
1125
        
 
1126
        authenticated();
 
1127
    } 
 
1128
 
 
1129
    public string get_access_phase_token() {
 
1130
        return access_phase_token;
 
1131
    }
 
1132
 
 
1133
 
 
1134
    public string get_access_phase_token_secret() {
 
1135
        return access_phase_token_secret;
 
1136
    }
 
1137
 
 
1138
    public string get_oauth_nonce() {
 
1139
        TimeVal currtime = TimeVal();
 
1140
        currtime.get_current_time();
 
1141
        
 
1142
        return Checksum.compute_for_string(ChecksumType.MD5, currtime.tv_sec.to_string() +
 
1143
            currtime.tv_usec.to_string());
 
1144
    }
 
1145
    
 
1146
    public string get_oauth_timestamp() {
 
1147
        return GLib.get_real_time().to_string().substring(0, 10);
 
1148
    }
 
1149
 
 
1150
}
 
1151
 
 
1152
 
 
1153
} //class TumblrPublisher
 
1154
 
 
1155
} //namespace Publishing.Tumblr
 
1156