1<!DOCTYPE html>
2<!--
3 * Copyright (c) 2010 The Chromium Authors. All rights reserved.  Use of this
4 * source code is governed by a BSD-style license that can be found in the
5 * LICENSE file.
6 *
7 * Author: Eric Bidelman <ericbidelman@chromium.org>
8-->
9<html>
10<head>
11<title>Your Google Documents List</title>
12<script type="text/javascript" src="js/jquery-1.4.1.min.js"></script>
13<style type="text/css">
14body {
15  font: 12px 'Myriad Pro', 'Tw Cen MT', Arial, Verdana, sans-serif;
16  color: #666666;
17  overflow-x: hidden;
18}
19ul {
20  padding: 0;
21  list-style: none;
22}
23li {
24  clear: both;
25  padding: 2px 0;
26}
27li div img {
28  margin: 0 5px;
29  vertical-align: middle;
30}
31li div {
32  text-overflow: ellipsis;
33  white-space: nowrap;
34  overflow: hidden;
35  width: 250px;
36  float: left;
37  padding: 2px 0;
38}
39li span {
40  margin-left: 5px;
41}
42li:hover {
43  background-color: #fffccc;
44}
45a {
46 color: #4E7DC2;
47 text-decoration: none;
48}
49a:hover {
50  color: #880000;
51  text-decoration: underline;
52}
53#butter {
54  color: #fff;
55  background-color: #000033;
56  padding: 5px 20px;
57  border-radius: 15px;
58  width: auto;
59  text-align: center;
60  float: right;
61  display: none;
62}
63#butter.error {
64  background-color: red;
65}
66#new_doc_container {
67  display: none;
68}
69#new_doc_container input[type='text'],textarea {
70  width: 100%;
71}
72#output {
73  width: 375px;
74  clear: both;
75}
76[contenteditable]:hover {
77  outline: 1px dotted #666;
78}
79.star {
80  margin-top: 1px;
81  margin-right: 3px;
82  width: 16px;
83  height: 16px;
84  background: no-repeat url(data:image/png;base64,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) !important;
85}
86.star.selected {
87  background: no-repeat url(data:image/png;base64,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) !important;
88}
89</style>
90</head>
91<body>
92
93<div style="height:15px;">
94  <div style="float:left;">
95    <a href="javascript:void(0);" onclick="gdocs.refreshDocs();return false;">Refresh list</a>,
96    <a href="javascript:void(0);" onclick="$('#new_doc_container').toggle();return false;">New Document</a>
97  </div>
98  <div id="butter">Fetching your docs</div>
99</div>
100<div id="new_doc_container">
101  Create a: <select id="doc_type">
102    <option value="document">document</option>
103    <option value="presentation">presentation</option>
104    <option value="spreadsheet">spreadsheet</option>
105  </select>
106  <input type="text" id="doc_title" placeholder="Enter a title"><br>
107  <textarea id="doc_content" placeholder="Enter document content"></textarea>
108  Star it? <input type="checkbox" id="doc_starred">
109  <button onclick="gdocs.createDoc();" style="float:right;">Create new doc</button>
110</div>
111<div id="output"></div>
112
113<script type="text/javascript">
114// Protected namespaces.
115var util = {};
116var gdocs = {};
117
118var bgPage = chrome.extension.getBackgroundPage();
119var pollIntervalMax = 1000 * 60 * 60;  // 1 hour
120var requestFailureCount = 0;  // used for exponential backoff
121var requestTimeout = 1000 * 2;  // 5 seconds
122
123var DEFAULT_MIMETYPES = {
124  'atom': 'application/atom+xml',
125  'document': 'text/plain',
126  'spreadsheet': 'text/csv',
127  'presentation': 'text/plain',
128  'pdf': 'application/pdf'
129};
130
131// Persistent click handler for star icons.
132$('#doc_type').change(function() {
133  if ($(this).val() === 'presentation') {
134    $('#doc_content').attr('disabled', 'true')
135                     .attr('placeholder', 'N/A for presentations');
136  } else {
137    $('#doc_content').removeAttr('disabled')
138                     .attr('placeholder', 'Enter document content');
139  }
140});
141
142
143// Persistent click handler for changing the title of a document.
144$('[contenteditable="true"]').live('blur', function(index) {
145  var index = $(this).parent().parent().attr('data-index');
146
147  // Only make the XHR if the user chose a new title.
148  if ($(this).text() != bgPage.docs[index].title) {
149    bgPage.docs[index].title = $(this).text();
150    gdocs.updateDoc(bgPage.docs[index]);
151  }
152});
153
154// Persistent click handler for star icons.
155$('.star').live('click', function() {
156  $(this).toggleClass('selected');
157
158  var index = $(this).parent().attr('data-index');
159  bgPage.docs[index].starred = $(this).hasClass('selected');
160  gdocs.updateDoc(bgPage.docs[index]);
161});
162
163/**
164 * Class to compartmentalize properties of a Google document.
165 * @param {Object} entry A JSON representation of a DocList atom entry.
166 * @constructor
167 */
168gdocs.GoogleDoc = function(entry) {
169  this.entry = entry;
170  this.title = entry.title.$t;
171  this.resourceId = entry.gd$resourceId.$t;
172  this.type = gdocs.getCategory(
173    entry.category, 'http://schemas.google.com/g/2005#kind');
174  this.starred = gdocs.getCategory(
175    entry.category, 'http://schemas.google.com/g/2005/labels',
176    'http://schemas.google.com/g/2005/labels#starred') ? true : false;
177  this.link = {
178    'alternate': gdocs.getLink(entry.link, 'alternate').href
179  };
180  this.contentSrc = entry.content.src;
181};
182
183/**
184 * Sets up a future poll for the user's document list.
185 */
186util.scheduleRequest = function() {
187  var exponent = Math.pow(2, requestFailureCount);
188  var delay = Math.min(bgPage.pollIntervalMin * exponent,
189                       pollIntervalMax);
190  delay = Math.round(delay);
191
192  if (bgPage.oauth.hasToken()) {
193    var req = bgPage.window.setTimeout(function() {
194      gdocs.getDocumentList();
195      util.scheduleRequest();
196    }, delay);
197    bgPage.requests.push(req);
198  }
199};
200
201/**
202 * Urlencodes a JSON object of key/value query parameters.
203 * @param {Object} parameters Key value pairs representing URL parameters.
204 * @return {string} query parameters concatenated together.
205 */
206util.stringify = function(parameters) {
207  var params = [];
208  for(var p in parameters) {
209    params.push(encodeURIComponent(p) + '=' +
210                encodeURIComponent(parameters[p]));
211  }
212  return params.join('&');
213};
214
215/**
216 * Creates a JSON object of key/value pairs
217 * @param {string} paramStr A string of Url query parmeters.
218 *    For example: max-results=5&startindex=2&showfolders=true
219 * @return {Object} The query parameters as key/value pairs.
220 */
221util.unstringify = function(paramStr) {
222  var parts = paramStr.split('&');
223
224  var params = {};
225  for (var i = 0, pair; pair = parts[i]; ++i) {
226    var param = pair.split('=');
227    params[decodeURIComponent(param[0])] = decodeURIComponent(param[1]);
228  }
229  return params;
230};
231
232/**
233 * Utility for displaying a message to the user.
234 * @param {string} msg The message.
235 */
236util.displayMsg = function(msg) {
237  $('#butter').removeClass('error').text(msg).show();
238};
239
240/**
241 * Utility for removing any messages currently showing to the user.
242 */
243util.hideMsg = function() {
244  $('#butter').fadeOut(1500);
245};
246
247/**
248 * Utility for displaying an error to the user.
249 * @param {string} msg The message.
250 */
251util.displayError = function(msg) {
252  util.displayMsg(msg);
253  $('#butter').addClass('error');
254};
255
256/**
257 * Returns the correct atom link corresponding to the 'rel' value passed in.
258 * @param {Array<Object>} links A list of atom link objects.
259 * @param {string} rel The rel value of the link to return. For example: 'next'.
260 * @return {string|null} The appropriate link for the 'rel' passed in, or null
261 *     if one is not found.
262 */
263gdocs.getLink = function(links, rel) {
264  for (var i = 0, link; link = links[i]; ++i) {
265    if (link.rel === rel) {
266      return link;
267    }
268  }
269  return null;
270};
271
272/**
273 * Returns the correct atom category corresponding to the scheme/term passed in.
274 * @param {Array<Object>} categories A list of atom category objects.
275 * @param {string} scheme The category's scheme to look up.
276 * @param {opt_term?} An optional term value for the category to look up.
277 * @return {string|null} The appropriate category, or null if one is not found.
278 */
279gdocs.getCategory = function(categories, scheme, opt_term) {
280  for (var i = 0, cat; cat = categories[i]; ++i) {
281    if (opt_term) {
282      if (cat.scheme === scheme && opt_term === cat.term) {
283        return cat;
284      }
285    } else if (cat.scheme === scheme) {
286      return cat;
287    }
288  }
289  return null;
290};
291
292/**
293 * A generic error handler for failed XHR requests.
294 * @param {XMLHttpRequest} xhr The xhr request that failed.
295 * @param {string} textStatus The server's returned status.
296 */
297gdocs.handleError = function(xhr, textStatus) {
298  util.displayError('Failed to fetch docs. Please try again.');
299  ++requestFailureCount;
300};
301
302/**
303 * A helper for constructing the raw Atom xml send in the body of an HTTP post.
304 * @param {XMLHttpRequest} xhr The xhr request that failed.
305 * @param {string} docTitle A title for the document.
306 * @param {string} docType The type of document to create.
307 *     (eg. 'document', 'spreadsheet', etc.)
308 * @param {boolean?} opt_starred Whether the document should be starred.
309 * @return {string} The Atom xml as a string.
310 */
311gdocs.constructAtomXml_ = function(docTitle, docType, opt_starred) {
312  var starred = opt_starred || null;
313
314  var starCat = ['<category scheme="http://schemas.google.com/g/2005/labels" ',
315                 'term="http://schemas.google.com/g/2005/labels#starred" ',
316                 'label="starred"/>'].join('');
317
318  var atom = ["<?xml version='1.0' encoding='UTF-8'?>", 
319              '<entry xmlns="http://www.w3.org/2005/Atom">',
320              '<category scheme="http://schemas.google.com/g/2005#kind"', 
321              ' term="http://schemas.google.com/docs/2007#', docType, '"/>',
322              starred ? starCat : '',
323              '<title>', docTitle, '</title>',
324              '</entry>'].join('');
325  return atom;
326};
327
328/**
329 * A helper for constructing the body of a mime-mutlipart HTTP request.
330 * @param {string} title A title for the new document.
331 * @param {string} docType The type of document to create.
332 *     (eg. 'document', 'spreadsheet', etc.)
333 * @param {string} body The body of the HTTP request.
334 * @param {string} contentType The Content-Type of the (non-Atom) portion of the
335 *     http body.
336 * @param {boolean?} opt_starred Whether the document should be starred.
337 * @return {string} The Atom xml as a string.
338 */
339gdocs.constructContentBody_ = function(title, docType, body, contentType,
340                                       opt_starred) {
341  var body = ['--END_OF_PART\r\n',
342              'Content-Type: application/atom+xml;\r\n\r\n',
343              gdocs.constructAtomXml_(title, docType, opt_starred), '\r\n',
344              '--END_OF_PART\r\n',
345              'Content-Type: ', contentType, '\r\n\r\n',
346              body, '\r\n',
347              '--END_OF_PART--\r\n'].join('');
348  return body;
349};
350
351/**
352 * Creates a new document in Google Docs.
353 */
354gdocs.createDoc = function() {
355  var title = $.trim($('#doc_title').val());
356  if (!title) {
357    alert('Please provide a title');
358    return;
359  }
360  var content = $('#doc_content').val();
361  var starred = $('#doc_starred').is(':checked');
362  var docType = $('#doc_type').val();
363
364  util.displayMsg('Creating doc...');
365
366  var handleSuccess = function(resp, xhr) {
367    bgPage.docs.splice(0, 0, new gdocs.GoogleDoc(JSON.parse(resp).entry));
368
369    gdocs.renderDocList();
370    bgPage.setIcon({'text': bgPage.docs.length.toString()});
371
372    $('#new_doc_container').hide();
373    $('#doc_title').val('');
374    $('#doc_content').val('');
375    util.displayMsg('Document created!');
376    util.hideMsg();
377
378    requestFailureCount = 0;
379  };
380
381  var params = {
382    'method': 'POST',
383    'headers': {
384      'GData-Version': '3.0',
385      'Content-Type': 'multipart/related; boundary=END_OF_PART',
386    },
387    'parameters': {'alt': 'json'},
388    'body': gdocs.constructContentBody_(title, docType, content,
389                                        DEFAULT_MIMETYPES[docType], starred)
390  };
391
392  // Presentation can only be created from binary content. Instead, create a
393  // blank presentation.
394  if (docType === 'presentation') {
395    params['headers']['Content-Type'] = DEFAULT_MIMETYPES['atom'];
396    params['body'] = gdocs.constructAtomXml_(title, docType, starred);
397  }
398
399  bgPage.oauth.sendSignedRequest(bgPage.DOCLIST_FEED, handleSuccess, params);
400};
401
402/**
403 * Updates a document's metadata (title, starred, etc.).
404 * @param {gdocs.GoogleDoc} googleDocObj An object containing the document to
405 *     update.
406 */
407gdocs.updateDoc = function(googleDocObj) {
408  var handleSuccess = function(resp) {
409    util.displayMsg('Updated!');
410    util.hideMsg();
411    requestFailureCount = 0;
412  };
413
414  var params = {
415    'method': 'PUT',
416    'headers': {
417      'GData-Version': '3.0',
418      'Content-Type': 'application/atom+xml',
419      'If-Match': '*'
420    },
421    'body': gdocs.constructAtomXml_(googleDocObj.title, googleDocObj.type,
422                                    googleDocObj.starred)
423  };
424
425  var url = bgPage.DOCLIST_FEED + googleDocObj.resourceId;
426  bgPage.oauth.sendSignedRequest(url, handleSuccess, params);
427};
428
429/**
430 * Deletes a document from the user's document list.
431 * @param {integer} index An index intro the background page's docs array.
432 */
433gdocs.deleteDoc = function(index) {
434  var handleSuccess = function(resp, xhr) {
435    util.displayMsg('Document trashed!');
436    util.hideMsg();
437    requestFailureCount = 0;
438    bgPage.docs.splice(index, 1);
439    bgPage.setIcon({'text': bgPage.docs.length.toString()});
440  }
441
442  var params = {
443    'method': 'DELETE',
444    'headers': {
445      'GData-Version': '3.0',
446      'If-Match': '*'
447    }
448  };
449
450  $('#output li').eq(index).fadeOut('slow');
451
452  bgPage.oauth.sendSignedRequest(
453      bgPage.DOCLIST_FEED + bgPage.docs[index].resourceId,
454      handleSuccess, params);
455};
456
457/**
458 * Callback for processing the JSON feed returned by the DocList API.
459 * @param {string} response The server's response.
460 * @param {XMLHttpRequest} xhr The xhr request that was made.
461 */
462gdocs.processDocListResults = function(response, xhr) {
463  if (xhr.status != 200) {
464    gdocs.handleError(xhr, response);
465    return;
466  } else {
467    requestFailureCount = 0;
468  }
469
470  var data = JSON.parse(response);
471
472  for (var i = 0, entry; entry = data.feed.entry[i]; ++i) {
473    bgPage.docs.push(new gdocs.GoogleDoc(entry));
474  }
475
476  var nextLink = gdocs.getLink(data.feed.link, 'next');
477  if (nextLink) {
478    gdocs.getDocumentList(nextLink.href); // Fetch next page of results.
479  } else {
480    gdocs.renderDocList();
481  }
482};
483
484/**
485 * Presents the in-memory documents that were fetched from the server as HTML.
486 */
487gdocs.renderDocList = function() {
488  util.hideMsg();
489
490  // Construct the iframe's HTML.
491  var html = [];
492  for (var i = 0, doc; doc = bgPage.docs[i]; ++i) {
493    // If we have an arbitrary file, use generic file icon.
494    var type = doc.type.label;
495    if (doc.type.term == 'http://schemas.google.com/docs/2007#file') {
496      type = 'file';
497    }
498
499    var starred = doc.starred ? ' selected' : '';
500    html.push(
501      '<li data-index="', i , '"><div class="star', starred, '"></div>',
502      '<div><img src="img/icons/', type, '.gif">',
503      '<span contenteditable="true" class="doc_title"></span></div>',
504      '<span>[<a href="', doc.link['alternate'],
505      '" target="_new">view</a> | <a href="javascript:void(0);" ',
506      'onclick="gdocs.deleteDoc(',i,
507      ');return false;">delete</a>]','</span></li>');
508  }
509  $('#output').html('<ul>' + html.join('') + '</ul>');
510
511  // Set each span's innerText to be the doc title. We're filling this after
512  // the html has been rendered to the page prevent XSS attacks when using
513  // innerHTML.
514  $('#output li span.doc_title').each(function(i, ul) {
515    $(ul).text(bgPage.docs[i].title);
516  });
517
518  bgPage.setIcon({'text': bgPage.docs.length.toString()});
519};
520
521/**
522 * Fetches the user's document list.
523 * @param {string?} opt_url A url to query the doclist API with. If omitted,
524 *     the main doclist feed uri is used.
525 */
526gdocs.getDocumentList = function(opt_url) {
527  var url = opt_url || null;
528
529  var params = {
530    'headers': {
531      'GData-Version': '3.0'
532    }
533  };
534
535  if (!url) {
536    util.displayMsg('Fetching your docs');
537    bgPage.setIcon({'text': '...'});
538
539    bgPage.docs = []; // Clear document list. We're doing a refresh.
540
541    url = bgPage.DOCLIST_FEED;
542    params['parameters'] = {
543      'alt': 'json',
544      'showfolders': 'true'
545    };
546  } else {
547    util.displayMsg($('#butter').text() + '.');
548
549    var parts = url.split('?');
550    if (parts.length > 1) {
551      url = parts[0]; // Extract base URI. Params are passed in separately.
552      params['parameters'] = util.unstringify(parts[1]);
553    }
554  }
555
556  bgPage.oauth.sendSignedRequest(url, gdocs.processDocListResults, params);
557};
558
559/**
560 * Refreshes the user's document list.
561 */
562gdocs.refreshDocs = function() {
563  bgPage.clearPendingRequests();
564  gdocs.getDocumentList();
565  util.scheduleRequest();
566};
567
568
569bgPage.oauth.authorize(function() {
570  if (!bgPage.docs.length) {
571    gdocs.getDocumentList();
572  } else {
573    gdocs.renderDocList();
574  }
575  util.scheduleRequest();
576});
577</script>
578</body>
579</html>
580