9 | OAuth Redirect Page
10 |
16 |
17 |
18 |
23 |
24 |
25 | Redirecting...
26 |
27 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Facebook Friend Exporter",
3 | "description": "Get *your* data contact out of Facebook to Google Contacts or CSV, whether they want you to or not.",
4 | "version": "2.2.0",
5 | "background_page": "background.html",
6 | "options_page": "options.html",
7 | "icons": {
8 | "16": "img/icon16.png",
9 | "48": "img/icon48.png",
10 | "128": "img/icon128.png"
11 | },
12 | "content_scripts": [ {
13 | "js": [ "/js/lib/jquery-1.5.2.min.js", "/js/content_script.js" ],
14 | "matches": [
15 | "http://*.facebook.com/*",
16 | "https://*.facebook.com/*"
17 | ],
18 | "run_at": "document_end"
19 | } ],
20 | "permissions": [
21 | "unlimitedStorage",
22 | "tabs",
23 | "https://m.facebook.com/*",
24 | "http://m.facebook.com/*",
25 | "http://www.google.com/m8/feeds/*",
26 | "https://www.google.com/accounts/OAuthAuthorizeToken",
27 | "https://www.google.com/accounts/OAuthGetAccessToken"
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2010
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/js/options.js:
--------------------------------------------------------------------------------
1 | // Extensions pages can all have access to the bacground page.
2 | var bkg = chrome.extension.getBackgroundPage();
3 |
4 | // When the DOM is loaded, make sure all the saved info is restored.
5 | window.addEventListener('load', onLoad, false);
6 |
7 | /**
8 | * Helper.
9 | */
10 | function $(id) {
11 | return document.getElementById(id);
12 | }
13 |
14 | /**
15 | * When the options window has been loaded.
16 | */
17 | function onLoad() {
18 | onRestore();
19 | $('button-save').addEventListener('click', onSave, false);
20 | $('button-close').addEventListener('click', onClose, false);
21 | }
22 |
23 | /**
24 | * When the options window is closed;
25 | */
26 | function onClose() {
27 | window.close();
28 | }
29 |
30 | /**
31 | * Saves options to localStorage.
32 | */
33 | function onSave() {
34 | // Save settings.
35 | bkg.settings.opt_out = $('opt_out').checked;
36 |
37 | // Update status to let user know options were saved.
38 | var info = $('info-message');
39 | info.style.display = 'inline';
40 | info.style.opacity = 1;
41 | setTimeout(function() {
42 | info.style.opacity = 0.0;
43 | }, 1000);
44 | }
45 |
46 | /**
47 | * Restore all options.
48 | */
49 | function onRestore() {
50 | // Restore settings.
51 | $('version').innerHTML = ' (v' + bkg.settings.version + ')';
52 | $('opt_out').checked = bkg.settings.opt_out;
53 | }
54 |
--------------------------------------------------------------------------------
/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Facebook Friend Exporter
5 |
6 |
7 |
8 |
9 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/css/options.css:
--------------------------------------------------------------------------------
1 | /* Extension Options Default Style */
2 |
3 | body {
4 | min-width: 650px;
5 | font-size: 87%;
6 | font-family: Arial, Helvetica, 'Helvetica Neue', Verdana, sans-serif;
7 | margin: 10px;
8 | }
9 |
10 | #header h1 {
11 | background: url("/img/icon48.png") 0px 30px no-repeat;
12 | display: inline;
13 | margin: 0;
14 | padding-bottom: 43px;
15 | padding-left: 55px;
16 | padding-top: 40px;
17 | font-size: 150%;
18 | font-weight: bold;
19 | }
20 |
21 | h3 {
22 | color: #444;
23 | }
24 |
25 | em {
26 | font-size: 50%;
27 | font-weight: normal;
28 | }
29 |
30 | dt {
31 | width: 15em;
32 | padding: .5em;
33 | float: left;
34 | margin: 0;
35 | }
36 |
37 | dd {
38 | margin-left: 16em;
39 | padding: .4em;
40 | }
41 |
42 | a {
43 | color: blue;
44 | font-size: 100%;
45 | }
46 |
47 | p {
48 | margin: 0;
49 | padding: 0;
50 | }
51 |
52 | .extension-template {
53 | border-bottom: 1px solid #cdcdcd;
54 | margin: 10px 0;
55 | }
56 |
57 | .extension-header {
58 | background: #ebeff9;
59 | border-top: 1px solid #b5c7de;
60 | padding: 3px 5px;
61 | font-weight: bold;
62 | }
63 |
64 | .extension-options {
65 | background: #f4f6fc;
66 | border-bottom: 1px solid #edeff5;
67 | font-size: 85%;
68 | padding: 0.8em 10px;
69 | }
70 |
71 | .note {
72 | background-color: #e0f2c0;
73 | border-radius: 5px;
74 | padding: 3px 5px;
75 | margin-right: 30px;
76 | }
77 |
78 | #extension-footer {
79 | text-align: right;
80 | padding: 10px 0px;
81 | }
82 |
83 | #header {
84 | margin-bottom: 1.05em;
85 | overflow: hidden;
86 | padding-bottom: 1.5em;
87 | padding-left: 0;
88 | padding-top: 1.5em;
89 | position: relative;
90 | }
91 |
92 | #info-message {
93 | -webkit-transition-duration: 1s;
94 | display: none;
95 | background:#FDFFBD none repeat scroll 0 0;
96 | border-radius: 5px;
97 | padding: 7px 10px;
98 | margin-right: 30px;
99 | }
100 |
101 | #credits {
102 | font-size: 85%;
103 | margin-top: 1em;
104 | text-align: right;
105 | }
--------------------------------------------------------------------------------
/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 | };
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Facebook Exporter Contact Exporter
2 | ==================================
3 |
4 | 
5 |
6 | Your friends on Facebook made their information public to you, such as their
7 | email, phonenumber, and websites. They want you to know about them! But there
8 | is no way to extract such data and place it in your most used contact book so
9 | you can keep in touch with them.
10 |
11 | fb-exporter extension will allow you to get their contact information so you
12 | can contact them with your favorite email client. You can extract their
13 | information and transfer it to your client.
14 |
15 | Your not stealing their information, your friends are making it public for you
16 | to use them. They want you to contact them. Thats what friends do eh?
17 |
18 | Functionality
19 | -------------------
20 | - I am reading your Facebook friends all at once in a variable.
21 | That variable allows us to instantly see the users photo in a nice collage,
22 | and export everyone at the same time without any manual process, or you can pick
23 | and choose anyone.
24 |
25 | - Not only exports your friends emails, it exports your contacts phone numbers,
26 | emails, end IM screen names such as Google Talk, Yahoo, ICQ, Skype, QQ, and MSN
27 | (Windows Live Messenger, Hotmail), and your friends birthday!
28 |
29 | - There are error handling routines as well, if Facebook doesn't like what your
30 | doing, it will stop instantaneously! You can start it back again next day or
31 | in a couple of hours. Just open up the log, and you can see errors when it happens.
32 |
33 | - The results are cached, so if you want to continue the process some other time,
34 | then no worries, it will continue from the last processed contact.
35 |
36 | - I have refactored some portions of the code and added comments where applicable
37 | I have added a better UI. For example, a Facbook style dialog instead of the
38 | custom one made before. I have changed the way the process works too.
39 |
40 | How to download
41 | ---------------
42 | - It is currently not available in the Chrome Web Store. I am decided to stop
43 | development to focus on better and more exciting things.
44 |
45 | Facebook
46 | -------------
47 | They are converting emails into images so you cannot copy it somewhere (say what?
48 | 99% of the people copy the email into their email client). I added mechanism that catches
49 | that instantly. So you don't have to worry if they see something. The process will
50 | stop, and you can start it again the next day. When you start it again, it will pick up
51 | where it left off.
52 |
53 |
54 | Have fun! Feel free to submit patches and fork :)
55 |
56 |
57 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/css/controller.css:
--------------------------------------------------------------------------------
1 |
2 | body {
3 | font-family: Verdana;
4 | color: #fff;
5 | background-color: #ccc;
6 | }
7 | ::selection {
8 | background:#FF5E99;
9 | color:#fff;
10 | text-shadow: none;
11 | }
12 | a {
13 | color: #FF5E99;
14 | }
15 | .bubble {
16 | background-color: #444;
17 | margin: 15pt;
18 | border: thick solid;
19 | position: relative;
20 | border-radius: 10px;
21 | padding: 15pt;
22 | }
23 |
24 | p {
25 | margin: 10pt;
26 | }
27 |
28 | .friend-row {
29 | position: relative;
30 | }
31 |
32 | #note {
33 | background-color: #FBEC5D;
34 | color: #615E3F;
35 | border: 1px solid #FFE600;
36 | display: none;
37 | margin: 10px 0px;
38 | padding: 0px 10px;
39 | box-shadow: 0 0 5px 5px #333;
40 | }
41 |
42 | #friendlist {
43 | padding-top: 30px;
44 | }
45 |
46 | #friendlist li {
47 | display: inline-block;
48 | position: relative;
49 | }
50 |
51 | #friendlist li span {
52 | position: absolute;
53 | bottom: 0;
54 | left: 5px;
55 | height: 20px;
56 | width: 100px;
57 | background-color: rgba(0,0,0,0.4);
58 | border-radius: 10px;
59 | font-size: 8px;
60 | line-height: 20px;
61 | text-align: center;
62 | font-weight: bold;
63 | }
64 |
65 | #friendlist li.processed span {
66 | background-color: rgba(255,255,0,0.4);
67 | }
68 |
69 | #friendlist li.success span {
70 | background-color: rgba(0,255,0,0.4);
71 | }
72 |
73 | #friendlist li.failed span {
74 | background-color: rgba(255,0,0,0.4);
75 | }
76 |
77 | #friendlist li.cached span {
78 | background-color: rgba(255,0,255,0.4);
79 | }
80 |
81 | #friendlist li.starting span {
82 | background-color: rgba(255,140,0,0.4);
83 | }
84 |
85 | #friendlist li img {
86 | width: 80px;
87 | height: 80px;
88 | margin: 5px;
89 | border: 5px solid #444;
90 | }
91 |
92 | #friendlist li img:hover {
93 | cursor:pointer;
94 | border-color: #fff;
95 | border-radius: 10px;
96 | }
97 |
98 | #friendlist li.processed img:hover {
99 | border-color: #444;
100 | }
101 |
102 | .hilite {
103 | color: #266A2E;
104 | }
105 |
106 | .tos-guarded {
107 | }
108 |
109 | #friendlist, #step2, #step3, #step4 {
110 | display: none;
111 | }
112 |
113 | #ajax-loader {
114 | display: none;
115 | }
116 |
117 | #remaining-friend-count {
118 | font-size: 11pt;
119 | position: absolute;
120 | display: none;
121 | padding: 3pt;
122 | top: 0px;
123 | right: 0px;
124 | }
125 |
126 | .friend-detail {
127 | font-size: 13pt;
128 | }
129 |
130 | .checkbox {
131 | size: 18pt;
132 | position: absolute;
133 | top: 5px;
134 | left: 5px;
135 | }
136 |
137 | .export-method {
138 | font-size: 12pt;
139 | }
140 |
141 | .export-method input {
142 | border: outset;
143 | padding: 0pt;
144 | }
145 |
146 | .export-method input:active {
147 | border: inset;
148 | }
149 |
150 | #csv-popup {
151 | position: absolute;
152 | background-color: gray;
153 | top: 40px;
154 | bottom: 40px;
155 | right: 40px;
156 | left: 40px;
157 | padding: 12pt;
158 | border: thick solid;
159 | }
160 |
161 | #csv-popup a {
162 | position: absolute;
163 | top: 0px;
164 | right: 20px;
165 | }
166 |
167 | #csv-popup span {
168 | position: absolute;
169 | top: 0px;
170 | left: 20px;
171 | }
172 |
173 | #csv-popup textarea {
174 | position: absolute;
175 | top: 30px;
176 | left: 30px;
177 | right: 30px;
178 | bottom: 30px;
179 |
180 | font-size: 10pt;
181 | }
182 |
183 | #controls {
184 | position: absolute;
185 | top: 5px;
186 | left: 30px;
187 | }
188 |
189 | #log {
190 | width: 100%;
191 | height: 200px;
192 | }
193 |
--------------------------------------------------------------------------------
/controller.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Facebook Friend Exporter
4 |
5 |
6 |
7 |
8 |
9 |
13 |
14 | Hi!
15 |
Couple of questions for you.
16 |
17 |
Does Facebook own your friends?
18 |
Do you think Facebook owns the data about your friends, such as their
19 | email, their website, their IM name, etc. that they made public for you
20 | to see?
21 |
22 |
Do you think your Facebook friends would mind if you, oh, we don't know,
23 | decided one day to email them, or visit their website? How about add them
24 | to your address book on your phone?
25 |
26 |
27 |
28 | If you answered "no" to the above questions, we can help you get your friends'
29 | contact info out of Facebook. We can export the info to your Google Contacts
30 | (GMail), among other things!
31 |
39 | THIS CHROME EXTENSION IS NOT AFFILIATED WITH GOOGLE OR FACEBOOK. HERE IS A LINK
40 | TO THE FACEBOOK TERMS OF SERVICE.
41 |
42 |
43 | I have read the Terms of Service.
44 |
45 |
46 |
47 |
51 |
52 |
Fetching friend list... done. See below.
53 |
These are all the frends that will be exported,
54 |
55 |
56 |
57 |
58 |
64 |
65 |
Fetching friend details... this could take a while, especially if you're
66 | really popular!
67 |
IMPORTANT: Please don't touch your Facebook tab
68 | until we're done, thanks! If you really must have your
69 | Facebook fix right now, here you go (this will open in a new window).
71 |
72 |
73 |
77 |
78 |
All done! Please check the list below. Uncheck the friends you want to skip.
79 |
Now the fun part! Pick an export button below, and we'll get started
80 | copying your friend data there!
81 |
82 |
83 |
84 | This will log you into your GMail account, and import your Facebook friend data
85 | into your Contacts. We'll create a group in your GMail contacts called
86 | "Imported from Facebook" and we'll avoid
87 | duplicating any existing contacts.
88 |
89 |
90 |
91 | This will export your contacts as a CSV file.
92 |
93 |
94 |
95 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
Processed
105 |
Success
106 |
Failed
107 |
Cached
108 |
109 |
110 |
111 |
NOTICE
112 |
You are logged off the mobile site. The mobile site is where we
113 | check your friends info. Please log back in and restart the process.
114 | The steps to follow are mentioned below:
Click on the "Export Friends" and resume the process!
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
--------------------------------------------------------------------------------
/background.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
178 |
179 |
--------------------------------------------------------------------------------
/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/content_script.js:
--------------------------------------------------------------------------------
1 | // Polling time.
2 | var PROFILE_POLL_DELAY=2000;
3 |
4 | // Communicate to both worlds, extension and website.
5 | var exportEvent = document.createEvent('Event');
6 | exportEvent.initEvent('friendExported', true, true);
7 |
8 | // Just draw the export friends link on the top next to the other links.
9 | renderExportFriendsLink();
10 |
11 | // Listen on extension requests.
12 | chrome.extension.onRequest.addListener(function(request, sender, sendResponse) {
13 | if(request.retrieveFriendsMap) {
14 | exportFacebookContacts();
15 | }
16 | });
17 |
18 | // Listen on the real DOM requests to check if friend has been exported.
19 | window.addEventListener('friendExported', function() {
20 | // Save the map to this content script world, so our extension can read it.
21 | var transferDOM = $('#fb-transfer-dom-area');
22 | var friendsMap = JSON.parse(transferDOM.text());
23 |
24 | // For testing, lets just view 2 users.
25 | if (0) {
26 | var i = 0;
27 | var testMap = {};
28 | $.each(friendsMap, function(key, value) {
29 | if (i == 2) {
30 | return false;
31 | }
32 | testMap[key] = value;
33 | i++;
34 | });
35 | delete friendsMap;
36 | friendsMap = testMap;
37 | }
38 | // Clean up since we no longer need this.
39 | $(transferDOM).remove();
40 | $('#fb-inject-area').remove();
41 |
42 | // Count the number of friends.
43 | var i = 0;
44 | $.each(friendsMap, function(key, value) {
45 | i++;
46 | });
47 |
48 | // Tell the worker tab that we are set!
49 | chrome.extension.sendRequest({friendsListReceived: friendsMap});
50 | chrome.extension.sendRequest({renderFriendsList: friendsMap, count: i});
51 | });
52 |
53 | /**
54 | * Switches back to the worker tab where you can see all your friends being
55 | * processed.
56 | */
57 | function switchToWorkerTab() {
58 | // The extension will handle the case if the worker tab already exists.
59 | chrome.extension.sendRequest({switchToWorkerTab: 1});
60 | }
61 |
62 | /**
63 | * Creates a custom Facebook lookalike dialog.
64 | * @param {Object} data A map with the following keys:
65 | * - id: The ID for this dialog.
66 | * - title: The title the appears on the header.
67 | * - message: The description of the dialog, appears middle.
68 | * - yes_text: The text for the yes button.
69 | * - yes_callback: The callback when yes clicked.
70 | * - cancel_callback: The callback when cancel clicked.
71 | * @returns {HTMLElement} The DOM of the dialog returned.
72 | */
73 | function createFacebookDialog(data) {
74 | // Creates the confirm component.
75 | var lblConfirm = $('