picasa_client.js revision ddb351dbec246cf1fab5ec20d2d5520909041de1
1// Copyright (c) 2011 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5picasa = {}
6
7/**
8 * LocalFile constructor.
9 *
10 * LocalFile object represents a file to be uploaded.
11 */
12picasa.LocalFile = function(file) {
13  this.file_ = file;
14  this.caption = file.name;
15
16  this.dataUrl_ = null;
17  this.mime_ = file.type;
18};
19
20picasa.LocalFile.prototype = {
21  /**
22   * Reads data url from local file to show in img element.
23   * @param {Function} callback Callback.
24   */
25  readData_: function(callback) {
26    if (this.dataUrl_) {
27      callback.call(this);
28      return;
29    }
30
31    var reader = new FileReader();
32    function onLoadCallback(e) {
33      this.dataUrl_ = e.target.result;
34      this.mime_ = this.dataUrl_.substring(0, this.dataUrl_.indexOf(';base64'));
35      this.mime_ = this.mime_.substr(5);  // skip 'data:'
36      callback.call(this);
37    }
38    reader.onload =  onLoadCallback.bind(this);
39    reader.readAsDataURL(this.file_);
40  },
41
42  showInImage: function(img) {
43    if (this.dataUrl_) {
44      img.setAttribute('src', this.dataUrl_);
45      return;
46    }
47
48    this.readData_(function() {
49      img.setAttribute('src', this.dataUrl_);
50    });
51  },
52
53  /**
54   * @return {string} Mime type of the file.
55   */
56  get mimeType() {
57    return this.mime_;
58  }
59};
60
61
62/**
63 * Album constructor.
64 *
65 * Album object stores information about picasa album.
66 */
67picasa.Album = function(id, title, location, description, link) {
68  this.id = id;
69  this.title = title;
70  this.location = location;
71  this.description = description;
72  this.link = link;
73};
74
75
76/**
77 * Client constructor.
78 *
79 * Client object stores user credentials and gets from and sends to picasa
80 * web server.
81 */
82picasa.Client = function() {
83};
84
85
86picasa.Client.prototype = {
87  __proto__: cr.EventTarget.prototype,
88
89  /**
90   * User credentials.
91   * @type {string}
92   * @private
93   */
94  authToken_: null,
95
96  /**
97   * User id.
98   * @type {string}
99   * @private
100   */
101  userID_: null,
102
103  /**
104   * List of user albums.
105   * @type {Array.<picasa.Album>}
106   * @private
107   */
108  albums_: null,
109
110  /**
111   * Url for captcha challenge, if required.
112   * @type {string}
113   * @private
114   */
115  captchaUrl_: null,
116
117  /**
118   * Captcha toekn, if required.
119   * @type {string}
120   * @private
121   */
122  captchaToken_: null,
123
124  /**
125   * Whether client is already authorized.
126   * @type {boolean}
127   */
128  get authorized() {
129    return !!this.authToken_;
130  },
131
132  /**
133   * User id.
134   * @type {string}
135   */
136  get userID() {
137    return this.userID_ || '';
138  },
139
140  /**
141   * List of albums.
142   * @type {Array.<picasa.Album>}
143   */
144  get albums() {
145    return this.albums_ || [];
146  },
147
148  /**
149   * Captcha url to show to user, if needed.
150   * @type {string}
151   */
152  get captchaUrl() {
153    return this.captchaUrl_;
154  },
155
156  /**
157   * Get user credential for picasa web server.
158   * @param {string} login User login.
159   * @param {string} password User password.
160   * @param {Function(string)} callback Callback, which is passed 'status'
161   *     parameter: either 'success', 'failure' or 'captcha'.
162   * @param {?string=} opt_captcha Captcha answer, if was required.
163   */
164  login: function(login, password, callback, opt_captcha) {
165    function xhrCallback(xhr) {
166      if (xhr.status == 200) {
167        this.authToken_ = this.extractResponseField_(xhr.responseText, 'Auth');
168       this.userID_ = login;
169        callback('success');
170      } else {
171        var response = xhr.responseText;
172        var error = this.extractResponseField_(response, 'Error');
173        if (error == 'CaptchaRequired') {
174          this.captchaToken_ = this.extractResponseField_(response,
175              'CaptchaToken');
176          // Captcha url should prefixed with this.
177          this.captchaUrl_ = 'http://www.google.com/accounts/' +
178              this.extractResponseField_(response, 'CaptchaUrl');
179          callback('captcha');
180          return;
181        }
182        callback('failure');
183      }
184    }
185
186    var content = 'accountType=HOSTED_OR_GOOGLE&Email=' + login +
187        '&Passwd=' + password + '&service=lh2&source=ChromeOsPWAUploader';
188    if (opt_captcha && this.captchaToken_) {
189      content += '&logintoken=' + this.captchaToken_;
190      content += '&logincaptcha=' + opt_captcha;
191    }
192    this.sendRequest('POST', 'https://www.google.com/accounts/ClientLogin',
193        {'Content-type': 'application/x-www-form-urlencoded'},
194        content,
195        xhrCallback.bind(this));
196  },
197
198  /**
199   * Logs out.
200   */
201  logout: function() {
202    this.authToken_ = null;
203    this.userID_ = null;
204    this.captchaToken_ = null;
205    this.captchatUrl_ = null;
206  },
207
208  /**
209   * Extracts text field from text response.
210   * @param {string} response The response.
211   * @param {string} field Field name to extract value of.
212   * @return {?string} Field value or null.
213   */
214  extractResponseField_: function(response, field) {
215    var lines = response.split('\n');
216    field += '=';
217    for (var line, i = 0; line = lines[i]; i++) {
218      if (line.indexOf(field) == 0) {
219        return line.substr(field.length);
220      }
221    }
222    return null;
223  },
224
225  /**
226   * Sends request to web server.
227   * @param {string} method Method to use (GET or POST).
228   * @param {string} url Request url.
229   * @param {Object.<string, string>} headers Request headers.
230   * @param {*} body Request body.
231   * @param {Function(XMLHttpRequest)} callback Callback.
232   */
233  sendRequest: function(method, url, headers, body, callback) {
234    var xhr = new XMLHttpRequest();
235    xhr.onreadystatechange = function() {
236      if (xhr.readyState == 4) {
237        callback(xhr);
238      }
239    };
240    xhr.open(method, url, true);
241    if (headers) {
242      for (var header in headers) {
243        if (headers.hasOwnProperty(header)) {
244          xhr.setRequestHeader(header, headers[header]);
245        }
246      }
247    }
248    xhr.send(body);
249    return xhr;
250  },
251
252  /**
253   * Gets the feed from web server and parses it. Appends user credentials.
254   * @param {string} url Feed url.
255   * @param {Function(*)} callback Callback.
256   */
257  getFeed: function(url, callback) {
258    var headers = {'Authorization': 'GoogleLogin auth=' + this.authToken_};
259    this.sendRequest('GET', url + '?alt=json', headers, null, function(xhr) {
260      if (xhr.status == 200) {
261        var feed = JSON.parse(xhr.responseText);
262        callback(feed);
263      } else {
264        callback(null);
265      }
266    });
267  },
268
269  /**
270   * Posts the feed to web server. Appends user credentials.
271   * @param {string} url Feed url.
272   * @param {Object.<string, string>} headers Request headers.
273   * @param {*} body Post body.
274   * @param {Function(!string)} callback Callback taking response text or
275   *     null in the case of failure.
276   */
277  postFeed: function(url, headers, body, callback) {
278    headers['Authorization'] = 'GoogleLogin auth=' + this.authToken_;
279    return this.sendRequest('POST', url, headers, body, function(xhr) {
280      if (xhr.status >= 200 && xhr.status <= 202) {
281        callback(xhr.responseText);
282      } else {
283        callback(null);
284      }
285    });
286  },
287
288  /**
289   * Requests albums for the user and passes them to callback.
290   * @param {Function(Array.<picasa.Album>)} callback Callback.
291   */
292  getAlbums: function(callback) {
293    function feedCallback(feed) {
294      feed = feed.feed;
295      if (!feed.entry) {
296        return;
297      }
298      this.albums_ = [];
299      for (var entry, i = 0; entry = feed.entry[i]; i++) {
300        this.albums_.push(this.albumFromEntry_(entry));
301      }
302      callback(this.albums_);
303    }
304
305    this.getFeed('https://picasaweb.google.com/data/feed/api/user/' +
306        this.userID_, feedCallback.bind(this));
307  },
308
309  /**
310   * Returns album object created from entry.
311   * @param {*} entry The feed entry corresponding to album.
312   * @return {picasa.Album} The album object.
313   */
314  albumFromEntry_: function(entry) {
315    var altLink = '';
316    for (var link, j = 0; link = entry.link[j]; j++) {
317      if (link.rel == 'alternate') {
318        altLink = link.href;
319      }
320    }
321    return new picasa.Album(entry['gphoto$id']['$t'], entry.title['$t'],
322        entry['gphoto$location']['$t'], entry.summary['$t'], altLink);
323  },
324
325  /**
326   * Send request to create album.
327   * @param {picasa.Album} album Album to create.
328   * @param {Function(picasa.Album)} callback Callback taking updated album
329   *     (for example, with created album id).
330   */
331  createAlbum: function(album, callback) {
332    function postCallback(response) {
333      if (response == null) {
334        callback(null);
335      } else {
336        var entry = JSON.parse(response).entry;
337        callback(this.albumFromEntry_(entry));
338      }
339    }
340
341    var eol = '\n';
342    var postData = '<entry xmlns="http://www.w3.org/2005/Atom"' + eol;
343    postData += 'xmlns:media="http://search.yahoo.com/mrss/"' + eol;
344    postData += 'xmlns:gphoto="http://schemas.google.com/photos/2007">' + eol;
345    postData += '<title type="text">' + escape(album.title) + '</title>' + eol;
346    postData += '<summary type="text">' + escape(album.description) +
347        '</summary>' + eol;
348    postData += '<gphoto:location>' + escape(album.location) +
349        '</gphoto:location>' + eol;
350    postData += '<gphoto:access>public</gphoto:access>';
351    postData += '<category scheme="http://schemas.google.com/g/2005#kind" ' +
352        'term="http://schemas.google.com/photos/2007#album"></category>' + eol;
353    postData += '</entry>' + eol;
354
355    var headers = {'Content-Type': 'application/atom+xml'};
356    this.postFeed('https://picasaweb.google.com/data/feed/api/user/' +
357        this.userID_ + '?alt=json', headers, postData, postCallback.bind(this));
358  },
359
360  /**
361   * Uploads file to the given album.
362   * @param {picasa.Album} album Album to upload to.
363   * @param {picasa.LocalFile} file File to upload.
364   * @param {Function(?string)} callback Callback.
365   */
366  uploadFile: function(album, file, callback) {
367    var postData = file.file_;
368    var headers = {
369        'Content-Type': file.mimeType,
370        'Slug': file.file_.name};
371    return this.postFeed('https://picasaweb.google.com/data/feed/api/user/' +
372        this.userID_ + '/albumid/' + album.id, headers, postData, callback);
373  }
374};
375