summary refs log tree commit diff
path: root/webclient/components
diff options
context:
space:
mode:
authorMark Haines <mark.haines@matrix.org>2014-08-26 16:07:05 +0100
committerMark Haines <mark.haines@matrix.org>2014-08-26 16:07:05 +0100
commit4b63b06cad3e72e3d3dee66d3d6c7801e4a7abaf (patch)
tree37b368d6039d986d70f153fb69d6345d1019d64a /webclient/components
parentfix a few pyflakes errors (diff)
parentFix pyflakes errors (diff)
downloadsynapse-4b63b06cad3e72e3d3dee66d3d6c7801e4a7abaf.tar.xz
Merge branch 'develop' into storage_transactions
Conflicts:
	synapse/api/auth.py
	synapse/handlers/room.py
	synapse/storage/__init__.py
Diffstat (limited to 'webclient/components')
-rw-r--r--webclient/components/fileUpload/file-upload-service.js144
-rw-r--r--webclient/components/matrix/event-handler-service.js30
-rw-r--r--webclient/components/matrix/event-stream-service.js51
-rw-r--r--webclient/components/matrix/matrix-service.js110
-rw-r--r--webclient/components/matrix/presence-service.js113
-rw-r--r--webclient/components/utilities/utilities-service.js110
6 files changed, 492 insertions, 66 deletions
diff --git a/webclient/components/fileUpload/file-upload-service.js b/webclient/components/fileUpload/file-upload-service.js
index 65c24f309c..5f01478fd1 100644
--- a/webclient/components/fileUpload/file-upload-service.js
+++ b/webclient/components/fileUpload/file-upload-service.js
@@ -20,19 +20,20 @@
 /*
  * Upload an HTML5 file to a server
  */
-angular.module('mFileUpload', [])
-.service('mFileUpload', ['matrixService', '$q', function (matrixService, $q) {
+angular.module('mFileUpload', ['matrixService', 'mUtilities'])
+.service('mFileUpload', ['$q', 'matrixService', 'mUtilities', function ($q, matrixService, mUtilities) {
         
     /*
-     * Upload an HTML5 file to a server and returned a promise
+     * Upload an HTML5 file or blob to a server and returned a promise
      * that will provide the URL of the uploaded file.
+     * @param {File|Blob} file the file data to send
      */
-    this.uploadFile = function(file, body) {
+    this.uploadFile = function(file) {
         var deferred = $q.defer();
         console.log("Uploading " + file.name + "... to /matrix/content");
-        matrixService.uploadContent(file, body).then(
+        matrixService.uploadContent(file).then(
             function(response) {
-                var content_url = location.origin + "/matrix/content/" + response.data.content_token;
+                var content_url = response.data.content_token;
                 console.log("   -> Successfully uploaded! Available at " + content_url);
                 deferred.resolve(content_url);
             },
@@ -44,4 +45,135 @@ angular.module('mFileUpload', [])
         
         return deferred.promise;
     };
+    
+    /*
+     * Upload an image file plus generate a thumbnail of it and upload it so that
+     * we will have all information to fulfill an image message request data.
+     * @param {File} imageFile the imageFile to send
+     * @param {Integer} thumbnailSize the max side size of the thumbnail to create
+     * @returns {promise} A promise that will be resolved by a image message object
+     *   ready to be send with the Matrix API
+     */
+    this.uploadImageAndThumbnail = function(imageFile, thumbnailSize) {
+        var self = this;
+        var deferred = $q.defer();
+
+        console.log("uploadImageAndThumbnail " + imageFile.name + " - thumbnailSize: " + thumbnailSize);
+
+        // The message structure that will be returned in the promise
+        var imageMessage = {
+            msgtype: "m.image",
+            url: undefined,
+            body: {
+                size: undefined,
+                w: undefined,
+                h: undefined,
+                mimetype: undefined
+            },
+            thumbnail_url: undefined,
+            thumbnail_info: {
+                size: undefined,
+                w: undefined,
+                h: undefined,
+                mimetype: undefined
+            }
+        };
+
+        // First, get the image size
+        mUtilities.getImageSize(imageFile).then(
+            function(size) {
+                console.log("image size: " + JSON.stringify(size));
+
+                // The final operation: send imageFile
+                var uploadImage = function() {
+                    self.uploadFile(imageFile).then(
+                        function(url) {
+                            // Update message metadata
+                            imageMessage.url = url;
+                            imageMessage.body = {
+                                size: imageFile.size,
+                                w: size.width,
+                                h: size.height,
+                                mimetype: imageFile.type
+                            };
+
+                            // If there is no thumbnail (because the original image is smaller than thumbnailSize),
+                            // reuse the original image info for thumbnail data
+                            if (!imageMessage.thumbnail_url) {
+                                imageMessage.thumbnail_url = imageMessage.url;
+                                imageMessage.thumbnail_info = imageMessage.body;
+                            }
+
+                            // We are done
+                            deferred.resolve(imageMessage);
+                        },
+                        function(error) {
+                            console.log("      -> Can't upload image");
+                            deferred.reject(error); 
+                        }
+                    );
+                };
+
+                // Create a thumbnail if the image size exceeds thumbnailSize
+                if (Math.max(size.width, size.height) > thumbnailSize) {
+                    console.log("    Creating thumbnail...");
+                    mUtilities.resizeImage(imageFile, thumbnailSize).then(
+                        function(thumbnailBlob) {
+
+                            // Get its size
+                            mUtilities.getImageSize(thumbnailBlob).then(
+                                function(thumbnailSize) {
+                                    console.log("      -> Thumbnail size: " + JSON.stringify(thumbnailSize));
+
+                                    // Upload it to the server
+                                    self.uploadFile(thumbnailBlob).then(
+                                        function(thumbnailUrl) {
+
+                                            // Update image message data
+                                            imageMessage.thumbnail_url = thumbnailUrl;
+                                            imageMessage.thumbnail_info = {
+                                                size: thumbnailBlob.size,
+                                                w: thumbnailSize.width,
+                                                h: thumbnailSize.height,
+                                                mimetype: thumbnailBlob.type
+                                            };
+
+                                            // Then, upload the original image
+                                            uploadImage();
+                                        },
+                                        function(error) {
+                                            console.log("      -> Can't upload thumbnail");
+                                            deferred.reject(error); 
+                                        }
+                                    );
+                                },
+                                function(error) {
+                                    console.log("      -> Failed to get thumbnail size");
+                                    deferred.reject(error); 
+                                }
+                            );
+
+                        },
+                        function(error) {
+                            console.log("      -> Failed to create thumbnail: " + error);
+                            deferred.reject(error); 
+                        }
+                    );
+                }
+                else {
+                    // No need of thumbnail
+                    console.log("   Thumbnail is not required");
+                    uploadImage();
+                }
+
+            },
+            function(error) {
+                console.log("   -> Failed to get image size");
+                deferred.reject(error); 
+            }
+        );
+
+        return deferred.promise;
+    };
+
 }]);
diff --git a/webclient/components/matrix/event-handler-service.js b/webclient/components/matrix/event-handler-service.js
index b8529895fe..6ea0f58bc5 100644
--- a/webclient/components/matrix/event-handler-service.js
+++ b/webclient/components/matrix/event-handler-service.js
@@ -35,6 +35,8 @@ angular.module('eventHandlerService', [])
     $rootScope.events = {
         rooms: {}, // will contain roomId: { messages:[], members:{userid1: event} }
     };
+
+    $rootScope.presence = {};
     
     var initRoom = function(room_id) {
         if (!(room_id in $rootScope.events.rooms)) {
@@ -44,12 +46,14 @@ angular.module('eventHandlerService', [])
             $rootScope.events.rooms[room_id].members = {};
         }
     }
+
+    var reInitRoom = function(room_id) {
+        $rootScope.events.rooms[room_id] = {};
+        $rootScope.events.rooms[room_id].messages = [];
+        $rootScope.events.rooms[room_id].members = {};
+    }
     
     var handleMessage = function(event, isLiveEvent) {
-        if ("membership_target" in event.content) {
-            event.user_id = event.content.membership_target;
-        }
-        
         initRoom(event.room_id);
         
         if (isLiveEvent) {
@@ -69,11 +73,23 @@ angular.module('eventHandlerService', [])
     
     var handleRoomMember = function(event, isLiveEvent) {
         initRoom(event.room_id);
+        
+        // add membership changes as if they were a room message if something interesting changed
+        if (event.content.prev !== event.content.membership) {
+            if (isLiveEvent) {
+                $rootScope.events.rooms[event.room_id].messages.push(event);
+            }
+            else {
+                $rootScope.events.rooms[event.room_id].messages.unshift(event);
+            }
+        }
+        
         $rootScope.events.rooms[event.room_id].members[event.user_id] = event;
         $rootScope.$broadcast(MEMBER_EVENT, event, isLiveEvent);
     };
     
     var handlePresence = function(event, isLiveEvent) {
+        $rootScope.presence[event.content.user_id] = event;
         $rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent);
     };
     
@@ -107,6 +123,10 @@ angular.module('eventHandlerService', [])
             for (var i=0; i<events.length; i++) {
                 this.handleEvent(events[i], isLiveEvents);
             }
-        }
+        },
+
+        reInitRoom: function(room_id) {
+            reInitRoom(room_id);
+        },
     };
 }]);
diff --git a/webclient/components/matrix/event-stream-service.js b/webclient/components/matrix/event-stream-service.js
index a446fad5d4..a1a98b2a36 100644
--- a/webclient/components/matrix/event-stream-service.js
+++ b/webclient/components/matrix/event-stream-service.js
@@ -48,11 +48,12 @@ angular.module('eventStreamService', [])
     var saveStreamSettings = function() {
         localStorage.setItem("streamSettings", JSON.stringify(settings));
     };
-    
-    var startEventStream = function() {
+
+    var doEventStream = function(deferred) {
         settings.shouldPoll = true;
         settings.isActive = true;
-        var deferred = $q.defer();
+        deferred = deferred || $q.defer();
+
         // run the stream from the latest token
         matrixService.getEventStream(settings.from, TIMEOUT_MS).then(
             function(response) {
@@ -63,13 +64,16 @@ angular.module('eventStreamService', [])
                 
                 settings.from = response.data.end;
                 
-                console.log("[EventStream] Got response from "+settings.from+" to "+response.data.end);
+                console.log(
+                    "[EventStream] Got response from "+settings.from+
+                    " to "+response.data.end
+                );
                 eventHandlerService.handleEvents(response.data.chunk, true);
                 
                 deferred.resolve(response);
                 
                 if (settings.shouldPoll) {
-                    $timeout(startEventStream, 0);
+                    $timeout(doEventStream, 0);
                 }
                 else {
                     console.log("[EventStream] Stopping poll.");
@@ -83,13 +87,48 @@ angular.module('eventStreamService', [])
                 deferred.reject(error);
                 
                 if (settings.shouldPoll) {
-                    $timeout(startEventStream, ERR_TIMEOUT_MS);
+                    $timeout(doEventStream, ERR_TIMEOUT_MS);
                 }
                 else {
                     console.log("[EventStream] Stopping polling.");
                 }
             }
         );
+
+        return deferred.promise;
+    }    
+
+    var startEventStream = function() {
+        settings.shouldPoll = true;
+        settings.isActive = true;
+        var deferred = $q.defer();
+
+        // FIXME: We are discarding all the messages.
+        matrixService.rooms().then(
+            function(response) {
+                var rooms = response.data.rooms;
+                for (var i = 0; i < rooms.length; ++i) {
+                    var room = rooms[i];
+                    if ("state" in room) {
+                        for (var j = 0; j < room.state.length; ++j) {
+                            eventHandlerService.handleEvents(room.state[j], false);
+                        }
+                    }
+                }
+
+                var presence = response.data.presence;
+                for (var i = 0; i < presence.length; ++i) {
+                    eventHandlerService.handleEvent(presence[i], false);
+                }
+
+                settings.from = response.data.end
+                doEventStream(deferred);        
+            },
+            function(error) {
+                $scope.feedback = "Failure: " + error.data;
+            }
+        );
+
         return deferred.promise;
     };
     
diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js
index cd37a0c234..237dd6d8a0 100644
--- a/webclient/components/matrix/matrix-service.js
+++ b/webclient/components/matrix/matrix-service.js
@@ -61,16 +61,23 @@ angular.module('matrixService', [])
         return doBaseRequest(config.homeserver, method, path, params, data, undefined);
     };
 
-    var doBaseRequest = function(baseUrl, method, path, params, data, headers) {
-        return $http({
+    var doBaseRequest = function(baseUrl, method, path, params, data, headers, $httpParams) {
+
+        var request = {
             method: method,
             url: baseUrl + path,
             params: params,
             data: data,
             headers: headers
-        });
-    };
+        };
 
+        // Add additional $http parameters
+        if ($httpParams) {
+            angular.extend(request, $httpParams);
+        }
+
+        return $http(request);
+    };
 
     return {
         /****** Home server API ******/
@@ -108,19 +115,7 @@ angular.module('matrixService', [])
 
         // Joins a room
         join: function(room_id) {
-            // The REST path spec
-            var path = "/rooms/$room_id/members/$user_id/state";
-
-            // Like the cmd client, escape room ids
-            room_id = encodeURIComponent(room_id);
-
-            // Customize it
-            path = path.replace("$room_id", room_id);
-            path = path.replace("$user_id", config.user_id);
-
-            return doRequest("PUT", path, undefined, {
-                 membership: "join"
-            });
+            return this.membershipChange(room_id, undefined, "join");
         },
 
         joinAlias: function(room_alias) {
@@ -134,34 +129,27 @@ angular.module('matrixService', [])
 
         // Invite a user to a room
         invite: function(room_id, user_id) {
-            // The REST path spec
-            var path = "/rooms/$room_id/members/$user_id/state";
-
-            // Like the cmd client, escape room ids
-            room_id = encodeURIComponent(room_id);
-
-            // Customize it
-            path = path.replace("$room_id", room_id);
-            path = path.replace("$user_id", user_id);
-
-            return doRequest("PUT", path, undefined, {
-                 membership: "invite"
-            });
+            return this.membershipChange(room_id, user_id, "invite");
         },
 
         // Leaves a room
         leave: function(room_id) {
-            // The REST path spec
-            var path = "/rooms/$room_id/members/$user_id/state";
+            return this.membershipChange(room_id, undefined, "leave");
+        },
 
-            // Like the cmd client, escape room ids
-            room_id = encodeURIComponent(room_id);
+        membershipChange: function(room_id, user_id, membershipValue) {
+            // The REST path spec
+            var path = "/rooms/$room_id/$membership";
+            path = path.replace("$room_id", encodeURIComponent(room_id));
+            path = path.replace("$membership", encodeURIComponent(membershipValue));
 
-            // Customize it
-            path = path.replace("$room_id", room_id);
-            path = path.replace("$user_id", config.user_id);
+            var data = {};
+            if (user_id !== undefined) {
+                data = { user_id: user_id };
+            }
 
-            return doRequest("DELETE", path, undefined, undefined);
+            // TODO: Use PUT with transaction IDs
+            return doRequest("POST", path, undefined, data);
         },
 
         // Retrieves the room ID corresponding to a room alias
@@ -302,17 +290,25 @@ angular.module('matrixService', [])
         },
 
         // hit the Identity Server for a 3PID request.
-        linkEmail: function(email) {
+        linkEmail: function(email, clientSecret, sendAttempt) {
             var path = "/matrix/identity/api/v1/validate/email/requestToken"
-            var data = "clientSecret=abc123&email=" + encodeURIComponent(email);
+            var data = "clientSecret="+clientSecret+"&email=" + encodeURIComponent(email)+"&sendAttempt="+sendAttempt;
             var headers = {};
             headers["Content-Type"] = "application/x-www-form-urlencoded";
             return doBaseRequest(config.identityServer, "POST", path, {}, data, headers); 
         },
 
-        authEmail: function(userId, tokenId, code) {
+        authEmail: function(clientSecret, tokenId, code) {
             var path = "/matrix/identity/api/v1/validate/email/submitToken";
-            var data = "token="+code+"&mxId="+encodeURIComponent(userId)+"&tokenId="+tokenId;
+            var data = "token="+code+"&sid="+tokenId+"&clientSecret="+clientSecret;
+            var headers = {};
+            headers["Content-Type"] = "application/x-www-form-urlencoded";
+            return doBaseRequest(config.identityServer, "POST", path, {}, data, headers);
+        },
+
+        bindEmail: function(userId, tokenId, clientSecret) {
+            var path = "/matrix/identity/api/v1/3pid/bind";
+            var data = "mxid="+encodeURIComponent(userId)+"&sid="+tokenId+"&clientSecret="+clientSecret;
             var headers = {};
             headers["Content-Type"] = "application/x-www-form-urlencoded";
             return doBaseRequest(config.identityServer, "POST", path, {}, data, headers); 
@@ -326,7 +322,17 @@ angular.module('matrixService', [])
             var params = {
                 access_token: config.access_token
             };
-            return doBaseRequest(config.homeserver, "POST", path, params, file, headers);
+
+            // If the file is actually a Blob object, prevent $http from JSON-stringified it before sending
+            // (Equivalent to jQuery ajax processData = false)
+            var $httpParams;
+            if (file instanceof Blob) {
+                $httpParams = {
+                    transformRequest: angular.identity
+                };
+            }
+
+            return doBaseRequest(config.homeserver, "POST", path, params, file, headers, $httpParams);
         },
         
         // start listening on /events
@@ -352,6 +358,23 @@ angular.module('matrixService', [])
                 return false;
             }
         },
+        
+        // Enum of presence state
+        presence: {
+            offline: "offline",
+            unavailable: "unavailable",
+            online: "online",
+            free_for_chat: "free_for_chat"
+        },
+        
+        // Set the logged in user presence state
+        setUserPresence: function(presence) {
+            var path = "/presence/$user_id/status";
+            path = path.replace("$user_id", config.user_id);
+            return doRequest("PUT", path, undefined, {
+                state: presence
+            });
+        },
 
         /****** Permanent storage of user information ******/
         
@@ -375,6 +398,7 @@ angular.module('matrixService', [])
         // Set a new config (Use saveConfig to actually store it permanently)
         setConfig: function(newConfig) {
             config = newConfig;
+            console.log("new IS: "+config.identityServer);
         },
         
         // Commits config into permanent storage
diff --git a/webclient/components/matrix/presence-service.js b/webclient/components/matrix/presence-service.js
new file mode 100644
index 0000000000..6a1edcaf43
--- /dev/null
+++ b/webclient/components/matrix/presence-service.js
@@ -0,0 +1,113 @@
+/*
+ Copyright 2014 matrix.org
+ 
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ 
+ http://www.apache.org/licenses/LICENSE-2.0
+ 
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+'use strict';
+
+/*
+ * This service tracks user activity on the page to determine his presence state.
+ * Any state change will be sent to the Home Server.
+ */
+angular.module('mPresence', [])
+.service('mPresence', ['$timeout', 'matrixService', function ($timeout, matrixService) {
+
+    // Time in ms after that a user is considered as offline/away
+    var OFFLINE_TIME = 5 * 60000; // 5 mins
+    
+    // The current presence state
+    var state = undefined;
+
+    var self =this;
+    var timer;
+    
+    /**
+     * Start listening the user activity to evaluate his presence state.
+     * Any state change will be sent to the Home Server.
+     */
+    this.start = function() {
+        if (undefined === state) {
+            // The user is online if he moves the mouser or press a key
+            document.onmousemove = resetTimer;
+            document.onkeypress = resetTimer;
+            
+            resetTimer();
+        }
+    };
+    
+    /**
+     * Stop tracking user activity
+     */
+    this.stop = function() {
+        if (timer) {
+            $timeout.cancel(timer);
+            timer = undefined;
+        }
+        state = undefined;
+    };
+    
+    /**
+     * Get the current presence state.
+     * @returns {matrixService.presence} the presence state
+     */
+    this.getState = function() {
+        return state;
+    };
+    
+    /**
+     * Set the presence state.
+     * If the state has changed, the Home Server will be notified.
+     * @param {matrixService.presence} newState the new presence state
+     */
+    this.setState = function(newState) {
+        if (newState !== state) {
+            console.log("mPresence - New state: " + newState);
+
+            state = newState;
+
+            // Inform the HS on the new user state
+            matrixService.setUserPresence(state).then(
+                function() {
+
+                },
+                function(error) {
+                    console.log("mPresence - Failed to send new presence state: " + JSON.stringify(error));
+                });
+        }
+    };
+    
+    /**
+     * Callback called when the user made no action on the page for OFFLINE_TIME ms.
+     * @private
+     */
+    function onOfflineTimerFire() {
+        self.setState(matrixService.presence.offline);
+    }
+
+    /**
+     * Callback called when the user made an action on the page
+     * @private
+     */
+    function resetTimer() {
+        // User is still here
+        self.setState(matrixService.presence.online);
+        
+        // Re-arm the timer
+        $timeout.cancel(timer);
+        timer = $timeout(onOfflineTimerFire, OFFLINE_TIME);
+    }    
+
+}]);
+
+
diff --git a/webclient/components/utilities/utilities-service.js b/webclient/components/utilities/utilities-service.js
index fc0ee580dc..3df2f04458 100644
--- a/webclient/components/utilities/utilities-service.js
+++ b/webclient/components/utilities/utilities-service.js
@@ -22,8 +22,8 @@
 angular.module('mUtilities', [])
 .service('mUtilities', ['$q', function ($q) {
     /*
-     * Gets the size of an image
-     * @param {File} imageFile the file containing the image
+     * Get the size of an image
+     * @param {File|Blob} imageFile the file containing the image
      * @returns {promise} A promise that will be resolved by an object with 2 members:
      *   width & height
      */
@@ -38,10 +38,15 @@ angular.module('mUtilities', [])
             img.src = e.target.result;
             
             // Once ready, returns its size
-            deferred.resolve({
-                width: img.width,
-                height: img.height
-            });
+            img.onload = function() {
+                deferred.resolve({
+                    width: img.width,
+                    height: img.height
+                });
+            };
+            img.onerror = function(e) {
+                deferred.reject(e);
+            };
         };
         reader.onerror = function(e) {
             deferred.reject(e);
@@ -50,4 +55,97 @@ angular.module('mUtilities', [])
         
         return deferred.promise;
     };
+
+    /*
+     * Resize the image to fit in a square of the side maxSize. 
+     * The aspect ratio is kept. The returned image data uses JPEG compression.
+     * Source: http://hacks.mozilla.org/2011/01/how-to-develop-a-html5-image-uploader/
+     * @param {File} imageFile the file containing the image 
+     * @param {Integer} maxSize the max side size 
+     * @returns {promise} A promise that will be resolved by a Blob object containing
+     *   the resized image data
+     */
+    this.resizeImage = function(imageFile, maxSize) {
+        var self = this;
+        var deferred = $q.defer();
+
+        var canvas = document.createElement("canvas");
+
+        var img = document.createElement("img");
+        var reader = new FileReader();  
+        reader.onload = function(e) {
+
+            img.src = e.target.result;
+            
+            // Once ready, returns its size
+            img.onload = function() {
+                var ctx = canvas.getContext("2d");
+                ctx.drawImage(img, 0, 0);
+
+                var MAX_WIDTH = maxSize;
+                var MAX_HEIGHT = maxSize;
+                var width = img.width;
+                var height = img.height;
+
+                if (width > height) {
+                    if (width > MAX_WIDTH) {
+                        height *= MAX_WIDTH / width;
+                        width = MAX_WIDTH;
+                    }
+                } else {
+                    if (height > MAX_HEIGHT) {
+                        width *= MAX_HEIGHT / height;
+                        height = MAX_HEIGHT;
+                    }
+                }
+                canvas.width = width;
+                canvas.height = height;
+                var ctx = canvas.getContext("2d");
+                ctx.drawImage(img, 0, 0, width, height);
+
+                // Extract image data in the same format as the original one.
+                // The 0.7 compression value will work with formats that supports it like JPEG.
+                var dataUrl = canvas.toDataURL(imageFile.type, 0.7); 
+                deferred.resolve(self.dataURItoBlob(dataUrl));
+            };
+            img.onerror = function(e) {
+                deferred.reject(e);
+            };
+        };
+        reader.onerror = function(e) {
+            deferred.reject(e);
+        };
+        reader.readAsDataURL(imageFile);
+
+        return deferred.promise;
+    };
+
+    /*
+     * Convert a dataURI string to a blob 
+     * Source: http://stackoverflow.com/a/17682951
+     * @param {String} dataURI the dataURI can be a base64 encoded string or an URL encoded string.
+     * @returns {Blob} the blob
+     */
+    this.dataURItoBlob = function(dataURI) {
+        // convert base64 to raw binary data held in a string
+        // doesn't handle URLEncoded DataURIs
+        var byteString;
+        if (dataURI.split(',')[0].indexOf('base64') >= 0)
+            byteString = atob(dataURI.split(',')[1]);
+        else
+            byteString = unescape(dataURI.split(',')[1]);
+        // separate out the mime component
+        var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
+
+        // write the bytes of the string to an ArrayBuffer
+        var ab = new ArrayBuffer(byteString.length);
+        var ia = new Uint8Array(ab);
+        for (var i = 0; i < byteString.length; i++) {
+            ia[i] = byteString.charCodeAt(i);
+        }
+
+        // write the ArrayBuffer to a blob, and you're done
+        return new Blob([ab],{type: mimeString});
+    };
+
 }]);
\ No newline at end of file