├── LICENSE.md ├── README.md ├── background_controller.js ├── content_script_api_bridge.js ├── img ├── icon128.png ├── icon16.png ├── icon32.png └── icon48.png ├── jsapi ├── jsapi_abstract_database.js ├── jsapi_database.js ├── jsapi_for_google_plus.js └── jsapi_helper.js ├── libs └── jquery-1.6.3.min.js ├── manifest.json ├── settings.js └── tests ├── api.css ├── api.html ├── api.js └── vendor ├── qunit.css └── qunit.js /LICENSE.md: -------------------------------------------------------------------------------- 1 | LICENSE For Usage 2 | ================= 3 | 4 | This software is 100% free. It would be ethical if you mention me in your 5 | extension that your using this API within an internal license file, and 6 | let me know what you create with it. I would be greatly happy! 7 | 8 | I spent a good amount of time creating this, so being recognized is the 9 | only thing I ask in return of supplying free software. Being recognized 10 | is a good thing because it helps bring more developers to this project 11 | to help make it even more awesome. 12 | 13 | I just don't like developers who steal peoples code and rebrand it as 14 | their own, it is ethically incorrect, but there is nothing I can do 15 | about that. 16 | 17 | Example of a mention: 18 | 19 | Made with Open Source software JSAPI by +Mohamed Mansour 20 | 21 | Enjoy! 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | An Unofficial Google+ JavaScript API 2 | ================================================ 3 | 4 | It has been 6 months since we have seen any Circle/Posts/Followers 5 | Write/Read API for Google+. Since Google+ is by nature asynchronous 6 | we could tap into their XHR calls and imitate the requests. 7 | 8 | Who uses this API: 9 | ---- 10 | 11 | ![](https://github.com/mohamedmansour/my-hangouts-extension/raw/master/img/icon32.png) My Hangouts Chrome Extension https://plus.google.com/116935358560979346551/about 12 | 13 | ![](https://lh4.googleusercontent.com/1wwa7CVUEhHPX-fD5GL6ip-waOagyr9Rbi-gnlXwn6TOPZUYxjEnsj5ui80-pZQQsa_Ku47PQw=s32-h32-e365) Circle Management Chrome Extension https://plus.google.com/100283465826629314254/about 14 | 15 | ![](https://lh6.googleusercontent.com/nV1nIGSHN9CsoRFpbM_oL-xuyCKOZEIOG4taCZrgKWDjvOWT9Ywuzpjy4AFFXWVGZLS2EurKFA=s32-h32-e365) Map My Circles for Google+™ https://chrome.google.com/webstore/detail/mcfifkeppchbjlepfbepfhjpkhfalcoa 16 | 17 | ![](https://lh5.googleusercontent.com/0Amg5dBgOUZaNSOcZl9b6xUeQOVnpjOGZk9DkCZn975X_XzhsIVpe6lGhOJTMFEpczzm22uDWw=s32-h32-e365) Nuke Comments on Google+ https://chrome.google.com/webstore/detail/nfgaadooldinkdjpjbnbgnoaepmajdfh 18 | 19 | ![](https://lh4.googleusercontent.com/SZ3-4hSckZj-fwuJICUIDk1E-eVwVYb6ZACVyFT9TokHbKJ_pGNS_b4-r6D4RSeKkS6_brzp0tE=s32-h32-e365) Do Share on Google+ https://chrome.google.com/webstore/detail/oglhhmnmdocfhmhlekfdecokagmbchnf 20 | 21 | Who made this: 22 | --- 23 | 24 | Core Contributors 25 | - Mohamed Mansour (Maintainer, Core Dev), - https://github.com/mohamedmansour 26 | - Tzafrir Rehan (Core Dev) - https://github.com/tzafrir 27 | 28 | Contributors 29 | - Jingqin Lynn (Contributed newPost) 30 | - Ryan Peggs (Contributed Bug fixes) 31 | 32 | 33 | What is it about: 34 | ---- 35 | 36 | I provide you a very basic asynchronous Google+ API, in the current 37 | release, you can do the following: 38 | 39 | - Create, Modify, Sort, Query, Remove Circles 40 | - Query, Modify your Profile Information 41 | - Add, Remove, Move People from and to Circles 42 | - Real-Time Search 43 | - Lookup Posts, Manage comments by reporting and deleting. 44 | 45 | This is a fully read and write API. 46 | 47 | Native Examples: 48 | ---- 49 | 50 | // Create an instance of the API. 51 | var plus = new GooglePlusAPI(); 52 | 53 | // Initialize the API so we could get a new session if it exists we reuse it. 54 | plus.init(); 55 | 56 | // Refresh your circle information. 57 | plus.refreshCircles(function() { 58 | 59 | // Let us see who added me to their circle. 60 | plus.getPeopleWhoAddedMe(function(people) { 61 | console.log(people); 62 | }); 63 | 64 | // Let us see who is in my circles. 65 | plus.getPeopleInMyCircles(function(people) { 66 | console.log(people); 67 | }); 68 | 69 | // Let us see who is in our circles but didn't add us to theirs. 70 | plus.getDatabase().getPersonEntity().find({in_my_circle: 'Y', added_me: 'N'}, function(people) { 71 | console.log(people); 72 | }); 73 | }); 74 | 75 | As you see, it is pretty easy to query everything. The possibilities are inifinite 76 | since the data is backed up in a WebSQL DataStore, so querying, reporting, etc, would 77 | be super quick. 78 | 79 | If you want to place that in an extension, I have created a bridge, so you can use 80 | this in a content script context and extension context safely. To do so, you send a 81 | message as follows: 82 | 83 | // Initialize the API so we get the authorization token. 84 | chrome.extension.sendRequest({method: 'PlusAPI', data: {service: 'Plus', method: 'init'}}, function(initResponse) { 85 | chrome.extension.sendRequest({method: 'PlusAPI', data: {service: 'Plus', method: 'refreshCircles'}}, function() { 86 | // etc ... The method is the same method we defined previously in the raw example. 87 | }); 88 | }); 89 | 90 | 91 | Another example, lets say we want to search for a hash tag: 92 | 93 | // Initialize the Google Plus API Wrapper. 94 | var api = new GooglePlusAPI(); 95 | 96 | // Lets initialize it so we can get the current logged in users session. 97 | api.init(); 98 | 99 | // Search for the API. You have the following enums to choose from for searching: 100 | // 101 | // GooglePlusAPI.SearchType.EVERYTHING 102 | // GooglePlusAPI.SearchType.PEOPLE_PAGES; 103 | // GooglePlusAPI.SearchType.POSTS 104 | // GooglePlusAPI.SearchType.SPARKS 105 | // GooglePlusAPI.SearchType.HANGOUTS 106 | // GooglePlusAPI.SearchType.HASHTAGS 107 | // 108 | // So lets search for hashtags that have Microsoft inside them. 109 | api.search(function(resp) { 110 | console.log("Hash results for Microsoft: " + resp.data.join(", ")); 111 | }, "microsoft", { type: GooglePlusAPI.SearchType.HASHTAGS }); 112 | 113 | A full blown example will be released by the end of this week showing how powerful this could be. 114 | As you already know, I am creating a simple circle management utility so you can manage your circles. 115 | 116 | API Documentation 117 | ---- 118 | 119 | AbstractEntity Members: 120 | 121 | - `String getName()` - The table name that this entity holds. 122 | - `void tableDefinition()` - Abstract method that you override to describe the table. 123 | - `void initialize()` - Private method that creates the DDL to execute from tableDefinition 124 | - `void drop(Function:doneCallback)` - Drops the table from cache including the definition. 125 | - `void clear(Function:doneCallback)` - Removes all rows from the table, keeps the definition. 126 | - `void create(Object[]:obj, Function:callback)` - Inserts object(s) into the table. 127 | - `void destroy(String[]:id, Function:callback)` - Deletes object(s) into the table. 128 | - `void update(Object[]:obj, Function:callback)` - Updates object(s) into the table. 129 | - `void find(Object:obj, Function:callback)` - Find a specific object(s) in the table. 130 | - `void findAll(Function:callback)` - Queries for everthing, all the data. 131 | - `void count(Object:obj, Function:callback)` - Counts the number of rows in the table. 132 | - `void save(Object[]:obj, Function:callback)` - Updates otherwise it creates. 133 | 134 | PlusDB Entities: 135 | 136 | - `void open()` - Opens the database 137 | - `void clearAll(Function:doneCallback)` - Drops all tables from the database. 138 | - `AbstractEntity getPersonEntity()` - Returns the PersonEntity 139 | - `AbstractEntity getCircleEntity()` - Returns the CircleEntity 140 | - `AbstractEntity getPersonCircleEntity()` - Returns the PersonCircleEntity 141 | 142 | Native querying: 143 | 144 | - `PlusDB getDatabase()` - Returns the native Database to do advanced queries 145 | 146 | Initialization, fill up the database: 147 | 148 | - `void init(Function:doneCallback)` - Initializes session and data, you can call it at app start. 149 | - `void refreshCircles(Function:doneCallback, boolean:opt_onlyCircles)` - Queries G+ Service for all circles and people information. 150 | - `void refreshFollowers(Function:doneCallback)` - Queries G+ Service for everyone who is following me. 151 | - `void refreshFindPeople(Function:doneCallback)` - Queries G+ Services for discovering similar people like me. 152 | - `void refreshInfo(Function:doneCallback(data))` - Refresh my information. Rarely used. 153 | 154 | Persistence: 155 | 156 | - `void addPeople(Function:doneCallback, String:circleName, Array:usersToAdd)` - Adding people to a circle. 157 | - `void removePeople(Function:doneCallback, String:circleName, Array:usersToRemove)` - Removing people from a circle 158 | - `void createCircle(Function:doneCallback, String:circleName, String:optionalDescription)` - Creating a circle. 159 | - `void removeCircle(Function:doneCallback, String:circleID)` - Removing a circle. 160 | - `void sortCircle(Function:doneCallback, String:circleID, Number:index)` - Sort a circle to the given index, G+ will deal with the order 161 | - `void modifyCircle(Function:doneCallback, String:circleID, String:optionalName, String:optionalDescription)` - Modifying circle meta. 162 | - `void modifyBlocked(Function:doneCallback, Array:usersToModify, boolean:opt_block)` - Modify the blocked state of people. Allows blocking and unblocking. 163 | - `void modifyMute(Function:doneCallback, String:activityID, Boolean:muteStatus)` - Sets the mute status for a specific item. 164 | - `void modifyLockPost(Function:doneCallback, String:activityID, Boolean:toLock)` - Sets the mute status for a specific item. 165 | - `void modifyDisableComments(Function:doneCallback, String:activityID, Boolean:toDisable)` - Sets the mute status for a specific item. 166 | - `void addComment(Function:doneCallback, String: postId, String: content)` - Adds a comment. 167 | - `void deleteComment(Function:doneCallback, String:commentId)` - Deleting a comment. 168 | - `void deleteActivity(Function:doneCallback, String:activityId)` - Deleting a post. 169 | - `void saveProfile(Function:doneCallback, String:introduction)` - Save a new introduction. 170 | - `void reportProfile(Function:doneCallback, String:userId, opt_abuseReason)` - Report an abusive profile. 171 | - `void newPost(Function:doneCallback, String:content)` - Creates a new post on the stream. 172 | - `void fetchLinkMedia(Function:doneCallback(data), String:url` - Fetches media items describing a URL such as images, title and description. 173 | 174 | Read: 175 | 176 | - `boolean isAuthenticated()` 177 | - `void getProfile(Function({introduction}):callback, String:googleProfileID)` 178 | - `void getInfo(Function({id, name, email, acl}:callback)` 179 | - `void getCircles(Function(CircleEntity[]):callback)` 180 | - `void getCircle(Function(String:circleID, CircleEntity):callback)` 181 | - `void getPeople(Object:obj, Function(PersonEntity[]):callback)` 182 | - `void getPerson(Function(String:googleProfileID, PersonEntity):callback)` 183 | - `void getPeopleInMyCircles(Function(PersonEntity[]):callback)` 184 | - `void getPersonInMyCircles(String:googleProfileID, Function(PersonEntity):callback)` 185 | - `void getPeopleWhoAddedMe(Function(PersonEntity[]):callback)` 186 | - `void getPersonWhoAddedMe(String:googleProfileID, Function(PersonEntity):callback)` 187 | - `void search(Function(data):callback, String:query, Object:{category, precache, burst, burst_size})` 188 | - `void lookupUsers(Function(data):callback, Array)` 189 | - `void lookupPost(Function(data):callback, String:googleProfileID, String:postProfileID)` 190 | - `void lookupActivities(Function(data):callback, String:circleID, String:personID, String:pageToken)` 191 | - `void getPages(Function(data):callback)` 192 | - `void getCommunities(Function(data):callback)` 193 | - `void getCommunity(Function(data):callback, String:communityId)` 194 | 195 | Private Members (only for internal API): 196 | 197 | - `Object _parseJSON(String:input)` - Parses the Google Irregular JSON 198 | - `XMLHttpRequest _requestService(Function:callback, String:url, String:postData` - Sends an XHR request to Google Service 199 | - `String _getSession()`- Unique user session that authenticates to persist to your account. 200 | 201 | Watch this space! 202 | 203 | 204 | -------------------------------------------------------------------------------- /background_controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Manages a single instance of the entire application. 3 | * 4 | * @author Mohamed Mansour 2011 (http://mohamedmansour.com) 5 | * @constructor 6 | */ 7 | BackgroundController = function() { 8 | this.plus = new ContentScriptAPIBridge(); 9 | this.onExtensionLoaded(); 10 | }; 11 | 12 | /** 13 | * @return the native Plus API. Goes past the content script bridge. 14 | */ 15 | BackgroundController.prototype.getAPI = function() { 16 | return this.plus.plus; 17 | }; 18 | 19 | /** 20 | * Triggered when the extension just loaded. Should be the first thing 21 | * that happens when chrome loads the extension. 22 | */ 23 | BackgroundController.prototype.onExtensionLoaded = function() { 24 | var currVersion = chrome.app.getDetails().version; 25 | var prevVersion = settings.version; 26 | if (currVersion != prevVersion) { 27 | // Check if we just installed this extension. 28 | if (typeof prevVersion == 'undefined') { 29 | this.onInstall(); 30 | } else { 31 | this.onUpdate(prevVersion, currVersion); 32 | } 33 | settings.version = currVersion; 34 | } 35 | }; 36 | 37 | /** 38 | * Triggered when the extension just installed. 39 | */ 40 | BackgroundController.prototype.onInstall = function() { 41 | }; 42 | 43 | /** 44 | * Triggered when the extension just uploaded to a new version. DB Migrations 45 | * notifications, etc should go here. 46 | * 47 | * @param {string} previous The previous version. 48 | * @param {string} current The new version updating to. 49 | */ 50 | BackgroundController.prototype.onUpdate = function(previous, current) { 51 | }; 52 | 53 | /** 54 | * Initialize the main Background Controller 55 | */ 56 | BackgroundController.prototype.init = function() { 57 | chrome.extension.onRequest.addListener(this.onExternalRequest.bind(this)); 58 | }; 59 | 60 | 61 | /** 62 | * Listen on requests coming from content scripts. 63 | * 64 | * @param {object} request The request object to match data. 65 | * @param {object} sender The sender object to know what the source it. 66 | * @param {Function} sendResponse The response callback. 67 | */ 68 | BackgroundController.prototype.onExternalRequest = function(request, sender, sendResponse) { 69 | if (request.method == 'PlusAPI') { // API Bridge 70 | this.plus.routeMessage(sendResponse, request.data) 71 | } 72 | else if (request.method == 'DataAPI') { // WebStorage 73 | this.plus.routeMessage(sendResponse, request.data) 74 | } 75 | else if (request.method == 'PersistSetting') { // LocalStorage 76 | settings[request.data.key] = request.data.value; 77 | } 78 | else if (request.method == 'GetSetting') { // LocalStorage 79 | sendResponse({data: settings[request.data]}); 80 | } 81 | else { 82 | sendResponse({}); 83 | } 84 | }; 85 | 86 | var backgroundController = new BackgroundController(); 87 | backgroundController.init(); 88 | -------------------------------------------------------------------------------- /content_script_api_bridge.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Content Script to Background Bridge that delegates asynchronous events 3 | * to the consumer responsible. This is where all the API hooks should go and 4 | * only one instence of the GAPI should be present. 5 | */ 6 | ContentScriptAPIBridge = function() { 7 | this.plus = new GooglePlusAPI(); 8 | this.data = { 9 | 'circle' : this.plus.getDatabase().getCircleEntity(), 10 | 'person' : this.plus.getDatabase().getPersonEntity(), 11 | 'person_circle' : this.plus.getDatabase().getPersonCircleEntity() 12 | } 13 | }; 14 | 15 | /** 16 | * Routes messages back to the content script. 17 | * @param {Function} callback The listener to call when the service 18 | * has completed successfully. 19 | * @param {Object} data The data to send to the specified service. 20 | */ 21 | ContentScriptAPIBridge.prototype.routeMessage = function(callback, data) { 22 | switch (data.service) { 23 | case 'DeleteDatabase': 24 | this.plus.getDatabase().clearAll(callback); 25 | break; 26 | case 'CountMetric': 27 | var self = this; 28 | self.plus.getDatabase().getCircleEntity().count({}, function(circleData) { 29 | self.plus.getDatabase().getPersonEntity().count({}, function(personData) { 30 | self.plus.getDatabase().getPersonCircleEntity().count({}, function(personCircleData) { 31 | self.fireCallback(callback, circleData.data + personData.data + personCircleData.data); 32 | }); 33 | }); 34 | }); 35 | break; 36 | case 'Plus': 37 | var args = []; 38 | if (callback) args.push(callback); 39 | if (data.arguments) args.concat(data.arguments); 40 | this.plus[data.method].apply(this.plus, args); 41 | break; 42 | case 'Database': 43 | var entity = this.data[data.entity]; 44 | // TODO: use the below routine, destroy needs refactoring. 45 | // entity[data.method](data.attributes, callback); 46 | switch (data.method) { 47 | case 'read': 48 | entity.find(data.attributes, callback); 49 | break; 50 | case 'create': 51 | entity.create(data.attributes, callback); 52 | break; 53 | case 'update': 54 | entity.update(data.attributes, callback); 55 | break; 56 | case 'delete': 57 | entity.destroy(data.attributes.id, callback); 58 | break; 59 | } 60 | break; 61 | default: 62 | this.fireCallback(callback, false); 63 | break; 64 | } 65 | }; 66 | 67 | /** 68 | * Helper to not fire callback if not called. 69 | * @see {routeMessage} 70 | */ 71 | ContentScriptAPIBridge.prototype.fireCallback = function(callback, data) { 72 | if (callback) callback(data); 73 | }; 74 | -------------------------------------------------------------------------------- /img/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamedmansour/google-plus-extension-jsapi/535f8557be9eb87f560077e09f964a499d54ef01/img/icon128.png -------------------------------------------------------------------------------- /img/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamedmansour/google-plus-extension-jsapi/535f8557be9eb87f560077e09f964a499d54ef01/img/icon16.png -------------------------------------------------------------------------------- /img/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamedmansour/google-plus-extension-jsapi/535f8557be9eb87f560077e09f964a499d54ef01/img/icon32.png -------------------------------------------------------------------------------- /img/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamedmansour/google-plus-extension-jsapi/535f8557be9eb87f560077e09f964a499d54ef01/img/icon48.png -------------------------------------------------------------------------------- /jsapi/jsapi_abstract_database.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mock Entity for testing. 3 | */ 4 | MockEntity = function(db, name) {}; 5 | MockEntity.prototype.tableDefinition = function() {}; 6 | MockEntity.prototype.initialize = function(callback) {callback({data: {}, status: true})}; 7 | MockEntity.prototype.getWhereObject = function(callback) {callback({keys: [], values: []})}; 8 | MockEntity.prototype.getName = function() {return 'mock'}; 9 | MockEntity.prototype.drop = function(callback) {callback({data: {}, status: true})}; 10 | MockEntity.prototype.create = function(obj, callback) {callback({data: {}, status: true})}; 11 | MockEntity.prototype.clear = function(callback) {callback({data: {}, status: true})}; 12 | MockEntity.prototype.fireCallback = function(obj, callback) {callback({data: {}, status: true})}; 13 | MockEntity.prototype.destroy = function(id, callback) {callback({data: {}, status: true})}; 14 | MockEntity.prototype.update = function(obj, callback) {callback({data: {}, status: true})}; 15 | MockEntity.prototype.find = function(select, where, callback) {callback({data: {}, status: true})}; 16 | MockEntity.prototype.findAll = function(callback) {callback({data: {}, status: true})}; 17 | MockEntity.prototype.count = function(obj, callback) {callback({data: {}, status: true})}; 18 | MockEntity.prototype.save = function(obj, callback) {callback({data: {}, status: true})}; 19 | MockEntity.prototype.processStatementObject = function(sql, obj) { return sql; }; 20 | 21 | /** 22 | * Represents a table entity. 23 | * 24 | * @param {Object} db The active database. 25 | * @param {string} name The entity name. 26 | * @constructor 27 | */ 28 | AbstractEntity = function(db, name) { 29 | if (!db || !name) throw new Error('Invalid AbstractEntity: ' + db + ' - ' + name); 30 | this.db = db; 31 | this.name = name; 32 | 33 | this.initialize(); 34 | }; 35 | 36 | /** 37 | * Reserved words, that shouldn't be included as regular keys. 38 | */ 39 | AbstractEntity.RESERVED_KEYS = { 40 | '_count': true, 41 | '_orderby': true 42 | }; 43 | 44 | /** 45 | * Abstract method that a user should define a method. 46 | * @return {Object} the data type. 47 | */ 48 | AbstractEntity.prototype.tableDefinition = function() {}; 49 | 50 | /** 51 | * 52 | * @param {function(!Object)} callback The listener to call when completed. 53 | */ 54 | AbstractEntity.prototype.initialize = function(callback) { 55 | var self = this; 56 | var obj = this.tableDefinition(); 57 | var sql = []; 58 | var indexes = []; 59 | sql.push('_id INTEGER PRIMARY KEY AUTOINCREMENT'); 60 | for (var key in obj) { 61 | if (obj.hasOwnProperty(key)) { 62 | 63 | // Catch errorness reserved keywords. 64 | if (AbstractEntity.RESERVED_KEYS[key.toLowerCase()]) { 65 | console.error('Reserved word collision', key); 66 | continue; 67 | } 68 | 69 | var val = obj[key]; 70 | if (JSAPIHelper.isString(val)) { 71 | sql.push(key + ' ' + val); 72 | } 73 | else if (key == 'unique') { 74 | val.forEach(function(uniqueItem, index) { 75 | sql.push('UNIQUE (' + uniqueItem.join(', ') + ')'); 76 | }); 77 | } 78 | else { // detailed column 79 | sql.push(key + ' ' + val.type); 80 | if (val.foreign) { 81 | indexes.push('FOREIGN KEY (' + key + ') REFERENCES ' + val.foreign + ' (id)'); 82 | } 83 | } 84 | } 85 | } 86 | sql = sql.concat(indexes); 87 | sql.push('UNIQUE (_id)'); 88 | this.db.transaction(function(tx) { 89 | tx.executeSql('CREATE TABLE IF NOT EXISTS ' + self.name + '(' + sql.join(',') + ')', [], 90 | function(tx, rs) { 91 | self.fireCallback({status: true, data: 'Success'}, callback); 92 | }, 93 | function(tx, e) { 94 | console.error(self.name, 'Initialize', e.message); 95 | self.fireCallback({status: false, data: 'Cannot create table'}, callback); 96 | }); 97 | }); 98 | }; 99 | 100 | /** 101 | * 102 | * @param {function(!Object)} callback The listener to call when completed. 103 | */ 104 | AbstractEntity.prototype.drop = function(callback) { 105 | var self = this; 106 | this.db.transaction(function(tx) { 107 | tx.executeSql('DROP TABLE IF EXISTS ' + self.name, [], 108 | function(tx, rs) { 109 | self.fireCallback({status: true, data: 'Success'}, callback); 110 | }, 111 | function(tx, e) { 112 | console.error(self.name, 'Drop', e.message); 113 | self.fireCallback({status: false, data: 'Cannot drop table'}, callback); 114 | }); 115 | }); 116 | }; 117 | 118 | /** 119 | * @param {!Object} the object to send to the callback. 120 | * @param {function(!Object)} callback The listener to call when completed. 121 | */ 122 | AbstractEntity.prototype.fireCallback = function(obj, callback) { 123 | if (callback) { 124 | callback(obj); 125 | } 126 | }; 127 | 128 | /** 129 | * Prepares the Where Clause for the entity. 130 | * 131 | * @param {Object} The query object. 132 | */ 133 | AbstractEntity.prototype.getWhereObject = function(obj) { 134 | var keys = []; 135 | var values = []; 136 | for (var key in obj) { 137 | if (obj.hasOwnProperty(key) && !AbstractEntity.RESERVED_KEYS[key.toLowerCase()]) { 138 | keys.push(key + ' = ?'); 139 | values.push(obj[key]); 140 | } 141 | } 142 | if (values.length == 0) { 143 | keys.push('1 = 1'); 144 | } 145 | return { 146 | keys: keys, 147 | values: values 148 | } 149 | }; 150 | 151 | /** 152 | * PRocess the statement Objects. 153 | * 154 | * @param {String} sql The Web SQL Query. 155 | * @param {Object} obj The data that was passed in to query. 156 | */ 157 | AbstractEntity.prototype.processStatementObject = function(sql, obj) { 158 | sql = this.appendOrderByObject(sql, obj); 159 | sql = this.appendLimitObject(sql, obj); 160 | return sql; 161 | }; 162 | 163 | /** 164 | * Appends the query with the ORDER By clause. This should be the last thing 165 | * within a query. 166 | * 167 | * @param {String} sql The Web SQL Query. 168 | * @param {Object} obj The data that was passed in to query. 169 | */ 170 | AbstractEntity.prototype.appendOrderByObject = function(sql, obj) { 171 | if (!obj._orderBy || !Array.isArray(obj._orderBy) || sql.toUpperCase().indexOf(' ORDER BY ') != -1) { 172 | return sql; 173 | } 174 | 175 | return sql + ' ORDER BY ' + obj._orderBy.join(', '); 176 | }; 177 | 178 | /** 179 | * Appends the query with the LIMIT clause. This should be the last thing 180 | * within a query. 181 | * 182 | * @param {String} sql The Web SQL Query. 183 | * @param {Object} obj The data that was passed in to query. 184 | */ 185 | AbstractEntity.prototype.appendLimitObject = function(sql, obj) { 186 | if (!obj._count || isNaN(obj._count) || sql.toUpperCase().indexOf(' LIMIT ') != -1) { 187 | return sql; 188 | } 189 | 190 | return sql + ' LIMIT 0, ' + parseInt(obj._count); 191 | }; 192 | 193 | /** 194 | * The entity name. 195 | * 196 | * @return {string} The name of the entity. 197 | */ 198 | AbstractEntity.prototype.getName = function() { 199 | return this.name; 200 | }; 201 | 202 | /** 203 | * Logging object. 204 | */ 205 | AbstractEntity.prototype.log = function(msg, obj_opt) { 206 | var obj = obj_opt || ''; 207 | //console.log(msg, obj); 208 | }; 209 | 210 | /** 211 | * Deletes everything from the table. 212 | * 213 | * @param {function(!Object)} callback The listener to call when completed. 214 | */ 215 | AbstractEntity.prototype.clear = function(callback) { 216 | var self = this; 217 | var sql = 'DELETE FROM ' + this.name; 218 | this.log(sql); 219 | this.db.transaction(function(tx) { 220 | tx.executeSql(sql, [], function(tx, rs) { 221 | self.fireCallback({status: true, data: rs}, callback); 222 | }, function(tx, e) { 223 | console.error(self.name, 'Clear', e.message); 224 | self.fireCallback({status: false, data: e.message}, callback); 225 | } 226 | ); 227 | }); 228 | }; 229 | 230 | /** 231 | * 232 | * @param {function(!Object)} callback The listener to call when completed. 233 | */ 234 | AbstractEntity.prototype.create = function(obj, callback) { 235 | var self = this; 236 | if (!Array.isArray(obj)) { 237 | obj = [obj]; 238 | } 239 | this.db.transaction(function(tx) { 240 | for (var i = 0; i < obj.length; i++) { 241 | var element = obj[i]; 242 | var parameterized = []; 243 | var keys = []; 244 | var values = []; 245 | for (var key in element) { 246 | if (element.hasOwnProperty(key) && !AbstractEntity.RESERVED_KEYS[key.toLowerCase()]) { 247 | keys.push(key); 248 | values.push(element[key]); 249 | parameterized.push('?'); 250 | } 251 | } 252 | var id = element.id; 253 | var sql = 'INSERT INTO ' + self.name + '(' + keys.join(', ') + ') VALUES(' + parameterized.join(', ') + ')'; 254 | self.log(sql, values); 255 | 256 | tx.executeSql(sql, values, function(tx, rs) { 257 | if (!id) id = rs.insertId; 258 | self.fireCallback({status: true, data: rs, id: id}, callback); 259 | }, function(tx, e) { 260 | console.error(self.name, 'Create', e.message, sql, values); 261 | self.fireCallback({status: false, data: e.message}, callback); 262 | } 263 | ); 264 | } 265 | }); 266 | }; 267 | 268 | /** 269 | * 270 | * @param {function(!Object)} callback The listener to call when completed. 271 | */ 272 | AbstractEntity.prototype.destroy = function(id, callback) { 273 | var self = this; 274 | var sql = 'DELETE FROM ' + this.name + ' WHERE _id = ?'; 275 | this.log(sql, id); 276 | this.db.transaction(function(tx) { 277 | tx.executeSql(sql, [id], function(tx, rs) { 278 | self.fireCallback({status: true, data: rs}, callback); 279 | }, function(tx, e) { 280 | console.error(self.name, 'Destroy', e.message); 281 | self.fireCallback({status: false, data: e.message}, callback); 282 | } 283 | ); 284 | }); 285 | }; 286 | 287 | /** 288 | * 289 | * @param {function(!Object)} callback The listener to call when completed. 290 | */ 291 | AbstractEntity.prototype.update = function(obj, callback) { 292 | var self = this; 293 | if (!Array.isArray(obj)) { 294 | obj = [obj]; 295 | } 296 | 297 | this.db.transaction(function(tx) { 298 | for (var i = 0; i < obj.length; i++) { 299 | var element = obj[i]; 300 | if (!element.id) { 301 | self.fireCallback({status: false, data: 'No ID present for ' + self.name}, callback); 302 | continue; 303 | } 304 | 305 | // Make sure we have at least two keys in the object. 306 | var keyCount = 0; 307 | var update = []; 308 | var data = []; 309 | for (var key in element) { 310 | if (element.hasOwnProperty(key) && !AbstractEntity.RESERVED_KEYS[key.toLowerCase()]) { 311 | keyCount++; 312 | if (key != '_id') { 313 | update.push(key + ' = ?') 314 | data.push(element[key]); 315 | } 316 | } 317 | } 318 | data.push(element.id) 319 | 320 | if (keyCount < 1) { 321 | self.fireCallback({status: false, data: 'No keys to update for ' + self.name}, callback); 322 | continue; 323 | } 324 | 325 | var sql = 'UPDATE ' + self.name + ' SET ' + update.join(', ') + ' WHERE id = ?'; 326 | self.log(sql, data); 327 | tx.executeSql(sql, data, function(tx, rs) { 328 | self.fireCallback({status: true, data: rs}, callback); 329 | }, function(tx, e) { 330 | console.error(self.name, 'Update', e.message); 331 | self.fireCallback({status: false, data: e.message}, callback); 332 | } 333 | ); 334 | } 335 | }); 336 | }; 337 | 338 | /** 339 | * 340 | * @param {function(!Object)} callback The listener to call when completed. 341 | */ 342 | AbstractEntity.prototype.find = function(select, obj, callback) { 343 | var self = this; 344 | var select = Array.isArray(select) ? select : ['*']; 345 | var where = this.getWhereObject(obj); 346 | var sql = this.processStatementObject('SELECT ' + select.join(',') + ' FROM ' + this.name + 347 | ' WHERE ' + where.keys.join(' AND '), obj); 348 | 349 | this.log(sql); 350 | 351 | this.db.readTransaction(function(tx) { 352 | tx.executeSql(sql, where.values, function (tx, rs) { 353 | var data = []; 354 | for (var i = 0; i < rs.rows.length; i++) { 355 | data.push(rs.rows.item(i)); 356 | } 357 | self.fireCallback({status: true, data: data}, callback); 358 | }, function(tx, e) { 359 | console.error(self.name, 'Find', e.message); 360 | self.fireCallback({status: false, data: e.message}, callback); 361 | } 362 | ); 363 | }); 364 | }; 365 | 366 | /** 367 | * 368 | * @param {function(!Object)} callback The listener to call when completed. 369 | */ 370 | AbstractEntity.prototype.findAll = function(callback) { 371 | this.find([], {}, callback); 372 | }; 373 | 374 | /** 375 | * 376 | * @param {function(!Object)} callback The listener to call when completed. 377 | */ 378 | AbstractEntity.prototype.count = function(obj, callback) { 379 | var self = this; 380 | 381 | var where = this.getWhereObject(obj); 382 | var sql = 'SELECT count(*) as count FROM ' + this.name + ' WHERE ' + where.keys.join(' AND '); 383 | this.log(sql); 384 | 385 | this.db.readTransaction(function(tx) { 386 | tx.executeSql(sql, where.values, function (tx, rs) { 387 | var count = rs.rows.item(0).count; 388 | self.fireCallback({status: true, data: count}, callback); 389 | }, function(e) { 390 | console.error(self.name, 'Count', e.message); 391 | self.fireCallback({status: false, data: e.message}, callback); 392 | } 393 | ); 394 | }); 395 | }; 396 | 397 | /** 398 | * @param {Object.} obj The object to save. 399 | * @param {function(!Object)} callback The listener to call when completed. 400 | */ 401 | AbstractEntity.prototype.save = function(obj, callback) { 402 | var self = this; 403 | self.count({id: obj.id}, function(result) { 404 | if (result.data == 0) { 405 | self.create(obj, callback); 406 | } 407 | else { 408 | self.update(obj, callback); 409 | } 410 | }); 411 | }; 412 | -------------------------------------------------------------------------------- /jsapi/jsapi_database.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mock DB for testing. 3 | */ 4 | MockDB = function () {}; 5 | MockDB.prototype.open = function() {}; 6 | MockDB.prototype.getCircleEntity = function() {return new MockEntity()}; 7 | MockDB.prototype.getPersonEntity = function() {return new MockEntity()}; 8 | MockDB.prototype.getPersonCircleEntity = function() {return new MockEntity()}; 9 | MockDB.prototype.clearAll = function(callback) {}; 10 | 11 | /** 12 | * Storage class responsible for managing the database tansactions for Google+ 13 | * 14 | * @constructor 15 | */ 16 | PlusDB = function (dbPostfix) { 17 | this.db = null; 18 | this.circleEntity = null; 19 | this.personEntity = null; 20 | this.personCircleEntity = null; 21 | this.dbPostfix = dbPostfix || ''; 22 | }; 23 | 24 | /** 25 | * Opens a connection to Web SQL table. 26 | */ 27 | PlusDB.prototype.open = function() { 28 | // 10MB should fit around 100K otherwise would take time to expand array. 29 | var db_size = 10 * 1024 * 1024; 30 | this.db = openDatabase('Circle Management' + this.dbPostfix, '1.0', 'circle-manager', db_size); 31 | this.initializeEntities(); 32 | }; 33 | 34 | /** 35 | * Initialize the entities so we can have accessible tables in a fake ORM way. 36 | */ 37 | PlusDB.prototype.initializeEntities = function() { 38 | this.circleEntity = new CircleEntity(this.db); 39 | this.personEntity = new PersonEntity(this.db); 40 | this.personCircleEntity = new PersonCircleEntity(this.db); 41 | }; 42 | 43 | /** 44 | * For simplicity, just show an alert when crazy error happens. 45 | */ 46 | PlusDB.prototype.onError = function(tx, e) { 47 | console.log('Error: ', e); 48 | alert('Something unexpected happened: ' + e.message ); 49 | }; 50 | 51 | /** 52 | * Removes every row from the table. 53 | */ 54 | PlusDB.prototype.clearAll = function(callback) { 55 | var self = this; 56 | // Drop them. 57 | self.personCircleEntity.drop(function() { 58 | self.circleEntity.drop(function() { 59 | self.personEntity.drop(function() { 60 | // Initialize them again. 61 | self.circleEntity.initialize(function() { 62 | self.personEntity.initialize(function() { 63 | self.personCircleEntity.initialize(function() { 64 | callback(); 65 | }); 66 | }); 67 | }); 68 | }); 69 | }); 70 | }); 71 | }; 72 | 73 | PlusDB.prototype.getCircleEntity = function() { 74 | return this.circleEntity; 75 | }; 76 | 77 | PlusDB.prototype.getPersonEntity = function() { 78 | return this.personEntity; 79 | }; 80 | 81 | PlusDB.prototype.getPersonCircleEntity = function() { 82 | return this.personCircleEntity; 83 | }; 84 | 85 | // ---[ Begin Defining AbstractEntity ]------------------------------------------------- 86 | /** 87 | * @constructor 88 | */ 89 | PersonEntity = function(db) { 90 | AbstractEntity.call(this, db, 'person'); 91 | }; 92 | JSAPIHelper.inherits(PersonEntity, AbstractEntity); 93 | 94 | PersonEntity.prototype.tableDefinition = function() { 95 | return { 96 | id: 'TEXT NOT NULL', 97 | email: 'TEXT', 98 | name: 'TEXT NOT NULL', 99 | photo: 'TEXT', 100 | location: 'TEXT', 101 | employment: 'TEXT', 102 | occupation: 'TEXT', 103 | score: 'REAL', 104 | in_my_circle: 'CHAR DEFAULT "N"', 105 | added_me: 'CHAR DEFAULT "N"', 106 | unique: [ 107 | ['id'] 108 | ] 109 | }; 110 | }; 111 | 112 | /** 113 | * Replace implementation with custom full joined implementation with all tables. 114 | * @override 115 | */ 116 | PersonEntity.prototype.find = function(select, where, callback) { 117 | var self = this; 118 | var where = this.getWhereObject(where); 119 | var sql = 'SELECT person.id as id, person.email as email, person.name as name, person.photo as photo, ' + 120 | 'person.location as location, person.employment as employment, person.occupation as occupation, ' + 121 | 'person.score as score, person.in_my_circle as in_my_circle, person.added_me as added_me, ' + 122 | 'circle.id as circle_id, circle.description as circle_description, circle.name as circle_name ' + 123 | 'FROM person LEFT JOIN circle_person ON person.id = circle_person.person_id LEFT JOIN circle ON circle.id = circle_person.circle_id WHERE ' + 124 | where.keys.join(' AND '); 125 | this.db.readTransaction(function(tx) { 126 | tx.executeSql(sql, where.values, function (tx, rs) { 127 | var data = []; 128 | var prevID = null; 129 | for (var i = 0; i < rs.rows.length; i++) { 130 | var item = rs.rows.item(i); 131 | if (!item.id) { 132 | continue; 133 | } 134 | if (prevID == item.id) { 135 | data[data.length - 1].circles.push({ 136 | id: item.circle_id, 137 | name: item.circle_name, 138 | description: item.circle_description 139 | }); 140 | } 141 | else { 142 | prevID = item.id; 143 | data.push(item); 144 | data[data.length - 1].circles = []; 145 | if (item.circle_id) { 146 | data[data.length - 1].circles.push({ 147 | id: item.circle_id, 148 | name: item.circle_name, 149 | description: item.circle_description 150 | }); 151 | } 152 | } 153 | } 154 | self.fireCallback({status: true, data: data}, callback); 155 | }, function(tx, e) { 156 | console.error(self.name, 'Find', e.message); 157 | self.fireCallback({status: false, data: e.message}, callback); 158 | }); 159 | }); 160 | }; 161 | 162 | /** 163 | * @constructor 164 | */ 165 | PersonCircleEntity = function(db) { 166 | AbstractEntity.call(this, db, 'circle_person'); 167 | }; 168 | JSAPIHelper.inherits(PersonCircleEntity, AbstractEntity); 169 | 170 | PersonCircleEntity.prototype.tableDefinition = function() { 171 | return { 172 | circle_id: {type: 'TEXT', foreign: 'circle'}, 173 | person_id: {type: 'TEXT', foreign: 'person'}, 174 | unique: [ 175 | ['circle_id', 'person_id'] 176 | ] 177 | }; 178 | }; 179 | 180 | /** 181 | * @constructor 182 | */ 183 | CircleEntity = function(db) { 184 | AbstractEntity.call(this, db, 'circle'); 185 | }; 186 | JSAPIHelper.inherits(CircleEntity, AbstractEntity); 187 | 188 | CircleEntity.prototype.tableDefinition = function() { 189 | return { 190 | id: 'TEXT NOT NULL', 191 | name: 'TEXT NOT NULL', 192 | position: 'TEXT', 193 | description: 'TEXT', 194 | unique: [ 195 | ['id'] 196 | ] 197 | }; 198 | }; 199 | 200 | /** 201 | * Replace implementation with custom full joined implementation with all tables. 202 | * @override 203 | */ 204 | CircleEntity.prototype.find = function(select, where, callback) { 205 | var self = this; 206 | var where = this.getWhereObject(where); 207 | var sql = ' SELECT circle.id as id, circle.name as name, circle.position as position, circle.description as description, count(circle_id) as count ' + 208 | ' FROM circle LEFT JOIN circle_person ON circle.id = circle_person.circle_id ' + 209 | ' WHERE ' + where.keys.join(' AND ') + 210 | ' GROUP BY id ORDER BY position'; 211 | 212 | this.db.readTransaction(function(tx) { 213 | tx.executeSql(sql, where.values, function (tx, rs) { 214 | var data = []; 215 | for (var i = 0; i < rs.rows.length; i++) { 216 | data.push(rs.rows.item(i)); 217 | } 218 | self.fireCallback({status: true, data: data}, callback); 219 | }, function(tx, e) { 220 | console.error(self.name, 'Find', e.message); 221 | self.fireCallback({status: false, data: e.message}, callback); 222 | } 223 | ); 224 | }); 225 | }; 226 | // ---[ End Defining AbstractEntity ]------------------------------------------------- 227 | -------------------------------------------------------------------------------- /jsapi/jsapi_for_google_plus.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Unofficial Google Plus API. It mainly supports user and circle management. 3 | * 4 | * Mohamed Mansour (http://mohamedmansour.com) * 5 | * @constructor 6 | */ 7 | GooglePlusAPI = function(opt) { 8 | //------------------------ Constants -------------------------- 9 | // Implemented API 10 | this.CIRCLE_API = 'https://plus.google.com/${pagetoken}/_/socialgraph/lookup/circles/?m=true'; 11 | this.FOLLOWERS_API = 'https://plus.google.com/${pagetoken}/_/socialgraph/lookup/followers/?m=1000000'; 12 | this.FIND_PEOPLE_API = 'https://plus.google.com/${pagetoken}/_/socialgraph/lookup/find_more_people/?m=10000'; 13 | this.MODIFYMEMBER_MUTATE_API = 'https://plus.google.com/${pagetoken}/_/socialgraph/mutate/modifymemberships/'; 14 | this.REMOVEMEMBER_MUTATE_API = 'https://plus.google.com/${pagetoken}/_/socialgraph/mutate/removemember/'; 15 | this.CREATE_MUTATE_API = 'https://plus.google.com/${pagetoken}/_/socialgraph/mutate/create/'; 16 | this.PROPERTIES_MUTATE_API = 'https://plus.google.com/${pagetoken}/_/socialgraph/mutate/properties/'; 17 | this.DELETE_MUTATE_API = 'https://plus.google.com/${pagetoken}/_/socialgraph/mutate/delete/'; 18 | this.SORT_MUTATE_API = 'https://plus.google.com/${pagetoken}/_/socialgraph/mutate/sortorder/'; 19 | this.BLOCK_MUTATE_API = 'https://plus.google.com/${pagetoken}/_/socialgraph/mutate/block_user/'; 20 | this.COMMENT_API = 'https://plus.google.com/${pagetoken}/_/stream/comment/'; 21 | this.DELETE_COMMENT_API = 'https://plus.google.com/${pagetoken}/_/stream/deletecomment/'; 22 | this.DELETE_ACTIVITY_API = 'https://plus.google.com/${pagetoken}/_/stream/deleteactivity/'; 23 | this.INITIAL_DATA_API = 'https://plus.google.com/${pagetoken}/_/initialdata?key=14'; 24 | this.PROFILE_GET_API = 'https://plus.google.com/${pagetoken}/_/profiles/get/'; 25 | this.PROFILE_SAVE_API = 'https://plus.google.com/${pagetoken}/_/profiles/save?_reqid=0'; 26 | this.PROFILE_REPORT_API = 'https://plus.google.com/${pagetoken}/_/profiles/reportabuse'; 27 | this.QUERY_API = 'https://plus.google.com/${pagetoken}/_/s/'; 28 | this.LOOKUP_API = 'https://plus.google.com/${pagetoken}/_/socialgraph/lookup/hovercards/'; 29 | this.ACTIVITY_API = 'https://plus.google.com/${pagetoken}/_/stream/getactivity/'; 30 | this.ACTIVITIES_API = 'https://plus.google.com/${pagetoken}/_/stream/getactivities/'; 31 | this.MUTE_ACTIVITY_API = 'https://plus.google.com/${pagetoken}/_/stream/muteactivity/'; 32 | this.LOCK_POST_API = 'https://plus.google.com/${pagetoken}/_/stream/disableshare/'; 33 | this.DISABLE_COMMENTS_API = 'https://plus.google.com/${pagetoken}/_/stream/disablecomments/'; 34 | this.COMMENT_API = 'https://plus.google.com/${pagetoken}/_/stream/comment/'; 35 | this.POST_API = 'https://plus.google.com/${pagetoken}/_/sharebox/post/?spam=20&rt=j'; 36 | this.LINK_DETAILS_API = 'https://plus.google.com/${pagetoken}/_/sharebox/linkpreview/'; 37 | this.PAGES_API = 'https://plus.google.com/${pagetoken}/_/pages/get/'; 38 | this.COMMUNITIES_API = 'https://plus.google.com/${pagetoken}/_/communities/getcommunities'; 39 | this.COMMUNITY_API = 'https://plus.google.com/${pagetoken}/_/communities/getcommunity'; 40 | this.HASHTAGS_API = 'https://plus.google.com/complete/search?hjson=t&client=es-hashtags&q='; 41 | 42 | // Not Yet Implemented API 43 | this.CIRCLE_ACTIVITIES_API = 'https://plus.google.com/u/0/_/stream/getactivities/'; // ?sp=[1,2,null,"7f2150328d791ede",null,null,null,"social.google.com",[]] 44 | this.SETTINGS_API = 'https://plus.google.com/u/0/_/socialgraph/lookup/settings/'; 45 | this.INCOMING_API = 'https://plus.google.com/u/0/_/socialgraph/lookup/incoming/?o=[null,null,"116805285176805120365"]&n=1000000'; 46 | this.SOCIAL_API = 'https://plus.google.com/u/0/_/socialgraph/lookup/socialbar/'; 47 | this.INVITES_API = 'https://plus.google.com/u/0/_/socialgraph/get/num_invites_remaining/'; 48 | this.PROFILE_PHOTOS_API = 'https://plus.google.com/u/0/_/profiles/getprofilepagephotos/116805285176805120365'; 49 | this.PLUS_API = 'https://plus.google.com/u/0/_/plusone'; 50 | this.MEMBER_SUGGESTION_API = 'https://plus.google.com/u/0/_/socialgraph/lookup/circle_member_suggestions/'; // s=[[[null, null, "116805285176805120365"]]]&at= 51 | 52 | //------------------------ Private Fields -------------------------- 53 | this._opt = opt || {}; 54 | this._pageid = this._opt.pageid; 55 | this._googleid = this._opt.googleid || 0; 56 | var dbPostfix = this._googleid + (this._pageid ? '_' + this._pageid : ''); 57 | if (dbPostfix == '0') { 58 | dbPostfix = ''; 59 | } 60 | this._db = this._opt.use_mockdb ? new MockDB() : new PlusDB(dbPostfix); 61 | 62 | this._session = null; 63 | this._info = null; 64 | 65 | // Time between requesting more pages in search resutls. 66 | this.PRECACHE_INTERVAL = 1000; 67 | 68 | // Time between requesting 'more/burst' search results. 69 | this.BURST_INTERVAL = 5000; 70 | 71 | 72 | this._db.open(); 73 | }; 74 | 75 | //------------------------ Private Functions -------------------------- 76 | 77 | /** 78 | * Parse JSON string in a clean way by removing a bunch of commas and brackets. These should only 79 | * be used in Google post requests. 80 | * 81 | * @param {string} input The irregular JSON string to parse. 82 | */ 83 | GooglePlusAPI.prototype._parseJSON = function(input) { 84 | // We could use eval, but what if Google is untrustworthy? 85 | //return eval('(' + input + ')'); 86 | 87 | var jsonString = input.replace(/\[,/g, '[null,'); 88 | jsonString = jsonString.replace(/,\]/g, ',null]'); 89 | jsonString = jsonString.replace(/,,/g, ',null,'); 90 | jsonString = jsonString.replace(/,,/g, ',null,'); 91 | jsonString = jsonString.replace(/{(\d+):/g, '{"$1":'); 92 | return JSON.parse(jsonString); 93 | 94 | }; 95 | 96 | /** 97 | * Cleansup the URL by replacing the template variables. 98 | * 99 | * @param {string} urlTemplate the URL to parse out the templates. 100 | */ 101 | GooglePlusAPI.prototype._parseURL = function(urlTemplate) { 102 | var pagetoken = 'u/' + this._googleid; 103 | if (this._pageid) { 104 | pagetoken += '/b/' + this._pageid; 105 | } 106 | return urlTemplate.replace(/\${pagetoken}/g, pagetoken); 107 | }; 108 | 109 | /** 110 | * Sends a request to Google+ through the extension. Does some parsing to fix 111 | * the data when retrieved. 112 | * 113 | * @param {function(Object.)} callback 114 | * @param {string} urlTemplate The URL template to request. 115 | * @param {string} postData If specified, it will do a POST with the data. 116 | * @return {XMLHttpRequest} The created XMLHttpRequest object. 117 | */ 118 | GooglePlusAPI.prototype._requestService = function(callback, urlTemplate, postData) { 119 | var self = this; 120 | if (!urlTemplate) { 121 | callback({error: true, text: 'URL to request is missing.'}); 122 | return; 123 | } 124 | 125 | var url = this._parseURL(urlTemplate); 126 | 127 | // When the XHR was successfull, do some post processing to clean up the data. 128 | var success = function(data, textStatus, jqXHR) { 129 | if (!data && jqXHR.status === 200) { 130 | var text = jqXHR.responseText; 131 | var uglyResults = text.substring(4); 132 | var results = self._parseJSON(uglyResults); 133 | callback(Array.isArray(results) ? results[0] : results); 134 | } 135 | else if (data && jqXHR.status === 200) { 136 | callback(data); 137 | } 138 | }; 139 | 140 | var error = function(jqXHR, textStatus, errorThrown) { 141 | if (textStatus == "parsererror") { 142 | if (jqXHR.status === 200) { 143 | success(null, textStatus, jqXHR); 144 | } 145 | return; 146 | } 147 | callback({ 148 | error: errorThrown || jqXHR.status || 'error', 149 | text: textStatus 150 | }); 151 | }; 152 | 153 | // TODO: This is the only jQuery part, try to convert it to plain old JavaScript so we could 154 | // remove the dependency of using the jQuery library! 155 | var xhr = $.ajax({ 156 | type: postData ? 'POST' : 'GET', 157 | url: url, 158 | data: postData || null, 159 | dataType: 'json', 160 | async: true, 161 | success: success, 162 | error: error 163 | }); 164 | 165 | return xhr; 166 | }; 167 | 168 | /** 169 | * Parse out the post object since it is mangled data which majority of the 170 | * entries are not needed. 171 | * 172 | * @param {Object} element Google datastructure. 173 | * @return {Object} The parsed post. 174 | */ 175 | GooglePlusAPI.prototype._parsePost = function(element) { 176 | var item = {}; 177 | item.type = element[2].toLowerCase(); 178 | item.time = element[5]; 179 | if (element[70]) { 180 | item.time_edited = parseInt((element[70] + '').substring(0, (item.time + '').length)); 181 | } 182 | 183 | item.url = this._buildProfileURLFromItem(element[21]); 184 | item.id = element[8]; 185 | item.is_public = (element[32] == '1'); 186 | 187 | item.owner = {}; 188 | item.owner.name = element[3]; 189 | item.owner.id = element[16]; 190 | item.owner.image = this._fixImage(element[18]); 191 | 192 | item.num_comments = element[93]; 193 | 194 | item.raw_media = element[11]; 195 | 196 | if (element[44]) { // Share? 197 | item.share = {}; 198 | item.share.name = element[44][0]; 199 | item.share.id = element[44][1]; 200 | item.share.image = this._fixImage(element[44][4]); 201 | item.share.html = element[44][4]; 202 | item.share.url = this._buildProfileURLFromItem(element[44][4]); 203 | item.share.original_content = element[4]; 204 | item.html = element[47]; 205 | } 206 | else { // Normal 207 | item.html = element[4]; 208 | } 209 | 210 | // Parse hangout item. 211 | if (element[2] == 'Hangout') { 212 | var hangoutData = element[82][2][1][0]; 213 | var hangoutURL = hangoutData[1]; 214 | var hangoutID = hangoutData[0]; 215 | var hangoutType = hangoutData[6]; 216 | var isActive = (!hangoutURL || hangoutURL == '') ? false : true; 217 | if (isActive) { 218 | hangoutURL += '#_' + element[8]; 219 | } 220 | // Skip this since it isn't a hangout. It is just youtube content. 221 | if (isActive && hangoutID == '' && hangoutType == 2 /*normal*/) { 222 | // Perhaps we want to deal with this later. 223 | } 224 | else { 225 | item.owner.status = true; 226 | item.data = {}; 227 | item.data.url = hangoutURL; 228 | item.data.type = hangoutType; 229 | item.data.active = isActive; 230 | item.data.id = hangoutID; 231 | item.data.participants = []; 232 | item.data.extra_data = hangoutData[15]; 233 | 234 | var cachedOnlineUsers = {}; 235 | var onlineParticipants = hangoutData[3]; 236 | for (var i in onlineParticipants) { 237 | var elt = onlineParticipants[i]; 238 | var user = this._buildUserFromItem(elt[2], elt[0], elt[1], true); 239 | cachedOnlineUsers[user.id] = true; 240 | item.data.participants.push(user); 241 | } 242 | var offlineParticipants = hangoutData[4]; 243 | for (var i in offlineParticipants) { 244 | var elt = offlineParticipants[i]; 245 | var user = this._buildUserFromItem(elt[2], elt[0], elt[1], false); 246 | if (!cachedOnlineUsers[user.id]) { 247 | item.data.participants.push(user); 248 | } 249 | } 250 | } 251 | } 252 | 253 | return item; 254 | }; 255 | 256 | /** 257 | * Parse out the user object since it is mangled data which majority of the 258 | * entries are not needed. 259 | * 260 | * @param {Object} element Google datastructure. 261 | * @param {boolean} extractCircles Extract circle information as well.. 262 | * @return {Array[person, userCircles]} The parsed person. 263 | */ 264 | GooglePlusAPI.prototype._parseUser = function(element, extractCircles) { 265 | var email = element[0][0]; 266 | var id = element[0][2]; 267 | var name = element[2][0]; 268 | var score = element[2][3]; 269 | var photo = element[2][8]; 270 | var location = element[2][11]; 271 | var employment = element[2][13]; 272 | var occupation = element[2][14]; 273 | 274 | // Only store what we need, saves memory but takes a tiny bit more time. 275 | var user = {} 276 | if (id) user.id = id; 277 | if (email) user.email = email; 278 | if (name) user.name = name; 279 | if (score) user.score = score; 280 | if (photo) { 281 | if (photo.indexOf('http') != 0) { 282 | photo = 'https:' + photo; 283 | } 284 | user.photo = photo; 285 | } 286 | if (location) user.location = location; 287 | if (employment) user.employment = employment; 288 | if (occupation) user.occupation = occupation; 289 | 290 | // Circle information for the user wanted. 291 | var cleanCircles = []; 292 | if (extractCircles) { 293 | var dirtyCircles = element[3]; 294 | dirtyCircles.forEach(function(element, index) { 295 | cleanCircles.push(element[2][0]); 296 | }); 297 | return [user, cleanCircles]; 298 | } 299 | else { 300 | return user; 301 | } 302 | }; 303 | 304 | /** 305 | * Fire callback safely. 306 | * 307 | * @param {Function} callback The callback to fire back. 308 | * @param {Object} The data to send in the callback. 309 | */ 310 | GooglePlusAPI.prototype._fireCallback = function(callback, data) { 311 | if (callback) { 312 | callback(data); 313 | } 314 | }; 315 | 316 | /** 317 | * Each Google+ user has their own unique session, fetch it and store 318 | * it. The only way getting that session is from their Google+ pages 319 | * since it is embedded within the page. 320 | * 321 | * @param {boolean} opt_reset If set, it will reset the internal cache. 322 | * @return {string} The Google+ user private session used for authentication. 323 | */ 324 | GooglePlusAPI.prototype._getSession = function(opt_reset) { 325 | if (opt_reset || !this._session) { 326 | var xhr = $.ajax({ 327 | type: 'GET', 328 | url: this._parseURL('https://plus.google.com/${pagetoken}/'), 329 | data: null, 330 | async: false 331 | }); 332 | 333 | /* 334 | var match = xhr.responseText.match(/,"((?:[a-zA-Z0-9]+_?)+:[0-9]+)",/); 335 | if (match) { 336 | this._session = (match && match[1]) || null; 337 | } 338 | */ 339 | // For some reason, the top command is becoming unstable in Chrome. It 340 | // freezes the entire browser. For now, we will just discover it since 341 | // indexOf doesn't freeze while search/match/exec freezes. 342 | var isLogged = false; 343 | var searchForString = ',"https://csi.gstatic.com/csi","'; 344 | var responseText = xhr.responseText; 345 | if (responseText != null) { 346 | var startIndex = responseText.indexOf(searchForString); 347 | if (startIndex != -1) { 348 | var remainingText = responseText.substring(startIndex + searchForString.length); 349 | var foundSession = remainingText.substring(0, remainingText.indexOf('"')); 350 | 351 | // Validates it. 352 | if (foundSession.match(/((?:[a-zA-Z0-9]+_?)+:[0-9]+)/)) { 353 | this._session = foundSession; 354 | isLogged = true; 355 | } 356 | } 357 | } 358 | if (!isLogged) { 359 | // TODO: Somehow bring that back to the user. 360 | this._session = null; 361 | console.error('Invalid session, please login to Google+'); 362 | } 363 | } 364 | return this._session; 365 | }; 366 | 367 | /** 368 | * For each post in the stream, it builds the user object. 369 | * 370 | * @param {string} id The userid. 371 | * @param {string} name The name. 372 | * @param {string} image The image url. 373 | * @param {boolean} status The active status. 374 | * @return {Object} the user object. 375 | */ 376 | GooglePlusAPI.prototype._buildUserFromItem = function(id, name, image, status) { 377 | var ret = {}; 378 | if (id) { ret.id = id; } 379 | if (name) { ret.name = name; } 380 | if (image) { 381 | // Some images don't have the protocols, so we fix that. 382 | if (image.indexOf('https') == -1) { 383 | image = 'https:' + image; 384 | } 385 | ret.image = image; 386 | } 387 | ret.status = status ? status : false; 388 | return ret; 389 | }; 390 | 391 | /** 392 | * Build URL from the stream item. 393 | * 394 | * @param {string} url The profile url. 395 | */ 396 | GooglePlusAPI.prototype._buildProfileURLFromItem = function(url) { 397 | if (url.indexOf('https') == -1) { 398 | url = 'http://plus.google.com/' + url; 399 | } 400 | return url; 401 | }; 402 | 403 | /** 404 | * Fix the image since some are corrupted with no https. 405 | */ 406 | GooglePlusAPI.prototype._fixImage = function(image) { 407 | if (image.indexOf('https') == -1) { 408 | image = 'https:' + image; 409 | } 410 | return image; 411 | }; 412 | 413 | /** 414 | * Verify the session is valid if not, log it and fire the callback quickly. 415 | * 416 | * Every caller must return if false. 417 | */ 418 | GooglePlusAPI.prototype._verifySession = function(name, args) { 419 | if (!this.isAuthenticated()) { 420 | var callback = args[0]; 421 | var params = JSON.stringify(args); // this will remove the functions 422 | this._fireCallback(callback, { status: false, data: 'Session error. Name: [' + name + '] Arguments: [' + params + ']'}); 423 | return false; 424 | } 425 | return true; 426 | }; 427 | 428 | /** 429 | * Verifies the response if successfull. 430 | * 431 | * @param {Function} callback The callback to fire back. 432 | * @param {Object} The error data to send in the callback. 433 | */ 434 | GooglePlusAPI.prototype._isResponseSuccess = function(callback, response) { 435 | if (response.error) { 436 | this._fireCallback(callback, { status: false, data: response.error + ' - ' + response.text }); 437 | return false; 438 | } 439 | else { 440 | return true; 441 | } 442 | }; 443 | 444 | /** 445 | * Create a base media item. 446 | * @param {Media} item A media item. (Media: .href, .mime, .type, .src, .mediaProvider(Optional)) 447 | */ 448 | GooglePlusAPI.prototype._createMediaBase = function(item) { 449 | var mediaDetails = [null,item.href,null,item.mime,item.type]; 450 | 451 | var mediaItem = JSAPIHelper.nullArray(48); 452 | mediaItem[9] = []; 453 | mediaItem[24] = mediaDetails; 454 | mediaItem[41] = [[null,item.src,null,null],[null,item.src,null,null]]; 455 | mediaItem[47] = [[null,item.mediaProvider || "","http://google.com/profiles/media/provider",""]]; 456 | 457 | return mediaItem; 458 | }; 459 | 460 | /** 461 | * Create a document media item. 462 | * @param {DocumentMedia} doc A document media item. (DocumentMedia: .href, .type = "document", .mime(Optional), .src(Optional), .mediaProvider(Optional)) 463 | */ 464 | GooglePlusAPI.prototype._createMediaDocument = function(doc) { 465 | doc.mime = doc.mime || "text/html"; 466 | if(!doc.src) { 467 | if(!doc.domain) { 468 | var match = doc.href.match(/(\w+\.)+(\w+)/); 469 | if(match) { 470 | doc.domain = match[0]; 471 | } 472 | } 473 | doc.src = doc.domain ? ("//s2.googleusercontent.com/s2/favicons?domain=" + doc.domain) : null; 474 | } 475 | 476 | var mediaItem = this._createMediaBase(doc); 477 | 478 | mediaItem[3] = doc.title || doc.href; 479 | mediaItem[21] = doc.content || ""; 480 | 481 | return mediaItem; 482 | }; 483 | 484 | /** 485 | * Create an image media item. 486 | * @param {ImageMedia} item An image media item. (ImageMedia: .type = "photo", .width, .height, .href or .src, .mime(Optional), .mediaProvider(Optional)) 487 | */ 488 | GooglePlusAPI.prototype._createMediaImage = function(image) { 489 | image.mediaProvider = image.mediaProvider || "images"; 490 | image.mime = image.mime || "image/jpeg"; 491 | image.href = image.href || image.src; 492 | image.src = image.src || image.href; 493 | var mediaItem = this._createMediaBase(image); 494 | 495 | mediaItem[5] = [null,image.src]; 496 | 497 | var imageDetails = JSAPIHelper.nullArray(9); 498 | imageDetails[7] = image.width; 499 | imageDetails[8] = image.height; 500 | 501 | mediaItem[24] = mediaItem[24].concat(imageDetails); 502 | 503 | return mediaItem; 504 | }; 505 | 506 | /** 507 | * Create an array which represents a media item. Can be used for adding new posts. 508 | * @param {Media} item A media item, either DocumentMedia or ImageMedia. 509 | */ 510 | GooglePlusAPI.prototype._createMediaItem = function(item) { 511 | switch(item.type) { 512 | case 'document': 513 | return this._createMediaDocument(item); 514 | case 'photo': 515 | return this._createMediaImage(item); 516 | } 517 | return null; 518 | }; 519 | 520 | /** 521 | * Create a wire format ACL string. 522 | * 523 | * @param {AclItem} aclItems 524 | * @return {Object} 525 | * 526 | * (AclItem: {GooglePlusAPI.ACL type, String id}, where id is a circle id for ACL.SPECIFIED_CIRCLE, 527 | * or a user's id for ACL.SPECIFIED_PERSON) 528 | */ 529 | GooglePlusAPI.prototype._parseAclItems = function(aclItems) { 530 | var resultAclEntries = aclItems.map(function(aclItem) { 531 | var selfId = this.getInfo().id; 532 | if (aclItem.type == GooglePlusAPI.AclType.PUBLIC) { 533 | return [null, null, 1]; 534 | } else if (aclItem.type == GooglePlusAPI.AclType.EXTENDED_CIRCLES) { 535 | return [null, null, 4]; 536 | } else if (aclItem.type == GooglePlusAPI.AclType.YOUR_CIRCLES) { 537 | return [null, null, 3]; 538 | } else if (aclItem.type == GooglePlusAPI.AclType.SPECIFIED_CIRCLE) { 539 | return [null, aclItem.id]; 540 | } else if (aclItem.type == GooglePlusAPI.AclType.SPECIFIED_PERSON) { 541 | return [[null, null, aclItem.id]]; 542 | } 543 | }.bind(this)); 544 | return [resultAclEntries]; 545 | }; 546 | 547 | 548 | //----------------------- Public Functions ------------------------. 549 | /** 550 | * @return True if session is valid to Google+. 551 | */ 552 | GooglePlusAPI.prototype.isAuthenticated = function() { 553 | return this._session != null; 554 | }; 555 | 556 | /** 557 | * @return Get the pointer to the native database entities. 558 | */ 559 | GooglePlusAPI.prototype.getDatabase = function() { 560 | return this._db; 561 | }; 562 | 563 | /** 564 | * Does the first prefetch. 565 | */ 566 | GooglePlusAPI.prototype.init = function(callback) { 567 | this._getSession(true); // Always reset the cache if called. 568 | var self = this; 569 | if(this.isAuthenticated()) { 570 | this.refreshInfo(function() { 571 | self._fireCallback(callback, {status: true}); 572 | }); 573 | } else { 574 | this._fireCallback(callback, {status: false}); 575 | } 576 | }; 577 | 578 | /** 579 | * Invalidate the circles and people in my circles cache and rebuild it. 580 | * 581 | * @param {boolean} opt_onlyCircles Optional parameter to just persist circle 582 | */ 583 | GooglePlusAPI.prototype.refreshCircles = function(callback, opt_onlyCircles) { 584 | if (!this._verifySession('refreshCircles', arguments)) { 585 | return; 586 | } 587 | 588 | var self = this; 589 | var onlyCircles = opt_onlyCircles || false; 590 | this._requestService(function(response) { 591 | var dirtyCircles = response[1]; 592 | self._db.getCircleEntity().clear(function(res) { 593 | if (!res.status) { 594 | self._fireCallback(callback, {status: false}); 595 | } 596 | else { 597 | var dirtyUsers = response[2]; 598 | 599 | var circleEntity = self._db.getCircleEntity(); 600 | var personEntity = self._db.getPersonEntity(); 601 | var personCircleEntity = self._db.getPersonCircleEntity(); 602 | 603 | // Batch variable.s 604 | var batchRemaining = [dirtyCircles.length, dirtyUsers.length, 0]; 605 | var batchInserts = [[], [], []]; 606 | var batchCounter = [0, 0, 0]; 607 | var batchEntity = [circleEntity, personEntity, personCircleEntity]; 608 | var batchNames = ['CircleEntity', 'PeopleEntity', 'PersonCircleEntity']; 609 | 610 | // Counter till we are done. 611 | var remaining = onlyCircles ? batchRemaining[0] : batchRemaining[0] + batchRemaining[1]; 612 | var onComplete = function(result) { 613 | if (--remaining == 0) { 614 | self._fireCallback(callback, {status: true}); 615 | } 616 | }; 617 | 618 | var onRecord = function(type, data) { 619 | batchCounter[type]++; 620 | batchInserts[type].push(data); 621 | if (batchCounter[type] % 1000 == 0 || batchCounter[type] == batchRemaining[type]) { 622 | batchEntity[type].create(batchInserts[type], onComplete); 623 | batchInserts[type] = []; 624 | } 625 | }; 626 | 627 | // Persist Circles. 628 | dirtyCircles.forEach(function(element, index) { 629 | var id = element[0][0]; 630 | var name = element[1][0]; 631 | var description = element[1][2]; 632 | var position = element[1][12]; 633 | onRecord(0, { 634 | id: id, 635 | name: name, 636 | position: position, 637 | description: description 638 | }); 639 | }); 640 | 641 | // Skip since we require only circle. 642 | if (!onlyCircles) { 643 | // Persist People in your circles. Count number of total circles as well. 644 | dirtyUsers.forEach(function(element, index) { 645 | var userTuple = self._parseUser(element, true); 646 | var user = userTuple[0]; 647 | user.in_my_circle = 'Y'; 648 | var userCircles = userTuple[1]; 649 | remaining += userCircles.length; 650 | batchRemaining[2] += userCircles.length; 651 | onRecord(1, user); 652 | }); 653 | 654 | // For each person, persist them in their circles. 655 | dirtyUsers.forEach(function(element, index) { 656 | var userTuple = self._parseUser(element, true); 657 | var user = userTuple[0]; 658 | var userCircles = userTuple[1]; 659 | userCircles.forEach(function(element, index) { 660 | onRecord(2, { 661 | circle_id: element, 662 | person_id: user.id 663 | }); 664 | }); 665 | }); 666 | } 667 | } 668 | }); 669 | }, this.CIRCLE_API); 670 | }; 671 | 672 | /** 673 | * Invalidate the people who added me cache and rebuild it. 674 | */ 675 | GooglePlusAPI.prototype.refreshFollowers = function(callback) { 676 | if (!this._verifySession('refreshFollowers', arguments)) { 677 | return; 678 | } 679 | var self = this; 680 | this._requestService(function(response) { 681 | var dirtyFollowers = response[2]; 682 | 683 | // Counter till we are done. 684 | var remaining = dirtyFollowers.length; 685 | var onComplete = function(result) { 686 | if (--remaining == 0) { 687 | self._fireCallback(callback, {status: true}); 688 | } 689 | }; 690 | 691 | var batchInserts = [], batchCounter = 0; 692 | var onRecord = function(entity, user) { 693 | batchCounter++; 694 | batchInserts.push(user); 695 | if (batchCounter % 1000 == 0 || batchCounter == remaining) { 696 | entity.create(batchInserts, onComplete); 697 | batchInserts = []; 698 | } 699 | }; 700 | 701 | var personEntity = self._db.getPersonEntity(); 702 | dirtyFollowers.forEach(function(element, index) { 703 | var user = self._parseUser(element); 704 | user.added_me = 'Y'; 705 | onRecord(personEntity, user); 706 | }); 707 | }, this.FOLLOWERS_API); 708 | }; 709 | 710 | /** 711 | * Invalidate the people to discover cache and rebuild it. 712 | */ 713 | GooglePlusAPI.prototype.refreshFindPeople = function(callback) { 714 | if (!this._verifySession('refreshFindPeople', arguments)) { 715 | return; 716 | } 717 | var self = this; 718 | this._requestService(function(response) { 719 | var dirtyUsers = response[1]; 720 | 721 | // Counter till we are done. 722 | var remaining = dirtyUsers.length; 723 | var onComplete = function(result) { 724 | if (--remaining == 0) { 725 | self._fireCallback(callback, {status: true}); 726 | } 727 | }; 728 | 729 | var batchInserts = [], batchCounter = 0; 730 | var onRecord = function(entity, user) { 731 | batchCounter++; 732 | batchInserts.push(user); 733 | if (batchCounter % 1000 == 0 || batchCounter == remaining) { 734 | entity.create(batchInserts, onComplete); 735 | batchInserts = []; 736 | } 737 | }; 738 | 739 | var personEntity = self._db.getPersonEntity(); 740 | dirtyUsers.forEach(function(element, index) { 741 | var user = self._parseUser(element[0]); 742 | onRecord(personEntity, user); 743 | }); 744 | }, this.FIND_PEOPLE_API); 745 | }; 746 | 747 | /** 748 | * Gets the initial data from the user to recognize their ACL to be used in other requests. 749 | * especially in the profile requests. 750 | * 751 | * You can get more stuff from this such as: 752 | * - circles (not ordered) 753 | * - identities (facebook, twitter, linkedin, etc) 754 | * 755 | * @param {function(boolean)} callback 756 | */ 757 | GooglePlusAPI.prototype.refreshInfo = function(callback) { 758 | if (!this._verifySession('refreshInfo', arguments)) { 759 | return; 760 | } 761 | var self = this; 762 | this._requestService(function(response) { 763 | var responseMap = self._parseJSON(response[1]); 764 | self._info = {}; 765 | // Just get the fist result of the Map. 766 | for (var i in responseMap) { 767 | var detail = responseMap[i]; 768 | var emailParse = detail[20] && detail[20].match && detail[20].match(/(.+) <(.+)>/); 769 | if (emailParse) { 770 | self._info.full_email = emailParse[0]; 771 | self._info.email = emailParse[2]; 772 | } 773 | self._info.name = detail[1][4][3]; 774 | self._info.id = detail[0]; 775 | self._info.image_url = 'https:' + detail[1][3]; 776 | // TODO: ACL was removes from this request. 777 | //self._info.acl = '"' + (detail[1][14][0][0]).replace(/"/g, '\\"') + '"'; 778 | self._info.circles = detail[10][1].map(function(element) { 779 | return {id: element[0], name: element[1]} 780 | }); 781 | break; 782 | } 783 | self._fireCallback(callback, { status: true, data: self._info }); 784 | }, this.INITIAL_DATA_API); 785 | }; 786 | 787 | /** 788 | * Add people to a circle in your account. 789 | * 790 | * @param {function(string)} callback The ids of the people added. 791 | * @param {string} circle the Circle to add the people to. 792 | * @param {{Array.}} users The people to add. 793 | */ 794 | GooglePlusAPI.prototype.addPeople = function(callback, circle, users) { 795 | if (!this._verifySession('addPeople', arguments)) { 796 | return; 797 | } 798 | var self = this; 799 | var usersArray = []; 800 | users.forEach(function(element, index) { 801 | usersArray.push('[[null,null,"' + element + '"],null,[]]'); 802 | }); 803 | var data = 'a=[[["' + circle + '"]]]&m=[[' + usersArray.join(',') + ']]&at=' + this._getSession(); 804 | this._requestService(function(response) { 805 | var dirtyPeople = response[2]; 806 | 807 | // Counter till we are done. 808 | var remaining = dirtyPeople.length; 809 | var onComplete = function(result) { 810 | if (--remaining == 0) { 811 | self._fireCallback(callback, {status: true}); 812 | } 813 | }; 814 | dirtyPeople.forEach(function(element, index) { 815 | var user = self._parseUser(element); 816 | user.in_my_circle = 'Y'; 817 | self._db.getPersonEntity().create(user, function(result) { 818 | self._db.getPersonCircleEntity().create({ 819 | circle_id: circle, 820 | person_id: user.id 821 | }, onComplete) 822 | }); 823 | }); 824 | }, this.MODIFYMEMBER_MUTATE_API, data); 825 | }; 826 | 827 | /** 828 | * Remove people from a circle in your account. 829 | * 830 | * @param {function(string)} callback 831 | * @param {string} circle the Circle to remove people from. 832 | * @param {{Array.}} users The people to add. 833 | */ 834 | GooglePlusAPI.prototype.removePeople = function(callback, circle, users) { 835 | if (!this._verifySession('removePeople', arguments)) { 836 | return; 837 | } 838 | var self = this; 839 | var usersArray = []; 840 | users.forEach(function(element, index) { 841 | usersArray.push('[null,null,"' + element + '"]'); 842 | }); 843 | var data = 'c=["' + circle + '"]&m=[[' + usersArray.join(',') + ']]&at=' + this._getSession(); 844 | this._requestService(function(response) { 845 | // Counter till we are done. 846 | var remaining = users.length; 847 | var onComplete = function(result) { 848 | if (--remaining == 0) { 849 | self._fireCallback(callback, {status: true}); 850 | } 851 | }; 852 | users.forEach(function(element, index) { 853 | self._db.getPersonEntity().remove(element, onComplete); 854 | }); 855 | }, this.REMOVEMEMBER_MUTATE_API, data); 856 | }; 857 | 858 | /** 859 | * Create a new empty circle in your account. 860 | * 861 | * @param {function(string)} callback The ID of the circle. 862 | * @param {string} name The circle names. 863 | * @param {string} opt_description Optional description. 864 | */ 865 | GooglePlusAPI.prototype.createCircle = function(callback, name, opt_description) { 866 | if (!this._verifySession('createCircle', arguments)) { 867 | return; 868 | } 869 | var self = this; 870 | var data = 't=2&n=' + encodeURIComponent(name) + '&m=[[]]'; 871 | if (opt_description) { 872 | data += '&d=' + encodeURIComponent(opt_description); 873 | } 874 | data += '&at=' + this._getSession(); 875 | this._requestService(function(response) { 876 | var id = response[1][0]; 877 | var position = response[2]; 878 | self._db.getCircleEntity().persist({ 879 | id: id, 880 | name: name, 881 | position: position, 882 | description: opt_description 883 | }, callback); 884 | }, this.CREATE_MUTATE_API, data); 885 | }; 886 | 887 | /** 888 | * Removes a circle from your profile. 889 | * 890 | * @param {function(boolean)} callback. 891 | * @param {string} id The circle ID. 892 | */ 893 | GooglePlusAPI.prototype.removeCircle = function(callback, id) { 894 | if (!this._verifySession('removeCircle', arguments)) { 895 | return; 896 | } 897 | var self = this; 898 | var data = 'c=["' + id + '"]&at=' + this._getSession(); 899 | this._requestService(function(response) { 900 | self._db.getCircleEntity().remove(id, callback); 901 | }, this.DELETE_MUTATE_API, data); 902 | }; 903 | 904 | /** 905 | * Modify a circle circle given their ID. 906 | * 907 | * @param {function(boolean)} callback 908 | * @param {string} id The circle ID. 909 | * @param {string} opt_name Optional name 910 | * @param {string} opt_description Optional description. 911 | */ 912 | GooglePlusAPI.prototype.modifyCircle = function(callback, id, opt_name, opt_description) { 913 | if (!this._verifySession('modifyCircle', arguments)) { 914 | return; 915 | } 916 | var self = this; 917 | var requestParams = '?c=["' + id + '"]'; 918 | if (opt_name) { 919 | requestParams += '&n=' + encodeURIComponent(opt_name); 920 | } 921 | if (opt_description) { 922 | requestParams += '&d=' + encodeURIComponent(opt_description); 923 | } 924 | var data = 'at=' + this._getSession(); 925 | this._requestService(function(response) { 926 | self._db.getCircleEntity().update({ 927 | id: id, 928 | name: opt_name, 929 | description: opt_description 930 | }, callback); 931 | }, this.PROPERTIES_MUTATE_API + requestParams, data); 932 | }; 933 | 934 | /** 935 | * Sorts the circle based on some index. 936 | * TODO: We need to refresh the circles entity since positions will be changed. 937 | * @param {function(boolean)} callback 938 | * @param {string} id The circle ID 939 | * @param {number} index The index to move that circle to. Must be > 0. 940 | */ 941 | GooglePlusAPI.prototype.sortCircle = function(callback, circle_id, index) { 942 | if (!this._verifySession('sortCircle', arguments)) { 943 | return; 944 | } 945 | var self = this; 946 | index = index > 0 || 0; 947 | var requestParams = '?c=["' + circle_id + '"]&i=' + parseInt(index); 948 | var data = 'at=' + this._getSession(); 949 | this._requestService(function(response) { 950 | self._fireCallback(callback, {status: true}); 951 | }, this.SORT_MUTATE_API + requestParams, data); 952 | }; 953 | 954 | /** 955 | * Blocks or unblocks users from your account. 956 | * @param {function(boolean)} callback 957 | * @param {{Array.}} users The people to add. 958 | * @param {boolean} opt_block Should the users be blocked or unblocked (defaults to block). 959 | */ 960 | GooglePlusAPI.prototype.modifyBlocked = function(callback, users, opt_block) { 961 | if (!this._verifySession('modifyBlocked', arguments)) { 962 | return; 963 | } 964 | var self = this; 965 | var usersArray = users.map(function(element) { 966 | return '[[null,null,"' + element + '"]]'; 967 | }); 968 | var toBlock = 'true'; 969 | if (opt_block == false) { 970 | toBlock = 'false'; 971 | } 972 | var data = 'm=[[' + usersArray.join(',') + '],' + toBlock + ']&at=' + this._getSession(); 973 | this._requestService(function(response) { 974 | self._fireCallback(callback, (!response.error)); 975 | }, this.BLOCK_MUTATE_API, data); 976 | }; 977 | 978 | /** 979 | * Adds a comment. 980 | * @param {function(Object)} callback 981 | * @param {string} postId The post onto which the comment should be added. 982 | * @param {string} content The comment to be added. 983 | */ 984 | GooglePlusAPI.prototype.addComment = function(callback, postId, content) { 985 | if (!this._verifySession('addComment', arguments)) { 986 | return; 987 | } 988 | var self = this; 989 | if (!postId) { 990 | self._fireCallback(callback, {status: false, data: 'Missing parameter: postId'}); 991 | return; 992 | } 993 | if (!content) { 994 | self._fireCallback(callback, {status: false, data: 'Missing parameter: content'}); 995 | return; 996 | } 997 | var data = 'f.req=' + JSON.stringify([ 998 | postId, 999 | 'os:' + postId + ':' + new Date().getTime(), 1000 | encodeURI(content), 1001 | new Date().getTime(), 1002 | null, 1003 | null, 1004 | 1 1005 | ]) + '&at=' + this._getSession(); 1006 | this._requestService(function(response) { 1007 | self._fireCallback(callback, {status: !response.error}); 1008 | }, this.COMMENT_API + '?rt=j', data); 1009 | }; 1010 | 1011 | /** 1012 | * Deletes a comment. 1013 | * @param {function(boolean)} callback 1014 | * @param {string} commentId The comment id. 1015 | */ 1016 | GooglePlusAPI.prototype.deleteComment = function(callback, commentId) { 1017 | if (!this._verifySession('deleteComment', arguments)) { 1018 | return; 1019 | } 1020 | var self = this; 1021 | if (!commentId) { 1022 | self._fireCallback(callback, {status: false, data: 'Missing parameter: commentId'}); 1023 | return; 1024 | } 1025 | var data = 'commentId=' + commentId + '&at=' + this._getSession(); 1026 | this._requestService(function(response) { 1027 | self._fireCallback(callback, {status: !response.error}); 1028 | }, this.DELETE_COMMENT_API, data); 1029 | }; 1030 | 1031 | /** 1032 | * Deletes a post. 1033 | * @param {function(Object)} callback 1034 | * @param {string} activityId The post's id. 1035 | */ 1036 | GooglePlusAPI.prototype.deleteActivity = function(callback, activityId) { 1037 | if (!this._verifySession('deleteActivity', arguments)) { 1038 | return; 1039 | } 1040 | var self = this; 1041 | if (!activityId) { 1042 | self._fireCallback(callback, {status: false, data: 'Missing parameter: activityId'}); 1043 | return; 1044 | } 1045 | var data = 'itemId=' + activityId + '&at=' + this._getSession(); 1046 | this._requestService(function(response) { 1047 | self._fireCallback(callback, {status: !response.error}); 1048 | }, this.DELETE_ACTIVITY_API, data); 1049 | }; 1050 | 1051 | /** 1052 | * Gets all communities for the signed in user. 1053 | * 1054 | * @param {function(Object)} callback 1055 | */ 1056 | GooglePlusAPI.prototype.getCommunities = function(callback) { 1057 | if (!this._verifySession('getCommunities', arguments)) { 1058 | return; 1059 | } 1060 | var data = 'f.req=[[1]]&at=' + this._getSession(); 1061 | var self = this; 1062 | this._requestService(function(response) { 1063 | var responseData = response[1] && response[1][2]; 1064 | if (response.error || !responseData) { 1065 | self._fireCallback(callback, {status: false, error: response.error}); 1066 | return; 1067 | } 1068 | self._fireCallback(callback, {status: true, data: responseData.map(function(comm) { 1069 | comm = comm[0]; 1070 | return { 1071 | id: comm[0][0], 1072 | name: comm[0][1][0], 1073 | tagLine: comm[0][1][1], 1074 | description: comm[0][1][8], 1075 | photoUrl: 'http:' + comm[0][1][3], 1076 | numMembers: comm[3][0] 1077 | }; 1078 | })}); 1079 | }, this.COMMUNITIES_API + '?rt=j', data); 1080 | }; 1081 | 1082 | /** 1083 | * Gets all categories for a community. 1084 | * 1085 | * @param {function(Object)} callback 1086 | * @param {string} communityId The community ID to fetch categories for. 1087 | */ 1088 | GooglePlusAPI.prototype.getCommunity = function(callback, communityId) { 1089 | if (!this._verifySession('getCommunities', arguments)) { 1090 | return; 1091 | } 1092 | if (!communityId) { 1093 | self._fireCallback(callback, {status: false, data: 'Missing parameter: communityId'}); 1094 | return; 1095 | } 1096 | var self = this; 1097 | var data = 'f.req=["' + communityId + '", false]&at=' + this._getSession(); 1098 | this._requestService(function(response) { 1099 | var categories = response[1] && response[1][2] && response[1][2][0]; 1100 | if (response.error || !categories) { 1101 | self._fireCallback(callback, {status: false, error: response.error}); 1102 | return; 1103 | } 1104 | self._fireCallback(callback, {status: true, data: categories.map(function(category) { 1105 | return { 1106 | id: category[0], 1107 | name: category[1] 1108 | }; 1109 | })}); 1110 | }, this.COMMUNITY_API, data); 1111 | }; 1112 | 1113 | /** 1114 | * Gets access to the entire profile for a specific user. 1115 | * 1116 | * @param {function(boolean)} callback 1117 | * @param {string} id The profile ID 1118 | */ 1119 | GooglePlusAPI.prototype.getProfile = function(callback, id) { 1120 | if (!this._verifySession('getProfile', arguments)) { 1121 | return; 1122 | } 1123 | var self = this; 1124 | if (isNaN(id)) { 1125 | self._fireCallback(callback, {status: false, data: 'Invalid ID: Not a number'}); 1126 | return; 1127 | } 1128 | this._requestService(function(response) { 1129 | var obj = { 1130 | introduction: response[1][2][14][1] 1131 | }; 1132 | self._fireCallback(callback, {status: true, data: obj}); 1133 | }, this.PROFILE_GET_API + id); 1134 | }; 1135 | 1136 | /** 1137 | * Gets a list of pages from the users profile. 1138 | * 1139 | * @param {function(boolean)} callback 1140 | * @param {string} id The profile ID 1141 | */ 1142 | GooglePlusAPI.prototype.getPages = function(callback) { 1143 | if (!this._verifySession('getPages', arguments)) { 1144 | return; 1145 | } 1146 | var self = this; 1147 | this._requestService(function(response) { 1148 | var dirtyPages = response[1]; 1149 | var cleanPages = []; 1150 | if (dirtyPages && dirtyPages.length) { 1151 | dirtyPages.forEach(function(element, i) { 1152 | var page = {}; 1153 | page.url = element[2]; 1154 | page.image = self._fixImage(element[3]); 1155 | page.name = element[4][1]; 1156 | // page links => element[11][0] 1157 | page.about = element[14][1]; 1158 | page.id = element[30]; 1159 | page.tagline = element[33][1]; 1160 | cleanPages.push(page); 1161 | }); 1162 | } 1163 | self._fireCallback(callback, { status: true, data: cleanPages }); 1164 | }, this.PAGES_API); 1165 | }; 1166 | 1167 | /** 1168 | * Lookups the information, user and circle data for a specific 1169 | * user. The circle data is basically just the circle ID. 1170 | * 1171 | * @param {function(boolean)} callback 1172 | * @param {Array} id The profile ID 1173 | */ 1174 | GooglePlusAPI.prototype.lookupUsers = function(callback, ids) { 1175 | if (!this._verifySession('lookupUsers', arguments)) { 1176 | return; 1177 | } 1178 | var self = this; 1179 | var allParams = []; 1180 | if (!Array.isArray(ids)) { 1181 | ids = [ids]; 1182 | } 1183 | ids.forEach(function(element, i) { 1184 | allParams.push('[null,null,"' + element + '"]'); 1185 | }); 1186 | 1187 | // We are just limited to the number of requests. In this case, we will create 1188 | // 40 items in each bucket slice. Then keep doing requests until we finish our 1189 | // buckets. It is like filling a tub of water with a cup, we keep pooring water 1190 | // in the cup until we finished filling the tub up. 1191 | var users = {}; 1192 | var MAX_SLICE = 12; 1193 | var indexSliced = 0; 1194 | 1195 | // Internal request. 1196 | var doRequest = function() { 1197 | var usersParam = allParams.slice(indexSliced, indexSliced + MAX_SLICE); 1198 | if (usersParam.length === 0) { 1199 | self._fireCallback(callback, { status: true, data: users }); 1200 | return; 1201 | } 1202 | indexSliced += usersParam.length; 1203 | 1204 | var params = '?n=6&m=[[' + usersParam.join(', ') + ']]'; 1205 | var data = 'at=' + self._getSession(); 1206 | self._requestService(function(response) { 1207 | if (!response || response.error) { 1208 | var error = 'Error during slice ' + indexSliced + '. ' + response.error + ' [' + response.text + ']'; 1209 | self._fireCallback(callback, { status: false, data: error }); 1210 | } 1211 | else { 1212 | var usersArr = response[1]; 1213 | usersArr.forEach(function(element, i) { 1214 | var userObj = self._parseUser(element[1], true); 1215 | var user = userObj[0]; 1216 | var circles = userObj[1]; 1217 | users[user.id] = { 1218 | data: user, 1219 | circles: circles 1220 | }; 1221 | }); 1222 | doRequest(); 1223 | } 1224 | }, self.LOOKUP_API + params, data); 1225 | }; 1226 | doRequest(); 1227 | }; 1228 | 1229 | /** 1230 | * Lookups the activities for the circle or person. 1231 | * 1232 | * @param {function(data)} callback The response for the call, where 1233 | * the parameter is the data for the activities. 1234 | * @param {string} circleID The ID of the circle. 1235 | * @param {string} personID The ID of the person (only used if circleID is not provided). 1236 | * @param {string} pageToken A token recieved in a previous call. A call with this token will fetch 1237 | * the next page of activities. 1238 | */ 1239 | GooglePlusAPI.prototype.lookupActivities = function(callback, circleID, personID, pageToken) { 1240 | if (!this._verifySession('lookupActivities', arguments)) { 1241 | return; 1242 | } 1243 | var self = this; 1244 | pageToken = pageToken || 'null'; 1245 | var personCirclePair = (circleID ? 'null,"' + circleID + '"' : '"' + personID + '",null'); 1246 | var params = '?f.req=' + encodeURIComponent('[[1,2,' + personCirclePair + ',null,null,null,"social.google.com",[],null,null,null,null,null,null,[]],' + pageToken + ']'); 1247 | this._requestService(function(response) { 1248 | var errorExists = !response[1]; 1249 | if (errorExists) { 1250 | self._fireCallback(callback, { 1251 | status: false, 1252 | data: [] 1253 | }); 1254 | } else { 1255 | var dirtyPosts = response[1][0]; 1256 | var cleanPosts = []; 1257 | var post = null; 1258 | for (post in dirtyPosts) { 1259 | cleanPosts.push(self._parsePost(dirtyPosts[post])); 1260 | } 1261 | self._fireCallback(callback, { 1262 | status: true, 1263 | data: cleanPosts, 1264 | pageToken: response[1][1] 1265 | }); 1266 | } 1267 | }, this.ACTIVITIES_API + params); 1268 | }; 1269 | 1270 | 1271 | /** 1272 | * Queries the postID for the specific user. 1273 | 1274 | * @param {function(boolean)} callback 1275 | * @param {string} userID The profile ID 1276 | * @param {string} postID The post ID 1277 | */ 1278 | GooglePlusAPI.prototype.lookupPost = function(callback, userID, postID) { 1279 | var self = this; 1280 | if (!userID || !postID) { 1281 | this._fireCallback(callback, { 1282 | status: false, 1283 | data: 'Missing parameters: userID and postID' 1284 | }); 1285 | return; 1286 | } 1287 | var params = userID + '?updateId=' + postID; 1288 | this._requestService(function(response) { 1289 | if (!self._isResponseSuccess(callback, response)) { 1290 | return; 1291 | } 1292 | 1293 | var item = self._parsePost(response[1]); 1294 | self._fireCallback(callback, { status: true, data: item }); 1295 | }, this.ACTIVITY_API + params); 1296 | }; 1297 | 1298 | /** 1299 | * Sets the mute activity for the specific item. 1300 | * 1301 | * @param {function(boolean)} callback 1302 | * @param {string} itemId The item id. 1303 | * @param {boolean} muteStatus True if requires a mute. 1304 | */ 1305 | GooglePlusAPI.prototype.modifyMute = function(callback, itemId, muteStatus) { 1306 | if (!this._verifySession('setPostMute', arguments)) { 1307 | return; 1308 | } 1309 | var self = this; 1310 | if (!itemId) { 1311 | self._fireCallback(callback, {status: false, data: 'Missing parameter: itemId'}); 1312 | return; 1313 | } 1314 | var mute = muteStatus || false; 1315 | var data = 'itemId=' + itemId + '&mute=' + mute + '&at=' + this._getSession(); 1316 | this._requestService(function(response) { 1317 | self._fireCallback(callback, {status: !response.error}); 1318 | }, this.DELETE_COMMENT_API, data); 1319 | }; 1320 | 1321 | /** 1322 | * Locks the post (A locked post cannot be reshared). 1323 | * 1324 | * @param {function(Object)} callback 1325 | * @param {string} postId The id of the post to be locked. 1326 | * @param {boolean} toLock When true, the post will be locked, when false, the post 1327 | * will be unlocked. 1328 | */ 1329 | GooglePlusAPI.prototype.modifyLockPost = function(callback, postId, toLock) { 1330 | if (!this._verifySession('modifyLockPost', arguments)) { 1331 | return; 1332 | } 1333 | var self = this; 1334 | if (!postId) { 1335 | self._fireCallback(callback, {status: false, data: 'Missing parameter: postId'}); 1336 | return; 1337 | } 1338 | var data = 'itemId=' + postId + '&disable=' + !!toLock + '&at=' + this._getSession(); 1339 | this._requestService(function(response) { 1340 | self._fireCallback(callback, {status: !response.error}); 1341 | }, this.LOCK_POST_API, data); 1342 | }; 1343 | 1344 | /** 1345 | * Disables comments on the post. 1346 | * 1347 | * @param {function(Object)} callback 1348 | * @param {string} postId The id of the post to modify. 1349 | * @param {boolean} toDisable When true, the post will be closed for comments. 1350 | */ 1351 | GooglePlusAPI.prototype.modifyDisableComments = function(callback, postId, toDisable) { 1352 | if (!this._verifySession('modifyDisableComments', arguments)) { 1353 | return; 1354 | } 1355 | var self = this; 1356 | if (!postId) { 1357 | self._fireCallback(callback, {status: false, data: 'Missing parameter: postId'}); 1358 | return; 1359 | } 1360 | var data = 'itemId=' + postId + '&disable=' + !!toDisable + '&at=' + this._getSession(); 1361 | this._requestService(function(response) { 1362 | self._fireCallback(callback, {status: !response.error}); 1363 | }, this.DISABLE_COMMENTS_API, data); 1364 | }; 1365 | 1366 | /** 1367 | * Saves the profile information back to the current logged in user. 1368 | * 1369 | * TODO: complete this for the entire profile. This will just persist the introduction portion 1370 | * not everything else. It is pretty neat how Google is doing this side. kudos. 1371 | * 1372 | * @param {function(boolean)} callback 1373 | * @param {string} introduction The content. 1374 | */ 1375 | GooglePlusAPI.prototype.saveProfile = function(callback, introduction) { 1376 | if (!this._verifySession('saveProfile', arguments)) { 1377 | return; 1378 | } 1379 | var self = this; 1380 | introduction = introduction ? introduction.replace(/"/g, '\\"') : 'null'; 1381 | 1382 | var acl = JSON.stringify({aclEntries: [ 1383 | {scope: scope, role: 20}, 1384 | {scope: scope, role: 60} 1385 | ]}); 1386 | var data = 'profile=' + encodeURIComponent('[null,null,null,null,null,null,null,null,null,null,null,null,null,null,[[' + 1387 | acl + ',null,null,null,[],1],"' + introduction + '"]]') + '&at=' + this._getSession(); 1388 | 1389 | this._requestService(function(response) { 1390 | self._fireCallback(callback, {status: !response.error}); 1391 | }, this.PROFILE_SAVE_API, data); 1392 | }; 1393 | 1394 | /** 1395 | * Reports a profile as abusive. 1396 | * @param {function(boolean)} callback 1397 | * @param {string} userId The user id to report 1398 | * @param {GooglePlusAPI.AbuseReason} opt_abuseReason The reason to report abuse. Defaults to spam. 1399 | */ 1400 | GooglePlusAPI.prototype.reportProfile = function(callback, userId, opt_abuseReason) { 1401 | if (!this._verifySession('reportProfile', arguments)) { 1402 | return; 1403 | } 1404 | var self = this; 1405 | if (!userId) { 1406 | self._fireCallback(callback, {status: false, data: 'Missing parameter: userId'}); 1407 | return; 1408 | } 1409 | 1410 | var reason = opt_abuseReason || GooglePlusAPI.AbuseReason.SPAM; 1411 | var data = 'itemId=' + userId + '&userInfo=[1]&abuseReport=[' + reason + 1412 | ']&at=' + this._getSession(); 1413 | this._requestService(function(response) { 1414 | self._fireCallback(callback, {status: !response.error}); 1415 | }, this.PROFILE_REPORT_API, data); 1416 | }; 1417 | 1418 | // Abuse Reason ENUM. Corresponds to values used by Google+'s abuse report calls. 1419 | GooglePlusAPI.AbuseReason = {}; 1420 | GooglePlusAPI.AbuseReason.SPAM = 1; 1421 | GooglePlusAPI.AbuseReason.NUDITY = 2; 1422 | GooglePlusAPI.AbuseReason.HATE = 3; 1423 | GooglePlusAPI.AbuseReason.FAKE = 8; 1424 | 1425 | // Search Type ENUM 1426 | GooglePlusAPI.SearchType = {}; 1427 | GooglePlusAPI.SearchType.EVERYTHING = 1; 1428 | GooglePlusAPI.SearchType.PEOPLE_PAGES = 2; 1429 | GooglePlusAPI.SearchType.POSTS = 3; 1430 | GooglePlusAPI.SearchType.SPARKS = 4; 1431 | GooglePlusAPI.SearchType.HANGOUTS = 5; 1432 | GooglePlusAPI.SearchType.HASHTAGS = 6; 1433 | 1434 | // Search Privacy ENUM 1435 | GooglePlusAPI.SearchPrivacy = {}; 1436 | GooglePlusAPI.SearchPrivacy.EVERYONE = 1; 1437 | GooglePlusAPI.SearchPrivacy.CIRCLES = 2; 1438 | GooglePlusAPI.SearchPrivacy.YOU = 5; 1439 | 1440 | // Search Category ENUM 1441 | GooglePlusAPI.SearchCategory = {}; 1442 | GooglePlusAPI.SearchCategory.BEST = 1; 1443 | GooglePlusAPI.SearchCategory.RECENT = 2; 1444 | 1445 | // ACL type ENUM 1446 | GooglePlusAPI.AclType = {}; 1447 | GooglePlusAPI.AclType.PUBLIC = 1; 1448 | GooglePlusAPI.AclType.EXTENDED_CIRCLES = 2; 1449 | GooglePlusAPI.AclType.YOUR_CIRCLES = 3; 1450 | GooglePlusAPI.AclType.SPECIFIED_CIRCLE = 4; 1451 | GooglePlusAPI.AclType.SPECIFIED_PERSON = 5; 1452 | 1453 | /** 1454 | * Searches Google+ for everything. 1455 | * 1456 | * @param {function(Object)} callback The response callback. 1457 | * @param {string} query The textual query to search on. 1458 | * @param {Object} opt_extra Optional extra params: 1459 | * category : GooglePlusAPI.SearchCategory | default RECENT 1460 | * privacy : GooglePlusAPI.SearchPrivacy | default EVERYONE 1461 | * type : GooglePlusAPI.SearchType | default EVERYTHING 1462 | * precache : | 1+ 1463 | * burst : false 1464 | * burst_size : 8 1465 | */ 1466 | GooglePlusAPI.prototype.search = function(callback, query, opt_extra) { 1467 | if (!this._verifySession('search', arguments)) { 1468 | return; 1469 | } 1470 | var self = this; 1471 | var extra = opt_extra || {}; 1472 | var category = extra.category || GooglePlusAPI.SearchCategory.RECENT; 1473 | var type = extra.type || GooglePlusAPI.SearchType.EVERYTHING; 1474 | var privacy = extra.privacy || GooglePlusAPI.SearchPrivacy.EVERYONE; 1475 | var precache = extra.precache || 1; 1476 | var burst = extra.burst || false; 1477 | var burst_size = extra.burst_size || 8; 1478 | var mode = 'query'; 1479 | query = query.replace(/"/g, '\\"'); // Escape only quotes for now. 1480 | 1481 | var data = 'srchrp=[["' + query + '",' + type + ',' + category + ',[' + privacy +']' + 1482 | ']$SESSION_ID]&at=' + this._getSession(); 1483 | var processedData = data.replace('$SESSION_ID', ''); 1484 | 1485 | var doRequest = function(searchResults) { 1486 | self._requestService(function(response) { 1487 | // Invalid response exists, it might mean we are doing a lot of searches 1488 | // or it might mean we have finished exhausting the realtime searching. 1489 | var invalidResponse = !response[1] || !response[1][1]; 1490 | if (invalidResponse) { 1491 | // This might be an error, prepare the response so the consumer can 1492 | // deal with it. 1493 | var lastResponseObj = { 1494 | data: searchResults, 1495 | status: false, 1496 | mode: mode 1497 | }; 1498 | // If it is a real time update, it just means it has completed 1499 | // successfully, no more realtime queries needed. 1500 | // TODO: Perhaps we need to wake it up, not important at this time. 1501 | if (mode === 'rt') { 1502 | lastResponseObj.status = true; 1503 | console.warn('precache:' + precache + ':' + extra.precache, 1504 | 'burst_size:' + burst_size + ':' + extra.burst_size, searchResults.length); 1505 | } 1506 | else { 1507 | lastResponseObj.status = false; 1508 | console.warn('precache:' + precache + ':' + extra.precache, 1509 | 'burst_size:' + burst_size + ':' + extra.burst_size, searchResults.length); 1510 | } 1511 | self._fireCallback(callback, lastResponseObj); 1512 | return; 1513 | } 1514 | 1515 | var streamID = response[1][1][2]; // Not Used. 1516 | var trends = response[1][3]; // Not Used. 1517 | var dirtySearchResults = response[1][1][0][0]; 1518 | processedData = data.replace('$SESSION_ID', ',null,["' + streamID + '"]'); 1519 | for (var i = 0; i < dirtySearchResults.length; i++) { 1520 | var item = self._parsePost(dirtySearchResults[i]); 1521 | searchResults.push(item); 1522 | }; 1523 | 1524 | // Page the results. 1525 | if (precache > 1) { 1526 | precache--; 1527 | // Recurse till we are done paging. 1528 | setTimeout(function() { 1529 | doRequest(searchResults); 1530 | }.bind(this), self.PRECACHE_INTERVAL); 1531 | } 1532 | else { 1533 | self._fireCallback(callback, { 1534 | status: true, 1535 | data: searchResults, 1536 | mode: mode 1537 | }); 1538 | // Decide whether to do bursts or not. 1539 | if (burst && 1540 | (mode === 'rt' || searchResults.length)){ // Bursts cannot start if there are initially no results 1541 | mode = 'rt'; 1542 | if (--burst_size > 0) { 1543 | setTimeout(function() { 1544 | doRequest([]); 1545 | }.bind(this), self.BURST_INTERVAL); 1546 | } 1547 | } 1548 | } 1549 | }, self.QUERY_API + mode, processedData); 1550 | }; 1551 | 1552 | var doHashTagRequest = function(query) { 1553 | var hashQueryUrl = self.HASHTAGS_API + encodeURIComponent(query); 1554 | self._requestService(function(response) { 1555 | var hashTags = []; 1556 | 1557 | if (response[1] && response[1].length) { 1558 | response[1].forEach(function(elt) { 1559 | hashTags.push(elt[0]); 1560 | }); 1561 | } 1562 | 1563 | self._fireCallback(callback, { 1564 | status: true, 1565 | mode: mode, 1566 | data: hashTags 1567 | }); 1568 | 1569 | }, hashQueryUrl); 1570 | }; 1571 | 1572 | 1573 | if (type === GooglePlusAPI.SearchType.HASHTAGS) { 1574 | doHashTagRequest(query); 1575 | } 1576 | else { 1577 | var searchResults = []; 1578 | doRequest(searchResults); // Initiate. 1579 | } 1580 | }; 1581 | 1582 | /** 1583 | * Creates a new Google+ post on the existing users stream. 1584 | * 1585 | * @param {function(Object)} callback The post has been shared. 1586 | * @param {Object} postObj the object that we are about to post that contains: 1587 | * String:content - The content of the new post. 1588 | * String:share_id - An existing post to share. 1589 | * Media[]:media - An array of media elements. 1590 | * RawMedia[]:rawMedia - An array of raw media items in wire format. 1591 | * This is the output format of fetchLinkMedia. 1592 | * Overrides the media parameter when present. 1593 | * AclItem:aclItems - An array of acl items describing the 1594 | * audience of the post. See _parseAclItems 1595 | * for description. 1596 | * Defaults to [{type: PUBLIC}] if not present. 1597 | * [String, String]:community - A pair of Community ID, Category ID, 1598 | * describing a community to post to. This 1599 | * overrides any items in aclItems if present. 1600 | * String[]:notify - An array of user IDs to be notified about this post. 1601 | */ 1602 | GooglePlusAPI.prototype.newPost = function(callback, postObj) { 1603 | if (!this._verifySession('newPost', arguments)) { 1604 | return; 1605 | } 1606 | 1607 | var content = postObj.content || null; 1608 | var sharedPostId = postObj.share_id || null; 1609 | var media = postObj.media || null; 1610 | var rawMedia = postObj.rawMedia; 1611 | var notify = postObj.notify || []; 1612 | 1613 | var self = this; 1614 | if (!content && !sharedPostId && !media && !rawMedia) { 1615 | self._fireCallback(callback, { 1616 | status: false, 1617 | data: 'Incomplete parameters: Must pass in content and sharedPostId' 1618 | }); 1619 | return; 1620 | } 1621 | 1622 | var sMedia = []; 1623 | if (media && !rawMedia) { 1624 | for (var i in media) { 1625 | sMedia.push(JSON.stringify(this._createMediaItem(media[i]))); 1626 | } 1627 | } 1628 | 1629 | var acl = this._parseAclItems(postObj.aclItems || [{type: GooglePlusAPI.AclType.PUBLIC}]); 1630 | 1631 | var data = JSAPIHelper.nullArray(37); 1632 | 1633 | data[0] = content || ''; 1634 | data[1] = 'oz:' + this.getInfo().id + '.' + new Date().getTime().toString(16) + '.0'; 1635 | data[2] = sharedPostId; 1636 | data[6] = JSON.stringify(postObj.rawMedia || sMedia); 1637 | data[9] = true; 1638 | data[10] = notify.map(function(userId) { 1639 | return [null, userId]; 1640 | }); 1641 | data[11] = false; 1642 | data[12] = false; 1643 | data[14] = []; 1644 | data[15] = null; 1645 | data[16] = false; 1646 | data[27] = false; 1647 | data[28] = false; 1648 | data[29] = false; 1649 | data[36] = postObj.community ? [postObj.community] : []; 1650 | data[37] = postObj.community ? [[[null,null,null,[postObj.community[0]]]]] : acl; 1651 | 1652 | var params = 'f.req=' + encodeURIComponent(JSON.stringify(data)) + 1653 | '&at=' + encodeURIComponent(this._getSession()); 1654 | 1655 | this._requestService(function(response) { 1656 | if (response.error) { 1657 | self._fireCallback(callback, {status: false}); 1658 | return; 1659 | } 1660 | 1661 | var postData = response[1][1][0][0]; 1662 | var data = { 1663 | id: postData[8], 1664 | content: postData[14], 1665 | htmlContent: postData[4], 1666 | url: 'https://plus.google.com/' + postData[21] 1667 | }; 1668 | 1669 | self._fireCallback(callback, {status: true, data: data}); 1670 | }, this.POST_API, params); 1671 | }; 1672 | 1673 | /** 1674 | * Fetch MediaDetail objects describing a URL. 1675 | * 1676 | * @param {String} url The url. 1677 | * @return An array containing Media Items, in the same format used by the newPost request. 1678 | */ 1679 | GooglePlusAPI.prototype.fetchLinkMedia = function(callback, url) { 1680 | if (!this._verifySession('fetchLinkMedia', arguments)) { 1681 | return; 1682 | } 1683 | var self = this; 1684 | var params = "?c=" + encodeURIComponent(url) + "&t=1&slpf=0&ml=1"; 1685 | var data = 'susp=false&at=' + this._getSession(); 1686 | this._requestService(function(response) { 1687 | if (response.error) { 1688 | self._fireCallback(callback, {status: false, data: response}); 1689 | } else { 1690 | // Response contains either a image/video single element at index 3, or an array of elements 1691 | // describing a link at index 2. In any case, both of those indices are arrays of length >= 0. 1692 | var items = response[2].concat(response[3]); 1693 | self._fireCallback(callback, {status: true, data: items}); 1694 | } 1695 | }, this.LINK_DETAILS_API + params, data); 1696 | }; 1697 | 1698 | /** 1699 | * Factory method, creates api instances for all user's identities, including pages. 1700 | * 1701 | * @param function(Object[]) callback 1702 | * 1703 | */ 1704 | GooglePlusAPI.prototype.getAllIdentitiesApis = function(callback) { 1705 | if (!this.isAuthenticated()) { 1706 | callback([]); 1707 | } else { 1708 | var result = [this]; 1709 | var self = this; 1710 | this.getPages(function(response){ 1711 | if (response.status) { 1712 | response.data.forEach(function(page) { 1713 | result.push(new GooglePlusAPI({ 1714 | googleid: self._googleid, 1715 | pageid: page.id 1716 | })); 1717 | }); 1718 | } 1719 | callback(result); 1720 | }); 1721 | } 1722 | } 1723 | 1724 | /** 1725 | * @return {Object.} The information from the user. 1726 | * - id | name | email | acl 1727 | */ 1728 | GooglePlusAPI.prototype.getInfo = function() { 1729 | return this._info; 1730 | }; 1731 | 1732 | /** 1733 | * @param {function(Object)} callback All the circles. 1734 | */ 1735 | GooglePlusAPI.prototype.getCircles = function(callback) { 1736 | this._db.getCircleEntity().find([], {}, callback); 1737 | }; 1738 | 1739 | /** 1740 | * @param {number} id The circle ID to query. 1741 | * @param {function(Object)} callback All the circles. 1742 | */ 1743 | GooglePlusAPI.prototype.getCircle = function(id, callback) { 1744 | this._db.getCircleEntity().find([], {id: id}, callback); 1745 | }; 1746 | 1747 | /** 1748 | * @param {Object} obj The search object. 1749 | * @param {function(Object)} callback All the circles. 1750 | */ 1751 | GooglePlusAPI.prototype.getPeople = function(obj, callback) { 1752 | this._db.getPersonEntity().find([], obj, callback); 1753 | }; 1754 | 1755 | /** 1756 | * @param {number} id The person ID. 1757 | * @param {function(Object)} callback The person involved. 1758 | */ 1759 | GooglePlusAPI.prototype.getPerson = function(id, callback) { 1760 | this._db.getPersonEntity().find([], {id: id}, callback); 1761 | }; 1762 | 1763 | /** 1764 | * @param {function(Object)} callback People in my circles. 1765 | */ 1766 | GooglePlusAPI.prototype.getPeopleInMyCircles = function(callback) { 1767 | this._db.getPersonEntity().find([], {in_my_circle: 'Y'}, callback); 1768 | }; 1769 | 1770 | /** 1771 | * @param {number id The person ID. 1772 | * @param {function(Object)} callback The person in my circle. 1773 | */ 1774 | GooglePlusAPI.prototype.getPersonInMyCircle = function(id, callback) { 1775 | this._db.getPersonEntity().find([], {in_my_circle: 'Y', id: id}, callback); 1776 | }; 1777 | 1778 | /** 1779 | * @param {function(Object)} callback The people who added me. 1780 | */ 1781 | GooglePlusAPI.prototype.getPeopleWhoAddedMe = function(callback) { 1782 | this._db.getPersonEntity().find([], {added_me: 'Y'}, callback); 1783 | }; 1784 | 1785 | /** 1786 | * @param {number} id The person ID. 1787 | * @param {function(Object)} callback The person who added me. 1788 | */ 1789 | GooglePlusAPI.prototype.getPersonWhoAddedMe = function(id, callback) { 1790 | this._db.getPersonEntity().find([], {added_me: 'Y', id: id}, callback); 1791 | }; 1792 | -------------------------------------------------------------------------------- /jsapi/jsapi_helper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Debug utility functions that help me figure out what Google is kinda doing 3 | * quickly and efficiently. I don't want to bloody do a binary search manually :( 4 | * 5 | * @author Mohamed Mansour 2011 (http://mohamedmansour.com) 6 | * @constructor 7 | */ 8 | JSAPIHelper = function() {}; 9 | 10 | /** 11 | * @see Google Closure goog.inherits 12 | */ 13 | JSAPIHelper.inherits = function(childCtor, parentCtor) { 14 | /** @constructor */ 15 | function tempCtor() {}; 16 | tempCtor.prototype = parentCtor.prototype; 17 | childCtor.superClass_ = parentCtor.prototype; 18 | childCtor.prototype = new tempCtor(); 19 | childCtor.prototype.constructor = childCtor; 20 | }; 21 | 22 | /** 23 | * Is a given value a string? 24 | */ 25 | JSAPIHelper.isString = function(obj) { 26 | return !!(obj === '' || (obj && obj.charCodeAt && obj.substr)); 27 | }; 28 | 29 | /** 30 | * Recursively searches the arrays so I can know when a needle was found in 31 | * the huge haystack. This saves a lot of time! We can make it auto discover 32 | * in the future. 33 | * 34 | * Basically this just visits each item in the array and if it found the item 35 | * it will return it back to the recursion buffer. So I am using recursion to 36 | * backtrack the paths it took to find that needle. Everytime it sees an array 37 | * it will recurse inside and return the result back to the stack. 38 | * 39 | * @param {?string=} needle The text to find. 40 | * @param {?*=} haystack The multi multi huge array to find. 41 | * @return {Array.|boolean} The path to the needle in the haystack, 42 | * false if not found. 43 | */ 44 | JSAPIHelper.searchArray = function(needle, haystack) { 45 | if (!Array.isArray(haystack)) { 46 | return false; 47 | } 48 | for (var i = 0; i < haystack.length; i++) { 49 | var currentValue = haystack[i]; 50 | if (Array.isArray(currentValue)) { 51 | path = JSAPIHelper.searchArray(needle, currentValue); 52 | if (path) { 53 | return [i].concat(path); 54 | } 55 | } 56 | if (currentValue == needle) { 57 | return [i]; 58 | } 59 | } 60 | return false; 61 | }; 62 | 63 | /** 64 | * Very basic string diff to see which character differs. 65 | * 66 | * @param {?string} a The first text to compare. 67 | * @param {?string} b The second text to compare 68 | * @return {number|boolean} The index of the convergence otherwise false if equal. 69 | */ 70 | JSAPIHelper.firstDifference = function(a, b) { 71 | if (!a || !b) { 72 | return false; 73 | } 74 | var aLength = a.length; 75 | var bLength = b.length; 76 | var length = aLength > bLength ? aLength : bLength; 77 | for (var i = 0; i < length; i++) { 78 | if (a[i] != b[i]) { 79 | return i; 80 | } 81 | } 82 | return false; 83 | }; 84 | 85 | /** 86 | * Create an array of null items. 87 | * 88 | * @param {int} length The length of the array. 89 | */ 90 | JSAPIHelper.nullArray = function(length) { 91 | var data = new Array(length); 92 | for(var i = 0; i < length; i++) { 93 | data[i] = null; 94 | } 95 | return data; 96 | }; 97 | 98 | /** 99 | * Testing stuff ... 100 | */ 101 | JSAPIHelper.assertEquals = function(expected, actual) { 102 | if (Array.isArray(expected)) { 103 | expected = expected.join(','); 104 | } 105 | if (Array.isArray(actual)) { 106 | actual = actual.join(','); 107 | } 108 | var results = expected == actual; 109 | if (results) { 110 | console.debug(results, expected, actual); 111 | } 112 | else { 113 | console.error(results, expected, actual); 114 | } 115 | }; 116 | 117 | /** 118 | * Encode stuff that only matter in the GAPI. 119 | * 120 | * @param {string} input The encoded string. 121 | * @return {string} The decoded string. 122 | */ 123 | JSAPIHelper.decodeHTMLCodes = function(input) { 124 | var htmlCodes = [ 125 | ['%5B', '['], 126 | ['%22', '"'], 127 | ['%5C', '\\'], 128 | ['%20', ' '], 129 | ['%2C', ','], 130 | ['%5D', ']'], 131 | ['%3A', ':'] 132 | ]; 133 | htmlCodes.forEach(function(element, index) { 134 | input = input.replace(new RegExp(element[0], 'g'), element[1]); 135 | }); 136 | return input; 137 | }; -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Google+ Unofficial API Demo", 3 | "version": "0.0.1", 4 | "manifest_version": 2, 5 | "description": "Testing Google+ API", 6 | "icons": { 7 | "16": "img/icon16.png", 8 | "32": "img/icon32.png", 9 | "48": "img/icon48.png", 10 | "128": "img/icon128.png" 11 | }, 12 | "permissions": [ 13 | "unlimitedStorage", 14 | "https://plus.google.com/*" 15 | ], 16 | "background": { 17 | "scripts": [ 18 | "/libs/jquery-1.6.3.min.js", 19 | "/jsapi/jsapi_helper.js", 20 | "/jsapi/jsapi_abstract_database.js", 21 | "/jsapi/jsapi_database.js", 22 | "/jsapi/jsapi_for_google_plus.js", 23 | "/content_script_api_bridge.js", 24 | "/settings.js", 25 | "/background_controller.js" 26 | ] 27 | }, 28 | "browser_action": { 29 | "default_icon": "img/icon32.png", 30 | "default_title": "API Test Runner", 31 | "default_popup": "tests/api.html" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Global Settings. 3 | * 4 | * @author Mohamed Mansour 2011 (http://mohamedmansour.com) 5 | */ 6 | settings = { 7 | get version() { 8 | return localStorage['version']; 9 | }, 10 | set version(val) { 11 | localStorage['version'] = val; 12 | }, 13 | get opt_out() { 14 | var key = localStorage['opt_out']; 15 | return (typeof key == 'undefined') ? false : key === 'true'; 16 | }, 17 | set opt_out(val) { 18 | localStorage['opt_out'] = val; 19 | } 20 | }; -------------------------------------------------------------------------------- /tests/api.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding:0 ; 4 | width: 700px; 5 | } -------------------------------------------------------------------------------- /tests/api.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | JSAPI Test Suite 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |

JSAPI Test Framework

20 |

21 |
    22 | 23 | -------------------------------------------------------------------------------- /tests/api.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | var plus = new GooglePlusAPI(); 3 | 4 | module('module'); 5 | 6 | test('JAPI Helper Arrays', function() { 7 | expect(11); 8 | stop(2000); 9 | var smartEquals = function(actual, expected) { 10 | if (Array.isArray(expected)) { 11 | expected = expected.join(','); 12 | } 13 | if (Array.isArray(actual)) { 14 | actual = actual.join(','); 15 | } 16 | equals(actual, expected); 17 | }; 18 | smartEquals(JSAPIHelper.searchArray(null, ['foo', 'hi']), false); 19 | smartEquals(JSAPIHelper.searchArray(null, ['foo', null]), 1); 20 | smartEquals(JSAPIHelper.searchArray(null, null), false); 21 | smartEquals(JSAPIHelper.searchArray('hi', ['foo', 'hi']), [1]); 22 | smartEquals(JSAPIHelper.searchArray('hi', ['foo', ['hi', 'bar']]), [1, 0]); 23 | smartEquals(JSAPIHelper.searchArray('hi', ['foo', ['test', ['1', '2'], ['hi', 'hie']]]), [1, 2, 0]); 24 | smartEquals(JSAPIHelper.firstDifference('abcd', 'abcd'), false); 25 | smartEquals(JSAPIHelper.firstDifference('abcd', 'abcde'), 4); 26 | smartEquals(JSAPIHelper.firstDifference('abbd', 'abcd'), 2); 27 | smartEquals(JSAPIHelper.firstDifference('a', 'abbb'), 1); 28 | smartEquals(JSAPIHelper.firstDifference(null, 'a'), false); 29 | start(); 30 | }); 31 | 32 | test('Setup', function() { 33 | expect(1); 34 | stop(2000); 35 | plus.getDatabase().clearAll(function() { 36 | ok(true, 'database wiped'); 37 | start(); 38 | }); 39 | }); 40 | 41 | test('Init Plus Api', function() { 42 | expect(1); 43 | stop(4000); 44 | plus.init(function(res) { 45 | ok(res.status, 'initialized'); 46 | start(); 47 | }); 48 | }); 49 | 50 | test('Fetch just circles', function() { 51 | expect(4); 52 | stop(5000); 53 | var counter = 3; 54 | function done() { --counter || start(); } 55 | plus.refreshCircles(function(res) { 56 | ok(res.status, 'refreshed just circles'); 57 | plus.getDatabase().getCircleEntity().count({}, function(count) { 58 | ok(count.data > 0, 'Contains Circles'); 59 | done(); 60 | }); 61 | plus.getDatabase().getPersonEntity().count({}, function(count) { 62 | equals(count.data, 0, 'Contains People'); 63 | done(); 64 | }); 65 | plus.getDatabase().getPersonCircleEntity().count({}, function(count) { 66 | equals(count.data, 0, 'Contains Circle People'); 67 | done(); 68 | }); 69 | }, true); 70 | }); 71 | 72 | test('Lookup User Info', function() { 73 | expect(9); 74 | stop(3000); 75 | var counter = 2; 76 | function done() { --counter || start(); } 77 | plus.lookupUsers(function(resp) { 78 | var data = resp.data; 79 | ok(resp.status, 'Response valid'); 80 | var user = data['116805285176805120365']; 81 | ok(user, 'User fetched ok'); 82 | equals(user.data.name, 'Mohamed Mansour', 'My name'); 83 | ok(user.circles.length > 0, 'Circle exists'); 84 | done(); 85 | }, '116805285176805120365', true); 86 | 87 | plus.lookupUsers(function(resp) { 88 | var data = resp.data; 89 | ok(resp.status, 'Response valid'); 90 | var userA = data['116805285176805120365']; 91 | var userB = data['117791034087176894458']; 92 | ok(userA, 'UserA fetched ok'); 93 | ok(userB, 'UserB fetched ok'); 94 | equals(userA.data.name, 'Mohamed Mansour', 'UserA name'); 95 | equals(userB.data.name, 'John Barrington Craggs', 'UserB name'); 96 | done(); 97 | }, ['116805285176805120365', '117791034087176894458', '116805285176805120365', '116805285176805120365', '116805285176805120365', 98 | '116805285176805120365', '116805285176805120365', '116805285176805120365', '116805285176805120365', '116805285176805120365', 99 | '116805285176805120365', '116805285176805120365', '116805285176805120365', '116805285176805120365', '116805285176805120365', 100 | '116805285176805120365', '116805285176805120365', '116805285176805120365', '116805285176805120365', '116805285176805120365', 101 | '116805285176805120365', '116805285176805120365', '116805285176805120365', '116805285176805120365', '116805285176805120365']); 102 | }); 103 | 104 | test('Current User Info', function() { 105 | expect(4); 106 | stop(2000); 107 | plus.refreshInfo(function(res) { 108 | ok(res.status, 'Info received'); 109 | //ok(res.data.acl.indexOf('"{\\"aclEntries\\":[{\\"') == 0, 'Access Control List exists'); 110 | ok(res.data.circles.length > 0, 'Circle exists'); 111 | ok(res.data.id.match(/\d+/), 'User id valid'); 112 | ok(res.data.full_email.indexOf(res.data.email) > 0, 'Email exists'); 113 | //equals(plus.getInfo().acl, res.data.acl, 'ACL are the same'); 114 | start(); 115 | }, '116805285176805120365', true); 116 | }); 117 | 118 | test('Find Post', function() { 119 | expect(10); 120 | stop(2000); 121 | var counter = 2; 122 | function done() { --counter || start(); } 123 | plus.lookupPost(function(res) { 124 | ok(res.data.is_public); 125 | equals(res.data.html, 'Trey Ratcliff hung out with 11 people.'); 126 | equals(res.data.type, 'hangout', 'Post is a hangout type'); 127 | equals(res.data.owner.id, '105237212888595777019', 'Hangout owner id'); 128 | equals(res.data.owner.name, 'Trey Ratcliff', 'Hangout owner'); 129 | equals(res.data.data.active, false, 'Hangout is not active'); 130 | equals(res.data.url, 'http://plus.google.com/105237212888595777019/posts/NRZNtwRpB4f'); 131 | equals(res.data.data.type, 2, 'OnAir Hangout'); 132 | done(); 133 | }, '105237212888595777019', 'NRZNtwRpB4f'); 134 | 135 | plus.lookupPost(function(res) { 136 | ok(!res.status, 'Error Occurred' ); 137 | equals(res.data, '400 - Bad Request' ); 138 | done(); 139 | }, '116805285176805120365', 'MYcz4xJRvDr'); 140 | 141 | }); 142 | 143 | test('Fetch link media', function() { 144 | expect(8); 145 | stop(2000); 146 | var counter = 2; 147 | function done() { --counter || start(); } 148 | plus.fetchLinkMedia(function(res) { 149 | ok(res.status); 150 | var data = res.data; 151 | equals(data[0][3], 'Is the internet down?', 'Title is as expected'); 152 | ok(data[0][21].match('Reload! Reload! Reload!'), 'Description is as expected'); 153 | equals(data[0][24][3], 'text/html', 'Mime type is as expected'); 154 | done(); 155 | }, 'http://istheinternetdown.com/'); 156 | 157 | plus.fetchLinkMedia(function(res) { 158 | ok(res.status); 159 | var data = res.data; 160 | equals(data[0][3], 'Rick Astley - Never Gonna Give You Up', 'Title is as expected'); 161 | ok(data[0][21].match('Music video by Rick Astley'), 'Description is as expected'); 162 | equals(data[0][24][3], 'application/x-shockwave-flash', 'Mime type is as expected'); 163 | done(); 164 | }, 'http://www.youtube.com/watch?v=dQw4w9WgXcQ'); 165 | }); 166 | 167 | }); 168 | -------------------------------------------------------------------------------- /tests/vendor/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2011 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 15px 15px 0 0; 42 | -moz-border-radius: 15px 15px 0 0; 43 | -webkit-border-top-right-radius: 15px; 44 | -webkit-border-top-left-radius: 15px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-banner { 58 | height: 5px; 59 | } 60 | 61 | #qunit-testrunner-toolbar { 62 | padding: 0.5em 0 0.5em 2em; 63 | color: #5E740B; 64 | background-color: #eee; 65 | } 66 | 67 | #qunit-userAgent { 68 | padding: 0.5em 0 0.5em 2.5em; 69 | background-color: #2b81af; 70 | color: #fff; 71 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 72 | } 73 | 74 | 75 | /** Tests: Pass/Fail */ 76 | 77 | #qunit-tests { 78 | list-style-position: inside; 79 | } 80 | 81 | #qunit-tests li { 82 | padding: 0.4em 0.5em 0.4em 2.5em; 83 | border-bottom: 1px solid #fff; 84 | list-style-position: inside; 85 | } 86 | 87 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 88 | display: none; 89 | } 90 | 91 | #qunit-tests li strong { 92 | cursor: pointer; 93 | } 94 | 95 | #qunit-tests li a { 96 | padding: 0.5em; 97 | color: #c2ccd1; 98 | text-decoration: none; 99 | } 100 | #qunit-tests li a:hover, 101 | #qunit-tests li a:focus { 102 | color: #000; 103 | } 104 | 105 | #qunit-tests ol { 106 | margin-top: 0.5em; 107 | padding: 0.5em; 108 | 109 | background-color: #fff; 110 | 111 | border-radius: 15px; 112 | -moz-border-radius: 15px; 113 | -webkit-border-radius: 15px; 114 | 115 | box-shadow: inset 0px 2px 13px #999; 116 | -moz-box-shadow: inset 0px 2px 13px #999; 117 | -webkit-box-shadow: inset 0px 2px 13px #999; 118 | } 119 | 120 | #qunit-tests table { 121 | border-collapse: collapse; 122 | margin-top: .2em; 123 | } 124 | 125 | #qunit-tests th { 126 | text-align: right; 127 | vertical-align: top; 128 | padding: 0 .5em 0 0; 129 | } 130 | 131 | #qunit-tests td { 132 | vertical-align: top; 133 | } 134 | 135 | #qunit-tests pre { 136 | margin: 0; 137 | white-space: pre-wrap; 138 | word-wrap: break-word; 139 | } 140 | 141 | #qunit-tests del { 142 | background-color: #e0f2be; 143 | color: #374e0c; 144 | text-decoration: none; 145 | } 146 | 147 | #qunit-tests ins { 148 | background-color: #ffcaca; 149 | color: #500; 150 | text-decoration: none; 151 | } 152 | 153 | /*** Test Counts */ 154 | 155 | #qunit-tests b.counts { color: black; } 156 | #qunit-tests b.passed { color: #5E740B; } 157 | #qunit-tests b.failed { color: #710909; } 158 | 159 | #qunit-tests li li { 160 | margin: 0.5em; 161 | padding: 0.4em 0.5em 0.4em 0.5em; 162 | background-color: #fff; 163 | border-bottom: none; 164 | list-style-position: inside; 165 | } 166 | 167 | /*** Passing Styles */ 168 | 169 | #qunit-tests li li.pass { 170 | color: #5E740B; 171 | background-color: #fff; 172 | border-left: 26px solid #C6E746; 173 | } 174 | 175 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 176 | #qunit-tests .pass .test-name { color: #366097; } 177 | 178 | #qunit-tests .pass .test-actual, 179 | #qunit-tests .pass .test-expected { color: #999999; } 180 | 181 | #qunit-banner.qunit-pass { background-color: #C6E746; } 182 | 183 | /*** Failing Styles */ 184 | 185 | #qunit-tests li li.fail { 186 | color: #710909; 187 | background-color: #fff; 188 | border-left: 26px solid #EE5757; 189 | } 190 | 191 | #qunit-tests > li:last-child { 192 | border-radius: 0 0 15px 15px; 193 | -moz-border-radius: 0 0 15px 15px; 194 | -webkit-border-bottom-right-radius: 15px; 195 | -webkit-border-bottom-left-radius: 15px; 196 | } 197 | 198 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 199 | #qunit-tests .fail .test-name, 200 | #qunit-tests .fail .module-name { color: #000000; } 201 | 202 | #qunit-tests .fail .test-actual { color: #EE5757; } 203 | #qunit-tests .fail .test-expected { color: green; } 204 | 205 | #qunit-banner.qunit-fail { background-color: #EE5757; } 206 | 207 | 208 | /** Result */ 209 | 210 | #qunit-testresult { 211 | padding: 0.5em 0.5em 0.5em 2.5em; 212 | 213 | color: #2b81af; 214 | background-color: #D2E0E6; 215 | 216 | border-bottom: 1px solid white; 217 | } 218 | 219 | /** Fixture */ 220 | 221 | #qunit-fixture { 222 | position: absolute; 223 | top: -10000px; 224 | left: -10000px; 225 | } -------------------------------------------------------------------------------- /tests/vendor/qunit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2011 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | (function(window) { 12 | 13 | var defined = { 14 | setTimeout: typeof window.setTimeout !== "undefined", 15 | sessionStorage: (function() { 16 | try { 17 | return !!sessionStorage.getItem; 18 | } catch(e){ 19 | return false; 20 | } 21 | })() 22 | }; 23 | 24 | var testId = 0; 25 | 26 | var Test = function(name, testName, expected, testEnvironmentArg, async, callback) { 27 | this.name = name; 28 | this.testName = testName; 29 | this.expected = expected; 30 | this.testEnvironmentArg = testEnvironmentArg; 31 | this.async = async; 32 | this.callback = callback; 33 | this.assertions = []; 34 | }; 35 | Test.prototype = { 36 | init: function() { 37 | var tests = id("qunit-tests"); 38 | if (tests) { 39 | var b = document.createElement("strong"); 40 | b.innerHTML = "Running " + this.name; 41 | var li = document.createElement("li"); 42 | li.appendChild( b ); 43 | li.className = "running"; 44 | li.id = this.id = "test-output" + testId++; 45 | tests.appendChild( li ); 46 | } 47 | }, 48 | setup: function() { 49 | if (this.module != config.previousModule) { 50 | if ( config.previousModule ) { 51 | QUnit.moduleDone( { 52 | name: config.previousModule, 53 | failed: config.moduleStats.bad, 54 | passed: config.moduleStats.all - config.moduleStats.bad, 55 | total: config.moduleStats.all 56 | } ); 57 | } 58 | config.previousModule = this.module; 59 | config.moduleStats = { all: 0, bad: 0 }; 60 | QUnit.moduleStart( { 61 | name: this.module 62 | } ); 63 | } 64 | 65 | config.current = this; 66 | this.testEnvironment = extend({ 67 | setup: function() {}, 68 | teardown: function() {} 69 | }, this.moduleTestEnvironment); 70 | if (this.testEnvironmentArg) { 71 | extend(this.testEnvironment, this.testEnvironmentArg); 72 | } 73 | 74 | QUnit.testStart( { 75 | name: this.testName 76 | } ); 77 | 78 | // allow utility functions to access the current test environment 79 | // TODO why?? 80 | QUnit.current_testEnvironment = this.testEnvironment; 81 | 82 | try { 83 | if ( !config.pollution ) { 84 | saveGlobal(); 85 | } 86 | 87 | this.testEnvironment.setup.call(this.testEnvironment); 88 | } catch(e) { 89 | QUnit.ok( false, "Setup failed on " + this.testName + ": " + e.message ); 90 | } 91 | }, 92 | run: function() { 93 | if ( this.async ) { 94 | QUnit.stop(); 95 | } 96 | 97 | if ( config.notrycatch ) { 98 | this.callback.call(this.testEnvironment); 99 | return; 100 | } 101 | try { 102 | this.callback.call(this.testEnvironment); 103 | } catch(e) { 104 | fail("Test " + this.testName + " died, exception and test follows", e, this.callback); 105 | QUnit.ok( false, "Died on test #" + (this.assertions.length + 1) + ": " + e.message + " - " + QUnit.jsDump.parse(e) ); 106 | // else next test will carry the responsibility 107 | saveGlobal(); 108 | 109 | // Restart the tests if they're blocking 110 | if ( config.blocking ) { 111 | start(); 112 | } 113 | } 114 | }, 115 | teardown: function() { 116 | try { 117 | this.testEnvironment.teardown.call(this.testEnvironment); 118 | checkPollution(); 119 | } catch(e) { 120 | QUnit.ok( false, "Teardown failed on " + this.testName + ": " + e.message ); 121 | } 122 | }, 123 | finish: function() { 124 | if ( this.expected && this.expected != this.assertions.length ) { 125 | QUnit.ok( false, "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run" ); 126 | } 127 | 128 | var good = 0, bad = 0, 129 | tests = id("qunit-tests"); 130 | 131 | config.stats.all += this.assertions.length; 132 | config.moduleStats.all += this.assertions.length; 133 | 134 | if ( tests ) { 135 | var ol = document.createElement("ol"); 136 | 137 | for ( var i = 0; i < this.assertions.length; i++ ) { 138 | var assertion = this.assertions[i]; 139 | 140 | var li = document.createElement("li"); 141 | li.className = assertion.result ? "pass" : "fail"; 142 | li.innerHTML = assertion.message || (assertion.result ? "okay" : "failed"); 143 | ol.appendChild( li ); 144 | 145 | if ( assertion.result ) { 146 | good++; 147 | } else { 148 | bad++; 149 | config.stats.bad++; 150 | config.moduleStats.bad++; 151 | } 152 | } 153 | 154 | // store result when possible 155 | if ( QUnit.config.reorder && defined.sessionStorage ) { 156 | if (bad) { 157 | sessionStorage.setItem("qunit-" + this.module + "-" + this.testName, bad); 158 | } else { 159 | sessionStorage.removeItem("qunit-" + this.module + "-" + this.testName); 160 | } 161 | } 162 | 163 | if (bad == 0) { 164 | ol.style.display = "none"; 165 | } 166 | 167 | var b = document.createElement("strong"); 168 | b.innerHTML = this.name + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 169 | 170 | var a = document.createElement("a"); 171 | a.innerHTML = "Rerun"; 172 | a.href = QUnit.url({ filter: getText([b]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 173 | 174 | addEvent(b, "click", function() { 175 | var next = b.nextSibling.nextSibling, 176 | display = next.style.display; 177 | next.style.display = display === "none" ? "block" : "none"; 178 | }); 179 | 180 | addEvent(b, "dblclick", function(e) { 181 | var target = e && e.target ? e.target : window.event.srcElement; 182 | if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { 183 | target = target.parentNode; 184 | } 185 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 186 | window.location = QUnit.url({ filter: getText([target]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 187 | } 188 | }); 189 | 190 | var li = id(this.id); 191 | li.className = bad ? "fail" : "pass"; 192 | li.removeChild( li.firstChild ); 193 | li.appendChild( b ); 194 | li.appendChild( a ); 195 | li.appendChild( ol ); 196 | 197 | } else { 198 | for ( var i = 0; i < this.assertions.length; i++ ) { 199 | if ( !this.assertions[i].result ) { 200 | bad++; 201 | config.stats.bad++; 202 | config.moduleStats.bad++; 203 | } 204 | } 205 | } 206 | 207 | try { 208 | QUnit.reset(); 209 | } catch(e) { 210 | fail("reset() failed, following Test " + this.testName + ", exception and reset fn follows", e, QUnit.reset); 211 | } 212 | 213 | QUnit.testDone( { 214 | name: this.testName, 215 | failed: bad, 216 | passed: this.assertions.length - bad, 217 | total: this.assertions.length 218 | } ); 219 | }, 220 | 221 | queue: function() { 222 | var test = this; 223 | synchronize(function() { 224 | test.init(); 225 | }); 226 | function run() { 227 | // each of these can by async 228 | synchronize(function() { 229 | test.setup(); 230 | }); 231 | synchronize(function() { 232 | test.run(); 233 | }); 234 | synchronize(function() { 235 | test.teardown(); 236 | }); 237 | synchronize(function() { 238 | test.finish(); 239 | }); 240 | } 241 | // defer when previous test run passed, if storage is available 242 | var bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem("qunit-" + this.module + "-" + this.testName); 243 | if (bad) { 244 | run(); 245 | } else { 246 | synchronize(run); 247 | }; 248 | } 249 | 250 | }; 251 | 252 | var QUnit = { 253 | 254 | // call on start of module test to prepend name to all tests 255 | module: function(name, testEnvironment) { 256 | config.currentModule = name; 257 | config.currentModuleTestEnviroment = testEnvironment; 258 | }, 259 | 260 | asyncTest: function(testName, expected, callback) { 261 | if ( arguments.length === 2 ) { 262 | callback = expected; 263 | expected = 0; 264 | } 265 | 266 | QUnit.test(testName, expected, callback, true); 267 | }, 268 | 269 | test: function(testName, expected, callback, async) { 270 | var name = '' + testName + '', testEnvironmentArg; 271 | 272 | if ( arguments.length === 2 ) { 273 | callback = expected; 274 | expected = null; 275 | } 276 | // is 2nd argument a testEnvironment? 277 | if ( expected && typeof expected === 'object') { 278 | testEnvironmentArg = expected; 279 | expected = null; 280 | } 281 | 282 | if ( config.currentModule ) { 283 | name = '' + config.currentModule + ": " + name; 284 | } 285 | 286 | if ( !validTest(config.currentModule + ": " + testName) ) { 287 | return; 288 | } 289 | 290 | var test = new Test(name, testName, expected, testEnvironmentArg, async, callback); 291 | test.module = config.currentModule; 292 | test.moduleTestEnvironment = config.currentModuleTestEnviroment; 293 | test.queue(); 294 | }, 295 | 296 | /** 297 | * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. 298 | */ 299 | expect: function(asserts) { 300 | config.current.expected = asserts; 301 | }, 302 | 303 | /** 304 | * Asserts true. 305 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 306 | */ 307 | ok: function(a, msg) { 308 | a = !!a; 309 | var details = { 310 | result: a, 311 | message: msg 312 | }; 313 | msg = escapeHtml(msg); 314 | QUnit.log(details); 315 | config.current.assertions.push({ 316 | result: a, 317 | message: msg 318 | }); 319 | }, 320 | 321 | /** 322 | * Checks that the first two arguments are equal, with an optional message. 323 | * Prints out both actual and expected values. 324 | * 325 | * Prefered to ok( actual == expected, message ) 326 | * 327 | * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); 328 | * 329 | * @param Object actual 330 | * @param Object expected 331 | * @param String message (optional) 332 | */ 333 | equal: function(actual, expected, message) { 334 | QUnit.push(expected == actual, actual, expected, message); 335 | }, 336 | 337 | notEqual: function(actual, expected, message) { 338 | QUnit.push(expected != actual, actual, expected, message); 339 | }, 340 | 341 | deepEqual: function(actual, expected, message) { 342 | QUnit.push(QUnit.equiv(actual, expected), actual, expected, message); 343 | }, 344 | 345 | notDeepEqual: function(actual, expected, message) { 346 | QUnit.push(!QUnit.equiv(actual, expected), actual, expected, message); 347 | }, 348 | 349 | strictEqual: function(actual, expected, message) { 350 | QUnit.push(expected === actual, actual, expected, message); 351 | }, 352 | 353 | notStrictEqual: function(actual, expected, message) { 354 | QUnit.push(expected !== actual, actual, expected, message); 355 | }, 356 | 357 | raises: function(block, expected, message) { 358 | var actual, ok = false; 359 | 360 | if (typeof expected === 'string') { 361 | message = expected; 362 | expected = null; 363 | } 364 | 365 | try { 366 | block(); 367 | } catch (e) { 368 | actual = e; 369 | } 370 | 371 | if (actual) { 372 | // we don't want to validate thrown error 373 | if (!expected) { 374 | ok = true; 375 | // expected is a regexp 376 | } else if (QUnit.objectType(expected) === "regexp") { 377 | ok = expected.test(actual); 378 | // expected is a constructor 379 | } else if (actual instanceof expected) { 380 | ok = true; 381 | // expected is a validation function which returns true is validation passed 382 | } else if (expected.call({}, actual) === true) { 383 | ok = true; 384 | } 385 | } 386 | 387 | QUnit.ok(ok, message); 388 | }, 389 | 390 | start: function() { 391 | config.semaphore--; 392 | if (config.semaphore > 0) { 393 | // don't start until equal number of stop-calls 394 | return; 395 | } 396 | if (config.semaphore < 0) { 397 | // ignore if start is called more often then stop 398 | config.semaphore = 0; 399 | } 400 | // A slight delay, to avoid any current callbacks 401 | if ( defined.setTimeout ) { 402 | window.setTimeout(function() { 403 | if ( config.timeout ) { 404 | clearTimeout(config.timeout); 405 | } 406 | 407 | config.blocking = false; 408 | process(); 409 | }, 13); 410 | } else { 411 | config.blocking = false; 412 | process(); 413 | } 414 | }, 415 | 416 | stop: function(timeout) { 417 | config.semaphore++; 418 | config.blocking = true; 419 | 420 | if ( timeout && defined.setTimeout ) { 421 | clearTimeout(config.timeout); 422 | config.timeout = window.setTimeout(function() { 423 | QUnit.ok( false, "Test timed out" ); 424 | QUnit.start(); 425 | }, timeout); 426 | } 427 | } 428 | }; 429 | 430 | // Backwards compatibility, deprecated 431 | QUnit.equals = QUnit.equal; 432 | QUnit.same = QUnit.deepEqual; 433 | 434 | // Maintain internal state 435 | var config = { 436 | // The queue of tests to run 437 | queue: [], 438 | 439 | // block until document ready 440 | blocking: true, 441 | 442 | // by default, run previously failed tests first 443 | // very useful in combination with "Hide passed tests" checked 444 | reorder: true, 445 | 446 | noglobals: false, 447 | notrycatch: false 448 | }; 449 | 450 | // Load paramaters 451 | (function() { 452 | var location = window.location || { search: "", protocol: "file:" }, 453 | params = location.search.slice( 1 ).split( "&" ), 454 | length = params.length, 455 | urlParams = {}, 456 | current; 457 | 458 | if ( params[ 0 ] ) { 459 | for ( var i = 0; i < length; i++ ) { 460 | current = params[ i ].split( "=" ); 461 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 462 | // allow just a key to turn on a flag, e.g., test.html?noglobals 463 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 464 | urlParams[ current[ 0 ] ] = current[ 1 ]; 465 | if ( current[ 0 ] in config ) { 466 | config[ current[ 0 ] ] = current[ 1 ]; 467 | } 468 | } 469 | } 470 | 471 | QUnit.urlParams = urlParams; 472 | config.filter = urlParams.filter; 473 | 474 | // Figure out if we're running the tests from a server or not 475 | QUnit.isLocal = !!(location.protocol === 'file:'); 476 | })(); 477 | 478 | // Expose the API as global variables, unless an 'exports' 479 | // object exists, in that case we assume we're in CommonJS 480 | if ( typeof exports === "undefined" || typeof require === "undefined" ) { 481 | extend(window, QUnit); 482 | window.QUnit = QUnit; 483 | } else { 484 | extend(exports, QUnit); 485 | exports.QUnit = QUnit; 486 | } 487 | 488 | // define these after exposing globals to keep them in these QUnit namespace only 489 | extend(QUnit, { 490 | config: config, 491 | 492 | // Initialize the configuration options 493 | init: function() { 494 | extend(config, { 495 | stats: { all: 0, bad: 0 }, 496 | moduleStats: { all: 0, bad: 0 }, 497 | started: +new Date, 498 | updateRate: 1000, 499 | blocking: false, 500 | autostart: true, 501 | autorun: false, 502 | filter: "", 503 | queue: [], 504 | semaphore: 0 505 | }); 506 | 507 | var tests = id( "qunit-tests" ), 508 | banner = id( "qunit-banner" ), 509 | result = id( "qunit-testresult" ); 510 | 511 | if ( tests ) { 512 | tests.innerHTML = ""; 513 | } 514 | 515 | if ( banner ) { 516 | banner.className = ""; 517 | } 518 | 519 | if ( result ) { 520 | result.parentNode.removeChild( result ); 521 | } 522 | 523 | if ( tests ) { 524 | result = document.createElement( "p" ); 525 | result.id = "qunit-testresult"; 526 | result.className = "result"; 527 | tests.parentNode.insertBefore( result, tests ); 528 | result.innerHTML = 'Running...
     '; 529 | } 530 | }, 531 | 532 | /** 533 | * Resets the test setup. Useful for tests that modify the DOM. 534 | * 535 | * If jQuery is available, uses jQuery's html(), otherwise just innerHTML. 536 | */ 537 | reset: function() { 538 | if ( window.jQuery ) { 539 | jQuery( "#qunit-fixture" ).html( config.fixture ); 540 | } else { 541 | var main = id( 'qunit-fixture' ); 542 | if ( main ) { 543 | main.innerHTML = config.fixture; 544 | } 545 | } 546 | }, 547 | 548 | /** 549 | * Trigger an event on an element. 550 | * 551 | * @example triggerEvent( document.body, "click" ); 552 | * 553 | * @param DOMElement elem 554 | * @param String type 555 | */ 556 | triggerEvent: function( elem, type, event ) { 557 | if ( document.createEvent ) { 558 | event = document.createEvent("MouseEvents"); 559 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 560 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 561 | elem.dispatchEvent( event ); 562 | 563 | } else if ( elem.fireEvent ) { 564 | elem.fireEvent("on"+type); 565 | } 566 | }, 567 | 568 | // Safe object type checking 569 | is: function( type, obj ) { 570 | return QUnit.objectType( obj ) == type; 571 | }, 572 | 573 | objectType: function( obj ) { 574 | if (typeof obj === "undefined") { 575 | return "undefined"; 576 | 577 | // consider: typeof null === object 578 | } 579 | if (obj === null) { 580 | return "null"; 581 | } 582 | 583 | var type = Object.prototype.toString.call( obj ) 584 | .match(/^\[object\s(.*)\]$/)[1] || ''; 585 | 586 | switch (type) { 587 | case 'Number': 588 | if (isNaN(obj)) { 589 | return "nan"; 590 | } else { 591 | return "number"; 592 | } 593 | case 'String': 594 | case 'Boolean': 595 | case 'Array': 596 | case 'Date': 597 | case 'RegExp': 598 | case 'Function': 599 | return type.toLowerCase(); 600 | } 601 | if (typeof obj === "object") { 602 | return "object"; 603 | } 604 | return undefined; 605 | }, 606 | 607 | push: function(result, actual, expected, message) { 608 | var details = { 609 | result: result, 610 | message: message, 611 | actual: actual, 612 | expected: expected 613 | }; 614 | 615 | message = escapeHtml(message) || (result ? "okay" : "failed"); 616 | message = '' + message + ""; 617 | expected = escapeHtml(QUnit.jsDump.parse(expected)); 618 | actual = escapeHtml(QUnit.jsDump.parse(actual)); 619 | var output = message + ''; 620 | if (actual != expected) { 621 | output += ''; 622 | output += ''; 623 | } 624 | if (!result) { 625 | var source = sourceFromStacktrace(); 626 | if (source) { 627 | details.source = source; 628 | output += ''; 629 | } 630 | } 631 | output += "
    Expected:
    ' + expected + '
    Result:
    ' + actual + '
    Diff:
    ' + QUnit.diff(expected, actual) +'
    Source:
    ' + escapeHtml(source) + '
    "; 632 | 633 | QUnit.log(details); 634 | 635 | config.current.assertions.push({ 636 | result: !!result, 637 | message: output 638 | }); 639 | }, 640 | 641 | url: function( params ) { 642 | params = extend( extend( {}, QUnit.urlParams ), params ); 643 | var querystring = "?", 644 | key; 645 | for ( key in params ) { 646 | querystring += encodeURIComponent( key ) + "=" + 647 | encodeURIComponent( params[ key ] ) + "&"; 648 | } 649 | return window.location.pathname + querystring.slice( 0, -1 ); 650 | }, 651 | 652 | // Logging callbacks; all receive a single argument with the listed properties 653 | // run test/logs.html for any related changes 654 | begin: function() {}, 655 | // done: { failed, passed, total, runtime } 656 | done: function() {}, 657 | // log: { result, actual, expected, message } 658 | log: function() {}, 659 | // testStart: { name } 660 | testStart: function() {}, 661 | // testDone: { name, failed, passed, total } 662 | testDone: function() {}, 663 | // moduleStart: { name } 664 | moduleStart: function() {}, 665 | // moduleDone: { name, failed, passed, total } 666 | moduleDone: function() {} 667 | }); 668 | 669 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 670 | config.autorun = true; 671 | } 672 | 673 | addEvent(window, "load", function() { 674 | QUnit.begin({}); 675 | 676 | // Initialize the config, saving the execution queue 677 | var oldconfig = extend({}, config); 678 | QUnit.init(); 679 | extend(config, oldconfig); 680 | 681 | config.blocking = false; 682 | 683 | var userAgent = id("qunit-userAgent"); 684 | if ( userAgent ) { 685 | userAgent.innerHTML = navigator.userAgent; 686 | } 687 | var banner = id("qunit-header"); 688 | if ( banner ) { 689 | banner.innerHTML = ' ' + banner.innerHTML + ' ' + 690 | '' + 691 | ''; 692 | addEvent( banner, "change", function( event ) { 693 | var params = {}; 694 | params[ event.target.name ] = event.target.checked ? true : undefined; 695 | window.location = QUnit.url( params ); 696 | }); 697 | } 698 | 699 | var toolbar = id("qunit-testrunner-toolbar"); 700 | if ( toolbar ) { 701 | var filter = document.createElement("input"); 702 | filter.type = "checkbox"; 703 | filter.id = "qunit-filter-pass"; 704 | addEvent( filter, "click", function() { 705 | var ol = document.getElementById("qunit-tests"); 706 | if ( filter.checked ) { 707 | ol.className = ol.className + " hidepass"; 708 | } else { 709 | var tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; 710 | ol.className = tmp.replace(/ hidepass /, " "); 711 | } 712 | if ( defined.sessionStorage ) { 713 | if (filter.checked) { 714 | sessionStorage.setItem("qunit-filter-passed-tests", "true"); 715 | } else { 716 | sessionStorage.removeItem("qunit-filter-passed-tests"); 717 | } 718 | } 719 | }); 720 | if ( defined.sessionStorage && sessionStorage.getItem("qunit-filter-passed-tests") ) { 721 | filter.checked = true; 722 | var ol = document.getElementById("qunit-tests"); 723 | ol.className = ol.className + " hidepass"; 724 | } 725 | toolbar.appendChild( filter ); 726 | 727 | var label = document.createElement("label"); 728 | label.setAttribute("for", "qunit-filter-pass"); 729 | label.innerHTML = "Hide passed tests"; 730 | toolbar.appendChild( label ); 731 | } 732 | 733 | var main = id('qunit-fixture'); 734 | if ( main ) { 735 | config.fixture = main.innerHTML; 736 | } 737 | 738 | if (config.autostart) { 739 | QUnit.start(); 740 | } 741 | }); 742 | 743 | function done() { 744 | config.autorun = true; 745 | 746 | // Log the last module results 747 | if ( config.currentModule ) { 748 | QUnit.moduleDone( { 749 | name: config.currentModule, 750 | failed: config.moduleStats.bad, 751 | passed: config.moduleStats.all - config.moduleStats.bad, 752 | total: config.moduleStats.all 753 | } ); 754 | } 755 | 756 | var banner = id("qunit-banner"), 757 | tests = id("qunit-tests"), 758 | runtime = +new Date - config.started, 759 | passed = config.stats.all - config.stats.bad, 760 | html = [ 761 | 'Tests completed in ', 762 | runtime, 763 | ' milliseconds.
    ', 764 | '', 765 | passed, 766 | ' tests of ', 767 | config.stats.all, 768 | ' passed, ', 769 | config.stats.bad, 770 | ' failed.' 771 | ].join(''); 772 | 773 | if ( banner ) { 774 | banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); 775 | } 776 | 777 | if ( tests ) { 778 | id( "qunit-testresult" ).innerHTML = html; 779 | } 780 | 781 | if ( typeof document !== "undefined" && document.title ) { 782 | // show ✖ for good, ✔ for bad suite result in title 783 | // use escape sequences in case file gets loaded with non-utf-8-charset 784 | document.title = (config.stats.bad ? "\u2716" : "\u2714") + " " + document.title; 785 | } 786 | 787 | QUnit.done( { 788 | failed: config.stats.bad, 789 | passed: passed, 790 | total: config.stats.all, 791 | runtime: runtime 792 | } ); 793 | } 794 | 795 | function validTest( name ) { 796 | var filter = config.filter, 797 | run = false; 798 | 799 | if ( !filter ) { 800 | return true; 801 | } 802 | 803 | var not = filter.charAt( 0 ) === "!"; 804 | if ( not ) { 805 | filter = filter.slice( 1 ); 806 | } 807 | 808 | if ( name.indexOf( filter ) !== -1 ) { 809 | return !not; 810 | } 811 | 812 | if ( not ) { 813 | run = true; 814 | } 815 | 816 | return run; 817 | } 818 | 819 | // so far supports only Firefox, Chrome and Opera (buggy) 820 | // could be extended in the future to use something like https://github.com/csnover/TraceKit 821 | function sourceFromStacktrace() { 822 | try { 823 | throw new Error(); 824 | } catch ( e ) { 825 | if (e.stacktrace) { 826 | // Opera 827 | return e.stacktrace.split("\n")[6]; 828 | } else if (e.stack) { 829 | // Firefox, Chrome 830 | return e.stack.split("\n")[4]; 831 | } 832 | } 833 | } 834 | 835 | function escapeHtml(s) { 836 | if (!s) { 837 | return ""; 838 | } 839 | s = s + ""; 840 | return s.replace(/[\&"<>\\]/g, function(s) { 841 | switch(s) { 842 | case "&": return "&"; 843 | case "\\": return "\\\\"; 844 | case '"': return '\"'; 845 | case "<": return "<"; 846 | case ">": return ">"; 847 | default: return s; 848 | } 849 | }); 850 | } 851 | 852 | function synchronize( callback ) { 853 | config.queue.push( callback ); 854 | 855 | if ( config.autorun && !config.blocking ) { 856 | process(); 857 | } 858 | } 859 | 860 | function process() { 861 | var start = (new Date()).getTime(); 862 | 863 | while ( config.queue.length && !config.blocking ) { 864 | if ( config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate) ) { 865 | config.queue.shift()(); 866 | } else { 867 | window.setTimeout( process, 13 ); 868 | break; 869 | } 870 | } 871 | if (!config.blocking && !config.queue.length) { 872 | done(); 873 | } 874 | } 875 | 876 | function saveGlobal() { 877 | config.pollution = []; 878 | 879 | if ( config.noglobals ) { 880 | for ( var key in window ) { 881 | config.pollution.push( key ); 882 | } 883 | } 884 | } 885 | 886 | function checkPollution( name ) { 887 | var old = config.pollution; 888 | saveGlobal(); 889 | 890 | var newGlobals = diff( config.pollution, old ); 891 | if ( newGlobals.length > 0 ) { 892 | ok( false, "Introduced global variable(s): " + newGlobals.join(", ") ); 893 | } 894 | 895 | var deletedGlobals = diff( old, config.pollution ); 896 | if ( deletedGlobals.length > 0 ) { 897 | ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") ); 898 | } 899 | } 900 | 901 | // returns a new Array with the elements that are in a but not in b 902 | function diff( a, b ) { 903 | var result = a.slice(); 904 | for ( var i = 0; i < result.length; i++ ) { 905 | for ( var j = 0; j < b.length; j++ ) { 906 | if ( result[i] === b[j] ) { 907 | result.splice(i, 1); 908 | i--; 909 | break; 910 | } 911 | } 912 | } 913 | return result; 914 | } 915 | 916 | function fail(message, exception, callback) { 917 | if ( typeof console !== "undefined" && console.error && console.warn ) { 918 | console.error(message); 919 | console.error(exception); 920 | console.warn(callback.toString()); 921 | 922 | } else if ( window.opera && opera.postError ) { 923 | opera.postError(message, exception, callback.toString); 924 | } 925 | } 926 | 927 | function extend(a, b) { 928 | for ( var prop in b ) { 929 | if ( b[prop] === undefined ) { 930 | delete a[prop]; 931 | } else { 932 | a[prop] = b[prop]; 933 | } 934 | } 935 | 936 | return a; 937 | } 938 | 939 | function addEvent(elem, type, fn) { 940 | if ( elem.addEventListener ) { 941 | elem.addEventListener( type, fn, false ); 942 | } else if ( elem.attachEvent ) { 943 | elem.attachEvent( "on" + type, fn ); 944 | } else { 945 | fn(); 946 | } 947 | } 948 | 949 | function id(name) { 950 | return !!(typeof document !== "undefined" && document && document.getElementById) && 951 | document.getElementById( name ); 952 | } 953 | 954 | // Test for equality any JavaScript type. 955 | // Discussions and reference: http://philrathe.com/articles/equiv 956 | // Test suites: http://philrathe.com/tests/equiv 957 | // Author: Philippe Rathé 958 | QUnit.equiv = function () { 959 | 960 | var innerEquiv; // the real equiv function 961 | var callers = []; // stack to decide between skip/abort functions 962 | var parents = []; // stack to avoiding loops from circular referencing 963 | 964 | // Call the o related callback with the given arguments. 965 | function bindCallbacks(o, callbacks, args) { 966 | var prop = QUnit.objectType(o); 967 | if (prop) { 968 | if (QUnit.objectType(callbacks[prop]) === "function") { 969 | return callbacks[prop].apply(callbacks, args); 970 | } else { 971 | return callbacks[prop]; // or undefined 972 | } 973 | } 974 | } 975 | 976 | var callbacks = function () { 977 | 978 | // for string, boolean, number and null 979 | function useStrictEquality(b, a) { 980 | if (b instanceof a.constructor || a instanceof b.constructor) { 981 | // to catch short annotaion VS 'new' annotation of a declaration 982 | // e.g. var i = 1; 983 | // var j = new Number(1); 984 | return a == b; 985 | } else { 986 | return a === b; 987 | } 988 | } 989 | 990 | return { 991 | "string": useStrictEquality, 992 | "boolean": useStrictEquality, 993 | "number": useStrictEquality, 994 | "null": useStrictEquality, 995 | "undefined": useStrictEquality, 996 | 997 | "nan": function (b) { 998 | return isNaN(b); 999 | }, 1000 | 1001 | "date": function (b, a) { 1002 | return QUnit.objectType(b) === "date" && a.valueOf() === b.valueOf(); 1003 | }, 1004 | 1005 | "regexp": function (b, a) { 1006 | return QUnit.objectType(b) === "regexp" && 1007 | a.source === b.source && // the regex itself 1008 | a.global === b.global && // and its modifers (gmi) ... 1009 | a.ignoreCase === b.ignoreCase && 1010 | a.multiline === b.multiline; 1011 | }, 1012 | 1013 | // - skip when the property is a method of an instance (OOP) 1014 | // - abort otherwise, 1015 | // initial === would have catch identical references anyway 1016 | "function": function () { 1017 | var caller = callers[callers.length - 1]; 1018 | return caller !== Object && 1019 | typeof caller !== "undefined"; 1020 | }, 1021 | 1022 | "array": function (b, a) { 1023 | var i, j, loop; 1024 | var len; 1025 | 1026 | // b could be an object literal here 1027 | if ( ! (QUnit.objectType(b) === "array")) { 1028 | return false; 1029 | } 1030 | 1031 | len = a.length; 1032 | if (len !== b.length) { // safe and faster 1033 | return false; 1034 | } 1035 | 1036 | //track reference to avoid circular references 1037 | parents.push(a); 1038 | for (i = 0; i < len; i++) { 1039 | loop = false; 1040 | for(j=0;j= 0) { 1185 | type = "array"; 1186 | } else { 1187 | type = typeof obj; 1188 | } 1189 | return type; 1190 | }, 1191 | separator:function() { 1192 | return this.multiline ? this.HTML ? '
    ' : '\n' : this.HTML ? ' ' : ' '; 1193 | }, 1194 | indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing 1195 | if ( !this.multiline ) 1196 | return ''; 1197 | var chr = this.indentChar; 1198 | if ( this.HTML ) 1199 | chr = chr.replace(/\t/g,' ').replace(/ /g,' '); 1200 | return Array( this._depth_ + (extra||0) ).join(chr); 1201 | }, 1202 | up:function( a ) { 1203 | this._depth_ += a || 1; 1204 | }, 1205 | down:function( a ) { 1206 | this._depth_ -= a || 1; 1207 | }, 1208 | setParser:function( name, parser ) { 1209 | this.parsers[name] = parser; 1210 | }, 1211 | // The next 3 are exposed so you can use them 1212 | quote:quote, 1213 | literal:literal, 1214 | join:join, 1215 | // 1216 | _depth_: 1, 1217 | // This is the list of parsers, to modify them, use jsDump.setParser 1218 | parsers:{ 1219 | window: '[Window]', 1220 | document: '[Document]', 1221 | error:'[ERROR]', //when no parser is found, shouldn't happen 1222 | unknown: '[Unknown]', 1223 | 'null':'null', 1224 | 'undefined':'undefined', 1225 | 'function':function( fn ) { 1226 | var ret = 'function', 1227 | name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE 1228 | if ( name ) 1229 | ret += ' ' + name; 1230 | ret += '('; 1231 | 1232 | ret = [ ret, QUnit.jsDump.parse( fn, 'functionArgs' ), '){'].join(''); 1233 | return join( ret, QUnit.jsDump.parse(fn,'functionCode'), '}' ); 1234 | }, 1235 | array: array, 1236 | nodelist: array, 1237 | arguments: array, 1238 | object:function( map ) { 1239 | var ret = [ ]; 1240 | QUnit.jsDump.up(); 1241 | for ( var key in map ) 1242 | ret.push( QUnit.jsDump.parse(key,'key') + ': ' + QUnit.jsDump.parse(map[key]) ); 1243 | QUnit.jsDump.down(); 1244 | return join( '{', ret, '}' ); 1245 | }, 1246 | node:function( node ) { 1247 | var open = QUnit.jsDump.HTML ? '<' : '<', 1248 | close = QUnit.jsDump.HTML ? '>' : '>'; 1249 | 1250 | var tag = node.nodeName.toLowerCase(), 1251 | ret = open + tag; 1252 | 1253 | for ( var a in QUnit.jsDump.DOMAttrs ) { 1254 | var val = node[QUnit.jsDump.DOMAttrs[a]]; 1255 | if ( val ) 1256 | ret += ' ' + a + '=' + QUnit.jsDump.parse( val, 'attribute' ); 1257 | } 1258 | return ret + close + open + '/' + tag + close; 1259 | }, 1260 | functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function 1261 | var l = fn.length; 1262 | if ( !l ) return ''; 1263 | 1264 | var args = Array(l); 1265 | while ( l-- ) 1266 | args[l] = String.fromCharCode(97+l);//97 is 'a' 1267 | return ' ' + args.join(', ') + ' '; 1268 | }, 1269 | key:quote, //object calls it internally, the key part of an item in a map 1270 | functionCode:'[code]', //function calls it internally, it's the content of the function 1271 | attribute:quote, //node calls it internally, it's an html attribute value 1272 | string:quote, 1273 | date:quote, 1274 | regexp:literal, //regex 1275 | number:literal, 1276 | 'boolean':literal 1277 | }, 1278 | DOMAttrs:{//attributes to dump from nodes, name=>realName 1279 | id:'id', 1280 | name:'name', 1281 | 'class':'className' 1282 | }, 1283 | HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) 1284 | indentChar:' ',//indentation unit 1285 | multiline:true //if true, items in a collection, are separated by a \n, else just a space. 1286 | }; 1287 | 1288 | return jsDump; 1289 | })(); 1290 | 1291 | // from Sizzle.js 1292 | function getText( elems ) { 1293 | var ret = "", elem; 1294 | 1295 | for ( var i = 0; elems[i]; i++ ) { 1296 | elem = elems[i]; 1297 | 1298 | // Get the text from text nodes and CDATA nodes 1299 | if ( elem.nodeType === 3 || elem.nodeType === 4 ) { 1300 | ret += elem.nodeValue; 1301 | 1302 | // Traverse everything else, except comment nodes 1303 | } else if ( elem.nodeType !== 8 ) { 1304 | ret += getText( elem.childNodes ); 1305 | } 1306 | } 1307 | 1308 | return ret; 1309 | }; 1310 | 1311 | /* 1312 | * Javascript Diff Algorithm 1313 | * By John Resig (http://ejohn.org/) 1314 | * Modified by Chu Alan "sprite" 1315 | * 1316 | * Released under the MIT license. 1317 | * 1318 | * More Info: 1319 | * http://ejohn.org/projects/javascript-diff-algorithm/ 1320 | * 1321 | * Usage: QUnit.diff(expected, actual) 1322 | * 1323 | * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over" 1324 | */ 1325 | QUnit.diff = (function() { 1326 | function diff(o, n){ 1327 | var ns = new Object(); 1328 | var os = new Object(); 1329 | 1330 | for (var i = 0; i < n.length; i++) { 1331 | if (ns[n[i]] == null) 1332 | ns[n[i]] = { 1333 | rows: new Array(), 1334 | o: null 1335 | }; 1336 | ns[n[i]].rows.push(i); 1337 | } 1338 | 1339 | for (var i = 0; i < o.length; i++) { 1340 | if (os[o[i]] == null) 1341 | os[o[i]] = { 1342 | rows: new Array(), 1343 | n: null 1344 | }; 1345 | os[o[i]].rows.push(i); 1346 | } 1347 | 1348 | for (var i in ns) { 1349 | if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { 1350 | n[ns[i].rows[0]] = { 1351 | text: n[ns[i].rows[0]], 1352 | row: os[i].rows[0] 1353 | }; 1354 | o[os[i].rows[0]] = { 1355 | text: o[os[i].rows[0]], 1356 | row: ns[i].rows[0] 1357 | }; 1358 | } 1359 | } 1360 | 1361 | for (var i = 0; i < n.length - 1; i++) { 1362 | if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && 1363 | n[i + 1] == o[n[i].row + 1]) { 1364 | n[i + 1] = { 1365 | text: n[i + 1], 1366 | row: n[i].row + 1 1367 | }; 1368 | o[n[i].row + 1] = { 1369 | text: o[n[i].row + 1], 1370 | row: i + 1 1371 | }; 1372 | } 1373 | } 1374 | 1375 | for (var i = n.length - 1; i > 0; i--) { 1376 | if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && 1377 | n[i - 1] == o[n[i].row - 1]) { 1378 | n[i - 1] = { 1379 | text: n[i - 1], 1380 | row: n[i].row - 1 1381 | }; 1382 | o[n[i].row - 1] = { 1383 | text: o[n[i].row - 1], 1384 | row: i - 1 1385 | }; 1386 | } 1387 | } 1388 | 1389 | return { 1390 | o: o, 1391 | n: n 1392 | }; 1393 | } 1394 | 1395 | return function(o, n){ 1396 | o = o.replace(/\s+$/, ''); 1397 | n = n.replace(/\s+$/, ''); 1398 | var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/)); 1399 | 1400 | var str = ""; 1401 | 1402 | var oSpace = o.match(/\s+/g); 1403 | if (oSpace == null) { 1404 | oSpace = [" "]; 1405 | } 1406 | else { 1407 | oSpace.push(" "); 1408 | } 1409 | var nSpace = n.match(/\s+/g); 1410 | if (nSpace == null) { 1411 | nSpace = [" "]; 1412 | } 1413 | else { 1414 | nSpace.push(" "); 1415 | } 1416 | 1417 | if (out.n.length == 0) { 1418 | for (var i = 0; i < out.o.length; i++) { 1419 | str += '' + out.o[i] + oSpace[i] + ""; 1420 | } 1421 | } 1422 | else { 1423 | if (out.n[0].text == null) { 1424 | for (n = 0; n < out.o.length && out.o[n].text == null; n++) { 1425 | str += '' + out.o[n] + oSpace[n] + ""; 1426 | } 1427 | } 1428 | 1429 | for (var i = 0; i < out.n.length; i++) { 1430 | if (out.n[i].text == null) { 1431 | str += '' + out.n[i] + nSpace[i] + ""; 1432 | } 1433 | else { 1434 | var pre = ""; 1435 | 1436 | for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) { 1437 | pre += '' + out.o[n] + oSpace[n] + ""; 1438 | } 1439 | str += " " + out.n[i].text + nSpace[i] + pre; 1440 | } 1441 | } 1442 | } 1443 | 1444 | return str; 1445 | }; 1446 | })(); 1447 | 1448 | })(this); --------------------------------------------------------------------------------