').appendTo(dialogContent);
106 | dialogButtons.appendTo(dialogContent);
107 |
108 | // Add the title to the the dialog and the contents as well.
109 | var dialog = document.createElement('div');
110 | $(dialog).addClass('pop_container_advanced')
111 | .attr('id', data.id)
112 | .css('position', 'absolute')
113 | .css("left", "25%")
114 | .css("right", "25%")
115 | .css("top", "20%")
116 | .css("zIndex", "999999");
117 | var dialogPopup = $('
');
118 | $('
' + data.title + ' ').appendTo(dialogPopup);
119 | dialogContent.appendTo(dialogPopup);
120 | dialogPopup.appendTo(dialog);
121 | return dialog;
122 | }
123 |
124 | /**
125 | * To transport JavaScript data from Facebook, it is faster and better to transfer
126 | * the internal map Facebook maintains. To do that, we create a transfer dom area
127 | * to the page so we dump the FriendSearchPane and then store it in the background
128 | * page right afterwards.
129 | */
130 | function exportFacebookContacts() {
131 | // JS script injection to the facebook's World.
132 | var postFriendMap = function() {
133 | // Use events to notify the content script. Replicate the event the content
134 | // script has, so we can pass this event to that world.
135 | var exportEvent = document.createEvent('Event');
136 | exportEvent.initEvent('friendExported', true, true);
137 |
138 | // Create a transfer node DOM, since that is the only way two worlds can
139 | // communicate with each other.
140 | var transferDOM = document.getElementById('fb-transfer-dom-area');
141 | transferDOM.innerText = JSON.stringify(FriendSearchPane._data);
142 |
143 | // Inform our content script that we have received the object from Facebook.
144 | window.dispatchEvent(exportEvent);
145 | };
146 |
147 | // Create a dummy textarea DOM.
148 | var transferDOM = document.createElement('div');
149 | $(transferDOM).attr('id', 'fb-transfer-dom-area')
150 | .hide()
151 | .appendTo($(document.body));
152 |
153 | // Start injecting the JS script.
154 | var script = document.createElement('script');
155 | script.setAttribute('id', 'fb-inject-area');
156 | script.appendChild(document.createTextNode('(' + postFriendMap + ')();'));
157 | document.body.appendChild(script);
158 | }
159 |
160 | /**
161 | * To make sure the user wants to start exporting, we need to prompt her to
162 | * see if it's OK to go to the right page (she might have some input in an
163 | * input field that is not saved, etc).
164 | */
165 | function goToFriendPageAndStart() {
166 | // See if we are at the right page to start. We need to be at the /friends/*
167 | // location, to get access to the list of friends. Any other page won't do.
168 | if (document.location.pathname.match('^/friends/edit') && document.location.search == 0) {
169 | // Do nothing now, the worker tab will manage the state since everything
170 | // is asynchronous, we will let events handle the state.
171 | switchToWorkerTab();
172 | }
173 | else {
174 | $(document.body).append(createFacebookDialog({
175 | id: 'fb-exporter-redirect',
176 | title: 'Redirection needed',
177 | message: 'First, you need to go to your friends page, do you want us to redirect you to it?',
178 | yes_text: 'Redirect now',
179 | yes_callback: (function() {
180 | $('#fb-exporter-redirect').remove();
181 | switchToWorkerTab();
182 | window.location = 'http://www.facebook.com/friends/edit';
183 | }),
184 | cancel_callback: (function() {
185 | $('#fb-exporter-redirect').remove();
186 | })
187 | }));
188 | }
189 | }
190 |
191 | /**
192 | * Adds a export friends link to the top of the worker tab.
193 | */
194 | function renderExportFriendsLink() {
195 | // Paint the Export friends to the top of the page.
196 | var exportFriendsLink = $('#pageNav a:contains("Home")').parent().clone();
197 | $('a', exportFriendsLink)
198 | .attr('id', 'export-friends-link')
199 | .attr('href', 'javascript:void(0);')
200 | .text('Export friends!')
201 | .css('color', 'white')
202 | .click(goToFriendPageAndStart);
203 | $(exportFriendsLink).attr('id', '');
204 | $('#pageNav a:contains("Home")').parent().after(exportFriendsLink);
205 | }
--------------------------------------------------------------------------------
/js/controller.js:
--------------------------------------------------------------------------------
1 | var bkg = chrome.extension.getBackgroundPage();
2 | var total_visible_friends = 0;
3 | var friends_remaining_count = 0;
4 | var logDOM = null;
5 |
6 | /**
7 | * Send a request to the Facebook page and tell it to get the friends list.
8 | * Keep the map stored there since we can guarantee to get it afterwards.
9 | * Asynchronous communication will not guarantee us to get it now.
10 | */
11 | function fetchFriendList() {
12 | chrome.tabs.sendRequest(bkg.facebook_id, {retrieveFriendsMap: 1});
13 | }
14 |
15 | /**
16 | * Render all my friends below. Display their profile pic and a link to their
17 | * profile page. As well, when hovered, show their name.
18 | * @param {Object
} friendsMap All the users friends in a map.
19 | * @param {number} count The number of friends.
20 | */
21 | function renderFriendList(friendsMap, count) {
22 | logDOM.val('');
23 | log('Rendering friends list ...');
24 | $('#step1').hide();
25 | $('#friendlist').show();
26 | $('#step2').show();
27 | $('#remaining-friend-count').show();
28 | $('#start-crunching').attr('disabled', true);
29 | $('#start-crunching').text('checking cached friends, please wait ...');
30 |
31 | // Reset counter of processed friends. This is just used to show how many
32 | // friends are showing.
33 | total_processed_friends = 0;
34 |
35 | $.each(friendsMap, function(key, value) {
36 | bkg.db.getFriend(key, function(result) {
37 | total_processed_friends++;
38 | $('#remaining-friend-count').text(
39 | 'Processsing ' + total_processed_friends + ' / ' + count + ' friends!'
40 | );
41 |
42 | // Create the list friend item, but first decide if its cached or not.
43 | var li = document.createElement('li');
44 | $(li).addClass('friend-row')
45 | .attr('id', key)
46 | .html(' ' +
47 | '' + value.text + ' ')
48 | .click(
49 | function() {
50 | chrome.tabs.create({url: 'http://facebook.com' + value.path });
51 | }
52 | );
53 | // When a friend is found, that means they are cached. Inform facebook.
54 | if (result.status) {
55 | $(li).addClass('cached');
56 | bkg.putFriendCache(result.data);
57 | }
58 | $('#friendlist').append(li);
59 |
60 | // The last friend finished processing.
61 | if (total_processed_friends == count) {
62 | $('#remaining-friend-count').text(count + ' friends!');
63 | $('#start-crunching').text('let\'s start!');
64 | $('#start-crunching').attr('disabled', false);
65 | }
66 | });
67 | });
68 |
69 | log('Found ' + count + ' friends!');
70 |
71 | // Check if we have any friends.
72 | if (count == 0) {
73 | var li = document.createElement('li');
74 | $(li).addClass('friend-row')
75 | .text('Looks like you have no friends? Impossible! You probably need ' +
76 | 'to pick a different network (see above).');
77 | }
78 |
79 | // Initialize the remaining count, used for step 3.
80 | friends_remaining_count = count;
81 | }
82 |
83 | /**
84 | * The main process to start the lengthy process.
85 | */
86 | function startCrunching() {
87 | log('Start crunching!');
88 | $('#step2').hide();
89 | $('#step3').show();
90 |
91 | $('#remaining-friend-count').text(friends_remaining_count + ' remaining');
92 |
93 | // Show pending for each element that was ready.
94 | $.each(document.querySelectorAll('#friendlist li span'), function(key, value) {
95 | if ($(value).text() == 'READY') {
96 | $(value).text('PENDING');
97 | }
98 | });
99 |
100 | // Start request, let the background page start the long long long process!
101 | bkg.startExportFriendData();
102 | }
103 |
104 |
105 | /**
106 | * Delete all the cache from database so we can start over.
107 | */
108 | function deleteCache() {
109 | log('Deleting cache!');
110 | bkg.db.clear();
111 | $.each(document.querySelectorAll('#friendlist li.cached'),
112 | function(key, value) {
113 | $(value).removeClass('cached');
114 | $(value).find('span').text('READY');
115 | }
116 | );
117 | bkg.clearCache();
118 | }
119 |
120 | /**
121 | * Friend information recieved that needs to be processed/
122 | * @param {object} friend An object that represents a single friend.
123 | */
124 | function gotInfoForFriend(friend) {
125 | var success = true;
126 |
127 | // If the email is empty
128 | if (friend.emails.length == 1 && friend.emails[0] == '') {
129 | log('Finished processing [' + friend.name + '] FAIL, no email.' );
130 | success = false;
131 | } else {
132 | log('Finished processing [' + friend.name + ']');
133 | }
134 | var item = $('#' + friend.id);
135 | item.find('span').text(friend.name);
136 | item.removeClass('starting');
137 | item.addClass(success ? 'processed' : 'failed');
138 |
139 | var checkbox = document.createElement('input');
140 | $(checkbox).attr('type', 'checkbox')
141 | .attr('checked', '1')
142 | .attr('id', 'checkbox' + friend.id)
143 | .addClass('checkbox');
144 | item.prepend($(checkbox));
145 |
146 | // Attach the friend object to the list item, for later retrieval.
147 | item.data(friend);
148 |
149 | // Create a detailed view, for now disable this until we make a better UI,
150 | // perhaps a hover (card) that shows the persons extracted information.
151 | var detail_ul = document.createElement('ul');
152 | $(detail_ul).addClass('friend-detail');
153 | // item.append($(detail_ul));
154 |
155 | $.each(friend, function(key, value) {
156 | if (key == 'name') {
157 | // No need to show name, since it's part of the parent li.
158 | return;
159 | }
160 |
161 | if (value) {
162 | if ($.isArray(value)) {
163 | $.each(value, function(k, v) {
164 | var detail_li = document.createElement('li');
165 | $(detail_li).text(key + ': ' + v);
166 | $(detail_ul).append($(detail_li));
167 | });
168 | } else {
169 | var detail_li = document.createElement('li');
170 | $(detail_li).text(key + ': ' + value);
171 | $(detail_ul).append($(detail_li));
172 | }
173 | }
174 | });
175 |
176 | friends_remaining_count -= 1;
177 |
178 | $('#remaining-friend-count').text(
179 | 'Processed ' + friend.name + ', ' +
180 | friends_remaining_count + ' remaining.'
181 | );
182 |
183 | if (friends_remaining_count == 0) {
184 | setupExportScreen();
185 | }
186 |
187 | return success;
188 | }
189 |
190 | function setupExportScreen() {
191 | log('Export screen is now visible.');
192 |
193 | // All of the friend info for the visible subset of friends has been
194 | // received. Show specific export buttons now.
195 | $('#step3').hide();
196 | $('#step4').show();
197 |
198 | // Remove the ajax loading gif.
199 | $('#export-methods img').remove();
200 |
201 | //chrome.tabs.sendRequest(bkg.facebook_id,
202 | // {hideTopBanner: 1});
203 |
204 | $('#remaining-friend-count').hide();
205 | }
206 | /**
207 | * Setup a list of the visible, checked friends that we want to send to
208 | * export.
209 | */
210 | function setupAndStartExport(request) {
211 | // Only get the checked friends, disregard all others.
212 | var requested_friends = $('li.friend-row').map( function(idx, e) {
213 | // First, see if this element's checkbox is checked or not.
214 | if ($('.checkbox', e).attr('checked') != '1') {
215 | return null;
216 | }
217 | return $(e).data();
218 | }).get();
219 |
220 | // Reset the remaining friends counter, to take into effect the checked friends.
221 | friends_remaining_count = requested_friends.length;
222 | if (friends_remaining_count != 0) {
223 | $('#remaining-friend-count').show().text(
224 | friends_remaining_count + ' remaining');
225 | } else {
226 | // Remove the ajax loading gif, if there are no friends_remaining_count.
227 | alert('You don\'t have any friends selected!');
228 | $('#export-methods img').remove();
229 | }
230 |
231 | // Send a request to the background page, so that we can start the export
232 | // module process.
233 | request.requestedFriends = requested_friends;
234 | chrome.extension.sendRequest(request);
235 | }
236 |
237 | /**
238 | * Format number to 2 digits.
239 | */
240 | function twoDigitsFormat(num) {
241 | return (num < 10) ? '0'+ num : num;
242 | }
243 | /**
244 | * Appends a |message| to the logger panel.
245 | */
246 | function log(message) {
247 | var d = new Date();
248 | var time = twoDigitsFormat(d.getHours()) + ':' +
249 | twoDigitsFormat(d.getMinutes()) + ':' + twoDigitsFormat(d.getSeconds());
250 | logDOM.val(logDOM.val() + '\n' + time + ' - ' + message);
251 | logDOM.attr({ scrollTop: logDOM.attr("scrollHeight") });
252 | }
253 |
254 | $(document).ready(function() {
255 | // Log Manager.
256 | logDOM = $('#log');
257 | logDOM.attr('disabled', 'disabled');
258 | logDOM.hide();
259 | $('#btnLog').click(function () {
260 | if (logDOM.is(':visible')) {
261 | $(this).text('View log');
262 | logDOM.slideUp();
263 | } else {
264 | $(this).text('Hide log');
265 | logDOM.slideDown();
266 | }
267 | });
268 |
269 | // Activate the Terms of Service. They must click it to continue.
270 | $('#tos').click( function() {
271 | if ($('#tos').attr('checked')) {
272 | $('.tos-guarded').attr('disabled', false);
273 | } else {
274 | $('.tos-guarded').attr('disabled', true);
275 | }
276 | });
277 |
278 | chrome.extension.onRequest.addListener(function(request, sender, sendResponse) {
279 | if (request.log) {
280 | log(request.log);
281 | }
282 | if (request.gotInfoForFriend) {
283 | var response = gotInfoForFriend(request.gotInfoForFriend);
284 | sendResponse({OK: response});
285 | }
286 | else if (request.csvExportFinished) {
287 | var csv_popup = $("
");
288 | $(csv_popup).attr("id", "csv-popup");
289 |
290 | var textarea = $("");
291 | $(textarea).text(request.csvExportFinished);
292 |
293 | var a = $(" ").attr("href", "javascript:void(0);")
294 | .text("close")
295 | .click(function() {
296 | $("#csv-popup").remove();
297 | });
298 |
299 | var info = $(" ").text("Here is your CSV. Copy and save it somewhere safe.");
300 |
301 | $(csv_popup).append(info);
302 | $(csv_popup).append(a);
303 | $(csv_popup).append(textarea);
304 |
305 | $(document.body).append(csv_popup);
306 | }
307 | else if (request.finishedProcessingFriend) {
308 | // The export finished for this contact. Update the list, based
309 | // on the success status, or show the error message.
310 | log('Export ' + (request.success ? 'passed' : 'failed') + ' [' +
311 | request.friend.name + '] ' + request.message);
312 | console.log(request.friend);
313 | var item = $('#' + request.friend.id);
314 | var status_text = request.success ? 'success' : 'failed';
315 | item.removeClass('starting');
316 | item.removeClass('processed');
317 | item.removeClass('cached');
318 | item.find('span').text(status_text.toUpperCase());
319 | item.addClass(status_text);
320 |
321 | friends_remaining_count -= 1;
322 | $('#remaining-friend-count').show().text(
323 | friends_remaining_count + ' remaining');
324 |
325 | if (friends_remaining_count == 0) {
326 | // Remove the ajax loading gif.
327 | $('#export-methods img').remove();
328 |
329 | //chrome.tabs.sendRequest(bkg.facebook_id,
330 | // {hideTopBanner: 1});
331 | }
332 | }
333 | else if (request.facebookError) {
334 | log('ERROR! Facebook error, please log back into http://m.facebook.com. Then close this tab and restart the process.');
335 | $('#note').show();
336 | setupExportScreen();
337 | }
338 | else if (request.friendExtractionStarted) {
339 | var item = $('#' + request.friendExtractionStarted);
340 | item.removeClass('processed');
341 | item.addClass('starting');
342 | item.find('span').text('STARTING');
343 | }
344 | else if (request.renderFriendsList) {
345 | renderFriendList(request.renderFriendsList, request.count);
346 | }
347 | });
348 |
349 |
350 | $('.continue1').click(fetchFriendList);
351 |
352 | $('#start-crunching').click(startCrunching);
353 |
354 | $('#delete-cache').click(deleteCache);
355 |
356 | // Gmail exportation:
357 | $('#export-to-gmail').click(function() {
358 | $('#export-to-gmail').parent().prepend(
359 | $('#ajax-loader').clone().attr('id', '').show());
360 |
361 | setupAndStartExport({doGmailExport: 1});
362 | });
363 |
364 | // CSV exportation:
365 | $('#export-to-csv').click(function() {
366 | $('#export-to-csv').parent().prepend(
367 | $('#ajax-loader').clone().attr('id', '').show());
368 |
369 | setupAndStartExport({doCSVExport: 1});
370 | });
371 | });
372 |
--------------------------------------------------------------------------------
/js/csv_exporter.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Routines to handle csv exporting.
3 | */
4 | CSVExporter = function(friends) {
5 | this.friends = friends;
6 | this.header = ['Name', 'E-mail Address 1', 'E-mail Address 2', 'E-mail Address 3',
7 | 'Phone 1', 'Phone 2',
8 | 'Google Talk', 'MSN', 'Skype', 'Yahoo',
9 | 'Website 1', 'Website 2', 'Website 3',
10 | 'Website Facebook', 'Home Address', 'Birthday'
11 | ];
12 | this.dump = '';
13 | };
14 |
15 | /**
16 | * Start processing csv file.
17 | * @param {Function} callback to fire after each friend processed.
18 | */
19 | CSVExporter.prototype.process = function(callback) {
20 | var csv_rows = [];
21 | var i = 0;
22 | var length = 0;
23 |
24 | for (var j = 0; j < this.friends.length; j++) {
25 | var friend = this.friends[j];
26 | var csv_row = [];
27 | csv_row.push(friend.name);
28 |
29 | // Email parsing.
30 | for (i = 0; i < 3; i++){
31 | this.addColumn_(csv_row, friend.emails[i]);
32 | }
33 |
34 | // Phones.
35 | for (i = 0; i < 2; i++){
36 | this.addColumn_(csv_row, friend.phones[i]);
37 | }
38 |
39 | // IM Parsing just 4.
40 | this.addColumn_(csv_row, friend.im.gtalk);
41 | this.addColumn_(csv_row, friend.im.hotmail);
42 | this.addColumn_(csv_row, friend.im.skype);
43 | this.addColumn_(csv_row, friend.im.yahoo);
44 |
45 | // Website parsing.
46 | for (i = 0; i < 3; i++){
47 | this.addColumn_(csv_row, friend.websites[i]);
48 | }
49 |
50 | // Friend FB parsing.
51 | this.addColumn_(csv_row, friend.fb);
52 |
53 | // Address parsing.
54 | this.addColumn_(csv_row, friend.address);
55 |
56 | // Birthday parsing.
57 | this.addColumn_(csv_row, friend.birthday);
58 |
59 | csv_rows.push(csv_row);
60 |
61 | // Callback to inform client
62 | callback({
63 | finishedProcessingFriend: true,
64 | friend: friend,
65 | success: 1,
66 | message: "Added to CSV!"
67 | });
68 | }
69 |
70 | this.dump = this.header.join(',') + '\n';
71 |
72 | for (i = 0; i < csv_rows.length; i++) {
73 | this.dump += csv_rows[i].join(',') + '\n';
74 | }
75 | };
76 |
77 | /**
78 | * Get the dump CSV text.
79 | */
80 | CSVExporter.prototype.getDump = function() {
81 | return this.dump;
82 | };
83 |
84 | /**
85 | * Adds a column safely to each spot. It will wrap each column with quotes, and
86 | * escape quotes that exists within.
87 | *
88 | * @private
89 | *
90 | * @param {String[]} row A reference to a row that we need to push columns to.
91 | * @param {String} column A string that will be added to the row.
92 | */
93 | CSVExporter.prototype.addColumn_ = function(row, column) {
94 | if (column) {
95 | row.push('"' + column.replace(/"/g, '\\"') + '"');
96 | } else {
97 | row.push('');
98 | }
99 | };
100 |
--------------------------------------------------------------------------------
/js/database.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Storage class responsible for managing the database
3 | * tansactions for friends
4 | */
5 | FriendDB = function () {
6 | this.db = null;
7 | };
8 |
9 | /**
10 | * Opens a connection to Web SQL table.
11 | */
12 | FriendDB.prototype.open = function() {
13 | var db_size = 5 * 1024 * 1024; // 5MB
14 | this.db = openDatabase('Facebook Friend Export', '1.0', 'fb-export', db_size);
15 | };
16 |
17 | /**
18 | * For simplicity, just show an alert when crazy error happens.
19 | */
20 | FriendDB.prototype.onError = function(tx, e) {
21 | alert('Something unexpected happened: ' + e.message );
22 | };
23 |
24 | /**
25 | * Creats a table with the following columns:
26 | * id - Integer
27 | * data - String
28 | * ts - Timestamp
29 | */
30 | FriendDB.prototype.createTable = function() {
31 | this.db.transaction(function(tx) {
32 | tx.executeSql('CREATE TABLE IF NOT EXISTS ' +
33 | 'friend(id INTEGER, data TEXT, ts DATETIME)', []);
34 | });
35 | };
36 |
37 | /**
38 | * Adds a |friend| to the database, the contents gets serialized to String.
39 | * Current time is tracked as well.
40 | */
41 | FriendDB.prototype.persistFriend = function(friend, onSuccess) {
42 | var ts = new Date();
43 | var data = JSON.stringify(friend);
44 | this.db.transaction(function(tx) {
45 | tx.executeSql('INSERT INTO friend(id, data, ts) VALUES (?,?,?)',
46 | [friend.id, data, ts], onSuccess, this.onError);
47 | });
48 | };
49 |
50 | /**
51 | * Retrieves a row from the table given |id|. The result goes to |response|.
52 | * This is an asynchronous action.
53 | */
54 | FriendDB.prototype.getFriend = function(id, response) {
55 | this.db.transaction(function(tx) {
56 | tx.executeSql('SELECT data FROM friend WHERE id = ?', [id],
57 | function (tx, rs) {
58 | if (rs.rows.length != 0) {
59 | response({status: true, data: JSON.parse(rs.rows.item(0).data)});
60 | }
61 | else {
62 | response({status: false});
63 | }
64 | }, this.onError
65 | );
66 | });
67 | };
68 |
69 | /**
70 | * Update the friend object.
71 | */
72 | FriendDB.prototype.updateFriend = function(friend) {
73 | var ts = new Date();
74 | var data = JSON.stringify(friend);
75 | this.db.transaction(function(tx) {
76 | tx.executeSql('UPDATE friend SET data = ?, ts = ? WHERE id = ?',
77 | [data, ts, friend.id], null, this.onError
78 | );
79 | });
80 | };
81 |
82 |
83 | /**
84 | * Removes every row from the table
85 | */
86 | FriendDB.prototype.clear = function() {
87 | this.db.transaction(function(tx) {
88 | tx.executeSql('DELETE FROM friend', [], null, this.onError);
89 | });
90 | };
--------------------------------------------------------------------------------
/js/exporter.js:
--------------------------------------------------------------------------------
1 | Exporter = {};
2 |
3 | Exporter.getScreenNameType = function(screenname) {
4 | switch (screenname) {
5 | case '(Google Talk)': return 'GOOGLE_TALK';
6 | case '(Skype)': return 'SKYPE';
7 | case '(AIM)': return 'AIM';
8 | case '(Windows Live Messenger)': return 'MSN';
9 | case '(Yahoo! Messenger)':
10 | case '(Yahoo Japan)': return 'YAHOO';
11 | case '(QQ)': return 'QQ';
12 | case '(ICQ)': return 'ICQ';
13 | }
14 | return null;
15 | };
16 |
17 | Exporter.getPhoneType = function(phone) {
18 | return (phone != '(Mobile)') ? 'mobile' : 'other';
19 | };
--------------------------------------------------------------------------------
/js/gmail_exporter.js:
--------------------------------------------------------------------------------
1 | // Routines to handle gmail contact importing.
2 |
3 | GoogleExport = function(friends) {
4 | this.requested_friends_to_import = friends;
5 |
6 | this.contact_group_id = 0;
7 |
8 | // This is a hash of email addresses that are ALREADY in the users google
9 | // contacts. If a user already exists, then we want to avoid adding him as a
10 | // duplicate contact from facebook.
11 | this.google_contacts_hash = Object();
12 |
13 | // There is a delicate order of what gets called and when, when interacting
14 | // with the google contacts API. By using a function call queue, we can easily
15 | // shift/unshift/push the next necessary call, and make the calls in the right
16 | // order. The alternative is to use synchronous ajax, which I guess is OK
17 | // too... but this feels cooler.
18 | this.function_queue = [];
19 |
20 | // This is for OAuth authentication with google, for contacts importation.
21 | this.oauth = ChromeExOAuth.initBackgroundPage({
22 | 'request_url' : 'https://www.google.com/accounts/OAuthGetRequestToken',
23 | 'authorize_url' : 'https://www.google.com/accounts/OAuthAuthorizeToken',
24 | 'access_url' : 'https://www.google.com/accounts/OAuthGetAccessToken',
25 | 'consumer_key' : 'anonymous',
26 | 'consumer_secret' : 'anonymous',
27 | 'scope' : 'https://www.google.com/m8/feeds/',
28 | 'app_name' : 'Facebook Contact Exporter (Chrome Extension)'
29 | });
30 |
31 | };
32 |
33 | GoogleExport.CONTACT_GROUP_NAME = 'Imported from Facebook';
34 | GoogleExport.GROUPS_FEED = 'https://www.google.com/m8/feeds/groups/default/full';
35 | GoogleExport.CONTACTS_FEED = 'https://www.google.com/m8/feeds/contacts/default/full';
36 |
37 |
38 | GoogleExport.prototype.process = function(callback) {
39 | this.callback = callback;
40 |
41 | console.log('startExportWithFriends');
42 | console.log(this.oauth.hasToken());
43 | this.function_queue.push(jQuery.proxy(this.ensureContactGroupExists, this));
44 | this.function_queue.push(jQuery.proxy(this.getGmailContacts, this));
45 | this.function_queue.push(jQuery.proxy(this.startExportingRequestedContacts, this));
46 |
47 | this.oauth.authorize(jQuery.proxy(this.didOAuthAuthorize, this));
48 | };
49 |
50 | GoogleExport.prototype.didOAuthAuthorize = function() {
51 | // Start doing things in the function queue.
52 | this.doNextAction();
53 | };
54 |
55 | GoogleExport.prototype.doNextAction = function() {
56 | // Execute the next function in the funciton queue.
57 | if (this.function_queue.length) {
58 | var next_function_to_call = this.function_queue.shift();
59 | next_function_to_call();
60 | }
61 | };
62 |
63 | GoogleExport.prototype.createAtomEntry = function() {
64 | // Create and return the raw element, with some default
65 | // attributes and children.
66 |
67 | var entry = document.createElementNS('http://www.w3.org/2005/Atom', 'atom:entry');
68 |
69 | $(entry).attr('xmlns:atom', 'http://www.w3.org/2005/Atom')
70 | .attr('xmlns:gd', 'http://schemas.google.com/g/2005')
71 | .attr('xmlns:gcontact', 'http://schemas.google.com/contact/2008');
72 |
73 | return entry;
74 | };
75 |
76 | GoogleExport.prototype.createContactGroup = function() {
77 | // Create the "Imported From Facebook" contact group, ensuring that the
78 | // "Imported From Facebook" group does not exist already.
79 |
80 | console.log('createContactGroup');
81 |
82 | var entry = this.createAtomEntry();
83 | // The below XML derived from:
84 | // http://code.google.com/apis/contacts/docs/3.0/developers_guide_protocol.html#CreatingGroups
85 | $(entry).append($(' ').attr('scheme', 'http://schemas.google.com/g/2005#kind')
86 | .attr('term', 'http://schemas.google.com/contact/2008#group'));
87 | $(entry).append(
88 | $(' ').attr('type', 'text')
89 | .text(GoogleExport.CONTACT_GROUP_NAME));
90 | $(entry).append(
91 | $(' ').attr('name', 'more info about the group')
92 | .append($(' ').text(
93 | 'Exported using Facebook Friend Exporter (Chrome Extension)')));
94 |
95 | // Must do the following to get the element as a string. The
96 | // "div" root element will not be included, but is necessary to call html().
97 | var s = $('
').append(entry).html();
98 | // Jquery doesn't give a damn about the case of the tags, making everything
99 | // lowercase. We need to fix that, as google expects tags in the right case.
100 | s = s.replace(/extendedproperty/g, 'extendedProperty');
101 |
102 | var request = {
103 | 'method': 'POST',
104 | 'headers': {
105 | 'GData-Version': '3.0',
106 | 'Content-Type': 'application/atom+xml'
107 | //'Content-Type': 'application/json' // Not a valid input type
108 | },
109 | 'parameters': {
110 | 'alt': 'json'
111 | },
112 | 'body': s
113 | };
114 |
115 | this.oauth.sendSignedRequest(GoogleExport.GROUPS_FEED,
116 | jQuery.proxy(this.onCreateContactGroup, this),
117 | request);
118 | };
119 |
120 | GoogleExport.prototype.onCreateContactGroup = function(text, xhr) {
121 | console.log('onCreateContactGroup');
122 | var data = JSON.parse(text);
123 | this.saveContactGroupHrefFromGroupObject(data.entry);
124 |
125 | // Don't need to do anything with the function queue.
126 | this.doNextAction();
127 | };
128 |
129 | GoogleExport.prototype.ensureContactGroupExists = function() {
130 | console.log('ensureContactGroupExists');
131 | // Get the entire groups list (since there is no search querying based on
132 | // exact group name) and see if we've created this group already. If the
133 | // group exists, avoid creating it again (because gmail will happily create
134 | // another one with the same name).
135 | this.oauth.sendSignedRequest(GoogleExport.GROUPS_FEED,
136 | jQuery.proxy(this.onGetContactGroups, this),
137 | { 'parameters' : { 'alt' : 'json' }});
138 | };
139 |
140 | GoogleExport.prototype.saveContactGroupHrefFromGroupObject = function(group) {
141 | // The group argument is an object representing the (possibly newly created)
142 | // group. It is an object (already parsed from JSON).
143 | console.log("saveContactGroupHrefFromGroupObject");
144 | this.contact_group_id = group.id.$t;
145 | };
146 |
147 | GoogleExport.prototype.onGetContactGroups = function(text, xhr) {
148 | console.log("onGetContactGroups");
149 |
150 | // TODO: Assuming "text" is valid JSON at this point? Is that wise? Error
151 | // checking?
152 | var feed = JSON.parse(text);
153 |
154 | if ('entry' in feed.feed) {
155 | // Some entries (ie, groups) exist, see if one of them is our group.
156 | for (var key = 0; key < feed.feed.entry.length; key++) {
157 | if (feed.feed.entry[key].title.$t == GoogleExport.CONTACT_GROUP_NAME) {
158 | this.saveContactGroupHrefFromGroupObject(feed.feed.entry[key]);
159 | return this.doNextAction();
160 | }
161 | }
162 | }
163 |
164 | // Group does not exist, need to create it before doing anything else.
165 | this.function_queue.unshift(jQuery.proxy(this.createContactGroup, this));
166 | this.doNextAction();
167 | };
168 |
169 |
170 | GoogleExport.prototype.logout = function() {
171 | this.oauth.clearTokens();
172 | };
173 |
174 |
175 | GoogleExport.prototype.onGetContacts = function(text, xhr) {
176 | console.log('onGetContacts');
177 |
178 | this.google_contacts_hash = Object();
179 | var data = JSON.parse(text);
180 | if (data.feed.entry) {
181 | for (var i = 0, entry; entry = data.feed.entry[i]; i++) {
182 | /*
183 | var contact = {
184 | 'name' : entry['title']['$t'],
185 | 'id' : entry['id']['$t'],
186 | 'emails' : []
187 | };
188 | */
189 |
190 | if (entry['gd$email']) {
191 | var emails = entry['gd$email'];
192 | for (var j = 0, email; email = emails[j]; j++) {
193 | this.google_contacts_hash[email['address']] = entry['id']['$t'];
194 | //contact['emails'].push(email['address']);
195 | }
196 | }
197 |
198 | /*
199 | if (!contact['name']) {
200 | contact['name'] = contact['emails'][0] || "";
201 | }
202 | */
203 | }
204 | } else {
205 | console.log('No Contacts');
206 | }
207 | console.log(this.google_contacts_hash);
208 |
209 | this.doNextAction();
210 | };
211 |
212 | GoogleExport.prototype.getGmailContacts = function() {
213 | console.log('getGmailContacts');
214 |
215 | this.oauth.sendSignedRequest(GoogleExport.CONTACTS_FEED,
216 | jQuery.proxy(this.onGetContacts, this),
217 | {
218 | 'parameters' : {
219 | 'max-results' : 100000,
220 | 'alt' : 'json',
221 | 'group' : this.contact_group_id
222 | }
223 | });
224 |
225 | /*
226 | console.log(google.accounts.user.checkLogin(GOOGLE_SCOPE));
227 | var token = google.accounts.user.login(GOOGLE_SCOPE);
228 | console.log(token);
229 |
230 | var contactsFeedUri = 'https://www.google.com/m8/feeds/contacts/default/full';
231 | var query = new google.gdata.contacts.ContactQuery(contactsFeedUri);
232 |
233 | // Set the maximum of the result set to be 5
234 | query.setMaxResults(5);
235 |
236 | contactsService.getContactFeed(query, handleContactsFeed, handleError);
237 | */
238 | };
239 |
240 | GoogleExport.prototype.addFriendToGoogleContacts = function(friend) {
241 | // This assumes that the contact group has already been created.
242 |
243 | var entry = this.createAtomEntry();
244 |
245 | // The below XML derived from:
246 | // http://code.google.com/apis/contacts/docs/3.0/developers_guide_protocol.html#Creating
247 | $(entry).append($(' ').attr('scheme', 'http://schemas.google.com/g/2005#kind')
248 | .attr('term', 'http://schemas.google.com/contact/2008#contact'));
249 |
250 | // Add the right stuff for each known attribute of friend. For additional
251 | // entries, add the right code below. See reference at:
252 | // http://code.google.com/apis/gdata/docs/2.0/elements.html
253 | //
254 | // For list of defined attributes that are set by the scraping script, look
255 | // at contet_script.js.
256 | var title = $(' ').attr('type', 'text').text(friend.name);
257 | $(entry).append(title);
258 | var name = $(' ')
259 | .append($(' ').text(friend.name));
260 | $(entry).append(name);
261 |
262 | if (friend.fb) {
263 | // The friend's FB page, direct website.
264 | var gdim = $(' ')
265 | .attr('label', 'Facebook Profile')
266 | .attr('href', friend.fb);
267 | $(entry).append(gdim);
268 | }
269 |
270 | if (friend.birthday) {
271 | var gdim = $(' ')
272 | .attr('when', friend.birthday);
273 | $(entry).append(gdim);
274 | }
275 |
276 | if (friend.phones) {
277 | for (var i = 0; i < friend.phones.length; i++) {
278 | var gdim = $(' ')
279 | .attr('rel', 'http://schemas.google.com/g/2005#other')
280 | .text(friend.phones[i]);
281 | $(entry).append(gdim);
282 | }
283 | }
284 |
285 | if (friend.address) {
286 | var gdim = $(' ')
287 | .attr('rel', 'http://schemas.google.com/g/2005#home')
288 | .text(friend.address);
289 | $(entry).append(gdim);
290 | }
291 |
292 | // Instant Messengers.
293 | // TODO: Make it nicer by looping every single IM found instead of
294 | // dealing each protocol seperately.
295 | if (friend.im.skype) {
296 | var gdim = $(' ')
297 | .attr('address', friend.im.skype)
298 | .attr('rel', 'http://schemas.google.com/g/2005#home')
299 | .attr('protocol', 'http://schemas.google.com/g/2005#SKYPE');
300 | $(entry).append(gdim);
301 | }
302 |
303 | if (friend.im.gtalk) {
304 | var gdim = $(' ')
305 | .attr('address', friend.im.gtalk)
306 | .attr('rel', 'http://schemas.google.com/g/2005#home')
307 | .attr('protocol', 'http://schemas.google.com/g/2005#GOOGLE_TALK');
308 | $(entry).append(gdim);
309 | }
310 |
311 | if (friend.im.hotmail) {
312 | var gdim = $(' ')
313 | .attr('address', friend.im.hotmail)
314 | .attr('rel', 'http://schemas.google.com/g/2005#home')
315 | .attr('protocol', 'http://schemas.google.com/g/2005#MSN');
316 | $(entry).append(gdim);
317 | }
318 |
319 | if (friend.im.yahoo) {
320 | var gdim = $(' ')
321 | .attr('address', friend.im.yahoo)
322 | .attr('rel', 'http://schemas.google.com/g/2005#home')
323 | .attr('protocol', 'http://schemas.google.com/g/2005#YAHOO');
324 | $(entry).append(gdim);
325 | }
326 |
327 | if (friend.emails) {
328 | // Handle multiple emails. The .email property is a list of defined
329 | // email.
330 | var primary_email_set = false;
331 | for (var i = 0; i < friend.emails.length; i++) {
332 | var gdemail = $(' ').attr('address', friend.emails[i]);
333 | gdemail.attr('displayName', friend.name);
334 | gdemail.attr('rel', 'http://schemas.google.com/g/2005#home');
335 | if (!primary_email_set) {
336 | gdemail.attr('primary', 'true');
337 | primary_email_set = true;
338 | }
339 | $(entry).append(gdemail);
340 | }
341 | }
342 |
343 | if (friend.websites) {
344 | for (var key = 0; key < friend.websites.length; key++) {
345 | var website = $(' ')
346 | .attr('label', 'homepage')
347 | .attr('href', friend.websites[key]);
348 | $(entry).append(website);
349 | }
350 | }
351 |
352 | // Your friends profile image.
353 | if (friend.photos) {
354 | var gdim = $(' ')
355 | .attr('rel', 'http://schemas.google.com/contacts/2008/rel#photo')
356 | .attr('type', 'image/*')
357 | .attr('href', friend.photo);
358 | $(entry).append(gdim);
359 | }
360 |
361 | // Finally, add the friend to the right group (the one we (possibly) created
362 | // above, that houses the facebook exports).
363 | var groupMembershipInfo = $(' ')
364 | .attr('deleted', 'false')
365 | .attr('href', this.contact_group_id);
366 | $(entry).append(groupMembershipInfo);
367 |
368 | // Must do the following to get the element as a string. The
369 | // "div" root element will not be included, but is necessary to call html().
370 | var s = $('
').append(entry).html();
371 |
372 | // JavaScript treats attribute as case-insensative the case of the tags, making everything
373 | // lowercase. We need to fix that, as google expects tags in the right case.
374 | // This really sucks.
375 | s = s.replace(/gd:fullname/g, 'gd:fullName');
376 | s = s.replace(/gd:phonenumber/g, 'gd:phoneNumber');
377 | s = s.replace(/gd:postaladdress/g, 'gd:postalAddress');
378 | s = s.replace(/displayname/g, 'displayName');
379 | s = s.replace(/gcontact:groupmembershipinfo/g, 'gcontact:groupMembershipInfo');
380 |
381 | var request = {
382 | 'method': 'POST',
383 | 'headers': {
384 | 'GData-Version': '3.0',
385 | 'Content-Type': 'application/atom+xml'
386 | //'Content-Type': 'application/json' // Not a valid input type
387 | },
388 | 'parameters': {
389 | //'alt': 'json'
390 | },
391 | 'body': s
392 | };
393 |
394 | this.oauth.sendSignedRequest(GoogleExport.CONTACTS_FEED,
395 | jQuery.proxy(this.onAddContact, this),
396 | request, friend);
397 | }
398 |
399 | GoogleExport.prototype.onAddContact = function(text, xhr, friend) {
400 | // This script runs in the context of background.html, so using
401 | // "worker_id" is valid.
402 | this.callback({
403 | finishedProcessingFriend: true,
404 | friend: friend,
405 | success: 1,
406 | message: 'Added to your Google Contacts!'
407 | });
408 | };
409 |
410 | GoogleExport.prototype.startExportingRequestedContacts = function() {
411 | console.log('startExportingRequestedContacts');
412 |
413 | // Prune out any friends that don't have any email addresses. We use email
414 | // addresses to determine if a contact already exists in google contacts, so
415 | // friends with no emails are problematic. Better to just not deal with
416 | // them.
417 | var friends_with_emails = this.requested_friends_to_import;
418 |
419 | // Keep a list of the friends that are requested for importation into Google
420 | // contacts that DON'T already exist there. We determine non-duplicate
421 | // friends based on their email address already being in the Google contacts.
422 | var non_duplicate_friends_to_import = [];
423 | for (var j = 0; j < friends_with_emails.length; j++) {
424 | var friend = friends_with_emails[j];
425 | // See if the emails address for this friend matches one in the existing
426 | // google contacts. If so, skip this friend.
427 | for (var i = 0; i < friend.emails.length; i++) {
428 | var email = friend.emails[i];
429 |
430 | if (!this.google_contacts_hash[email]) {
431 | non_duplicate_friends_to_import.push(friend);
432 | // Don't want to add the same friend twice, if this friend has another
433 | // email address, for example.
434 | break;
435 | }
436 | }
437 | }
438 |
439 | // The difference now between friends_with_emails and
440 | // non_duplicate_friends_to_import is the list of friends that we are NOT
441 | // adding because they already exist in google contacts. We need to report
442 | // these back to the work tab as well.
443 | for (var i = 0; i < non_duplicate_friends_to_import.length; i++) {
444 | var friend = non_duplicate_friends_to_import[i];
445 | if ($.inArray(friend, friends_with_emails) != -1) {
446 | delete friends_with_emails[$.inArray(friend, friends_with_emails)];
447 | }
448 | }
449 |
450 | // friends_with_emails has now been pruned to remove all non-duplicate
451 | // emails. The remaining friends_with_emails contains only duplicate friends
452 | // that we don't intend to add, so notify the work tab.
453 | for (var i = 0; i < friends_with_emails.length; i++) {
454 | var friend = friends_with_emails[i];
455 | this.callback({
456 | finishedProcessingFriend: true,
457 | friend: friend,
458 | success: 0,
459 | message: 'Not added: Friend is already in your Google Contacts or No email address exists!'
460 | });
461 | }
462 |
463 | // Now we're ready to add the remaining, non-duplicate friends to google
464 | // contacts.
465 | for (var i = 0; i < non_duplicate_friends_to_import.length; i++) {
466 | var friend = non_duplicate_friends_to_import[i];
467 | this.addFriendToGoogleContacts(friend);
468 | }
469 |
470 | this.doNextAction();
471 | };
472 |
--------------------------------------------------------------------------------
/js/grabber.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * Profile Grabbers main responsibility to parse profiles data.
4 | */
5 | ProfileGrabber = function() {
6 | this.domParser = new DOMParser();
7 | // For some reason $.ajax doesn't work with Facebook. Anyhow, jQuery will
8 | // be remove in the future, too overkill for what we need.
9 | this.xhr = new XMLHttpRequest();
10 | this.xhr.overrideMimeType('application/xml');
11 | };
12 |
13 | /**
14 | * Add a leading 0 if necessary.
15 | *
16 | * @param {number} num the number to format.
17 | * @return {string} formatted number with two digits.
18 | */
19 | ProfileGrabber.prototype.twoDigitsFormat = function(num) {
20 | return (num < 10) ? '0'+ num : num;
21 | };
22 |
23 | /**
24 | * Parses friends birthday given in format YYYY-MM-DD (with the year),
25 | * or --MM-DD (without the year).
26 | *
27 | * @param birthday string representation, ex: January 1st or January 1st, 2009.
28 | * @return correctly formatted birthday.
29 | */
30 | ProfileGrabber.prototype.parseBirthday = function(birthday) {
31 | var valid_birthday = birthday.match(/^\w+\s\d+(?:,\s(\d{4}))?$/)
32 | if (valid_birthday) {
33 | var date = new Date(birthday);
34 | var month = this.twoDigitsFormat(date.getMonth() + 1);
35 | var day = this.twoDigitsFormat(date.getDate());
36 | var year = '-';
37 | if (valid_birthday[1]) {
38 | year = valid_birthday[1];
39 | }
40 | birthday = year + '-' + month + '-' + day;
41 | }
42 | return birthday;
43 | };
44 |
45 | /**
46 | * Parses friends facebook address...
47 | * Note, some users don't have a unique textual profile id, if thats the case,
48 | * the url can be constructed via unique id instead.
49 | *
50 | * @param {string} fb The FB unique profile.
51 | * @param {string} id The FB unique ID.
52 | * @return {string} the url for the FB page.
53 | */
54 | ProfileGrabber.prototype.parseFacebookURL = function(fb, id) {
55 | if (fb == '') {
56 | fb = 'facebook.com/profile.php?id=' + id;
57 | }
58 | return 'http://' + fb;
59 | };
60 |
61 | /**
62 | * Parses friends list of emails.
63 | *
64 | * @param {Array} emails An array of emails.
65 | * @return {Array} emails as an array of strings.
66 | */
67 | ProfileGrabber.prototype.parseEmails = function(emails) {
68 | return emails.map(function() {
69 | return $(this).text();
70 | }).get();
71 | };
72 |
73 | /**
74 | * Parses friends list of phones.
75 | *
76 | * @param {Array} phones An array of emails.
77 | * @return {Array} phones as an array of strings.
78 | */
79 | ProfileGrabber.prototype.parsePhones = function(phones) {
80 | return phones.map(function() {
81 | return $(this).text();
82 | }).get();
83 | };
84 |
85 | /**
86 | * Parses friends websites that they like to share.
87 | * This will remove the garbage from the beginning of the string if exists and
88 | * just extracts the href from each link.
89 | *
90 | * @param {Array} websites An array of websites as a DOM.
91 | * @return {Array} websites as an array of strings.
92 | */
93 | ProfileGrabber.prototype.parseWebsites = function(websites) {
94 | return websites.map(function() {
95 | var url = decodeURIComponent($(this).attr('href'));
96 | // The patten extracts the URL from the following patterns if they match:
97 | // - A clean URL
98 | // - Prefixed with "/l.php?u=" stripped from URL.
99 | // - Request parameters in the form of ?h=\w+&refid=\d+
100 | // - Request parameters in an irregular form of &h=\w+&refid=\d+
101 | var pattern = /^(?:\/l.php\?u=)?(.*?)(?:(?:&|\?|&)h=\w+(?:&|&)refid=\d+)?$/;
102 | return url.replace(pattern, '$1');
103 | }).get();
104 | };
105 |
106 | /**
107 | * Fetches the friends information live.
108 | *
109 | * NOTE TO FUTURE SELF: If this extension breaks, it's probably because the
110 | * following lines no longer work. Namely, the fragile selector access
111 | * being done below to get at the corresponding fields might need to be
112 | * updated. Look at the actual FB page in question to make the right
113 | * fixes.
114 | *
115 | * @param {object} friend FB's internal friend storage.
116 | * @param {string} url Friends FB page.
117 | * @param {Function} callback This is needed somehow to do synchronous loading.
118 | * once a profile been fetched, calls this callback.
119 | */
120 | ProfileGrabber.prototype.extractInfo = function(friend, url, callback) {
121 | that = this;
122 | this.xhr.onload = function() {
123 | var dom = that.xhr.responseXML;
124 | // Some users have problem reading the response type for XML. Perhaps this
125 | // is due to the mangled headers or perhaps firewall denying access.
126 | // This is a small fix to read it from responseText since it should be
127 | // available then.
128 | if (!dom) {
129 | dom = that.domParser.parseFromString(that.xhr.responseText, 'application/xml');
130 | }
131 | else {
132 | dom = $(dom);
133 | }
134 |
135 | // Check if your not logged into the mobile page. This hack is dangerous.
136 | // But it makes sure none of your contacts are invalid. A better approach
137 | // would be to do an XHR to the main page, but that will waste resources.
138 | if ($('*', dom).html().indexOf('"/login.php') != -1) {
139 | friend.error = true;
140 | callback(friend);
141 | }
142 |
143 | // This is the extreme case, I believe there is a bug in Google Chrome the
144 | // way it handles irregular Facebook pages. It just quits without indication
145 | // somehow the DOMParser fails and doesn't continue.
146 | // https://github.com/mohamedmansour/fb-exporter/issues/#issue/4
147 | //
148 | // Use jQuery to convert it automatically. This will throw and error on the
149 | // page every single time. I really hate this approach, but there is no way
150 | // to supress the error as far as I know without doing mucky stuff.
151 | if (!dom || dom.querySelector('parsererror')) {
152 | dom = $(that.xhr.responseText);
153 | }
154 |
155 |
156 | // To gather additional friend information, add the right selector here.
157 | var emails = $('td:last a', $('td.label:contains("Email")', dom).parent());
158 | var fb = $('td:last', $('td.label:contains("Profile")', dom).parent());
159 | var phones = $('td:last a', $('td.label:contains("Phone")', dom).parent());
160 | var address = $('td:last', $('td.label:contains("Address")', dom).parent());
161 | var skype = $('td:last', $('td.label:contains("Skype")', dom).parent());
162 | var gtalk = $('td:last', $('td.label:contains("Google Talk")', dom).parent());
163 | var hotmail = $('td:last', $('td.label:contains("Windows")', dom).parent());
164 | var yahoo = $('td:last', $('td.label:contains("Yahoo! Messenger")', dom).parent());
165 | var websites = $('td a', $('td.label:contains("Website")', dom).parent());
166 | var birthday = $('td:last', $('td.label:contains("Birthday")', dom).parent());
167 |
168 | // Storage for post processing. Cleanup and parse groups.
169 | friend.fb = that.parseFacebookURL(fb.text(), friend.id);
170 | friend.phones = that.parsePhones(phones);
171 | friend.address = address.text();
172 | friend.birthday = that.parseBirthday(birthday.text());
173 | friend.im = {};
174 | friend.im.skype = skype.text();
175 | friend.im.gtalk = gtalk.text();
176 | friend.im.yahoo = gtalk.text();
177 | friend.im.hotmail = hotmail.text();
178 | friend.emails = that.parseEmails(emails);
179 | friend.websites = that.parseWebsites(websites);
180 | callback(friend);
181 | };
182 | this.xhr.open('GET', url);
183 | this.xhr.send(null);
184 | };
185 |
--------------------------------------------------------------------------------
/js/lib/chrome_ex_oauth.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2010 The Chromium Authors. All rights reserved. Use of this
3 | * source code is governed by a BSD-style license that can be found in the
4 | * LICENSE file.
5 | */
6 |
7 | /**
8 | * Constructor - no need to invoke directly, call initBackgroundPage instead.
9 | * @constructor
10 | * @param {String} url_request_token The OAuth request token URL.
11 | * @param {String} url_auth_token The OAuth authorize token URL.
12 | * @param {String} url_access_token The OAuth access token URL.
13 | * @param {String} consumer_key The OAuth consumer key.
14 | * @param {String} consumer_secret The OAuth consumer secret.
15 | * @param {String} oauth_scope The OAuth scope parameter.
16 | * @param {Object} opt_args Optional arguments. Recognized parameters:
17 | * "app_name" {String} Name of the current application
18 | * "callback_page" {String} If you renamed chrome_ex_oauth.html, the name
19 | * this file was renamed to.
20 | */
21 | function ChromeExOAuth(url_request_token, url_auth_token, url_access_token,
22 | consumer_key, consumer_secret, oauth_scope, opt_args) {
23 | this.url_request_token = url_request_token;
24 | this.url_auth_token = url_auth_token;
25 | this.url_access_token = url_access_token;
26 | this.consumer_key = consumer_key;
27 | this.consumer_secret = consumer_secret;
28 | this.oauth_scope = oauth_scope;
29 | this.app_name = opt_args && opt_args['app_name'] ||
30 | "ChromeExOAuth Library";
31 | this.key_token = "oauth_token";
32 | this.key_token_secret = "oauth_token_secret";
33 | this.callback_page = opt_args && opt_args['callback_page'] ||
34 | "chrome_ex_oauth.html";
35 | this.auth_params = {};
36 | if (opt_args && opt_args['auth_params']) {
37 | for (key in opt_args['auth_params']) {
38 | if (opt_args['auth_params'].hasOwnProperty(key)) {
39 | this.auth_params[key] = opt_args['auth_params'][key];
40 | }
41 | }
42 | }
43 | };
44 |
45 | /*******************************************************************************
46 | * PUBLIC API METHODS
47 | * Call these from your background page.
48 | ******************************************************************************/
49 |
50 | /**
51 | * Initializes the OAuth helper from the background page. You must call this
52 | * before attempting to make any OAuth calls.
53 | * @param {Object} oauth_config Configuration parameters in a JavaScript object.
54 | * The following parameters are recognized:
55 | * "request_url" {String} OAuth request token URL.
56 | * "authorize_url" {String} OAuth authorize token URL.
57 | * "access_url" {String} OAuth access token URL.
58 | * "consumer_key" {String} OAuth consumer key.
59 | * "consumer_secret" {String} OAuth consumer secret.
60 | * "scope" {String} OAuth access scope.
61 | * "app_name" {String} Application name.
62 | * "auth_params" {Object} Additional parameters to pass to the
63 | * Authorization token URL. For an example, 'hd', 'hl', 'btmpl':
64 | * http://code.google.com/apis/accounts/docs/OAuth_ref.html#GetAuth
65 | * @return {ChromeExOAuth} An initialized ChromeExOAuth object.
66 | */
67 | ChromeExOAuth.initBackgroundPage = function(oauth_config) {
68 | window.chromeExOAuthConfig = oauth_config;
69 | window.chromeExOAuth = ChromeExOAuth.fromConfig(oauth_config);
70 | window.chromeExOAuthRedirectStarted = false;
71 | window.chromeExOAuthRequestingAccess = false;
72 |
73 | var url_match = chrome.extension.getURL(window.chromeExOAuth.callback_page);
74 | var tabs = {};
75 | chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
76 | if (changeInfo.url &&
77 | changeInfo.url.substr(0, url_match.length) === url_match &&
78 | changeInfo.url != tabs[tabId] &&
79 | window.chromeExOAuthRequestingAccess == false) {
80 | chrome.tabs.create({ 'url' : changeInfo.url }, function(tab) {
81 | tabs[tab.id] = tab.url;
82 | chrome.tabs.remove(tabId);
83 | });
84 | }
85 | });
86 |
87 | return window.chromeExOAuth;
88 | };
89 |
90 | /**
91 | * Authorizes the current user with the configued API. You must call this
92 | * before calling sendSignedRequest.
93 | * @param {Function} callback A function to call once an access token has
94 | * been obtained. This callback will be passed the following arguments:
95 | * token {String} The OAuth access token.
96 | * secret {String} The OAuth access token secret.
97 | */
98 | ChromeExOAuth.prototype.authorize = function(callback) {
99 | if (this.hasToken()) {
100 | callback(this.getToken(), this.getTokenSecret());
101 | } else {
102 | window.chromeExOAuthOnAuthorize = function(token, secret) {
103 | callback(token, secret);
104 | };
105 | chrome.tabs.create({ 'url' :chrome.extension.getURL(this.callback_page) });
106 | }
107 | };
108 |
109 | /**
110 | * Clears any OAuth tokens stored for this configuration. Effectively a
111 | * "logout" of the configured OAuth API.
112 | */
113 | ChromeExOAuth.prototype.clearTokens = function() {
114 | delete localStorage[this.key_token + encodeURI(this.oauth_scope)];
115 | delete localStorage[this.key_token_secret + encodeURI(this.oauth_scope)];
116 | };
117 |
118 | /**
119 | * Returns whether a token is currently stored for this configuration.
120 | * Effectively a check to see whether the current user is "logged in" to
121 | * the configured OAuth API.
122 | * @return {Boolean} True if an access token exists.
123 | */
124 | ChromeExOAuth.prototype.hasToken = function() {
125 | return !!this.getToken();
126 | };
127 |
128 | /**
129 | * Makes an OAuth-signed HTTP request with the currently authorized tokens.
130 | * @param {String} url The URL to send the request to. Querystring parameters
131 | * should be omitted.
132 | * @param {Function} callback A function to be called once the request is
133 | * completed. This callback will be passed the following arguments:
134 | * responseText {String} The text response.
135 | * xhr {XMLHttpRequest} The XMLHttpRequest object which was used to
136 | * send the request. Useful if you need to check response status
137 | * code, etc.
138 | * passThru {Object} Additional static arguments, see opt_passThru.
139 | * @param {Object} opt_params Additional parameters to configure the request.
140 | * The following parameters are accepted:
141 | * "method" {String} The HTTP method to use. Defaults to "GET".
142 | * "body" {String} A request body to send. Defaults to null.
143 | * "parameters" {Object} Query parameters to include in the request.
144 | * "headers" {Object} Additional headers to include in the request.
145 | * @param {Object} opt_passThru Additional parameter to pass to the callback.
146 | */
147 | ChromeExOAuth.prototype.sendSignedRequest = function(url, callback,
148 | opt_params,
149 | opt_passThru) {
150 | var method = opt_params && opt_params['method'] || 'GET';
151 | var body = opt_params && opt_params['body'] || null;
152 | var params = opt_params && opt_params['parameters'] || {};
153 | var headers = opt_params && opt_params['headers'] || {};
154 |
155 | var signedUrl = this.signURL(url, method, params);
156 |
157 | ChromeExOAuth.sendRequest(method, signedUrl, headers, body, function (xhr) {
158 | if (xhr.readyState == 4) {
159 | callback(xhr.responseText, xhr, opt_passThru);
160 | }
161 | });
162 | };
163 |
164 | /**
165 | * Adds the required OAuth parameters to the given url and returns the
166 | * result. Useful if you need a signed url but don't want to make an XHR
167 | * request.
168 | * @param {String} method The http method to use.
169 | * @param {String} url The base url of the resource you are querying.
170 | * @param {Object} opt_params Query parameters to include in the request.
171 | * @return {String} The base url plus any query params plus any OAuth params.
172 | */
173 | ChromeExOAuth.prototype.signURL = function(url, method, opt_params) {
174 | var token = this.getToken();
175 | var secret = this.getTokenSecret();
176 | if (!token || !secret) {
177 | throw new Error("No oauth token or token secret");
178 | }
179 |
180 | var params = opt_params || {};
181 |
182 | var result = OAuthSimple().sign({
183 | action : method,
184 | path : url,
185 | parameters : params,
186 | signatures: {
187 | consumer_key : this.consumer_key,
188 | shared_secret : this.consumer_secret,
189 | oauth_secret : secret,
190 | oauth_token: token
191 | }
192 | });
193 |
194 | return result.signed_url;
195 | };
196 |
197 | /**
198 | * Generates the Authorization header based on the oauth parameters.
199 | * @param {String} url The base url of the resource you are querying.
200 | * @param {Object} opt_params Query parameters to include in the request.
201 | * @return {String} An Authorization header containing the oauth_* params.
202 | */
203 | ChromeExOAuth.prototype.getAuthorizationHeader = function(url, method,
204 | opt_params) {
205 | var token = this.getToken();
206 | var secret = this.getTokenSecret();
207 | if (!token || !secret) {
208 | throw new Error("No oauth token or token secret");
209 | }
210 |
211 | var params = opt_params || {};
212 |
213 | return OAuthSimple().getHeaderString({
214 | action: method,
215 | path : url,
216 | parameters : params,
217 | signatures: {
218 | consumer_key : this.consumer_key,
219 | shared_secret : this.consumer_secret,
220 | oauth_secret : secret,
221 | oauth_token: token
222 | }
223 | });
224 | };
225 |
226 | /*******************************************************************************
227 | * PRIVATE API METHODS
228 | * Used by the library. There should be no need to call these methods directly.
229 | ******************************************************************************/
230 |
231 | /**
232 | * Creates a new ChromeExOAuth object from the supplied configuration object.
233 | * @param {Object} oauth_config Configuration parameters in a JavaScript object.
234 | * The following parameters are recognized:
235 | * "request_url" {String} OAuth request token URL.
236 | * "authorize_url" {String} OAuth authorize token URL.
237 | * "access_url" {String} OAuth access token URL.
238 | * "consumer_key" {String} OAuth consumer key.
239 | * "consumer_secret" {String} OAuth consumer secret.
240 | * "scope" {String} OAuth access scope.
241 | * "app_name" {String} Application name.
242 | * "auth_params" {Object} Additional parameters to pass to the
243 | * Authorization token URL. For an example, 'hd', 'hl', 'btmpl':
244 | * http://code.google.com/apis/accounts/docs/OAuth_ref.html#GetAuth
245 | * @return {ChromeExOAuth} An initialized ChromeExOAuth object.
246 | */
247 | ChromeExOAuth.fromConfig = function(oauth_config) {
248 | return new ChromeExOAuth(
249 | oauth_config['request_url'],
250 | oauth_config['authorize_url'],
251 | oauth_config['access_url'],
252 | oauth_config['consumer_key'],
253 | oauth_config['consumer_secret'],
254 | oauth_config['scope'],
255 | {
256 | 'app_name' : oauth_config['app_name'],
257 | 'auth_params' : oauth_config['auth_params']
258 | }
259 | );
260 | };
261 |
262 | /**
263 | * Initializes chrome_ex_oauth.html and redirects the page if needed to start
264 | * the OAuth flow. Once an access token is obtained, this function closes
265 | * chrome_ex_oauth.html.
266 | */
267 | ChromeExOAuth.initCallbackPage = function() {
268 | var background_page = chrome.extension.getBackgroundPage();
269 | var oauth_config = background_page.chromeExOAuthConfig;
270 | var oauth = ChromeExOAuth.fromConfig(oauth_config);
271 | background_page.chromeExOAuthRedirectStarted = true;
272 | oauth.initOAuthFlow(function (token, secret) {
273 | background_page.chromeExOAuthOnAuthorize(token, secret);
274 | background_page.chromeExOAuthRedirectStarted = false;
275 | chrome.tabs.getSelected(null, function (tab) {
276 | chrome.tabs.remove(tab.id);
277 | });
278 | });
279 | };
280 |
281 | /**
282 | * Sends an HTTP request. Convenience wrapper for XMLHttpRequest calls.
283 | * @param {String} method The HTTP method to use.
284 | * @param {String} url The URL to send the request to.
285 | * @param {Object} headers Optional request headers in key/value format.
286 | * @param {String} body Optional body content.
287 | * @param {Function} callback Function to call when the XMLHttpRequest's
288 | * ready state changes. See documentation for XMLHttpRequest's
289 | * onreadystatechange handler for more information.
290 | */
291 | ChromeExOAuth.sendRequest = function(method, url, headers, body, callback) {
292 | var xhr = new XMLHttpRequest();
293 | xhr.onreadystatechange = function(data) {
294 | callback(xhr, data);
295 | }
296 | xhr.open(method, url, true);
297 | if (headers) {
298 | for (var header in headers) {
299 | if (headers.hasOwnProperty(header)) {
300 | xhr.setRequestHeader(header, headers[header]);
301 | }
302 | }
303 | }
304 | xhr.send(body);
305 | };
306 |
307 | /**
308 | * Decodes a URL-encoded string into key/value pairs.
309 | * @param {String} encoded An URL-encoded string.
310 | * @return {Object} An object representing the decoded key/value pairs found
311 | * in the encoded string.
312 | */
313 | ChromeExOAuth.formDecode = function(encoded) {
314 | var params = encoded.split("&");
315 | var decoded = {};
316 | for (var i = 0, param; param = params[i]; i++) {
317 | var keyval = param.split("=");
318 | if (keyval.length == 2) {
319 | var key = ChromeExOAuth.fromRfc3986(keyval[0]);
320 | var val = ChromeExOAuth.fromRfc3986(keyval[1]);
321 | decoded[key] = val;
322 | }
323 | }
324 | return decoded;
325 | };
326 |
327 | /**
328 | * Returns the current window's querystring decoded into key/value pairs.
329 | * @return {Object} A object representing any key/value pairs found in the
330 | * current window's querystring.
331 | */
332 | ChromeExOAuth.getQueryStringParams = function() {
333 | var urlparts = window.location.href.split("?");
334 | if (urlparts.length >= 2) {
335 | var querystring = urlparts.slice(1).join("?");
336 | return ChromeExOAuth.formDecode(querystring);
337 | }
338 | return {};
339 | };
340 |
341 | /**
342 | * Binds a function call to a specific object. This function will also take
343 | * a variable number of additional arguments which will be prepended to the
344 | * arguments passed to the bound function when it is called.
345 | * @param {Function} func The function to bind.
346 | * @param {Object} obj The object to bind to the function's "this".
347 | * @return {Function} A closure that will call the bound function.
348 | */
349 | ChromeExOAuth.bind = function(func, obj) {
350 | var newargs = Array.prototype.slice.call(arguments).slice(2);
351 | return function() {
352 | var combinedargs = newargs.concat(Array.prototype.slice.call(arguments));
353 | func.apply(obj, combinedargs);
354 | };
355 | };
356 |
357 | /**
358 | * Encodes a value according to the RFC3986 specification.
359 | * @param {String} val The string to encode.
360 | */
361 | ChromeExOAuth.toRfc3986 = function(val){
362 | return encodeURIComponent(val)
363 | .replace(/\!/g, "%21")
364 | .replace(/\*/g, "%2A")
365 | .replace(/'/g, "%27")
366 | .replace(/\(/g, "%28")
367 | .replace(/\)/g, "%29");
368 | };
369 |
370 | /**
371 | * Decodes a string that has been encoded according to RFC3986.
372 | * @param {String} val The string to decode.
373 | */
374 | ChromeExOAuth.fromRfc3986 = function(val){
375 | var tmp = val
376 | .replace(/%21/g, "!")
377 | .replace(/%2A/g, "*")
378 | .replace(/%27/g, "'")
379 | .replace(/%28/g, "(")
380 | .replace(/%29/g, ")");
381 | return decodeURIComponent(tmp);
382 | };
383 |
384 | /**
385 | * Adds a key/value parameter to the supplied URL.
386 | * @param {String} url An URL which may or may not contain querystring values.
387 | * @param {String} key A key
388 | * @param {String} value A value
389 | * @return {String} The URL with URL-encoded versions of the key and value
390 | * appended, prefixing them with "&" or "?" as needed.
391 | */
392 | ChromeExOAuth.addURLParam = function(url, key, value) {
393 | var sep = (url.indexOf('?') >= 0) ? "&" : "?";
394 | return url + sep +
395 | ChromeExOAuth.toRfc3986(key) + "=" + ChromeExOAuth.toRfc3986(value);
396 | };
397 |
398 | /**
399 | * Stores an OAuth token for the configured scope.
400 | * @param {String} token The token to store.
401 | */
402 | ChromeExOAuth.prototype.setToken = function(token) {
403 | localStorage[this.key_token + encodeURI(this.oauth_scope)] = token;
404 | };
405 |
406 | /**
407 | * Retrieves any stored token for the configured scope.
408 | * @return {String} The stored token.
409 | */
410 | ChromeExOAuth.prototype.getToken = function() {
411 | return localStorage[this.key_token + encodeURI(this.oauth_scope)];
412 | };
413 |
414 | /**
415 | * Stores an OAuth token secret for the configured scope.
416 | * @param {String} secret The secret to store.
417 | */
418 | ChromeExOAuth.prototype.setTokenSecret = function(secret) {
419 | localStorage[this.key_token_secret + encodeURI(this.oauth_scope)] = secret;
420 | };
421 |
422 | /**
423 | * Retrieves any stored secret for the configured scope.
424 | * @return {String} The stored secret.
425 | */
426 | ChromeExOAuth.prototype.getTokenSecret = function() {
427 | return localStorage[this.key_token_secret + encodeURI(this.oauth_scope)];
428 | };
429 |
430 | /**
431 | * Starts an OAuth authorization flow for the current page. If a token exists,
432 | * no redirect is needed and the supplied callback is called immediately.
433 | * If this method detects that a redirect has finished, it grabs the
434 | * appropriate OAuth parameters from the URL and attempts to retrieve an
435 | * access token. If no token exists and no redirect has happened, then
436 | * an access token is requested and the page is ultimately redirected.
437 | * @param {Function} callback The function to call once the flow has finished.
438 | * This callback will be passed the following arguments:
439 | * token {String} The OAuth access token.
440 | * secret {String} The OAuth access token secret.
441 | */
442 | ChromeExOAuth.prototype.initOAuthFlow = function(callback) {
443 | if (!this.hasToken()) {
444 | var params = ChromeExOAuth.getQueryStringParams();
445 | if (params['chromeexoauthcallback'] == 'true') {
446 | var oauth_token = params['oauth_token'];
447 | var oauth_verifier = params['oauth_verifier']
448 | this.getAccessToken(oauth_token, oauth_verifier, callback);
449 | } else {
450 | var request_params = {
451 | 'url_callback_param' : 'chromeexoauthcallback'
452 | }
453 | this.getRequestToken(function(url) {
454 | window.location.href = url;
455 | }, request_params);
456 | }
457 | } else {
458 | callback(this.getToken(), this.getTokenSecret());
459 | }
460 | };
461 |
462 | /**
463 | * Requests an OAuth request token.
464 | * @param {Function} callback Function to call once the authorize URL is
465 | * calculated. This callback will be passed the following arguments:
466 | * url {String} The URL the user must be redirected to in order to
467 | * approve the token.
468 | * @param {Object} opt_args Optional arguments. The following parameters
469 | * are accepted:
470 | * "url_callback" {String} The URL the OAuth provider will redirect to.
471 | * "url_callback_param" {String} A parameter to include in the callback
472 | * URL in order to indicate to this library that a redirect has
473 | * taken place.
474 | */
475 | ChromeExOAuth.prototype.getRequestToken = function(callback, opt_args) {
476 | if (typeof callback !== "function") {
477 | throw new Error("Specified callback must be a function.");
478 | }
479 | var url = opt_args && opt_args['url_callback'] ||
480 | window && window.top && window.top.location &&
481 | window.top.location.href;
482 |
483 | var url_param = opt_args && opt_args['url_callback_param'] ||
484 | "chromeexoauthcallback";
485 | var url_callback = ChromeExOAuth.addURLParam(url, url_param, "true");
486 |
487 | var result = OAuthSimple().sign({
488 | path : this.url_request_token,
489 | parameters: {
490 | "xoauth_displayname" : this.app_name,
491 | "scope" : this.oauth_scope,
492 | "oauth_callback" : url_callback
493 | },
494 | signatures: {
495 | consumer_key : this.consumer_key,
496 | shared_secret : this.consumer_secret
497 | }
498 | });
499 | var onToken = ChromeExOAuth.bind(this.onRequestToken, this, callback);
500 | ChromeExOAuth.sendRequest("GET", result.signed_url, null, null, onToken);
501 | };
502 |
503 | /**
504 | * Called when a request token has been returned. Stores the request token
505 | * secret for later use and sends the authorization url to the supplied
506 | * callback (for redirecting the user).
507 | * @param {Function} callback Function to call once the authorize URL is
508 | * calculated. This callback will be passed the following arguments:
509 | * url {String} The URL the user must be redirected to in order to
510 | * approve the token.
511 | * @param {XMLHttpRequest} xhr The XMLHttpRequest object used to fetch the
512 | * request token.
513 | */
514 | ChromeExOAuth.prototype.onRequestToken = function(callback, xhr) {
515 | if (xhr.readyState == 4) {
516 | if (xhr.status == 200) {
517 | var params = ChromeExOAuth.formDecode(xhr.responseText);
518 | var token = params['oauth_token'];
519 | this.setTokenSecret(params['oauth_token_secret']);
520 | var url = ChromeExOAuth.addURLParam(this.url_auth_token,
521 | "oauth_token", token);
522 | for (var key in this.auth_params) {
523 | if (this.auth_params.hasOwnProperty(key)) {
524 | url = ChromeExOAuth.addURLParam(url, key, this.auth_params[key]);
525 | }
526 | }
527 | callback(url);
528 | } else {
529 | throw new Error("Fetching request token failed. Status " + xhr.status);
530 | }
531 | }
532 | };
533 |
534 | /**
535 | * Requests an OAuth access token.
536 | * @param {String} oauth_token The OAuth request token.
537 | * @param {String} oauth_verifier The OAuth token verifier.
538 | * @param {Function} callback The function to call once the token is obtained.
539 | * This callback will be passed the following arguments:
540 | * token {String} The OAuth access token.
541 | * secret {String} The OAuth access token secret.
542 | */
543 | ChromeExOAuth.prototype.getAccessToken = function(oauth_token, oauth_verifier,
544 | callback) {
545 | if (typeof callback !== "function") {
546 | throw new Error("Specified callback must be a function.");
547 | }
548 | var bg = chrome.extension.getBackgroundPage();
549 | if (bg.chromeExOAuthRequestingAccess == false) {
550 | bg.chromeExOAuthRequestingAccess = true;
551 |
552 | var result = OAuthSimple().sign({
553 | path : this.url_access_token,
554 | parameters: {
555 | "oauth_token" : oauth_token,
556 | "oauth_verifier" : oauth_verifier
557 | },
558 | signatures: {
559 | consumer_key : this.consumer_key,
560 | shared_secret : this.consumer_secret,
561 | oauth_secret : this.getTokenSecret(this.oauth_scope)
562 | }
563 | });
564 |
565 | var onToken = ChromeExOAuth.bind(this.onAccessToken, this, callback);
566 | ChromeExOAuth.sendRequest("GET", result.signed_url, null, null, onToken);
567 | }
568 | };
569 |
570 | /**
571 | * Called when an access token has been returned. Stores the access token and
572 | * access token secret for later use and sends them to the supplied callback.
573 | * @param {Function} callback The function to call once the token is obtained.
574 | * This callback will be passed the following arguments:
575 | * token {String} The OAuth access token.
576 | * secret {String} The OAuth access token secret.
577 | * @param {XMLHttpRequest} xhr The XMLHttpRequest object used to fetch the
578 | * access token.
579 | */
580 | ChromeExOAuth.prototype.onAccessToken = function(callback, xhr) {
581 | if (xhr.readyState == 4) {
582 | var bg = chrome.extension.getBackgroundPage();
583 | if (xhr.status == 200) {
584 | var params = ChromeExOAuth.formDecode(xhr.responseText);
585 | var token = params["oauth_token"];
586 | var secret = params["oauth_token_secret"];
587 | this.setToken(token);
588 | this.setTokenSecret(secret);
589 | bg.chromeExOAuthRequestingAccess = false;
590 | callback(token, secret);
591 | } else {
592 | bg.chromeExOAuthRequestingAccess = false;
593 | throw new Error("Fetching access token failed with status " + xhr.status);
594 | }
595 | }
596 | };
597 |
--------------------------------------------------------------------------------
/js/lib/chrome_ex_oauthsimple.js:
--------------------------------------------------------------------------------
1 | /* OAuthSimple
2 | * A simpler version of OAuth
3 | *
4 | * author: jr conlin
5 | * mail: src@anticipatr.com
6 | * copyright: unitedHeroes.net
7 | * version: 1.0
8 | * url: http://unitedHeroes.net/OAuthSimple
9 | *
10 | * Copyright (c) 2009, unitedHeroes.net
11 | * All rights reserved.
12 | *
13 | * Redistribution and use in source and binary forms, with or without
14 | * modification, are permitted provided that the following conditions are met:
15 | * * Redistributions of source code must retain the above copyright
16 | * notice, this list of conditions and the following disclaimer.
17 | * * Redistributions in binary form must reproduce the above copyright
18 | * notice, this list of conditions and the following disclaimer in the
19 | * documentation and/or other materials provided with the distribution.
20 | * * Neither the name of the unitedHeroes.net nor the
21 | * names of its contributors may be used to endorse or promote products
22 | * derived from this software without specific prior written permission.
23 | *
24 | * THIS SOFTWARE IS PROVIDED BY UNITEDHEROES.NET ''AS IS'' AND ANY
25 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
26 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27 | * DISCLAIMED. IN NO EVENT SHALL UNITEDHEROES.NET BE LIABLE FOR ANY
28 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
30 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
31 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
32 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
33 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34 | */
35 | var OAuthSimple;
36 |
37 | if (OAuthSimple === undefined)
38 | {
39 | /* Simple OAuth
40 | *
41 | * This class only builds the OAuth elements, it does not do the actual
42 | * transmission or reception of the tokens. It does not validate elements
43 | * of the token. It is for client use only.
44 | *
45 | * api_key is the API key, also known as the OAuth consumer key
46 | * shared_secret is the shared secret (duh).
47 | *
48 | * Both the api_key and shared_secret are generally provided by the site
49 | * offering OAuth services. You need to specify them at object creation
50 | * because nobody ing uses OAuth without that minimal set of
51 | * signatures.
52 | *
53 | * If you want to use the higher order security that comes from the
54 | * OAuth token (sorry, I don't provide the functions to fetch that because
55 | * sites aren't horribly consistent about how they offer that), you need to
56 | * pass those in either with .setTokensAndSecrets() or as an argument to the
57 | * .sign() or .getHeaderString() functions.
58 | *
59 | * Example:
60 |
61 | var oauthObject = OAuthSimple().sign({path:'http://example.com/rest/',
62 | parameters: 'foo=bar&gorp=banana',
63 | signatures:{
64 | api_key:'12345abcd',
65 | shared_secret:'xyz-5309'
66 | }});
67 | document.getElementById('someLink').href=oauthObject.signed_url;
68 |
69 | *
70 | * that will sign as a "GET" using "SHA1-MAC" the url. If you need more than
71 | * that, read on, McDuff.
72 | */
73 |
74 | /** OAuthSimple creator
75 | *
76 | * Create an instance of OAuthSimple
77 | *
78 | * @param api_key {string} The API Key (sometimes referred to as the consumer key) This value is usually supplied by the site you wish to use.
79 | * @param shared_secret (string) The shared secret. This value is also usually provided by the site you wish to use.
80 | */
81 | OAuthSimple = function (consumer_key,shared_secret)
82 | {
83 | /* if (api_key == undefined)
84 | throw("Missing argument: api_key (oauth_consumer_key) for OAuthSimple. This is usually provided by the hosting site.");
85 | if (shared_secret == undefined)
86 | throw("Missing argument: shared_secret (shared secret) for OAuthSimple. This is usually provided by the hosting site.");
87 | */ this._secrets={};
88 | this._parameters={};
89 |
90 | // General configuration options.
91 | if (consumer_key !== undefined) {
92 | this._secrets['consumer_key'] = consumer_key;
93 | }
94 | if (shared_secret !== undefined) {
95 | this._secrets['shared_secret'] = shared_secret;
96 | }
97 | this._default_signature_method= "HMAC-SHA1";
98 | this._action = "GET";
99 | this._nonce_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
100 |
101 |
102 | this.reset = function() {
103 | this._parameters={};
104 | this._path=undefined;
105 | return this;
106 | };
107 |
108 | /** set the parameters either from a hash or a string
109 | *
110 | * @param {string,object} List of parameters for the call, this can either be a URI string (e.g. "foo=bar&gorp=banana" or an object/hash)
111 | */
112 | this.setParameters = function (parameters) {
113 | if (parameters === undefined) {
114 | parameters = {};
115 | }
116 | if (typeof(parameters) == 'string') {
117 | parameters=this._parseParameterString(parameters);
118 | }
119 | this._parameters = parameters;
120 | if (this._parameters['oauth_nonce'] === undefined) {
121 | this._getNonce();
122 | }
123 | if (this._parameters['oauth_timestamp'] === undefined) {
124 | this._getTimestamp();
125 | }
126 | if (this._parameters['oauth_method'] === undefined) {
127 | this.setSignatureMethod();
128 | }
129 | if (this._parameters['oauth_consumer_key'] === undefined) {
130 | this._getApiKey();
131 | }
132 | if(this._parameters['oauth_token'] === undefined) {
133 | this._getAccessToken();
134 | }
135 |
136 | return this;
137 | };
138 |
139 | /** convienence method for setParameters
140 | *
141 | * @param parameters {string,object} See .setParameters
142 | */
143 | this.setQueryString = function (parameters) {
144 | return this.setParameters(parameters);
145 | };
146 |
147 | /** Set the target URL (does not include the parameters)
148 | *
149 | * @param path {string} the fully qualified URI (excluding query arguments) (e.g "http://example.org/foo")
150 | */
151 | this.setURL = function (path) {
152 | if (path == '') {
153 | throw ('No path specified for OAuthSimple.setURL');
154 | }
155 | this._path = path;
156 | return this;
157 | };
158 |
159 | /** convienence method for setURL
160 | *
161 | * @param path {string} see .setURL
162 | */
163 | this.setPath = function(path){
164 | return this.setURL(path);
165 | };
166 |
167 | /** set the "action" for the url, (e.g. GET,POST, DELETE, etc.)
168 | *
169 | * @param action {string} HTTP Action word.
170 | */
171 | this.setAction = function(action) {
172 | if (action === undefined) {
173 | action="GET";
174 | }
175 | action = action.toUpperCase();
176 | if (action.match('[^A-Z]')) {
177 | throw ('Invalid action specified for OAuthSimple.setAction');
178 | }
179 | this._action = action;
180 | return this;
181 | };
182 |
183 | /** set the signatures (as well as validate the ones you have)
184 | *
185 | * @param signatures {object} object/hash of the token/signature pairs {api_key:, shared_secret:, oauth_token: oauth_secret:}
186 | */
187 | this.setTokensAndSecrets = function(signatures) {
188 | if (signatures)
189 | {
190 | for (var i in signatures) {
191 | this._secrets[i] = signatures[i];
192 | }
193 | }
194 | // Aliases
195 | if (this._secrets['api_key']) {
196 | this._secrets.consumer_key = this._secrets.api_key;
197 | }
198 | if (this._secrets['access_token']) {
199 | this._secrets.oauth_token = this._secrets.access_token;
200 | }
201 | if (this._secrets['access_secret']) {
202 | this._secrets.oauth_secret = this._secrets.access_secret;
203 | }
204 | // Gauntlet
205 | if (this._secrets.consumer_key === undefined) {
206 | throw('Missing required consumer_key in OAuthSimple.setTokensAndSecrets');
207 | }
208 | if (this._secrets.shared_secret === undefined) {
209 | throw('Missing required shared_secret in OAuthSimple.setTokensAndSecrets');
210 | }
211 | if ((this._secrets.oauth_token !== undefined) && (this._secrets.oauth_secret === undefined)) {
212 | throw('Missing oauth_secret for supplied oauth_token in OAuthSimple.setTokensAndSecrets');
213 | }
214 | return this;
215 | };
216 |
217 | /** set the signature method (currently only Plaintext or SHA-MAC1)
218 | *
219 | * @param method {string} Method of signing the transaction (only PLAINTEXT and SHA-MAC1 allowed for now)
220 | */
221 | this.setSignatureMethod = function(method) {
222 | if (method === undefined) {
223 | method = this._default_signature_method;
224 | }
225 | //TODO: accept things other than PlainText or SHA-MAC1
226 | if (method.toUpperCase().match(/(PLAINTEXT|HMAC-SHA1)/) === undefined) {
227 | throw ('Unknown signing method specified for OAuthSimple.setSignatureMethod');
228 | }
229 | this._parameters['oauth_signature_method']= method.toUpperCase();
230 | return this;
231 | };
232 |
233 | /** sign the request
234 | *
235 | * note: all arguments are optional, provided you've set them using the
236 | * other helper functions.
237 | *
238 | * @param args {object} hash of arguments for the call
239 | * {action:, path:, parameters:, method:, signatures:}
240 | * all arguments are optional.
241 | */
242 | this.sign = function (args) {
243 | if (args === undefined) {
244 | args = {};
245 | }
246 | // Set any given parameters
247 | if(args['action'] !== undefined) {
248 | this.setAction(args['action']);
249 | }
250 | if (args['path'] !== undefined) {
251 | this.setPath(args['path']);
252 | }
253 | if (args['method'] !== undefined) {
254 | this.setSignatureMethod(args['method']);
255 | }
256 | this.setTokensAndSecrets(args['signatures']);
257 | if (args['parameters'] !== undefined){
258 | this.setParameters(args['parameters']);
259 | }
260 | // check the parameters
261 | var normParams = this._normalizedParameters();
262 | this._parameters['oauth_signature']=this._generateSignature(normParams);
263 | return {
264 | parameters: this._parameters,
265 | signature: this._oauthEscape(this._parameters['oauth_signature']),
266 | signed_url: this._path + '?' + this._normalizedParameters(),
267 | header: this.getHeaderString()
268 | };
269 | };
270 |
271 | /** Return a formatted "header" string
272 | *
273 | * NOTE: This doesn't set the "Authorization: " prefix, which is required.
274 | * I don't set it because various set header functions prefer different
275 | * ways to do that.
276 | *
277 | * @param args {object} see .sign
278 | */
279 | this.getHeaderString = function(args) {
280 | if (this._parameters['oauth_signature'] === undefined) {
281 | this.sign(args);
282 | }
283 |
284 | var result = 'OAuth ';
285 | for (var pName in this._parameters)
286 | {
287 | if (!pName.match(/^oauth/)) {
288 | continue;
289 | }
290 | if ((this._parameters[pName]) instanceof Array)
291 | {
292 | var pLength = this._parameters[pName].length;
293 | for (var j=0;j>16)+(y>>16)+(l>>16);return(m<<16)|(l&0xFFFF);}function _r(n,c){return(n<>>(32-c));}function _c(x,l){x[l>>5]|=0x80<<(24-l%32);x[((l+64>>9)<<4)+15]=l;var w=[80],a=1732584193,b=-271733879,c=-1732584194,d=271733878,e=-1009589776;for(var i=0;i>5]|=(s.charCodeAt(i/8)&m)<<(32-_z-i%32);}return b;}function _h(k,d){var b=_b(k);if(b.length>16){b=_c(b,k.length*_z);}var p=[16],o=[16];for(var i=0;i<16;i++){p[i]=b[i]^0x36363636;o[i]=b[i]^0x5C5C5C5C;}var h=_c(p.concat(_b(d)),512+d.length*_z);return _c(o.concat(h),512+160);}function _n(b){var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",s='';for(var i=0;i>2]>>8*(3-i%4))&0xFF)<<16)|(((b[i+1>>2]>>8*(3-(i+1)%4))&0xFF)<<8)|((b[i+2>>2]>>8*(3-(i+2)%4))&0xFF);for(var j=0;j<4;j++){if(i*8+j*6>b.length*32){s+=_p;}else{s+=t.charAt((r>>6*(3-j))&0x3F);}}}return s;}function _x(k,d){return _n(_h(k,d));}return _x(k,d);
398 | }
399 |
400 |
401 | this._normalizedParameters = function() {
402 | var elements = new Array();
403 | var paramNames = [];
404 | var ra =0;
405 | for (var paramName in this._parameters)
406 | {
407 | if (ra++ > 1000) {
408 | throw('runaway 1');
409 | }
410 | paramNames.unshift(paramName);
411 | }
412 | paramNames = paramNames.sort();
413 | pLen = paramNames.length;
414 | for (var i=0;i 1000) {
427 | throw('runaway 1');
428 | }
429 | elements.push(this._oauthEscape(paramName) + '=' +
430 | this._oauthEscape(sorted[j]));
431 | }
432 | continue;
433 | }
434 | elements.push(this._oauthEscape(paramName) + '=' +
435 | this._oauthEscape(this._parameters[paramName]));
436 | }
437 | return elements.join('&');
438 | };
439 |
440 | this._generateSignature = function() {
441 |
442 | var secretKey = this._oauthEscape(this._secrets.shared_secret)+'&'+
443 | this._oauthEscape(this._secrets.oauth_secret);
444 | if (this._parameters['oauth_signature_method'] == 'PLAINTEXT')
445 | {
446 | return secretKey;
447 | }
448 | if (this._parameters['oauth_signature_method'] == 'HMAC-SHA1')
449 | {
450 | var sigString = this._oauthEscape(this._action)+'&'+this._oauthEscape(this._path)+'&'+this._oauthEscape(this._normalizedParameters());
451 | return this.b64_hmac_sha1(secretKey,sigString);
452 | }
453 | return null;
454 | };
455 |
456 | return this;
457 | };
458 | }
459 |
--------------------------------------------------------------------------------
/js/lib/jquery-1.5.2.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * jQuery JavaScript Library v1.5.2
3 | * http://jquery.com/
4 | *
5 | * Copyright 2011, John Resig
6 | * Dual licensed under the MIT or GPL Version 2 licenses.
7 | * http://jquery.org/license
8 | *
9 | * Includes Sizzle.js
10 | * http://sizzlejs.com/
11 | * Copyright 2011, The Dojo Foundation
12 | * Released under the MIT, BSD, and GPL Licenses.
13 | *
14 | * Date: Thu Mar 31 15:28:23 2011 -0400
15 | */
16 | (function(a,b){function ci(a){return d.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cf(a){if(!b_[a]){var b=d("<"+a+">").appendTo("body"),c=b.css("display");b.remove();if(c==="none"||c==="")c="block";b_[a]=c}return b_[a]}function ce(a,b){var c={};d.each(cd.concat.apply([],cd.slice(0,b)),function(){c[this]=a});return c}function b$(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function bZ(){try{return new a.XMLHttpRequest}catch(b){}}function bY(){d(a).unload(function(){for(var a in bW)bW[a](0,1)})}function bS(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var e=a.dataTypes,f={},g,h,i=e.length,j,k=e[0],l,m,n,o,p;for(g=1;g=0===c})}function P(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function H(a,b){return(a&&a!=="*"?a+".":"")+b.replace(t,"`").replace(u,"&")}function G(a){var b,c,e,f,g,h,i,j,k,l,m,n,o,p=[],q=[],s=d._data(this,"events");if(a.liveFired!==this&&s&&s.live&&!a.target.disabled&&(!a.button||a.type!=="click")){a.namespace&&(n=new RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)")),a.liveFired=this;var t=s.live.slice(0);for(i=0;ic)break;a.currentTarget=f.elem,a.data=f.handleObj.data,a.handleObj=f.handleObj,o=f.handleObj.origHandler.apply(f.elem,arguments);if(o===!1||a.isPropagationStopped()){c=f.level,o===!1&&(b=!1);if(a.isImmediatePropagationStopped())break}}return b}}function E(a,c,e){var f=d.extend({},e[0]);f.type=a,f.originalEvent={},f.liveFired=b,d.event.handle.call(c,f),f.isDefaultPrevented()&&e[0].preventDefault()}function y(){return!0}function x(){return!1}function i(a){for(var b in a)if(b!=="toJSON")return!1;return!0}function h(a,c,e){if(e===b&&a.nodeType===1){e=a.getAttribute("data-"+c);if(typeof e==="string"){try{e=e==="true"?!0:e==="false"?!1:e==="null"?null:d.isNaN(e)?g.test(e)?d.parseJSON(e):e:parseFloat(e)}catch(f){}d.data(a,c,e)}else e=b}return e}var c=a.document,d=function(){function G(){if(!d.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(G,1);return}d.ready()}}var d=function(a,b){return new d.fn.init(a,b,g)},e=a.jQuery,f=a.$,g,h=/^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]+)$)/,i=/\S/,j=/^\s+/,k=/\s+$/,l=/\d/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=navigator.userAgent,w,x,y,z=Object.prototype.toString,A=Object.prototype.hasOwnProperty,B=Array.prototype.push,C=Array.prototype.slice,D=String.prototype.trim,E=Array.prototype.indexOf,F={};d.fn=d.prototype={constructor:d,init:function(a,e,f){var g,i,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!e&&c.body){this.context=c,this[0]=c.body,this.selector="body",this.length=1;return this}if(typeof a==="string"){g=h.exec(a);if(!g||!g[1]&&e)return!e||e.jquery?(e||f).find(a):this.constructor(e).find(a);if(g[1]){e=e instanceof d?e[0]:e,k=e?e.ownerDocument||e:c,j=m.exec(a),j?d.isPlainObject(e)?(a=[c.createElement(j[1])],d.fn.attr.call(a,e,!0)):a=[k.createElement(j[1])]:(j=d.buildFragment([g[1]],[k]),a=(j.cacheable?d.clone(j.fragment):j.fragment).childNodes);return d.merge(this,a)}i=c.getElementById(g[2]);if(i&&i.parentNode){if(i.id!==g[2])return f.find(a);this.length=1,this[0]=i}this.context=c,this.selector=a;return this}if(d.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return d.makeArray(a,this)},selector:"",jquery:"1.5.2",length:0,size:function(){return this.length},toArray:function(){return C.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var e=this.constructor();d.isArray(a)?B.apply(e,a):d.merge(e,a),e.prevObject=this,e.context=this.context,b==="find"?e.selector=this.selector+(this.selector?" ":"")+c:b&&(e.selector=this.selector+"."+b+"("+c+")");return e},each:function(a,b){return d.each(this,a,b)},ready:function(a){d.bindReady(),x.done(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(C.apply(this,arguments),"slice",C.call(arguments).join(","))},map:function(a){return this.pushStack(d.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:B,sort:[].sort,splice:[].splice},d.fn.init.prototype=d.fn,d.extend=d.fn.extend=function(){var a,c,e,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i==="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!=="object"&&!d.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;x.resolveWith(c,[d]),d.fn.trigger&&d(c).trigger("ready").unbind("ready")}},bindReady:function(){if(!x){x=d._Deferred();if(c.readyState==="complete")return setTimeout(d.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",y,!1),a.addEventListener("load",d.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",y),a.attachEvent("onload",d.ready);var b=!1;try{b=a.frameElement==null}catch(e){}c.documentElement.doScroll&&b&&G()}}},isFunction:function(a){return d.type(a)==="function"},isArray:Array.isArray||function(a){return d.type(a)==="array"},isWindow:function(a){return a&&typeof a==="object"&&"setInterval"in a},isNaN:function(a){return a==null||!l.test(a)||isNaN(a)},type:function(a){return a==null?String(a):F[z.call(a)]||"object"},isPlainObject:function(a){if(!a||d.type(a)!=="object"||a.nodeType||d.isWindow(a))return!1;if(a.constructor&&!A.call(a,"constructor")&&!A.call(a.constructor.prototype,"isPrototypeOf"))return!1;var c;for(c in a){}return c===b||A.call(a,c)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw a},parseJSON:function(b){if(typeof b!=="string"||!b)return null;b=d.trim(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return a.JSON&&a.JSON.parse?a.JSON.parse(b):(new Function("return "+b))();d.error("Invalid JSON: "+b)},parseXML:function(b,c,e){a.DOMParser?(e=new DOMParser,c=e.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b)),e=c.documentElement,(!e||!e.nodeName||e.nodeName==="parsererror")&&d.error("Invalid XML: "+b);return c},noop:function(){},globalEval:function(a){if(a&&i.test(a)){var b=c.head||c.getElementsByTagName("head")[0]||c.documentElement,e=c.createElement("script");d.support.scriptEval()?e.appendChild(c.createTextNode(a)):e.text=a,b.insertBefore(e,b.firstChild),b.removeChild(e)}},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,e){var f,g=0,h=a.length,i=h===b||d.isFunction(a);if(e){if(i){for(f in a)if(c.apply(a[f],e)===!1)break}else for(;g1?f.call(arguments,0):c,--g||h.resolveWith(h,f.call(b,0))}}var b=arguments,c=0,e=b.length,g=e,h=e<=1&&a&&d.isFunction(a.promise)?a:d.Deferred();if(e>1){for(;ca ";var e=b.getElementsByTagName("*"),f=b.getElementsByTagName("a")[0],g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=b.getElementsByTagName("input")[0];if(e&&e.length&&f){d.support={leadingWhitespace:b.firstChild.nodeType===3,tbody:!b.getElementsByTagName("tbody").length,htmlSerialize:!!b.getElementsByTagName("link").length,style:/red/.test(f.getAttribute("style")),hrefNormalized:f.getAttribute("href")==="/a",opacity:/^0.55$/.test(f.style.opacity),cssFloat:!!f.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,deleteExpando:!0,optDisabled:!1,checkClone:!1,noCloneEvent:!0,noCloneChecked:!0,boxModel:null,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableHiddenOffsets:!0,reliableMarginRight:!0},i.checked=!0,d.support.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,d.support.optDisabled=!h.disabled;var j=null;d.support.scriptEval=function(){if(j===null){var b=c.documentElement,e=c.createElement("script"),f="script"+d.now();try{e.appendChild(c.createTextNode("window."+f+"=1;"))}catch(g){}b.insertBefore(e,b.firstChild),a[f]?(j=!0,delete a[f]):j=!1,b.removeChild(e)}return j};try{delete b.test}catch(k){d.support.deleteExpando=!1}!b.addEventListener&&b.attachEvent&&b.fireEvent&&(b.attachEvent("onclick",function l(){d.support.noCloneEvent=!1,b.detachEvent("onclick",l)}),b.cloneNode(!0).fireEvent("onclick")),b=c.createElement("div"),b.innerHTML=" ";var m=c.createDocumentFragment();m.appendChild(b.firstChild),d.support.checkClone=m.cloneNode(!0).cloneNode(!0).lastChild.checked,d(function(){var a=c.createElement("div"),b=c.getElementsByTagName("body")[0];if(b){a.style.width=a.style.paddingLeft="1px",b.appendChild(a),d.boxModel=d.support.boxModel=a.offsetWidth===2,"zoom"in a.style&&(a.style.display="inline",a.style.zoom=1,d.support.inlineBlockNeedsLayout=a.offsetWidth===2,a.style.display="",a.innerHTML="
",d.support.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="";var e=a.getElementsByTagName("td");d.support.reliableHiddenOffsets=e[0].offsetHeight===0,e[0].style.display="",e[1].style.display="none",d.support.reliableHiddenOffsets=d.support.reliableHiddenOffsets&&e[0].offsetHeight===0,a.innerHTML="",c.defaultView&&c.defaultView.getComputedStyle&&(a.style.width="1px",a.style.marginRight="0",d.support.reliableMarginRight=(parseInt(c.defaultView.getComputedStyle(a,null).marginRight,10)||0)===0),b.removeChild(a).style.display="none",a=e=null}});var n=function(a){var b=c.createElement("div");a="on"+a;if(!b.attachEvent)return!0;var d=a in b;d||(b.setAttribute(a,"return;"),d=typeof b[a]==="function");return d};d.support.submitBubbles=n("submit"),d.support.changeBubbles=n("change"),b=e=f=null}}();var g=/^(?:\{.*\}|\[.*\])$/;d.extend({cache:{},uuid:0,expando:"jQuery"+(d.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?d.cache[a[d.expando]]:a[d.expando];return!!a&&!i(a)},data:function(a,c,e,f){if(d.acceptData(a)){var g=d.expando,h=typeof c==="string",i,j=a.nodeType,k=j?d.cache:a,l=j?a[d.expando]:a[d.expando]&&d.expando;if((!l||f&&l&&!k[l][g])&&h&&e===b)return;l||(j?a[d.expando]=l=++d.uuid:l=d.expando),k[l]||(k[l]={},j||(k[l].toJSON=d.noop));if(typeof c==="object"||typeof c==="function")f?k[l][g]=d.extend(k[l][g],c):k[l]=d.extend(k[l],c);i=k[l],f&&(i[g]||(i[g]={}),i=i[g]),e!==b&&(i[c]=e);if(c==="events"&&!i[c])return i[g]&&i[g].events;return h?i[c]:i}},removeData:function(b,c,e){if(d.acceptData(b)){var f=d.expando,g=b.nodeType,h=g?d.cache:b,j=g?b[d.expando]:d.expando;if(!h[j])return;if(c){var k=e?h[j][f]:h[j];if(k){delete k[c];if(!i(k))return}}if(e){delete h[j][f];if(!i(h[j]))return}var l=h[j][f];d.support.deleteExpando||h!=a?delete h[j]:h[j]=null,l?(h[j]={},g||(h[j].toJSON=d.noop),h[j][f]=l):g&&(d.support.deleteExpando?delete b[d.expando]:b.removeAttribute?b.removeAttribute(d.expando):b[d.expando]=null)}},_data:function(a,b,c){return d.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=d.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),d.fn.extend({data:function(a,c){var e=null;if(typeof a==="undefined"){if(this.length){e=d.data(this[0]);if(this[0].nodeType===1){var f=this[0].attributes,g;for(var i=0,j=f.length;i-1)return!0;return!1},val:function(a){if(!arguments.length){var c=this[0];if(c){if(d.nodeName(c,"option")){var e=c.attributes.value;return!e||e.specified?c.value:c.text}if(d.nodeName(c,"select")){var f=c.selectedIndex,g=[],h=c.options,i=c.type==="select-one";if(f<0)return null;for(var j=i?f:0,k=i?f+1:h.length;j=0;else if(d.nodeName(this,"select")){var f=d.makeArray(e);d("option",this).each(function(){this.selected=d.inArray(d(this).val(),f)>=0}),f.length||(this.selectedIndex=-1)}else this.value=e}})}}),d.extend({attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,e,f){if(!a||a.nodeType===3||a.nodeType===8||a.nodeType===2)return b;if(f&&c in d.attrFn)return d(a)[c](e);var g=a.nodeType!==1||!d.isXMLDoc(a),h=e!==b;c=g&&d.props[c]||c;if(a.nodeType===1){var i=m.test(c);if(c==="selected"&&!d.support.optSelected){var j=a.parentNode;j&&(j.selectedIndex,j.parentNode&&j.parentNode.selectedIndex)}if((c in a||a[c]!==b)&&g&&!i){h&&(c==="type"&&n.test(a.nodeName)&&a.parentNode&&d.error("type property can't be changed"),e===null?a.nodeType===1&&a.removeAttribute(c):a[c]=e);if(d.nodeName(a,"form")&&a.getAttributeNode(c))return a.getAttributeNode(c).nodeValue;if(c==="tabIndex"){var k=a.getAttributeNode("tabIndex");return k&&k.specified?k.value:o.test(a.nodeName)||p.test(a.nodeName)&&a.href?0:b}return a[c]}if(!d.support.style&&g&&c==="style"){h&&(a.style.cssText=""+e);return a.style.cssText}h&&a.setAttribute(c,""+e);if(!a.attributes[c]&&(a.hasAttribute&&!a.hasAttribute(c)))return b;var l=!d.support.hrefNormalized&&g&&i?a.getAttribute(c,2):a.getAttribute(c);return l===null?b:l}h&&(a[c]=e);return a[c]}});var r=/\.(.*)$/,s=/^(?:textarea|input|select)$/i,t=/\./g,u=/ /g,v=/[^\w\s.|`]/g,w=function(a){return a.replace(v,"\\$&")};d.event={add:function(c,e,f,g){if(c.nodeType!==3&&c.nodeType!==8){try{d.isWindow(c)&&(c!==a&&!c.frameElement)&&(c=a)}catch(h){}if(f===!1)f=x;else if(!f)return;var i,j;f.handler&&(i=f,f=i.handler),f.guid||(f.guid=d.guid++);var k=d._data(c);if(!k)return;var l=k.events,m=k.handle;l||(k.events=l={}),m||(k.handle=m=function(a){return typeof d!=="undefined"&&d.event.triggered!==a.type?d.event.handle.apply(m.elem,arguments):b}),m.elem=c,e=e.split(" ");var n,o=0,p;while(n=e[o++]){j=i?d.extend({},i):{handler:f,data:g},n.indexOf(".")>-1?(p=n.split("."),n=p.shift(),j.namespace=p.slice(0).sort().join(".")):(p=[],j.namespace=""),j.type=n,j.guid||(j.guid=f.guid);var q=l[n],r=d.event.special[n]||{};if(!q){q=l[n]=[];if(!r.setup||r.setup.call(c,g,p,m)===!1)c.addEventListener?c.addEventListener(n,m,!1):c.attachEvent&&c.attachEvent("on"+n,m)}r.add&&(r.add.call(c,j),j.handler.guid||(j.handler.guid=f.guid)),q.push(j),d.event.global[n]=!0}c=null}},global:{},remove:function(a,c,e,f){if(a.nodeType!==3&&a.nodeType!==8){e===!1&&(e=x);var g,h,i,j,k=0,l,m,n,o,p,q,r,s=d.hasData(a)&&d._data(a),t=s&&s.events;if(!s||!t)return;c&&c.type&&(e=c.handler,c=c.type);if(!c||typeof c==="string"&&c.charAt(0)==="."){c=c||"";for(h in t)d.event.remove(a,h+c);return}c=c.split(" ");while(h=c[k++]){r=h,q=null,l=h.indexOf(".")<0,m=[],l||(m=h.split("."),h=m.shift(),n=new RegExp("(^|\\.)"+d.map(m.slice(0).sort(),w).join("\\.(?:.*\\.)?")+"(\\.|$)")),p=t[h];if(!p)continue;if(!e){for(j=0;j=0&&(a.type=f=f.slice(0,-1),a.exclusive=!0),e||(a.stopPropagation(),d.event.global[f]&&d.each(d.cache,function(){var b=d.expando,e=this[b];e&&e.events&&e.events[f]&&d.event.trigger(a,c,e.handle.elem)}));if(!e||e.nodeType===3||e.nodeType===8)return b;a.result=b,a.target=e,c=d.makeArray(c),c.unshift(a)}a.currentTarget=e;var h=d._data(e,"handle");h&&h.apply(e,c);var i=e.parentNode||e.ownerDocument;try{e&&e.nodeName&&d.noData[e.nodeName.toLowerCase()]||e["on"+f]&&e["on"+f].apply(e,c)===!1&&(a.result=!1,a.preventDefault())}catch(j){}if(!a.isPropagationStopped()&&i)d.event.trigger(a,c,i,!0);else if(!a.isDefaultPrevented()){var k,l=a.target,m=f.replace(r,""),n=d.nodeName(l,"a")&&m==="click",o=d.event.special[m]||{};if((!o._default||o._default.call(e,a)===!1)&&!n&&!(l&&l.nodeName&&d.noData[l.nodeName.toLowerCase()])){try{l[m]&&(k=l["on"+m],k&&(l["on"+m]=null),d.event.triggered=a.type,l[m]())}catch(p){}k&&(l["on"+m]=k),d.event.triggered=b}}},handle:function(c){var e,f,g,h,i,j=[],k=d.makeArray(arguments);c=k[0]=d.event.fix(c||a.event),c.currentTarget=this,e=c.type.indexOf(".")<0&&!c.exclusive,e||(g=c.type.split("."),c.type=g.shift(),j=g.slice(0).sort(),h=new RegExp("(^|\\.)"+j.join("\\.(?:.*\\.)?")+"(\\.|$)")),c.namespace=c.namespace||j.join("."),i=d._data(this,"events"),f=(i||{})[c.type];if(i&&f){f=f.slice(0);for(var l=0,m=f.length;l-1?d.map(a.options,function(a){return a.selected}).join("-"):"":a.nodeName.toLowerCase()==="select"&&(c=a.selectedIndex);return c},D=function D(a){var c=a.target,e,f;if(s.test(c.nodeName)&&!c.readOnly){e=d._data(c,"_change_data"),f=C(c),(a.type!=="focusout"||c.type!=="radio")&&d._data(c,"_change_data",f);if(e===b||f===e)return;if(e!=null||f)a.type="change",a.liveFired=b,d.event.trigger(a,arguments[1],c)}};d.event.special.change={filters:{focusout:D,beforedeactivate:D,click:function(a){var b=a.target,c=b.type;(c==="radio"||c==="checkbox"||b.nodeName.toLowerCase()==="select")&&D.call(this,a)},keydown:function(a){var b=a.target,c=b.type;(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(c==="checkbox"||c==="radio")||c==="select-multiple")&&D.call(this,a)},beforeactivate:function(a){var b=a.target;d._data(b,"_change_data",C(b))}},setup:function(a,b){if(this.type==="file")return!1;for(var c in B)d.event.add(this,c+".specialChange",B[c]);return s.test(this.nodeName)},teardown:function(a){d.event.remove(this,".specialChange");return s.test(this.nodeName)}},B=d.event.special.change.filters,B.focus=B.beforeactivate}c.addEventListener&&d.each({focus:"focusin",blur:"focusout"},function(a,b){function f(a){var c=d.event.fix(a);c.type=b,c.originalEvent={},d.event.trigger(c,null,c.target),c.isDefaultPrevented()&&a.preventDefault()}var e=0;d.event.special[b]={setup:function(){e++===0&&c.addEventListener(a,f,!0)},teardown:function(){--e===0&&c.removeEventListener(a,f,!0)}}}),d.each(["bind","one"],function(a,c){d.fn[c]=function(a,e,f){if(typeof a==="object"){for(var g in a)this[c](g,e,a[g],f);return this}if(d.isFunction(e)||e===!1)f=e,e=b;var h=c==="one"?d.proxy(f,function(a){d(this).unbind(a,h);return f.apply(this,arguments)}):f;if(a==="unload"&&c!=="one")this.one(a,e,f);else for(var i=0,j=this.length;i0?this.bind(b,a,c):this.trigger(b)},d.attrFn&&(d.attrFn[b]=!0)}),function(){function u(a,b,c,d,e,f){for(var g=0,h=d.length;g0){j=i;break}}i=i[a]}d[g]=j}}}function t(a,b,c,d,e,f){for(var g=0,h=d.length;g+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,e=0,f=Object.prototype.toString,g=!1,h=!0,i=/\\/g,j=/\W/;[0,0].sort(function(){h=!1;return 0});var k=function(b,d,e,g){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!=="string")return e;var i,j,n,o,q,r,s,t,u=!0,w=k.isXML(d),x=[],y=b;do{a.exec(""),i=a.exec(y);if(i){y=i[3],x.push(i[1]);if(i[2]){o=i[3];break}}}while(i);if(x.length>1&&m.exec(b))if(x.length===2&&l.relative[x[0]])j=v(x[0]+x[1],d);else{j=l.relative[x[0]]?[d]:k(x.shift(),d);while(x.length)b=x.shift(),l.relative[b]&&(b+=x.shift()),j=v(b,j)}else{!g&&x.length>1&&d.nodeType===9&&!w&&l.match.ID.test(x[0])&&!l.match.ID.test(x[x.length-1])&&(q=k.find(x.shift(),d,w),d=q.expr?k.filter(q.expr,q.set)[0]:q.set[0]);if(d){q=g?{expr:x.pop(),set:p(g)}:k.find(x.pop(),x.length===1&&(x[0]==="~"||x[0]==="+")&&d.parentNode?d.parentNode:d,w),j=q.expr?k.filter(q.expr,q.set):q.set,x.length>0?n=p(j):u=!1;while(x.length)r=x.pop(),s=r,l.relative[r]?s=x.pop():r="",s==null&&(s=d),l.relative[r](n,s,w)}else n=x=[]}n||(n=j),n||k.error(r||b);if(f.call(n)==="[object Array]")if(u)if(d&&d.nodeType===1)for(t=0;n[t]!=null;t++)n[t]&&(n[t]===!0||n[t].nodeType===1&&k.contains(d,n[t]))&&e.push(j[t]);else for(t=0;n[t]!=null;t++)n[t]&&n[t].nodeType===1&&e.push(j[t]);else e.push.apply(e,n);else p(n,e);o&&(k(o,h,e,g),k.uniqueSort(e));return e};k.uniqueSort=function(a){if(r){g=h,a.sort(r);if(g)for(var b=1;b0},k.find=function(a,b,c){var d;if(!a)return[];for(var e=0,f=l.order.length;e":function(a,b){var c,d=typeof b==="string",e=0,f=a.length;if(d&&!j.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(i,"")},TAG:function(a,b){return a[1].replace(i,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||k.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&k.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(i,"");!f&&l.attrMap[g]&&(a[1]=l.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(i,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=k(b[3],null,null,c);else{var g=k.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(l.match.POS.test(b[0])||l.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!k(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return"text"===c&&(b===c||b===null)},radio:function(a){return"radio"===a.type},checkbox:function(a){return"checkbox"===a.type},file:function(a){return"file"===a.type},password:function(a){return"password"===a.type},submit:function(a){return"submit"===a.type},image:function(a){return"image"===a.type},reset:function(a){return"reset"===a.type},button:function(a){return"button"===a.type||a.nodeName.toLowerCase()==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=l.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||k.getText([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=l.attrHandle[c]?l.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=l.setFilters[e];if(f)return f(a,c,b,d)}}},m=l.match.POS,n=function(a,b){return"\\"+(b-0+1)};for(var o in l.match)l.match[o]=new RegExp(l.match[o].source+/(?![^\[]*\])(?![^\(]*\))/.source),l.leftMatch[o]=new RegExp(/(^(?:.|\r|\n)*?)/.source+l.match[o].source.replace(/\\(\d+)/g,n));var p=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(q){p=function(a,b){var c=0,d=b||[];if(f.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length==="number")for(var e=a.length;c ",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(l.find.ID=function(a,c,d){if(typeof c.getElementById!=="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!=="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},l.filter.ID=function(a,b){var c=typeof a.getAttributeNode!=="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(l.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML=" ",a.firstChild&&typeof a.firstChild.getAttribute!=="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(l.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=k,b=c.createElement("div"),d="__sizzle__";b.innerHTML="
";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){k=function(b,e,f,g){e=e||c;if(!g&&!k.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return p(e.getElementsByTagName(b),f);if(h[2]&&l.find.CLASS&&e.getElementsByClassName)return p(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return p([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return p([],f);if(i.id===h[3])return p([i],f)}try{return p(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var m=e,n=e.getAttribute("id"),o=n||d,q=e.parentNode,r=/^\s*[+~]/.test(b);n?o=o.replace(/'/g,"\\$&"):e.setAttribute("id",o),r&&q&&(e=e.parentNode);try{if(!r||q)return p(e.querySelectorAll("[id='"+o+"'] "+b),f)}catch(s){}finally{n||m.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)k[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}k.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!k.isXML(a))try{if(e||!l.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return k(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
";if(a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;l.order.splice(1,0,"CLASS"),l.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!=="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?k.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?k.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:k.contains=function(){return!1},k.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var v=function(a,b){var c,d=[],e="",f=b.nodeType?[b]:b;while(c=l.match.PSEUDO.exec(a))e+=c[0],a=a.replace(l.match.PSEUDO,"");a=l.relative[a]?a+"*":a;for(var g=0,h=f.length;g0)for(var g=c;g0},closest:function(a,b){var c=[],e,f,g=this[0];if(d.isArray(a)){var h,i,j={},k=1;if(g&&a.length){for(e=0,f=a.length;e-1:d(g).is(h))&&c.push({selector:i,elem:g,level:k});g=g.parentNode,k++}}return c}var l=N.test(a)?d(a,b||this.context):null;for(e=0,f=this.length;e-1:d.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b)break}}c=c.length>1?d.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a||typeof a==="string")return d.inArray(this[0],a?d(a):this.parent().children());return d.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a==="string"?d(a,b):d.makeArray(a),e=d.merge(this.get(),c);return this.pushStack(P(c[0])||P(e[0])?e:d.unique(e))},andSelf:function(){return this.add(this.prevObject)}}),d.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return d.dir(a,"parentNode")},parentsUntil:function(a,b,c){return d.dir(a,"parentNode",c)},next:function(a){return d.nth(a,2,"nextSibling")},prev:function(a){return d.nth(a,2,"previousSibling")},nextAll:function(a){return d.dir(a,"nextSibling")},prevAll:function(a){return d.dir(a,"previousSibling")},nextUntil:function(a,b,c){return d.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return d.dir(a,"previousSibling",c)},siblings:function(a){return d.sibling(a.parentNode.firstChild,a)},children:function(a){return d.sibling(a.firstChild)},contents:function(a){return d.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:d.makeArray(a.childNodes)}},function(a,b){d.fn[a]=function(c,e){var f=d.map(this,b,c),g=M.call(arguments);I.test(a)||(e=c),e&&typeof e==="string"&&(f=d.filter(e,f)),f=this.length>1&&!O[a]?d.unique(f):f,(this.length>1||K.test(e))&&J.test(a)&&(f=f.reverse());return this.pushStack(f,a,g.join(","))}}),d.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?d.find.matchesSelector(b[0],a)?[b[0]]:[]:d.find.matches(a,b)},dir:function(a,c,e){var f=[],g=a[c];while(g&&g.nodeType!==9&&(e===b||g.nodeType!==1||!d(g).is(e)))g.nodeType===1&&f.push(g),g=g[c];return f},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var R=/ jQuery\d+="(?:\d+|null)"/g,S=/^\s+/,T=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,U=/<([\w:]+)/,V=/",""],legend:[1,""," "],thead:[1,""],tr:[2,""],td:[3,""],col:[2,""],area:[1,""," "],_default:[0,"",""]};Z.optgroup=Z.option,Z.tbody=Z.tfoot=Z.colgroup=Z.caption=Z.thead,Z.th=Z.td,d.support.htmlSerialize||(Z._default=[1,"div","
"]),d.fn.extend({text:function(a){if(d.isFunction(a))return this.each(function(b){var c=d(this);c.text(a.call(this,b,c.text()))});if(typeof a!=="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return d.text(this)},wrapAll:function(a){if(d.isFunction(a))return this.each(function(b){d(this).wrapAll(a.call(this,b))});if(this[0]){var b=d(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(d.isFunction(a))return this.each(function(b){d(this).wrapInner(a.call(this,b))});return this.each(function(){var b=d(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){d(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){d.nodeName(this,"body")||d(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=d(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,d(arguments[0]).toArray());return a}},remove:function(a,b){for(var c=0,e;(e=this[c])!=null;c++)if(!a||d.filter(a,[e]).length)!b&&e.nodeType===1&&(d.cleanData(e.getElementsByTagName("*")),d.cleanData([e])),e.parentNode&&e.parentNode.removeChild(e);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&d.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return d.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(R,""):null;if(typeof a!=="string"||X.test(a)||!d.support.leadingWhitespace&&S.test(a)||Z[(U.exec(a)||["",""])[1].toLowerCase()])d.isFunction(a)?this.each(function(b){var c=d(this);c.html(a.call(this,b,c.html()))}):this.empty().append(a);else{a=a.replace(T,"<$1>$2>");try{for(var c=0,e=this.length;c1&&l0?this.clone(!0):this).get();d(f[h])[b](j),e=e.concat(j)}return this.pushStack(e,a,f.selector)}}),d.extend({clone:function(a,b,c){var e=a.cloneNode(!0),f,g,h;if((!d.support.noCloneEvent||!d.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!d.isXMLDoc(a)){ba(a,e),f=bb(a),g=bb(e);for(h=0;f[h];++h)ba(f[h],g[h])}if(b){_(a,e);if(c){f=bb(a),g=bb(e);for(h=0;f[h];++h)_(f[h],g[h])}}return e},clean:function(a,b,e,f){b=b||c,typeof b.createElement==="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var g=[];for(var h=0,i;(i=a[h])!=null;h++){typeof i==="number"&&(i+="");if(!i)continue;if(typeof i!=="string"||W.test(i)){if(typeof i==="string"){i=i.replace(T,"<$1>$2>");var j=(U.exec(i)||["",""])[1].toLowerCase(),k=Z[j]||Z._default,l=k[0],m=b.createElement("div");m.innerHTML=k[1]+i+k[2];while(l--)m=m.lastChild;if(!d.support.tbody){var n=V.test(i),o=j==="table"&&!n?m.firstChild&&m.firstChild.childNodes:k[1]===""&&!n?m.childNodes:[];for(var p=o.length-1;p>=0;--p)d.nodeName(o[p],"tbody")&&!o[p].childNodes.length&&o[p].parentNode.removeChild(o[p])}!d.support.leadingWhitespace&&S.test(i)&&m.insertBefore(b.createTextNode(S.exec(i)[0]),m.firstChild),i=m.childNodes}}else i=b.createTextNode(i);i.nodeType?g.push(i):g=d.merge(g,i)}if(e)for(h=0;g[h];h++)!f||!d.nodeName(g[h],"script")||g[h].type&&g[h].type.toLowerCase()!=="text/javascript"?(g[h].nodeType===1&&g.splice.apply(g,[h+1,0].concat(d.makeArray(g[h].getElementsByTagName("script")))),e.appendChild(g[h])):f.push(g[h].parentNode?g[h].parentNode.removeChild(g[h]):g[h]);return g},cleanData:function(a){var b,c,e=d.cache,f=d.expando,g=d.event.special,h=d.support.deleteExpando;for(var i=0,j;(j=a[i])!=null;i++){if(j.nodeName&&d.noData[j.nodeName.toLowerCase()])continue;c=j[d.expando];if(c){b=e[c]&&e[c][f];if(b&&b.events){for(var k in b.events)g[k]?d.event.remove(j,k):d.removeEvent(j,k,b.handle);b.handle&&(b.handle.elem=null)}h?delete j[d.expando]:j.removeAttribute&&j.removeAttribute(d.expando),delete e[c]}}}});var bd=/alpha\([^)]*\)/i,be=/opacity=([^)]*)/,bf=/-([a-z])/ig,bg=/([A-Z]|^ms)/g,bh=/^-?\d+(?:px)?$/i,bi=/^-?\d/,bj={position:"absolute",visibility:"hidden",display:"block"},bk=["Left","Right"],bl=["Top","Bottom"],bm,bn,bo,bp=function(a,b){return b.toUpperCase()};d.fn.css=function(a,c){if(arguments.length===2&&c===b)return this;return d.access(this,a,c,!0,function(a,c,e){return e!==b?d.style(a,c,e):d.css(a,c)})},d.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bm(a,"opacity","opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{zIndex:!0,fontWeight:!0,opacity:!0,zoom:!0,lineHeight:!0},cssProps:{"float":d.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,e,f){if(a&&a.nodeType!==3&&a.nodeType!==8&&a.style){var g,h=d.camelCase(c),i=a.style,j=d.cssHooks[h];c=d.cssProps[h]||h;if(e===b){if(j&&"get"in j&&(g=j.get(a,!1,f))!==b)return g;return i[c]}if(typeof e==="number"&&isNaN(e)||e==null)return;typeof e==="number"&&!d.cssNumber[h]&&(e+="px");if(!j||!("set"in j)||(e=j.set(a,e))!==b)try{i[c]=e}catch(k){}}},css:function(a,c,e){var f,g=d.camelCase(c),h=d.cssHooks[g];c=d.cssProps[g]||g;if(h&&"get"in h&&(f=h.get(a,!0,e))!==b)return f;if(bm)return bm(a,c,g)},swap:function(a,b,c){var d={};for(var e in b)d[e]=a.style[e],a.style[e]=b[e];c.call(a);for(e in b)a.style[e]=d[e]},camelCase:function(a){return a.replace(bf,bp)}}),d.curCSS=d.css,d.each(["height","width"],function(a,b){d.cssHooks[b]={get:function(a,c,e){var f;if(c){a.offsetWidth!==0?f=bq(a,b,e):d.swap(a,bj,function(){f=bq(a,b,e)});if(f<=0){f=bm(a,b,b),f==="0px"&&bo&&(f=bo(a,b,b));if(f!=null)return f===""||f==="auto"?"0px":f}if(f<0||f==null){f=a.style[b];return f===""||f==="auto"?"0px":f}return typeof f==="string"?f:f+"px"}},set:function(a,b){if(!bh.test(b))return b;b=parseFloat(b);if(b>=0)return b+"px"}}}),d.support.opacity||(d.cssHooks.opacity={get:function(a,b){return be.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style;c.zoom=1;var e=d.isNaN(b)?"":"alpha(opacity="+b*100+")",f=c.filter||"";c.filter=bd.test(f)?f.replace(bd,e):c.filter+" "+e}}),d(function(){d.support.reliableMarginRight||(d.cssHooks.marginRight={get:function(a,b){var c;d.swap(a,{display:"inline-block"},function(){b?c=bm(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bn=function(a,c,e){var f,g,h;e=e.replace(bg,"-$1").toLowerCase();if(!(g=a.ownerDocument.defaultView))return b;if(h=g.getComputedStyle(a,null))f=h.getPropertyValue(e),f===""&&!d.contains(a.ownerDocument.documentElement,a)&&(f=d.style(a,e));return f}),c.documentElement.currentStyle&&(bo=function(a,b){var c,d=a.currentStyle&&a.currentStyle[b],e=a.runtimeStyle&&a.runtimeStyle[b],f=a.style;!bh.test(d)&&bi.test(d)&&(c=f.left,e&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":d||0,d=f.pixelLeft+"px",f.left=c,e&&(a.runtimeStyle.left=e));return d===""?"auto":d}),bm=bn||bo,d.expr&&d.expr.filters&&(d.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!d.support.reliableHiddenOffsets&&(a.style.display||d.css(a,"display"))==="none"},d.expr.filters.visible=function(a){return!d.expr.filters.hidden(a)});var br=/%20/g,bs=/\[\]$/,bt=/\r?\n/g,bu=/#.*$/,bv=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bw=/^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bx=/^(?:about|app|app\-storage|.+\-extension|file|widget):$/,by=/^(?:GET|HEAD)$/,bz=/^\/\//,bA=/\?/,bB=/
7 |
8 |
9 |
50 |
51 |