diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 681502c38..861574862 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -16,7 +16,3 @@ @import "bootstrap-sprockets"; @import "bootstrap"; - -.cover-bg-blue{background-color:#2275CA;} -.cover-text-white{color:#FFFFFF;} -.a {color:#FFFFF;} \ No newline at end of file diff --git a/app/models/custom_widget.rb b/app/models/custom_widget.rb new file mode 100644 index 000000000..b4698bb5d --- /dev/null +++ b/app/models/custom_widget.rb @@ -0,0 +1,5 @@ +class CustomWidget < Widget + attribute :image, :reference + attribute :headline, :string + attribute :text, :string +end diff --git a/app/models/paragraph_widget.rb b/app/models/paragraph_widget.rb deleted file mode 100644 index d2075638e..000000000 --- a/app/models/paragraph_widget.rb +++ /dev/null @@ -1,6 +0,0 @@ -class ParagraphWidget < Widget - attribute :image, :reference - attribute :headline, :string - attribute :text, :string - attribute :color, :enum, values: %w[red green blue], default: 'blue' -end diff --git a/app/views/paragraph_widget/details.html.erb b/app/views/custom_widget/details.html.erb similarity index 56% rename from app/views/paragraph_widget/details.html.erb rename to app/views/custom_widget/details.html.erb index a63407642..7c9f3fc40 100644 --- a/app/views/paragraph_widget/details.html.erb +++ b/app/views/custom_widget/details.html.erb @@ -1,5 +1,5 @@ <%= scrivito_medium_dialog do %> - <%= scrivito_details_for ParagraphWidget.description_for_editor do %> + <%= scrivito_details_for CustomWidget.description_for_editor do %> A cover with head, text and image <% end %> <% end %> diff --git a/app/views/custom_widget/show.html.erb b/app/views/custom_widget/show.html.erb new file mode 100644 index 000000000..9e1fa1705 --- /dev/null +++ b/app/views/custom_widget/show.html.erb @@ -0,0 +1,115 @@ +
子どものためのプログラミング道場
+CoderDojo は7〜17歳の子どもを対象にしたプログラミング道場です。2011年にアイルランドで始まり、世界では66カ国・1,150の道場、日本では全国に67以上の道場があります (2016年12月現在)。
+各道場では、主に次のような内容を学ぶことができます。
+各道場で学べる内容はそれぞれ異なりますが、CoderDojo の雰囲気は一様です。「どんな雰囲気か知りたい」という方は、下記のガイダンス資料をご参考にしてください。
+さらに詳しい道場の様子については、「CoderDojoとは? 運営者に話を聞いてみた! (バケモノ.jp)」や「子どものための無料プログラミング道場 CoderDojo をはじめてみませんか (ICT教育ニュース)」を読んでみてください。それぞれインタビュー形式で話が構成されているので、より具体的なイメージが掴めるかなと思います :)
+{{ mentor.description }}
+下記ウェブサイトから、より詳細な情報や、最新の情報にアクセスできます。
+ +本家サイトである CoderDojo (英語) では世界中の活動の様子が見れます。また、Facebook の CoderDojo Japan では日本全国の活動の様子を知ることができます。
+「これまでの成り立ちを知りたい」「地元で CoderDojo を立ち上げてみたい」といった場合には CoderDojo Kata を参照してみてください。
+取材や支援などのお問い合わせについては、下記フォームからお気軽にご連絡ください :)
+ メールをご希望の場合は、担当の安川要平まで (yohei@coderdojo.jp) ご連絡ください。
Available in Cloud Code and Node.js only. + *
+ */ + Parse.Cloud.useMasterKey = function() { + Parse._useMasterKey = true; + }; + } + + /** + * Returns prefix for Storage keys used by this instance of Parse. + * @param {String} path The relative suffix to append to it. + * null or undefined is treated as the empty string. + * @return {String} The full key name. + */ + Parse._getParsePath = function(path) { + if (!Parse.applicationId) { + throw "You need to call Parse.initialize before using Parse."; + } + if (!path) { + path = ""; + } + if (!Parse._.isString(path)) { + throw "Tried to get a Storage path that wasn't a String."; + } + if (path[0] === "/") { + path = path.substring(1); + } + return "Parse/" + Parse.applicationId + "/" + path; + }; + + /** + * Returns a Promise that is resolved with the unique string for this app on + * this machine. + * Gets reset when Storage is cleared. + */ + Parse._installationId = null; + Parse._getInstallationId = function() { + // See if it's cached in RAM. + if (Parse._installationId) { + return Parse.Promise.as(Parse._installationId); + } + + // Try to get it from Storage. + var path = Parse._getParsePath("installationId"); + return (Parse.Storage.getItemAsync(path) + .then(function(value) { + Parse._installationId = value; + + if (!Parse._installationId || Parse._installationId === "") { + // It wasn't in Storage, so create a new one. + var hexOctet = function() { + return ( + Math.floor((1+Math.random())*0x10000).toString(16).substring(1) + ); + }; + Parse._installationId = ( + hexOctet() + hexOctet() + "-" + + hexOctet() + "-" + + hexOctet() + "-" + + hexOctet() + "-" + + hexOctet() + hexOctet() + hexOctet()); + return Parse.Storage.setItemAsync(path, Parse._installationId); + } + + return Parse.Promise.as(Parse._installationId); + }) + ); + }; + + Parse._parseDate = function(iso8601) { + var regexp = new RegExp( + "^([0-9]{1,4})-([0-9]{1,2})-([0-9]{1,2})" + "T" + + "([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2})" + + "(.([0-9]+))?" + "Z$"); + var match = regexp.exec(iso8601); + if (!match) { + return null; + } + + var year = match[1] || 0; + var month = (match[2] || 1) - 1; + var day = match[3] || 0; + var hour = match[4] || 0; + var minute = match[5] || 0; + var second = match[6] || 0; + var milli = match[8] || 0; + + return new Date(Date.UTC(year, month, day, hour, minute, second, milli)); + }; + + Parse._ajaxIE8 = function(method, url, data) { + var promise = new Parse.Promise(); + var xdr = new XDomainRequest(); + xdr.onload = function() { + var response; + try { + response = JSON.parse(xdr.responseText); + } catch (e) { + promise.reject(e); + } + if (response) { + promise.resolve(response); + } + }; + xdr.onerror = xdr.ontimeout = function() { + // Let's fake a real error message. + var fakeResponse = { + responseText: JSON.stringify({ + code: Parse.Error.X_DOMAIN_REQUEST, + error: "IE's XDomainRequest does not supply error info." + }) + }; + promise.reject(fakeResponse); + }; + xdr.onprogress = function() {}; + xdr.open(method, url); + xdr.send(data); + return promise; + }; + + Parse._useXDomainRequest = function() { + if (typeof(XDomainRequest) !== "undefined") { + // We're in IE 8+. + if ('withCredentials' in new XMLHttpRequest()) { + // We're in IE 10+. + return false; + } + return true; + } + return false; + }; + + + Parse._ajax = function(method, url, data, success, error) { + var options = { + success: success, + error: error + }; + + if (Parse._useXDomainRequest()) { + return Parse._ajaxIE8(method, url, data)._thenRunCallbacks(options); + } + + var promise = new Parse.Promise(); + var attempts = 0; + + var dispatch = function() { + var handled = false; + var xhr = new Parse.XMLHttpRequest(); + + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + if (handled) { + return; + } + handled = true; + + if (xhr.status >= 200 && xhr.status < 300) { + var response; + try { + response = JSON.parse(xhr.responseText); + } catch (e) { + promise.reject(e); + } + if (response) { + promise.resolve(response, xhr.status, xhr); + } + } else if (xhr.status >= 500) { // Retry on 5XX + if (++attempts < 5) { + // Exponentially-growing delay + var delay = Math.round( + Math.random() * 125 * Math.pow(2, attempts) + ); + setTimeout(dispatch, delay); + } else { + // After 5 retries, fail + promise.reject(xhr); + } + } else { + promise.reject(xhr); + } + } + }; + + xhr.open(method, url, true); + xhr.setRequestHeader('Content-Type', 'text/plain'); // avoid pre-flight. + if (Parse._isNode) { + // Add a special user agent just for request from node.js. + xhr.setRequestHeader("User-Agent", + "Parse/" + Parse.VERSION + + " (NodeJS " + process.versions.node + ")"); + } + xhr.send(data); + }; + + dispatch(); + return promise._thenRunCallbacks(options); + }; + + // A self-propagating extend function. + Parse._extend = function(protoProps, classProps) { + var child = inherits(this, protoProps, classProps); + child.extend = this.extend; + return child; + }; + + /** + * Options: + * route: is classes, users, login, etc. + * objectId: null if there is no associated objectId. + * method: the http method for the REST API. + * dataObject: the payload as an object, or null if there is none. + * useMasterKey: overrides whether to use the master key if set. + * @ignore + */ + Parse._request = function(options) { + var route = options.route; + var className = options.className; + var objectId = options.objectId; + var method = options.method; + var useMasterKey = options.useMasterKey; + var sessionToken = options.sessionToken; + var dataObject = options.data; + + if (!Parse.applicationId) { + throw "You must specify your applicationId using Parse.initialize."; + } + + if (!Parse.javaScriptKey && !Parse.masterKey) { + throw "You must specify a key using Parse.initialize."; + } + + + if (route !== "batch" && + route !== "classes" && + route !== "events" && + route !== "files" && + route !== "functions" && + route !== "login" && + route !== "logout" && + route !== "push" && + route !== "requestPasswordReset" && + route !== "rest_verify_analytics" && + route !== "users" && + route !== "jobs" && + route !== "config" && + route !== "sessions" && + route !== "upgradeToRevocableSession") { + throw "Bad route: '" + route + "'."; + } + + var url = Parse.serverURL; + if (url.charAt(url.length - 1) !== "/") { + url += "/"; + } + url += "1/" + route; + if (className) { + url += "/" + className; + } + if (objectId) { + url += "/" + objectId; + } + + dataObject = Parse._.clone(dataObject || {}); + if (method !== "POST") { + dataObject._method = method; + method = "POST"; + } + + if (Parse._.isUndefined(useMasterKey)) { + useMasterKey = Parse._useMasterKey; + } + + dataObject._ApplicationId = Parse.applicationId; + if (!useMasterKey) { + dataObject._JavaScriptKey = Parse.javaScriptKey; + } else if (!Parse.masterKey) { + throw new Error('Cannot use the Master Key, it has not been provided.'); + } else { + dataObject._MasterKey = Parse.masterKey; + } + + dataObject._ClientVersion = Parse.VERSION; + + return Parse._getInstallationId().then(function(iid) { + dataObject._InstallationId = iid; + + if (sessionToken) { + return Parse.Promise.as({ _sessionToken: sessionToken }); + } + if (!Parse.User._canUseCurrentUser()) { + return Parse.Promise.as(null); + } + + return Parse.User._currentAsync(); + }).then(function(currentUser) { + if (currentUser && currentUser._sessionToken) { + dataObject._SessionToken = currentUser._sessionToken; + } + + if (Parse.User._isRevocableSessionEnabled) { + dataObject._RevocableSession = '1'; + } + + var data = JSON.stringify(dataObject); + + return Parse._ajax(method, url, data); + }).then(null, function(response) { + // Transform the error into an instance of Parse.Error by trying to parse + // the error string as JSON. + var error; + if (response && response.responseText) { + try { + var errorJSON = JSON.parse(response.responseText); + error = new Parse.Error(errorJSON.code, errorJSON.error); + } catch (e) { + // If we fail to parse the error text, that's okay. + error = new Parse.Error( + Parse.Error.INVALID_JSON, + "Received an error with invalid JSON from Parse: " + + response.responseText); + } + } else { + error = new Parse.Error( + Parse.Error.CONNECTION_FAILED, + "XMLHttpRequest failed: " + JSON.stringify(response)); + } + // By explicitly returning a rejected Promise, this will work with + // either jQuery or Promises/A semantics. + return Parse.Promise.error(error); + }); + }; + + // Helper function to get a value from a Backbone object as a property + // or as a function. + Parse._getValue = function(object, prop) { + if (!(object && object[prop])) { + return null; + } + return Parse._.isFunction(object[prop]) ? object[prop]() : object[prop]; + }; + + /** + * Converts a value in a Parse Object into the appropriate representation. + * This is the JS equivalent of Java's Parse.maybeReferenceAndEncode(Object) + * if seenObjects is falsey. Otherwise any Parse.Objects not in + * seenObjects will be fully embedded rather than encoded + * as a pointer. This array will be used to prevent going into an infinite + * loop because we have circular references. If seenObjects + * is set, then none of the Parse Objects that are serialized can be dirty. + */ + Parse._encode = function(value, seenObjects, disallowObjects) { + var _ = Parse._; + if (value instanceof Parse.Object) { + if (disallowObjects) { + throw "Parse.Objects not allowed here"; + } + if (!seenObjects || _.include(seenObjects, value) || !value._hasData) { + return value._toPointer(); + } + if (!value.dirty()) { + seenObjects = seenObjects.concat(value); + return Parse._encode(value._toFullJSON(seenObjects), + seenObjects, + disallowObjects); + } + throw "Tried to save an object with a pointer to a new, unsaved object."; + } + if (value instanceof Parse.ACL) { + return value.toJSON(); + } + if (_.isDate(value)) { + if (isNaN(value)) { + throw new Error('Cannot encode invalid Date'); + } + return { "__type": "Date", "iso": value.toJSON() }; + } + if (value instanceof Parse.GeoPoint) { + return value.toJSON(); + } + if (_.isArray(value)) { + return _.map(value, function(x) { + return Parse._encode(x, seenObjects, disallowObjects); + }); + } + if (_.isRegExp(value)) { + return value.source; + } + if (value instanceof Parse.Relation) { + return value.toJSON(); + } + if (value instanceof Parse.Op) { + return value.toJSON(); + } + if (value instanceof Parse.File) { + if (!value.url()) { + throw "Tried to save an object containing an unsaved file."; + } + return { + __type: "File", + name: value.name(), + url: value.url() + }; + } + if (_.isObject(value)) { + var output = {}; + Parse._objectEach(value, function(v, k) { + output[k] = Parse._encode(v, seenObjects, disallowObjects); + }); + return output; + } + return value; + }; + + /** + * The inverse function of Parse._encode. + * TODO: make decode not mutate value. + */ + Parse._decode = function(key, value) { + var _ = Parse._; + if (!_.isObject(value)) { + return value; + } + if (_.isArray(value)) { + Parse._arrayEach(value, function(v, k) { + value[k] = Parse._decode(k, v); + }); + return value; + } + if (value instanceof Parse.Object) { + return value; + } + if (value instanceof Parse.File) { + return value; + } + if (value instanceof Parse.Op) { + return value; + } + if (value.__op) { + return Parse.Op._decode(value); + } + if (value.__type === "Pointer" && value.className) { + var pointer = Parse.Object._create(value.className); + pointer._finishFetch({ objectId: value.objectId }, false); + return pointer; + } + if (value.__type === "Object" && value.className) { + // It's an Object included in a query result. + var className = value.className; + delete value.__type; + delete value.className; + var object = Parse.Object._create(className); + object._finishFetch(value, true); + return object; + } + if (value.__type === "Date") { + return Parse._parseDate(value.iso); + } + if (value.__type === "GeoPoint") { + return new Parse.GeoPoint({ + latitude: value.latitude, + longitude: value.longitude + }); + } + if (key === "ACL") { + if (value instanceof Parse.ACL) { + return value; + } + return new Parse.ACL(value); + } + if (value.__type === "Relation") { + var relation = new Parse.Relation(null, key); + relation.targetClassName = value.className; + return relation; + } + if (value.__type === "File") { + var file = new Parse.File(value.name); + file._url = value.url; + return file; + } + Parse._objectEach(value, function(v, k) { + value[k] = Parse._decode(k, v); + }); + return value; + }; + + Parse._arrayEach = Parse._.each; + + /** + * Does a deep traversal of every item in object, calling func on every one. + * @param {Object} object The object or array to traverse deeply. + * @param {Function} func The function to call for every item. It will + * be passed the item as an argument. If it returns a truthy value, that + * value will replace the item in its parent container. + * @returns {} the result of calling func on the top-level object itself. + */ + Parse._traverse = function(object, func, seen) { + if (object instanceof Parse.Object) { + seen = seen || []; + if (Parse._.indexOf(seen, object) >= 0) { + // We've already visited this object in this call. + return; + } + seen.push(object); + Parse._traverse(object.attributes, func, seen); + return func(object); + } + if (object instanceof Parse.Relation || object instanceof Parse.File) { + // Nothing needs to be done, but we don't want to recurse into the + // object's parent infinitely, so we catch this case. + return func(object); + } + if (Parse._.isArray(object)) { + Parse._.each(object, function(child, index) { + var newChild = Parse._traverse(child, func, seen); + if (newChild) { + object[index] = newChild; + } + }); + return func(object); + } + if (Parse._.isObject(object)) { + Parse._each(object, function(child, key) { + var newChild = Parse._traverse(child, func, seen); + if (newChild) { + object[key] = newChild; + } + }); + return func(object); + } + return func(object); + }; + + /** + * This is like _.each, except: + * * it doesn't work for so-called array-like objects, + * * it does work for dictionaries with a "length" attribute. + */ + Parse._objectEach = Parse._each = function(obj, callback) { + var _ = Parse._; + if (_.isObject(obj)) { + _.each(_.keys(obj), function(key) { + callback(obj[key], key); + }); + } else { + _.each(obj, callback); + } + }; + + // Helper function to check null or undefined. + Parse._isNullOrUndefined = function(x) { + return Parse._.isNull(x) || Parse._.isUndefined(x); + }; +}(this)); + +/* global require: false, localStorage: false */ +(function(root) { + root.Parse = root.Parse || {}; + var Parse = root.Parse; + + var Storage = { + async: false, + }; + + var hasLocalStorage = (typeof localStorage !== 'undefined'); + if (hasLocalStorage) { + try { + localStorage.setItem('supported', true); + localStorage.removeItem('supported'); + } catch(e) { + hasLocalStorage = false; + } + } + if (hasLocalStorage) { + Storage.getItem = function(path) { + return localStorage.getItem(path); + }; + + Storage.setItem = function(path, value) { + return localStorage.setItem(path, value); + }; + + Storage.removeItem = function(path) { + return localStorage.removeItem(path); + }; + + Storage.clear = function() { + return localStorage.clear(); + }; + } else if (typeof require === 'function') { + var AsyncStorage; + try { + AsyncStorage = eval("require('AsyncStorage')"); // jshint ignore:line + + Storage.async = true; + + Storage.getItemAsync = function(path) { + var p = new Parse.Promise(); + AsyncStorage.getItem(path, function(err, value) { + if (err) { + p.reject(err); + } else { + p.resolve(value); + } + }); + return p; + }; + + Storage.setItemAsync = function(path, value) { + var p = new Parse.Promise(); + AsyncStorage.setItem(path, value, function(err) { + if (err) { + p.reject(err); + } else { + p.resolve(value); + } + }); + return p; + }; + + Storage.removeItemAsync = function(path) { + var p = new Parse.Promise(); + AsyncStorage.removeItem(path, function(err) { + if (err) { + p.reject(err); + } else { + p.resolve(); + } + }); + return p; + }; + + Storage.clear = function() { + AsyncStorage.clear(); + }; + } catch (e) { } + } + if (!Storage.async && !Storage.getItem) { + var memMap = Storage.inMemoryMap = {}; + Storage.getItem = function(path) { + if (memMap.hasOwnProperty(path)) { + return memMap[path]; + } + return null; + }; + + Storage.setItem = function(path, value) { + memMap[path] = String(value); + }; + + Storage.removeItem = function(path) { + delete memMap[path]; + }; + + Storage.clear = function() { + for (var key in memMap) { + if (memMap.hasOwnProperty(key)) { + delete memMap[key]; + } + } + }; + } + + // We can use synchronous methods from async scenarios, but not vice-versa + if (!Storage.async) { + Storage.getItemAsync = function(path) { + return Parse.Promise.as( + Storage.getItem(path) + ); + }; + + Storage.setItemAsync = function(path, value) { + Storage.setItem(path, value); + return Parse.Promise.as(value); + }; + + Storage.removeItemAsync = function(path) { + return Parse.Promise.as( + Storage.removeItem(path) + ); + }; + } + + Parse.Storage = Storage; + +})(this); + +(function(root) { + root.Parse = root.Parse || {}; + var Parse = root.Parse; + var _ = Parse._; + + /** + * @namespace Provides an interface to Parse's logging and analytics backend. + */ + Parse.Analytics = Parse.Analytics || {}; + + _.extend(Parse.Analytics, /** @lends Parse.Analytics */ { + /** + * Tracks the occurrence of a custom event with additional dimensions. + * Parse will store a data point at the time of invocation with the given + * event name. + * + * Dimensions will allow segmentation of the occurrences of this custom + * event. Keys and values should be {@code String}s, and will throw + * otherwise. + * + * To track a user signup along with additional metadata, consider the + * following: + *+ * var dimensions = { + * gender: 'm', + * source: 'web', + * dayType: 'weekend' + * }; + * Parse.Analytics.track('signup', dimensions); + *+ * + * There is a default limit of 8 dimensions per event tracked. + * + * @param {String} name The name of the custom event to report to Parse as + * having happened. + * @param {Object} dimensions The dictionary of information by which to + * segment this event. + * @param {Object} options A Backbone-style callback object. + * @return {Parse.Promise} A promise that is resolved when the round-trip + * to the server completes. + */ + track: function(name, dimensions, options) { + name = name || ''; + name = name.replace(/^\s*/, ''); + name = name.replace(/\s*$/, ''); + if (name.length === 0) { + throw 'A name for the custom event must be provided'; + } + + _.each(dimensions, function(val, key) { + if (!_.isString(key) || !_.isString(val)) { + throw 'track() dimensions expects keys and values of type "string".'; + } + }); + + options = options || {}; + return Parse._request({ + route: 'events', + className: name, + method: 'POST', + data: { dimensions: dimensions } + })._thenRunCallbacks(options); + } + }); +}(this)); + +(function(root) { + root.Parse = root.Parse || {}; + var Parse = root.Parse; + var _ = Parse._; + + /** + * @class Parse.Config is a local representation of configuration data that + * can be set from the Parse dashboard. + */ + Parse.Config = function() { + this.attributes = {}; + this._escapedAttributes = {}; + }; + + /** + * Retrieves the most recently-fetched configuration object, either from + * memory or from local storage if necessary. + * + * @return {Parse.Config} The most recently-fetched Parse.Config if it + * exists, else an empty Parse.Config. + */ + Parse.Config.current = function() { + if (Parse.Config._currentConfig) { + return Parse.Config._currentConfig; + } + + var config = new Parse.Config(); + + if (Parse.Storage.async) { + return config; + } + + var configData = Parse.Storage.getItem(Parse._getParsePath( + Parse.Config._CURRENT_CONFIG_KEY)); + + if (configData) { + config._finishFetch(JSON.parse(configData)); + Parse.Config._currentConfig = config; + } + return config; + }; + + /** + * Gets a new configuration object from the server. + * @param {Object} options A Backbone-style options object. + * Valid options are:
Parse.Error
.
+ * @param {String} message A detailed description of the error.
+ * @class
+ *
+ * Class used for all objects passed to error callbacks.
+ */ + Parse.Error = function(code, message) { + this.code = code; + this.message = message; + }; + + _.extend(Parse.Error, /** @lends Parse.Error */ { + /** + * Error code indicating some error other than those enumerated here. + * @constant + */ + OTHER_CAUSE: -1, + + /** + * Error code indicating that something has gone wrong with the server. + * If you get this error code, it is Parse's fault. Contact us at + * https://parse.com/help + * @constant + */ + INTERNAL_SERVER_ERROR: 1, + + /** + * Error code indicating the connection to the Parse servers failed. + * @constant + */ + CONNECTION_FAILED: 100, + + /** + * Error code indicating the specified object doesn't exist. + * @constant + */ + OBJECT_NOT_FOUND: 101, + + /** + * Error code indicating you tried to query with a datatype that doesn't + * support it, like exact matching an array or object. + * @constant + */ + INVALID_QUERY: 102, + + /** + * Error code indicating a missing or invalid classname. Classnames are + * case-sensitive. They must start with a letter, and a-zA-Z0-9_ are the + * only valid characters. + * @constant + */ + INVALID_CLASS_NAME: 103, + + /** + * Error code indicating an unspecified object id. + * @constant + */ + MISSING_OBJECT_ID: 104, + + /** + * Error code indicating an invalid key name. Keys are case-sensitive. They + * must start with a letter, and a-zA-Z0-9_ are the only valid characters. + * @constant + */ + INVALID_KEY_NAME: 105, + + /** + * Error code indicating a malformed pointer. You should not see this unless + * you have been mucking about changing internal Parse code. + * @constant + */ + INVALID_POINTER: 106, + + /** + * Error code indicating that badly formed JSON was received upstream. This + * either indicates you have done something unusual with modifying how + * things encode to JSON, or the network is failing badly. + * @constant + */ + INVALID_JSON: 107, + + /** + * Error code indicating that the feature you tried to access is only + * available internally for testing purposes. + * @constant + */ + COMMAND_UNAVAILABLE: 108, + + /** + * You must call Parse.initialize before using the Parse library. + * @constant + */ + NOT_INITIALIZED: 109, + + /** + * Error code indicating that a field was set to an inconsistent type. + * @constant + */ + INCORRECT_TYPE: 111, + + /** + * Error code indicating an invalid channel name. A channel name is either + * an empty string (the broadcast channel) or contains only a-zA-Z0-9_ + * characters and starts with a letter. + * @constant + */ + INVALID_CHANNEL_NAME: 112, + + /** + * Error code indicating that push is misconfigured. + * @constant + */ + PUSH_MISCONFIGURED: 115, + + /** + * Error code indicating that the object is too large. + * @constant + */ + OBJECT_TOO_LARGE: 116, + + /** + * Error code indicating that the operation isn't allowed for clients. + * @constant + */ + OPERATION_FORBIDDEN: 119, + + /** + * Error code indicating the result was not found in the cache. + * @constant + */ + CACHE_MISS: 120, + + /** + * Error code indicating that an invalid key was used in a nested + * JSONObject. + * @constant + */ + INVALID_NESTED_KEY: 121, + + /** + * Error code indicating that an invalid filename was used for ParseFile. + * A valid file name contains only a-zA-Z0-9_. characters and is between 1 + * and 128 characters. + * @constant + */ + INVALID_FILE_NAME: 122, + + /** + * Error code indicating an invalid ACL was provided. + * @constant + */ + INVALID_ACL: 123, + + /** + * Error code indicating that the request timed out on the server. Typically + * this indicates that the request is too expensive to run. + * @constant + */ + TIMEOUT: 124, + + /** + * Error code indicating that the email address was invalid. + * @constant + */ + INVALID_EMAIL_ADDRESS: 125, + + /** + * Error code indicating a missing content type. + * @constant + */ + MISSING_CONTENT_TYPE: 126, + + /** + * Error code indicating a missing content length. + * @constant + */ + MISSING_CONTENT_LENGTH: 127, + + /** + * Error code indicating an invalid content length. + * @constant + */ + INVALID_CONTENT_LENGTH: 128, + + /** + * Error code indicating a file that was too large. + * @constant + */ + FILE_TOO_LARGE: 129, + + /** + * Error code indicating an error saving a file. + * @constant + */ + FILE_SAVE_ERROR: 130, + + /** + * Error code indicating that a unique field was given a value that is + * already taken. + * @constant + */ + DUPLICATE_VALUE: 137, + + /** + * Error code indicating that a role's name is invalid. + * @constant + */ + INVALID_ROLE_NAME: 139, + + /** + * Error code indicating that an application quota was exceeded. Upgrade to + * resolve. + * @constant + */ + EXCEEDED_QUOTA: 140, + + /** + * Error code indicating that a Cloud Code script failed. + * @constant + */ + SCRIPT_FAILED: 141, + + /** + * Error code indicating that a Cloud Code validation failed. + * @constant + */ + VALIDATION_ERROR: 142, + + /** + * Error code indicating that invalid image data was provided. + * @constant + */ + INVALID_IMAGE_DATA: 150, + + /** + * Error code indicating an unsaved file. + * @constant + */ + UNSAVED_FILE_ERROR: 151, + + /** + * Error code indicating an invalid push time. + */ + INVALID_PUSH_TIME_ERROR: 152, + + /** + * Error code indicating an error deleting a file. + * @constant + */ + FILE_DELETE_ERROR: 153, + + /** + * Error code indicating that the application has exceeded its request + * limit. + * @constant + */ + REQUEST_LIMIT_EXCEEDED: 155, + + /** + * Error code indicating an invalid event name. + */ + INVALID_EVENT_NAME: 160, + + /** + * Error code indicating that the username is missing or empty. + * @constant + */ + USERNAME_MISSING: 200, + + /** + * Error code indicating that the password is missing or empty. + * @constant + */ + PASSWORD_MISSING: 201, + + /** + * Error code indicating that the username has already been taken. + * @constant + */ + USERNAME_TAKEN: 202, + + /** + * Error code indicating that the email has already been taken. + * @constant + */ + EMAIL_TAKEN: 203, + + /** + * Error code indicating that the email is missing, but must be specified. + * @constant + */ + EMAIL_MISSING: 204, + + /** + * Error code indicating that a user with the specified email was not found. + * @constant + */ + EMAIL_NOT_FOUND: 205, + + /** + * Error code indicating that a user object without a valid session could + * not be altered. + * @constant + */ + SESSION_MISSING: 206, + + /** + * Error code indicating that a user can only be created through signup. + * @constant + */ + MUST_CREATE_USER_THROUGH_SIGNUP: 207, + + /** + * Error code indicating that an an account being linked is already linked + * to another user. + * @constant + */ + ACCOUNT_ALREADY_LINKED: 208, + + /** + * Error code indicating that the current session token is invalid. + * @constant + */ + INVALID_SESSION_TOKEN: 209, + + /** + * Error code indicating that a user cannot be linked to an account because + * that account's id could not be found. + * @constant + */ + LINKED_ID_MISSING: 250, + + /** + * Error code indicating that a user with a linked (e.g. Facebook) account + * has an invalid session. + * @constant + */ + INVALID_LINKED_SESSION: 251, + + /** + * Error code indicating that a service being linked (e.g. Facebook or + * Twitter) is unsupported. + * @constant + */ + UNSUPPORTED_SERVICE: 252, + + /** + * Error code indicating that there were multiple errors. Aggregate errors + * have an "errors" property, which is an array of error objects with more + * detail about each error that occurred. + * @constant + */ + AGGREGATE_ERROR: 600, + + /** + * Error code indicating the client was unable to read an input file. + * @constant + */ + FILE_READ_ERROR: 601, + + /** + * Error code indicating a real error code is unavailable because + * we had to use an XDomainRequest object to allow CORS requests in + * Internet Explorer, which strips the body from HTTP responses that have + * a non-2XX status code. + * @constant + */ + X_DOMAIN_REQUEST: 602 + }); + +}(this)); + +/*global _: false */ +(function() { + var root = this; + var Parse = (root.Parse || (root.Parse = {})); + var eventSplitter = /\s+/; + var slice = Array.prototype.slice; + + /** + * @class + * + *Parse.Events is a fork of Backbone's Events module, provided for your + * convenience.
+ * + *A module that can be mixed in to any object in order to provide + * it with custom events. You may bind callback functions to an event + * with `on`, or remove these functions with `off`. + * Triggering an event fires all callbacks in the order that `on` was + * called. + * + *
+ * var object = {}; + * _.extend(object, Parse.Events); + * object.on('expand', function(){ alert('expanded'); }); + * object.trigger('expand');+ * + *
For more information, see the + * Backbone + * documentation.
+ */ + Parse.Events = { + /** + * Bind one or more space separated events, `events`, to a `callback` + * function. Passing `"all"` will bind the callback to all events fired. + */ + on: function(events, callback, context) { + + var calls, event, node, tail, list; + if (!callback) { + return this; + } + events = events.split(eventSplitter); + calls = this._callbacks || (this._callbacks = {}); + + // Create an immutable callback list, allowing traversal during + // modification. The tail is an empty object that will always be used + // as the next node. + event = events.shift(); + while (event) { + list = calls[event]; + node = list ? list.tail : {}; + node.next = tail = {}; + node.context = context; + node.callback = callback; + calls[event] = {tail: tail, next: list ? list.next : node}; + event = events.shift(); + } + + return this; + }, + + /** + * Remove one or many callbacks. If `context` is null, removes all callbacks + * with that function. If `callback` is null, removes all callbacks for the + * event. If `events` is null, removes all bound callbacks for all events. + */ + off: function(events, callback, context) { + var event, calls, node, tail, cb, ctx; + + // No events, or removing *all* events. + if (!(calls = this._callbacks)) { + return; + } + if (!(events || callback || context)) { + delete this._callbacks; + return this; + } + + // Loop through the listed events and contexts, splicing them out of the + // linked list of callbacks if appropriate. + events = events ? events.split(eventSplitter) : Object.keys(calls); + event = events.shift(); + while (event) { + node = calls[event]; + delete calls[event]; + if (!node || !(callback || context)) { + event = events.shift(); + continue; + } + // Create a new list, omitting the indicated callbacks. + tail = node.tail; + node = node.next; + while (node !== tail) { + cb = node.callback; + ctx = node.context; + if ((callback && cb !== callback) || (context && ctx !== context)) { + this.on(event, cb, ctx); + } + node = node.next; + } + event = events.shift(); + } + + return this; + }, + + /** + * Trigger one or many events, firing all bound callbacks. Callbacks are + * passed the same arguments as `trigger` is, apart from the event name + * (unless you're listening on `"all"`, which will cause your callback to + * receive the true name of the event as the first argument). + */ + trigger: function(events) { + var event, node, calls, tail, args, all, rest; + if (!(calls = this._callbacks)) { + return this; + } + all = calls.all; + events = events.split(eventSplitter); + rest = slice.call(arguments, 1); + + // For each event, walk through the linked list of callbacks twice, + // first to trigger the event, then to trigger any `"all"` callbacks. + event = events.shift(); + while (event) { + node = calls[event]; + if (node) { + tail = node.tail; + while ((node = node.next) !== tail) { + node.callback.apply(node.context || this, rest); + } + } + node = all; + if (node) { + tail = node.tail; + args = [event].concat(rest); + while ((node = node.next) !== tail) { + node.callback.apply(node.context || this, args); + } + } + event = events.shift(); + } + + return this; + } + }; + + /** + * @function + */ + Parse.Events.bind = Parse.Events.on; + + /** + * @function + */ + Parse.Events.unbind = Parse.Events.off; +}.call(this)); + + +/*global navigator: false */ +(function(root) { + root.Parse = root.Parse || {}; + var Parse = root.Parse; + var _ = Parse._; + + /** + * Creates a new GeoPoint with any of the following forms:+ * new GeoPoint(otherGeoPoint) + * new GeoPoint(30, 30) + * new GeoPoint([30, 30]) + * new GeoPoint({latitude: 30, longitude: 30}) + * new GeoPoint() // defaults to (0, 0) + *+ * @class + * + *
Represents a latitude / longitude point that may be associated + * with a key in a ParseObject or used as a reference point for geo queries. + * This allows proximity-based queries on the key.
+ * + *Only one key in a class may contain a GeoPoint.
+ * + *Example:
+ * var point = new Parse.GeoPoint(30.0, -20.0); + * var object = new Parse.Object("PlaceObject"); + * object.set("location", point); + * object.save();+ */ + Parse.GeoPoint = function(arg1, arg2) { + if (_.isArray(arg1)) { + Parse.GeoPoint._validate(arg1[0], arg1[1]); + this.latitude = arg1[0]; + this.longitude = arg1[1]; + } else if (_.isObject(arg1)) { + Parse.GeoPoint._validate(arg1.latitude, arg1.longitude); + this.latitude = arg1.latitude; + this.longitude = arg1.longitude; + } else if (_.isNumber(arg1) && _.isNumber(arg2)) { + Parse.GeoPoint._validate(arg1, arg2); + this.latitude = arg1; + this.longitude = arg2; + } else { + this.latitude = 0; + this.longitude = 0; + } + + // Add properties so that anyone using Webkit or Mozilla will get an error + // if they try to set values that are out of bounds. + var self = this; + if (this.__defineGetter__ && this.__defineSetter__) { + // Use _latitude and _longitude to actually store the values, and add + // getters and setters for latitude and longitude. + this._latitude = this.latitude; + this._longitude = this.longitude; + this.__defineGetter__("latitude", function() { + return self._latitude; + }); + this.__defineGetter__("longitude", function() { + return self._longitude; + }); + this.__defineSetter__("latitude", function(val) { + Parse.GeoPoint._validate(val, self.longitude); + self._latitude = val; + }); + this.__defineSetter__("longitude", function(val) { + Parse.GeoPoint._validate(self.latitude, val); + self._longitude = val; + }); + } + }; + + /** + * @lends Parse.GeoPoint.prototype + * @property {float} latitude North-south portion of the coordinate, in range + * [-90, 90]. Throws an exception if set out of range in a modern browser. + * @property {float} longitude East-west portion of the coordinate, in range + * [-180, 180]. Throws if set out of range in a modern browser. + */ + + /** + * Throws an exception if the given lat-long is out of bounds. + */ + Parse.GeoPoint._validate = function(latitude, longitude) { + if (latitude < -90.0) { + throw "Parse.GeoPoint latitude " + latitude + " < -90.0."; + } + if (latitude > 90.0) { + throw "Parse.GeoPoint latitude " + latitude + " > 90.0."; + } + if (longitude < -180.0) { + throw "Parse.GeoPoint longitude " + longitude + " < -180.0."; + } + if (longitude > 180.0) { + throw "Parse.GeoPoint longitude " + longitude + " > 180.0."; + } + }; + + /** + * Creates a GeoPoint with the user's current location, if available. + * Calls options.success with a new GeoPoint instance or calls options.error. + * @param {Object} options An object with success and error callbacks. + */ + Parse.GeoPoint.current = function(options) { + var promise = new Parse.Promise(); + navigator.geolocation.getCurrentPosition(function(location) { + promise.resolve(new Parse.GeoPoint({ + latitude: location.coords.latitude, + longitude: location.coords.longitude + })); + + }, function(error) { + promise.reject(error); + }); + + return promise._thenRunCallbacks(options); + }; + + Parse.GeoPoint.prototype = { + /** + * Returns a JSON representation of the GeoPoint, suitable for Parse. + * @return {Object} + */ + toJSON: function() { + Parse.GeoPoint._validate(this.latitude, this.longitude); + return { + "__type": "GeoPoint", + latitude: this.latitude, + longitude: this.longitude + }; + }, + + /** + * Returns the distance from this GeoPoint to another in radians. + * @param {Parse.GeoPoint} point the other Parse.GeoPoint. + * @return {Number} + */ + radiansTo: function(point) { + var d2r = Math.PI / 180.0; + var lat1rad = this.latitude * d2r; + var long1rad = this.longitude * d2r; + var lat2rad = point.latitude * d2r; + var long2rad = point.longitude * d2r; + var deltaLat = lat1rad - lat2rad; + var deltaLong = long1rad - long2rad; + var sinDeltaLatDiv2 = Math.sin(deltaLat / 2); + var sinDeltaLongDiv2 = Math.sin(deltaLong / 2); + // Square of half the straight line chord distance between both points. + var a = ((sinDeltaLatDiv2 * sinDeltaLatDiv2) + + (Math.cos(lat1rad) * Math.cos(lat2rad) * + sinDeltaLongDiv2 * sinDeltaLongDiv2)); + a = Math.min(1.0, a); + return 2 * Math.asin(Math.sqrt(a)); + }, + + /** + * Returns the distance from this GeoPoint to another in kilometers. + * @param {Parse.GeoPoint} point the other Parse.GeoPoint. + * @return {Number} + */ + kilometersTo: function(point) { + return this.radiansTo(point) * 6371.0; + }, + + /** + * Returns the distance from this GeoPoint to another in miles. + * @param {Parse.GeoPoint} point the other Parse.GeoPoint. + * @return {Number} + */ + milesTo: function(point) { + return this.radiansTo(point) * 3958.8; + } + }; +}(this)); + +/*global navigator: false */ +(function(root) { + root.Parse = root.Parse || {}; + var Parse = root.Parse; + var _ = Parse._; + + var PUBLIC_KEY = "*"; + + /** + * Creates a new ACL. + * If no argument is given, the ACL has no permissions for anyone. + * If the argument is a Parse.User, the ACL will have read and write + * permission for only that user. + * If the argument is any other JSON object, that object will be interpretted + * as a serialized ACL created with toJSON(). + * @see Parse.Object#setACL + * @class + * + *
An ACL, or Access Control List can be added to any
+ * Parse.Object
to restrict access to only a subset of users
+ * of your application.
object.set("foo", "bar")
+ * is an example of a Parse.Op.Set. Calling object.unset("foo")
+ * is a Parse.Op.Unset. These operations are stored in a Parse.Object and
+ * sent to the server as part of object.save()
operations.
+ * Instances of Parse.Op should be immutable.
+ *
+ * You should not create subclasses of Parse.Op or instantiate Parse.Op
+ * directly.
+ */
+ Parse.Op = function() {
+ this._initialize.apply(this, arguments);
+ };
+
+ Parse.Op.prototype = {
+ _initialize: function() {}
+ };
+
+ _.extend(Parse.Op, {
+ /**
+ * To create a new Op, call Parse.Op._extend();
+ */
+ _extend: Parse._extend,
+
+ // A map of __op string to decoder function.
+ _opDecoderMap: {},
+
+ /**
+ * Registers a function to convert a json object with an __op field into an
+ * instance of a subclass of Parse.Op.
+ */
+ _registerDecoder: function(opName, decoder) {
+ Parse.Op._opDecoderMap[opName] = decoder;
+ },
+
+ /**
+ * Converts a json object into an instance of a subclass of Parse.Op.
+ */
+ _decode: function(json) {
+ var decoder = Parse.Op._opDecoderMap[json.__op];
+ if (decoder) {
+ return decoder(json);
+ } else {
+ return undefined;
+ }
+ }
+ });
+
+ /*
+ * Add a handler for Batch ops.
+ */
+ Parse.Op._registerDecoder("Batch", function(json) {
+ var op = null;
+ Parse._arrayEach(json.ops, function(nextOp) {
+ nextOp = Parse.Op._decode(nextOp);
+ op = nextOp._mergeWithPrevious(op);
+ });
+ return op;
+ });
+
+ /**
+ * @class
+ * A Set operation indicates that either the field was changed using
+ * Parse.Object.set, or it is a mutable container that was detected as being
+ * changed.
+ */
+ Parse.Op.Set = Parse.Op._extend(/** @lends Parse.Op.Set.prototype */ {
+ _initialize: function(value) {
+ this._value = value;
+ },
+
+ /**
+ * Returns the new value of this field after the set.
+ */
+ value: function() {
+ return this._value;
+ },
+
+ /**
+ * Returns a JSON version of the operation suitable for sending to Parse.
+ * @return {Object}
+ */
+ toJSON: function() {
+ return Parse._encode(this.value());
+ },
+
+ _mergeWithPrevious: function(previous) {
+ return this;
+ },
+
+ _estimate: function(oldValue) {
+ return this.value();
+ }
+ });
+
+ /**
+ * A sentinel value that is returned by Parse.Op.Unset._estimate to
+ * indicate the field should be deleted. Basically, if you find _UNSET as a
+ * value in your object, you should remove that key.
+ */
+ Parse.Op._UNSET = {};
+
+ /**
+ * @class
+ * An Unset operation indicates that this field has been deleted from the
+ * object.
+ */
+ Parse.Op.Unset = Parse.Op._extend(/** @lends Parse.Op.Unset.prototype */ {
+ /**
+ * Returns a JSON version of the operation suitable for sending to Parse.
+ * @return {Object}
+ */
+ toJSON: function() {
+ return { __op: "Delete" };
+ },
+
+ _mergeWithPrevious: function(previous) {
+ return this;
+ },
+
+ _estimate: function(oldValue) {
+ return Parse.Op._UNSET;
+ }
+ });
+
+ Parse.Op._registerDecoder("Delete", function(json) {
+ return new Parse.Op.Unset();
+ });
+
+ /**
+ * @class
+ * An Increment is an atomic operation where the numeric value for the field
+ * will be increased by a given amount.
+ */
+ Parse.Op.Increment = Parse.Op._extend(
+ /** @lends Parse.Op.Increment.prototype */ {
+
+ _initialize: function(amount) {
+ this._amount = amount;
+ },
+
+ /**
+ * Returns the amount to increment by.
+ * @return {Number} the amount to increment by.
+ */
+ amount: function() {
+ return this._amount;
+ },
+
+ /**
+ * Returns a JSON version of the operation suitable for sending to Parse.
+ * @return {Object}
+ */
+ toJSON: function() {
+ return { __op: "Increment", amount: this._amount };
+ },
+
+ _mergeWithPrevious: function(previous) {
+ if (!previous) {
+ return this;
+ } else if (previous instanceof Parse.Op.Unset) {
+ return new Parse.Op.Set(this.amount());
+ } else if (previous instanceof Parse.Op.Set) {
+ return new Parse.Op.Set(previous.value() + this.amount());
+ } else if (previous instanceof Parse.Op.Increment) {
+ return new Parse.Op.Increment(this.amount() + previous.amount());
+ } else {
+ throw "Op is invalid after previous op.";
+ }
+ },
+
+ _estimate: function(oldValue) {
+ if (!oldValue) {
+ return this.amount();
+ }
+ return oldValue + this.amount();
+ }
+ });
+
+ Parse.Op._registerDecoder("Increment", function(json) {
+ return new Parse.Op.Increment(json.amount);
+ });
+
+ /**
+ * @class
+ * Add is an atomic operation where the given objects will be appended to the
+ * array that is stored in this field.
+ */
+ Parse.Op.Add = Parse.Op._extend(/** @lends Parse.Op.Add.prototype */ {
+ _initialize: function(objects) {
+ this._objects = objects;
+ },
+
+ /**
+ * Returns the objects to be added to the array.
+ * @return {Array} The objects to be added to the array.
+ */
+ objects: function() {
+ return this._objects;
+ },
+
+ /**
+ * Returns a JSON version of the operation suitable for sending to Parse.
+ * @return {Object}
+ */
+ toJSON: function() {
+ return { __op: "Add", objects: Parse._encode(this.objects()) };
+ },
+
+ _mergeWithPrevious: function(previous) {
+ if (!previous) {
+ return this;
+ } else if (previous instanceof Parse.Op.Unset) {
+ return new Parse.Op.Set(this.objects());
+ } else if (previous instanceof Parse.Op.Set) {
+ return new Parse.Op.Set(this._estimate(previous.value()));
+ } else if (previous instanceof Parse.Op.Add) {
+ return new Parse.Op.Add(previous.objects().concat(this.objects()));
+ } else {
+ throw "Op is invalid after previous op.";
+ }
+ },
+
+ _estimate: function(oldValue) {
+ if (!oldValue) {
+ return _.clone(this.objects());
+ } else {
+ return oldValue.concat(this.objects());
+ }
+ }
+ });
+
+ Parse.Op._registerDecoder("Add", function(json) {
+ return new Parse.Op.Add(Parse._decode(undefined, json.objects));
+ });
+
+ /**
+ * @class
+ * AddUnique is an atomic operation where the given items will be appended to
+ * the array that is stored in this field only if they were not already
+ * present in the array.
+ */
+ Parse.Op.AddUnique = Parse.Op._extend(
+ /** @lends Parse.Op.AddUnique.prototype */ {
+
+ _initialize: function(objects) {
+ this._objects = _.uniq(objects);
+ },
+
+ /**
+ * Returns the objects to be added to the array.
+ * @return {Array} The objects to be added to the array.
+ */
+ objects: function() {
+ return this._objects;
+ },
+
+ /**
+ * Returns a JSON version of the operation suitable for sending to Parse.
+ * @return {Object}
+ */
+ toJSON: function() {
+ return { __op: "AddUnique", objects: Parse._encode(this.objects()) };
+ },
+
+ _mergeWithPrevious: function(previous) {
+ if (!previous) {
+ return this;
+ } else if (previous instanceof Parse.Op.Unset) {
+ return new Parse.Op.Set(this.objects());
+ } else if (previous instanceof Parse.Op.Set) {
+ return new Parse.Op.Set(this._estimate(previous.value()));
+ } else if (previous instanceof Parse.Op.AddUnique) {
+ return new Parse.Op.AddUnique(this._estimate(previous.objects()));
+ } else {
+ throw "Op is invalid after previous op.";
+ }
+ },
+
+ _estimate: function(oldValue) {
+ if (!oldValue) {
+ return _.clone(this.objects());
+ } else {
+ // We can't just take the _.uniq(_.union(...)) of oldValue and
+ // this.objects, because the uniqueness may not apply to oldValue
+ // (especially if the oldValue was set via .set())
+ var newValue = _.clone(oldValue);
+ Parse._arrayEach(this.objects(), function(obj) {
+ if (obj instanceof Parse.Object && obj.id) {
+ var matchingObj = _.find(newValue, function(anObj) {
+ return (anObj instanceof Parse.Object) && (anObj.id === obj.id);
+ });
+ if (!matchingObj) {
+ newValue.push(obj);
+ } else {
+ var index = _.indexOf(newValue, matchingObj);
+ newValue[index] = obj;
+ }
+ } else if (!_.contains(newValue, obj)) {
+ newValue.push(obj);
+ }
+ });
+ return newValue;
+ }
+ }
+ });
+
+ Parse.Op._registerDecoder("AddUnique", function(json) {
+ return new Parse.Op.AddUnique(Parse._decode(undefined, json.objects));
+ });
+
+ /**
+ * @class
+ * Remove is an atomic operation where the given objects will be removed from
+ * the array that is stored in this field.
+ */
+ Parse.Op.Remove = Parse.Op._extend(/** @lends Parse.Op.Remove.prototype */ {
+ _initialize: function(objects) {
+ this._objects = _.uniq(objects);
+ },
+
+ /**
+ * Returns the objects to be removed from the array.
+ * @return {Array} The objects to be removed from the array.
+ */
+ objects: function() {
+ return this._objects;
+ },
+
+ /**
+ * Returns a JSON version of the operation suitable for sending to Parse.
+ * @return {Object}
+ */
+ toJSON: function() {
+ return { __op: "Remove", objects: Parse._encode(this.objects()) };
+ },
+
+ _mergeWithPrevious: function(previous) {
+ if (!previous) {
+ return this;
+ } else if (previous instanceof Parse.Op.Unset) {
+ return previous;
+ } else if (previous instanceof Parse.Op.Set) {
+ return new Parse.Op.Set(this._estimate(previous.value()));
+ } else if (previous instanceof Parse.Op.Remove) {
+ return new Parse.Op.Remove(_.union(previous.objects(), this.objects()));
+ } else {
+ throw "Op is invalid after previous op.";
+ }
+ },
+
+ _estimate: function(oldValue) {
+ if (!oldValue) {
+ return [];
+ } else {
+ var newValue = _.difference(oldValue, this.objects());
+ // If there are saved Parse Objects being removed, also remove them.
+ Parse._arrayEach(this.objects(), function(obj) {
+ if (obj instanceof Parse.Object && obj.id) {
+ newValue = _.reject(newValue, function(other) {
+ return (other instanceof Parse.Object) && (other.id === obj.id);
+ });
+ }
+ });
+ return newValue;
+ }
+ }
+ });
+
+ Parse.Op._registerDecoder("Remove", function(json) {
+ return new Parse.Op.Remove(Parse._decode(undefined, json.objects));
+ });
+
+ /**
+ * @class
+ * A Relation operation indicates that the field is an instance of
+ * Parse.Relation, and objects are being added to, or removed from, that
+ * relation.
+ */
+ Parse.Op.Relation = Parse.Op._extend(
+ /** @lends Parse.Op.Relation.prototype */ {
+
+ _initialize: function(adds, removes) {
+ this._targetClassName = null;
+
+ var self = this;
+
+ var pointerToId = function(object) {
+ if (object instanceof Parse.Object) {
+ if (!object.id) {
+ throw "You can't add an unsaved Parse.Object to a relation.";
+ }
+ if (!self._targetClassName) {
+ self._targetClassName = object.className;
+ }
+ if (self._targetClassName !== object.className) {
+ throw "Tried to create a Parse.Relation with 2 different types: " +
+ self._targetClassName + " and " + object.className + ".";
+ }
+ return object.id;
+ }
+ return object;
+ };
+
+ this.relationsToAdd = _.uniq(_.map(adds, pointerToId));
+ this.relationsToRemove = _.uniq(_.map(removes, pointerToId));
+ },
+
+ /**
+ * Returns an array of unfetched Parse.Object that are being added to the
+ * relation.
+ * @return {Array}
+ */
+ added: function() {
+ var self = this;
+ return _.map(this.relationsToAdd, function(objectId) {
+ var object = Parse.Object._create(self._targetClassName);
+ object.id = objectId;
+ return object;
+ });
+ },
+
+ /**
+ * Returns an array of unfetched Parse.Object that are being removed from
+ * the relation.
+ * @return {Array}
+ */
+ removed: function() {
+ var self = this;
+ return _.map(this.relationsToRemove, function(objectId) {
+ var object = Parse.Object._create(self._targetClassName);
+ object.id = objectId;
+ return object;
+ });
+ },
+
+ /**
+ * Returns a JSON version of the operation suitable for sending to Parse.
+ * @return {Object}
+ */
+ toJSON: function() {
+ var adds = null;
+ var removes = null;
+ var self = this;
+ var idToPointer = function(id) {
+ return { __type: 'Pointer',
+ className: self._targetClassName,
+ objectId: id };
+ };
+ var pointers = null;
+ if (this.relationsToAdd.length > 0) {
+ pointers = _.map(this.relationsToAdd, idToPointer);
+ adds = { "__op": "AddRelation", "objects": pointers };
+ }
+
+ if (this.relationsToRemove.length > 0) {
+ pointers = _.map(this.relationsToRemove, idToPointer);
+ removes = { "__op": "RemoveRelation", "objects": pointers };
+ }
+
+ if (adds && removes) {
+ return { "__op": "Batch", "ops": [adds, removes]};
+ }
+
+ return adds || removes || {};
+ },
+
+ _mergeWithPrevious: function(previous) {
+ if (!previous) {
+ return this;
+ } else if (previous instanceof Parse.Op.Unset) {
+ throw "You can't modify a relation after deleting it.";
+ } else if (previous instanceof Parse.Op.Relation) {
+ if (previous._targetClassName &&
+ previous._targetClassName !== this._targetClassName) {
+ throw "Related object must be of class " + previous._targetClassName +
+ ", but " + this._targetClassName + " was passed in.";
+ }
+ var newAdd = _.union(_.difference(previous.relationsToAdd,
+ this.relationsToRemove),
+ this.relationsToAdd);
+ var newRemove = _.union(_.difference(previous.relationsToRemove,
+ this.relationsToAdd),
+ this.relationsToRemove);
+
+ var newRelation = new Parse.Op.Relation(newAdd, newRemove);
+ newRelation._targetClassName = this._targetClassName;
+ return newRelation;
+ } else {
+ throw "Op is invalid after previous op.";
+ }
+ },
+
+ _estimate: function(oldValue, object, key) {
+ if (!oldValue) {
+ var relation = new Parse.Relation(object, key);
+ relation.targetClassName = this._targetClassName;
+ } else if (oldValue instanceof Parse.Relation) {
+ if (this._targetClassName) {
+ if (oldValue.targetClassName) {
+ if (oldValue.targetClassName !== this._targetClassName) {
+ throw "Related object must be a " + oldValue.targetClassName +
+ ", but a " + this._targetClassName + " was passed in.";
+ }
+ } else {
+ oldValue.targetClassName = this._targetClassName;
+ }
+ }
+ return oldValue;
+ } else {
+ throw "Op is invalid after previous op.";
+ }
+ }
+ });
+
+ Parse.Op._registerDecoder("AddRelation", function(json) {
+ return new Parse.Op.Relation(Parse._decode(undefined, json.objects), []);
+ });
+ Parse.Op._registerDecoder("RemoveRelation", function(json) {
+ return new Parse.Op.Relation([], Parse._decode(undefined, json.objects));
+ });
+
+}(this));
+
+(function(root) {
+ root.Parse = root.Parse || {};
+ var Parse = root.Parse;
+ var _ = Parse._;
+
+ /**
+ * Creates a new Relation for the given parent object and key. This
+ * constructor should rarely be used directly, but rather created by
+ * Parse.Object.relation.
+ * @param {Parse.Object} parent The parent of this relation.
+ * @param {String} key The key for this relation on the parent.
+ * @see Parse.Object#relation
+ * @class
+ *
+ * + * A class that is used to access all of the children of a many-to-many + * relationship. Each instance of Parse.Relation is associated with a + * particular parent object and key. + *
+ */ + Parse.Relation = function(parent, key) { + this.parent = parent; + this.key = key; + this.targetClassName = null; + }; + + Parse.Relation.prototype = { + /** + * Makes sure that this relation has the right parent and key. + */ + _ensureParentAndKey: function(parent, key) { + this.parent = this.parent || parent; + this.key = this.key || key; + if (this.parent !== parent) { + throw "Internal Error. Relation retrieved from two different Objects."; + } + if (this.key !== key) { + throw "Internal Error. Relation retrieved from two different keys."; + } + }, + + /** + * Adds a Parse.Object or an array of Parse.Objects to the relation. + * @param {} objects The item or items to add. + */ + add: function(objects) { + if (!_.isArray(objects)) { + objects = [objects]; + } + + var change = new Parse.Op.Relation(objects, []); + this.parent.set(this.key, change); + this.targetClassName = change._targetClassName; + }, + + /** + * Removes a Parse.Object or an array of Parse.Objects from this relation. + * @param {} objects The item or items to remove. + */ + remove: function(objects) { + if (!_.isArray(objects)) { + objects = [objects]; + } + + var change = new Parse.Op.Relation([], objects); + this.parent.set(this.key, change); + this.targetClassName = change._targetClassName; + }, + + /** + * Returns a JSON version of the object suitable for saving to disk. + * @return {Object} + */ + toJSON: function() { + return { "__type": "Relation", "className": this.targetClassName }; + }, + + /** + * Returns a Parse.Query that is limited to objects in this + * relation. + * @return {Parse.Query} + */ + query: function() { + var targetClass; + var query; + if (!this.targetClassName) { + targetClass = Parse.Object._getSubclass(this.parent.className); + query = new Parse.Query(targetClass); + query._extraOptions.redirectClassNameForKey = this.key; + } else { + targetClass = Parse.Object._getSubclass(this.targetClassName); + query = new Parse.Query(targetClass); + } + query._addCondition("$relatedTo", "object", this.parent._toPointer()); + query._addCondition("$relatedTo", "key", this.key); + + return query; + } + }; +}(this)); + +/*global window: false, process: false */ +(function(root) { + root.Parse = root.Parse || {}; + var Parse = root.Parse; + var _ = Parse._; + + /** + * A Promise is returned by async methods as a hook to provide callbacks to be + * called when the async task is fulfilled. + * + *Typical usage would be like:
+ * query.find().then(function(results) { + * results[0].set("foo", "bar"); + * return results[0].saveAsync(); + * }).then(function(result) { + * console.log("Updated " + result.id); + * }); + *+ * + * @see Parse.Promise.prototype.then + * @class + */ + Parse.Promise = function() { + this._resolved = false; + this._rejected = false; + this._resolvedCallbacks = []; + this._rejectedCallbacks = []; + }; + + _.extend(Parse.Promise, /** @lends Parse.Promise */ { + + _isPromisesAPlusCompliant: false, + + /** + * Returns true iff the given object fulfils the Promise interface. + * @return {Boolean} + */ + is: function(promise) { + return promise && promise.then && _.isFunction(promise.then); + }, + + /** + * Returns a new promise that is resolved with a given value. + * @return {Parse.Promise} the new promise. + */ + as: function() { + var promise = new Parse.Promise(); + promise.resolve.apply(promise, arguments); + return promise; + }, + + /** + * Returns a new promise that is rejected with a given error. + * @return {Parse.Promise} the new promise. + */ + error: function() { + var promise = new Parse.Promise(); + promise.reject.apply(promise, arguments); + return promise; + }, + + /** + * Returns a new promise that is fulfilled when all of the input promises + * are resolved. If any promise in the list fails, then the returned promise + * will fail with the last error. If they all succeed, then the returned + * promise will succeed, with the results being the results of all the input + * promises. For example:
+ * var p1 = Parse.Promise.as(1); + * var p2 = Parse.Promise.as(2); + * var p3 = Parse.Promise.as(3); + * + * Parse.Promise.when(p1, p2, p3).then(function(r1, r2, r3) { + * console.log(r1); // prints 1 + * console.log(r2); // prints 2 + * console.log(r3); // prints 3 + * });+ * + * The input promises can also be specified as an array:
+ * var promises = [p1, p2, p3]; + * Parse.Promise.when(promises).then(function(r1, r2, r3) { + * console.log(r1); // prints 1 + * console.log(r2); // prints 2 + * console.log(r3); // prints 3 + * }); + *+ * @param {Array} promises a list of promises to wait for. + * @return {Parse.Promise} the new promise. + */ + when: function(promises) { + // Allow passing in Promises as separate arguments instead of an Array. + var objects; + if (promises && Parse._isNullOrUndefined(promises.length)) { + objects = arguments; + } else { + objects = promises; + } + + var total = objects.length; + var hadError = false; + var results = []; + var errors = []; + results.length = objects.length; + errors.length = objects.length; + + if (total === 0) { + return Parse.Promise.as.apply(this, results); + } + + var promise = new Parse.Promise(); + + var resolveOne = function() { + total = total - 1; + if (total === 0) { + if (hadError) { + promise.reject(errors); + } else { + promise.resolve.apply(promise, results); + } + } + }; + + Parse._arrayEach(objects, function(object, i) { + if (Parse.Promise.is(object)) { + object.then(function(result) { + results[i] = result; + resolveOne(); + }, function(error) { + errors[i] = error; + hadError = true; + resolveOne(); + }); + } else { + results[i] = object; + resolveOne(); + } + }); + + return promise; + }, + + /** + * Runs the given asyncFunction repeatedly, as long as the predicate + * function returns a truthy value. Stops repeating if asyncFunction returns + * a rejected promise. + * @param {Function} predicate should return false when ready to stop. + * @param {Function} asyncFunction should return a Promise. + */ + _continueWhile: function(predicate, asyncFunction) { + if (predicate()) { + return asyncFunction().then(function() { + return Parse.Promise._continueWhile(predicate, asyncFunction); + }); + } + return Parse.Promise.as(); + } + }); + + _.extend(Parse.Promise.prototype, /** @lends Parse.Promise.prototype */ { + + /** + * Marks this promise as fulfilled, firing any callbacks waiting on it. + * @param {Object} result the result to pass to the callbacks. + */ + resolve: function(result) { + if (this._resolved || this._rejected) { + throw "A promise was resolved even though it had already been " + + (this._resolved ? "resolved" : "rejected") + "."; + } + this._resolved = true; + this._result = arguments; + var results = arguments; + Parse._arrayEach(this._resolvedCallbacks, function(resolvedCallback) { + resolvedCallback.apply(this, results); + }); + this._resolvedCallbacks = []; + this._rejectedCallbacks = []; + }, + + /** + * Marks this promise as fulfilled, firing any callbacks waiting on it. + * @param {Object} error the error to pass to the callbacks. + */ + reject: function(error) { + if (this._resolved || this._rejected) { + throw "A promise was rejected even though it had already been " + + (this._resolved ? "resolved" : "rejected") + "."; + } + this._rejected = true; + this._error = error; + Parse._arrayEach(this._rejectedCallbacks, function(rejectedCallback) { + rejectedCallback(error); + }); + this._resolvedCallbacks = []; + this._rejectedCallbacks = []; + }, + + /** + * Adds callbacks to be called when this promise is fulfilled. Returns a new + * Promise that will be fulfilled when the callback is complete. It allows + * chaining. If the callback itself returns a Promise, then the one returned + * by "then" will not be fulfilled until that one returned by the callback + * is fulfilled. + * @param {Function} resolvedCallback Function that is called when this + * Promise is resolved. Once the callback is complete, then the Promise + * returned by "then" will also be fulfilled. + * @param {Function} rejectedCallback Function that is called when this + * Promise is rejected with an error. Once the callback is complete, then + * the promise returned by "then" with be resolved successfully. If + * rejectedCallback is null, or it returns a rejected Promise, then the + * Promise returned by "then" will be rejected with that error. + * @return {Parse.Promise} A new Promise that will be fulfilled after this + * Promise is fulfilled and either callback has completed. If the callback + * returned a Promise, then this Promise will not be fulfilled until that + * one is. + */ + then: function(resolvedCallback, rejectedCallback) { + var promise = new Parse.Promise(); + + var wrappedResolvedCallback = function() { + var result = arguments; + if (resolvedCallback) { + if (Parse.Promise._isPromisesAPlusCompliant) { + try { + result = [resolvedCallback.apply(this, result)]; + } catch (e) { + result = [Parse.Promise.error(e)]; + } + } else { + result = [resolvedCallback.apply(this, result)]; + } + } + if (result.length === 1 && Parse.Promise.is(result[0])) { + result[0].then(function() { + promise.resolve.apply(promise, arguments); + }, function(error) { + promise.reject(error); + }); + } else { + promise.resolve.apply(promise, result); + } + }; + + var wrappedRejectedCallback = function(error) { + var result = []; + if (rejectedCallback) { + if (Parse.Promise._isPromisesAPlusCompliant) { + try { + result = [rejectedCallback(error)]; + } catch (e) { + result = [Parse.Promise.error(e)]; + } + } else { + result = [rejectedCallback(error)]; + } + if (result.length === 1 && Parse.Promise.is(result[0])) { + result[0].then(function() { + promise.resolve.apply(promise, arguments); + }, function(error) { + promise.reject(error); + }); + } else { + if (Parse.Promise._isPromisesAPlusCompliant) { + promise.resolve.apply(promise, result); + } else { + promise.reject(result[0]); + } + } + } else { + promise.reject(error); + } + }; + + var runLater = function(func) { + func.call(); + }; + if (Parse.Promise._isPromisesAPlusCompliant) { + if (typeof(window) !== 'undefined' && window.setTimeout) { + runLater = function(func) { + window.setTimeout(func, 0); + }; + } else if (typeof(process) !== 'undefined' && process.nextTick) { + runLater = function(func) { + process.nextTick(func); + }; + } + } + + var self = this; + if (this._resolved) { + runLater(function() { + wrappedResolvedCallback.apply(self, self._result); + }); + } else if (this._rejected) { + runLater(function() { + wrappedRejectedCallback(self._error); + }); + } else { + this._resolvedCallbacks.push(wrappedResolvedCallback); + this._rejectedCallbacks.push(wrappedRejectedCallback); + } + + return promise; + }, + + /** + * Add handlers to be called when the promise + * is either resolved or rejected + */ + always: function(callback) { + return this.then(callback, callback); + }, + + /** + * Add handlers to be called when the Promise object is resolved + */ + done: function(callback) { + return this.then(callback); + }, + + /** + * Add handlers to be called when the Promise object is rejected + */ + fail: function(callback) { + return this.then(null, callback); + }, + + /** + * Run the given callbacks after this promise is fulfilled. + * @param optionsOrCallback {} A Backbone-style options callback, or a + * callback function. If this is an options object and contains a "model" + * attributes, that will be passed to error callbacks as the first argument. + * @param model {} If truthy, this will be passed as the first result of + * error callbacks. This is for Backbone-compatability. + * @return {Parse.Promise} A promise that will be resolved after the + * callbacks are run, with the same result as this. + */ + _thenRunCallbacks: function(optionsOrCallback, model) { + var options; + if (_.isFunction(optionsOrCallback)) { + var callback = optionsOrCallback; + options = { + success: function(result) { + callback(result, null); + }, + error: function(error) { + callback(null, error); + } + }; + } else { + options = _.clone(optionsOrCallback); + } + options = options || {}; + + return this.then(function(result) { + if (options.success) { + options.success.apply(this, arguments); + } else if (model) { + // When there's no callback, a sync event should be triggered. + model.trigger('sync', model, result, options); + } + return Parse.Promise.as.apply(Parse.Promise, arguments); + }, function(error) { + if (options.error) { + if (!_.isUndefined(model)) { + options.error(model, error); + } else { + options.error(error); + } + } else if (model) { + // When there's no error callback, an error event should be triggered. + model.trigger('error', model, error, options); + } + // By explicitly returning a rejected Promise, this will work with + // either jQuery or Promises/A semantics. + return Parse.Promise.error(error); + }); + }, + + /** + * Adds a callback function that should be called regardless of whether + * this promise failed or succeeded. The callback will be given either the + * array of results for its first argument, or the error as its second, + * depending on whether this Promise was rejected or resolved. Returns a + * new Promise, like "then" would. + * @param {Function} continuation the callback. + */ + _continueWith: function(continuation) { + return this.then(function() { + return continuation(arguments, null); + }, function(error) { + return continuation(null, error); + }); + } + + }); + +}(this)); + +/*jshint bitwise:false *//*global FileReader: true, File: true */ +(function(root) { + root.Parse = root.Parse || {}; + var Parse = root.Parse; + var _ = Parse._; + + var b64Digit = function(number) { + if (number < 26) { + return String.fromCharCode(65 + number); + } + if (number < 52) { + return String.fromCharCode(97 + (number - 26)); + } + if (number < 62) { + return String.fromCharCode(48 + (number - 52)); + } + if (number === 62) { + return "+"; + } + if (number === 63) { + return "/"; + } + throw "Tried to encode large digit " + number + " in base64."; + }; + + var encodeBase64 = function(array) { + var chunks = []; + chunks.length = Math.ceil(array.length / 3); + _.times(chunks.length, function(i) { + var b1 = array[i * 3]; + var b2 = array[i * 3 + 1] || 0; + var b3 = array[i * 3 + 2] || 0; + + var has2 = (i * 3 + 1) < array.length; + var has3 = (i * 3 + 2) < array.length; + + chunks[i] = [ + b64Digit((b1 >> 2) & 0x3F), + b64Digit(((b1 << 4) & 0x30) | ((b2 >> 4) & 0x0F)), + has2 ? b64Digit(((b2 << 2) & 0x3C) | ((b3 >> 6) & 0x03)) : "=", + has3 ? b64Digit(b3 & 0x3F) : "=" + ].join(""); + }); + return chunks.join(""); + }; + + /** + * Reads a File using a FileReader. + * @param file {File} the File to read. + * @param type {String} (optional) the mimetype to override with. + * @return {Parse.Promise} A Promise that will be fulfilled with a + * base64-encoded string of the data and its mime type. + */ + var readAsync = function(file, type) { + var promise = new Parse.Promise(); + + if (typeof(FileReader) === "undefined") { + return Parse.Promise.error(new Parse.Error( + Parse.Error.FILE_READ_ERROR, + "Attempted to use a FileReader on an unsupported browser.")); + } + + var reader = new FileReader(); + reader.onloadend = function() { + if (reader.readyState !== 2) { + promise.reject(new Parse.Error( + Parse.Error.FILE_READ_ERROR, + "Error reading file.")); + return; + } + + var dataURL = reader.result; + var matches = /^data:([^;]*);base64,(.*)$/.exec(dataURL); + if (!matches) { + promise.reject(new Parse.Error( + Parse.Error.FILE_READ_ERROR, + "Unable to interpret data URL: " + dataURL)); + return; + } + + promise.resolve(matches[2], type || matches[1]); + }; + reader.readAsDataURL(file); + return promise; + }; + + /** + * A Parse.File is a local representation of a file that is saved to the Parse + * cloud. + * @class + * @param name {String} The file's name. This will be prefixed by a unique + * value once the file has finished saving. The file name must begin with + * an alphanumeric character, and consist of alphanumeric characters, + * periods, spaces, underscores, or dashes. + * @param data {Array} The data for the file, as either: + * 1. an Array of byte value Numbers, or + * 2. an Object like { base64: "..." } with a base64-encoded String. + * 3. a File object selected with a file upload control. (3) only works + * in Firefox 3.6+, Safari 6.0.2+, Chrome 7+, and IE 10+. + * For example:
+ * var fileUploadControl = $("#profilePhotoFileUpload")[0]; + * if (fileUploadControl.files.length > 0) { + * var file = fileUploadControl.files[0]; + * var name = "photo.jpg"; + * var parseFile = new Parse.File(name, file); + * parseFile.save().then(function() { + * // The file has been saved to Parse. + * }, function(error) { + * // The file either could not be read, or could not be saved to Parse. + * }); + * }+ * @param type {String} Optional Content-Type header to use for the file. If + * this is omitted, the content type will be inferred from the name's + * extension. + */ + Parse.File = function(name, data, type) { + this._name = name; + + // Guess the content type from the extension if we need to. + var extension = /\.([^.]*)$/.exec(name); + if (extension) { + extension = extension[1].toLowerCase(); + } + var specifiedType = type || ''; + + if (_.isArray(data)) { + this._source = Parse.Promise.as(encodeBase64(data), specifiedType); + } else if (data && data.base64) { + // if it contains data uri, extract based64 and the type out of it. + /*jslint maxlen: 1000*/ + var dataUriRegexp = /^data:([a-zA-Z]*\/[a-zA-Z+.-]*);(charset=[a-zA-Z0-9\-\/\s]*,)?base64,(\S+)/; + /*jslint maxlen: 80*/ + + var matches = dataUriRegexp.exec(data.base64); + if (matches && matches.length > 0) { + // if data URI with charset, there will have 4 matches. + this._source = Parse.Promise.as( + (matches.length === 4 ? matches[3] : matches[2]), matches[1] + ); + } else { + this._source = Parse.Promise.as(data.base64, specifiedType); + } + } else if (typeof(File) !== "undefined" && data instanceof File) { + this._source = readAsync(data, type); + } else if (_.isString(data)) { + throw "Creating a Parse.File from a String is not yet supported."; + } + }; + + Parse.File.prototype = { + + /** + * Gets the name of the file. Before save is called, this is the filename + * given by the user. After save is called, that name gets prefixed with a + * unique identifier. + */ + name: function() { + return this._name; + }, + + /** + * Gets the url of the file. It is only available after you save the file or + * after you get the file from a Parse.Object. + * @return {String} + */ + url: function() { + return this._url; + }, + + /** + * Saves the file to the Parse cloud. + * @param {Object} options A Backbone-style options object. + * @return {Parse.Promise} Promise that is resolved when the save finishes. + */ + save: function(options) { + options= options || {}; + + var self = this; + if (!self._previousSave) { + self._previousSave = self._source.then(function(base64, type) { + var data = { + base64: base64, + _ContentType: type + }; + return Parse._request({ + route: "files", + className: self._name, + method: 'POST', + data: data, + useMasterKey: options.useMasterKey + }); + + }).then(function(response) { + self._name = response.name; + self._url = response.url; + return self; + }); + } + return self._previousSave._thenRunCallbacks(options); + } + }; + +}(this)); + +// Parse.Object is analogous to the Java ParseObject. +// It also implements the same interface as a Backbone model. + +(function(root) { + root.Parse = root.Parse || {}; + var Parse = root.Parse; + var _ = Parse._; + + /** + * Creates a new model with defined attributes. A client id (cid) is + * automatically generated and assigned for you. + * + *
You won't normally call this method directly. It is recommended that
+ * you use a subclass of Parse.Object
instead, created by calling
+ * extend
.
However, if you don't want to use a subclass, or aren't sure which + * subclass is appropriate, you can use this form:
+ * var object = new Parse.Object("ClassName"); + *+ * That is basically equivalent to:
+ * var MyClass = Parse.Object.extend("ClassName"); + * var object = new MyClass(); + *+ * + * @param {Object} attributes The initial set of data to store in the object. + * @param {Object} options A set of Backbone-like options for creating the + * object. The only option currently supported is "collection". + * @see Parse.Object.extend + * + * @class + * + *
The fundamental unit of Parse data, which implements the Backbone Model + * interface.
+ */ + Parse.Object = function(attributes, options) { + // Allow new Parse.Object("ClassName") as a shortcut to _create. + if (_.isString(attributes)) { + return Parse.Object._create.apply(this, arguments); + } + + attributes = attributes || {}; + if (options && options.parse) { + attributes = this.parse(attributes); + } + var defaults = Parse._getValue(this, 'defaults'); + if (defaults) { + attributes = _.extend({}, defaults, attributes); + } + if (options && options.collection) { + this.collection = options.collection; + } + + this._serverData = {}; // The last known data for this object from cloud. + this._opSetQueue = [{}]; // List of sets of changes to the data. + this.attributes = {}; // The best estimate of this's current data. + + this._hashedJSON = {}; // Hash of values of containers at last save. + this._escapedAttributes = {}; + this.cid = _.uniqueId('c'); + this.changed = {}; + this._silent = {}; + this._pending = {}; + if (!this.set(attributes, {silent: true})) { + throw new Error("Can't create an invalid Parse.Object"); + } + this.changed = {}; + this._silent = {}; + this._pending = {}; + this._hasData = true; + this._previousAttributes = _.clone(this.attributes); + this.initialize.apply(this, arguments); + }; + + /** + * The ID of this object, unique within its class. + * @name id + * @type String + * @field + * @memberOf Parse.Object.prototype + */ + + /** + * The first time this object was saved on the server. + * @name createdAt + * @type Date + * @field + * @memberOf Parse.Object.prototype + */ + + /** + * The last time this object was updated on the server. + * @name updatedAt + * @type Date + * @field + * @memberOf Parse.Object.prototype + */ + + /** + * Saves the given list of Parse.Object. + * If any error is encountered, stops and calls the error handler. + * + *+ * Parse.Object.saveAll([object1, object2, ...], { + * success: function(list) { + * // All the objects were saved. + * }, + * error: function(error) { + * // An error occurred while saving one of the objects. + * }, + * }); + *+ * + * @param {Array} list A list of
Parse.Object
.
+ * @param {Object} options A Backbone-style callback object.
+ * Valid options are:Unlike saveAll, if an error occurs while deleting an individual model, + * this method will continue trying to delete the rest of the models if + * possible, except in the case of a fatal error like a connection error. + * + *
In particular, the Parse.Error object returned in the case of error may + * be one of two types: + * + *
+ * Parse.Object.destroyAll([object1, object2, ...], { + * success: function() { + * // All the objects were deleted. + * }, + * error: function(error) { + * // An error occurred while deleting one or more of the objects. + * // If this is an aggregate error, then we can inspect each error + * // object individually to determine the reason why a particular + * // object was not deleted. + * if (error.code == Parse.Error.AGGREGATE_ERROR) { + * for (var i = 0; i < error.errors.length; i++) { + * console.log("Couldn't delete " + error.errors[i].object.id + + * "due to " + error.errors[i].message); + * } + * } else { + * console.log("Delete aborted because of " + error.message); + * } + * }, + * }); + *+ * + * @param {Array} list A list of
Parse.Object
.
+ * @param {Object} options A Backbone-style callback object.
+ * Valid options are:+ * Parse.Object.fetchAll([object1, object2, ...], { + * success: function(list) { + * // All the objects were fetched. + * }, + * error: function(error) { + * // An error occurred while fetching one of the objects. + * }, + * }); + *+ * + * @param {Array} list A list of
Parse.Object
.
+ * @param {Object} options A Backbone-style callback object.
+ * Valid options are:+ * Parse.Object.fetchAllIfNeeded([object1, ...], { + * success: function(list) { + * // Objects were fetched and updated. + * }, + * error: function(error) { + * // An error occurred while fetching one of the objects. + * }, + * }); + *+ * + * @param {Array} list A list of
Parse.Object
.
+ * @param {Object} options A Backbone-style callback object.
+ * Valid options are:true
if the attribute contains a value that is not
+ * null or undefined.
+ * @param {String} attr The string name of the attribute.
+ * @return {Boolean}
+ */
+ has: function(attr) {
+ return !Parse._isNullOrUndefined(this.attributes[attr]);
+ },
+
+ /**
+ * Pulls "special" fields like objectId, createdAt, etc. out of attrs
+ * and puts them on "this" directly. Removes them from attrs.
+ * @param attrs - A dictionary with the data for this Parse.Object.
+ */
+ _mergeMagicFields: function(attrs) {
+ // Check for changes of magic fields.
+ var model = this;
+ var specialFields = ["id", "objectId", "createdAt", "updatedAt"];
+ Parse._arrayEach(specialFields, function(attr) {
+ if (attrs[attr]) {
+ if (attr === "objectId") {
+ model.id = attrs[attr];
+ } else if ((attr === "createdAt" || attr === "updatedAt") &&
+ !_.isDate(attrs[attr])) {
+ model[attr] = Parse._parseDate(attrs[attr]);
+ } else {
+ model[attr] = attrs[attr];
+ }
+ delete attrs[attr];
+ }
+ });
+ },
+
+ /**
+ * Copies the given serverData to "this", refreshes attributes, and
+ * clears pending changes;
+ */
+ _copyServerData: function(serverData) {
+ // Copy server data
+ var tempServerData = {};
+ Parse._objectEach(serverData, function(value, key) {
+ tempServerData[key] = Parse._decode(key, value);
+ });
+ this._serverData = tempServerData;
+
+ // Refresh the attributes.
+ this._rebuildAllEstimatedData();
+
+
+ // Clear out any changes the user might have made previously.
+ this._refreshCache();
+ this._opSetQueue = [{}];
+
+ // Refresh the attributes again.
+ this._rebuildAllEstimatedData();
+ },
+
+ /**
+ * Merges another object's attributes into this object.
+ */
+ _mergeFromObject: function(other) {
+ if (!other) {
+ return;
+ }
+
+ // This does the inverse of _mergeMagicFields.
+ this.id = other.id;
+ this.createdAt = other.createdAt;
+ this.updatedAt = other.updatedAt;
+
+ this._copyServerData(other._serverData);
+
+ this._hasData = true;
+ },
+
+ /**
+ * Returns the json to be sent to the server.
+ */
+ _startSave: function() {
+ this._opSetQueue.push({});
+ },
+
+ /**
+ * Called when a save fails because of an error. Any changes that were part
+ * of the save need to be merged with changes made after the save. This
+ * might throw an exception is you do conflicting operations. For example,
+ * if you do:
+ * object.set("foo", "bar");
+ * object.set("invalid field name", "baz");
+ * object.save();
+ * object.increment("foo");
+ * then this will throw when the save fails and the client tries to merge
+ * "bar" with the +1.
+ */
+ _cancelSave: function() {
+ var self = this;
+ var failedChanges = _.first(this._opSetQueue);
+ this._opSetQueue = _.rest(this._opSetQueue);
+ var nextChanges = _.first(this._opSetQueue);
+ Parse._objectEach(failedChanges, function(op, key) {
+ var op1 = failedChanges[key];
+ var op2 = nextChanges[key];
+ if (op1 && op2) {
+ nextChanges[key] = op2._mergeWithPrevious(op1);
+ } else if (op1) {
+ nextChanges[key] = op1;
+ }
+ });
+ this._saving = this._saving - 1;
+ },
+
+ /**
+ * Called when a save completes successfully. This merges the changes that
+ * were saved into the known server data, and overrides it with any data
+ * sent directly from the server.
+ */
+ _finishSave: function(serverData) {
+ // Grab a copy of any object referenced by this object. These instances
+ // may have already been fetched, and we don't want to lose their data.
+ // Note that doing it like this means we will unify separate copies of the
+ // same object, but that's a risk we have to take.
+ var fetchedObjects = {};
+ Parse._traverse(this.attributes, function(object) {
+ if (object instanceof Parse.Object && object.id && object._hasData) {
+ fetchedObjects[object.id] = object;
+ }
+ });
+
+ var savedChanges = _.first(this._opSetQueue);
+ this._opSetQueue = _.rest(this._opSetQueue);
+ this._applyOpSet(savedChanges, this._serverData);
+ this._mergeMagicFields(serverData);
+ var self = this;
+ Parse._objectEach(serverData, function(value, key) {
+ self._serverData[key] = Parse._decode(key, value);
+
+ // Look for any objects that might have become unfetched and fix them
+ // by replacing their values with the previously observed values.
+ var fetched = Parse._traverse(self._serverData[key], function(object) {
+ if (object instanceof Parse.Object && fetchedObjects[object.id]) {
+ return fetchedObjects[object.id];
+ }
+ });
+ if (fetched) {
+ self._serverData[key] = fetched;
+ }
+ });
+ this._rebuildAllEstimatedData();
+ this._saving = this._saving - 1;
+ },
+
+ /**
+ * Called when a fetch or login is complete to set the known server data to
+ * the given object.
+ */
+ _finishFetch: function(serverData, hasData) {
+
+ this._opSetQueue = [{}];
+
+ // Bring in all the new server data.
+ this._mergeMagicFields(serverData);
+ this._copyServerData(serverData);
+
+ this._hasData = hasData;
+ },
+
+ /**
+ * Applies the set of Parse.Op in opSet to the object target.
+ */
+ _applyOpSet: function(opSet, target) {
+ var self = this;
+ Parse._objectEach(opSet, function(change, key) {
+ target[key] = change._estimate(target[key], self, key);
+ if (target[key] === Parse.Op._UNSET) {
+ delete target[key];
+ }
+ });
+ },
+
+ /**
+ * Replaces the cached value for key with the current value.
+ * Returns true if the new value is different than the old value.
+ */
+ _resetCacheForKey: function(key) {
+ var value = this.attributes[key];
+ if (_.isObject(value) &&
+ !(value instanceof Parse.Object) &&
+ !(value instanceof Parse.File)) {
+ value = value.toJSON ? value.toJSON() : value;
+ var json = JSON.stringify(value);
+ if (this._hashedJSON[key] !== json) {
+ var wasSet = !!this._hashedJSON[key];
+ this._hashedJSON[key] = json;
+ return wasSet;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Populates attributes[key] by starting with the last known data from the
+ * server, and applying all of the local changes that have been made to that
+ * key since then.
+ */
+ _rebuildEstimatedDataForKey: function(key) {
+ var self = this;
+ delete this.attributes[key];
+ if (this._serverData[key]) {
+ this.attributes[key] = this._serverData[key];
+ }
+ Parse._arrayEach(this._opSetQueue, function(opSet) {
+ var op = opSet[key];
+ if (op) {
+ self.attributes[key] = op._estimate(self.attributes[key], self, key);
+ if (self.attributes[key] === Parse.Op._UNSET) {
+ delete self.attributes[key];
+ } else {
+ self._resetCacheForKey(key);
+ }
+ }
+ });
+ },
+
+ /**
+ * Populates attributes by starting with the last known data from the
+ * server, and applying all of the local changes that have been made since
+ * then.
+ */
+ _rebuildAllEstimatedData: function() {
+ var self = this;
+
+ var previousAttributes = _.clone(this.attributes);
+
+ this.attributes = _.clone(this._serverData);
+ Parse._arrayEach(this._opSetQueue, function(opSet) {
+ self._applyOpSet(opSet, self.attributes);
+ Parse._objectEach(opSet, function(op, key) {
+ self._resetCacheForKey(key);
+ });
+ });
+
+ // Trigger change events for anything that changed because of the fetch.
+ Parse._objectEach(previousAttributes, function(oldValue, key) {
+ if (self.attributes[key] !== oldValue) {
+ self.trigger('change:' + key, self, self.attributes[key], {});
+ }
+ });
+ Parse._objectEach(this.attributes, function(newValue, key) {
+ if (!_.has(previousAttributes, key)) {
+ self.trigger('change:' + key, self, newValue, {});
+ }
+ });
+ },
+
+ /**
+ * Sets a hash of model attributes on the object, firing
+ * "change"
unless you choose to silence it.
+ *
+ * You can call it with an object containing keys and values, or with one + * key and value. For example:
+ * gameTurn.set({ + * player: player1, + * diceRoll: 2 + * }, { + * error: function(gameTurnAgain, error) { + * // The set failed validation. + * } + * }); + * + * game.set("currentPlayer", player2, { + * error: function(gameTurnAgain, error) { + * // The set failed validation. + * } + * }); + * + * game.set("finished", true);+ * + * @param {String} key The key to set. + * @param {} value The value to give it. + * @param {Object} options A set of Backbone-like options for the set. + * The only supported options are
silent
,
+ * error
, and promise
.
+ * @return {Boolean} true if the set succeeded.
+ * @see Parse.Object#validate
+ * @see Parse.Error
+ */
+ set: function(key, value, options) {
+ var attrs, attr;
+ if (_.isObject(key) || Parse._isNullOrUndefined(key)) {
+ attrs = key;
+ Parse._objectEach(attrs, function(v, k) {
+ attrs[k] = Parse._decode(k, v);
+ });
+ options = value;
+ } else {
+ attrs = {};
+ attrs[key] = Parse._decode(key, value);
+ }
+
+ // Extract attributes and options.
+ options = options || {};
+ if (!attrs) {
+ return this;
+ }
+ if (attrs instanceof Parse.Object) {
+ attrs = attrs.attributes;
+ }
+
+ var self = this;
+ Parse._objectEach(attrs, function(unused_value, key) {
+ if (self.constructor.readOnlyAttributes &&
+ self.constructor.readOnlyAttributes[key]) {
+ throw new Error('Cannot modify readonly key: ' + key);
+ }
+ });
+
+ // If the unset option is used, every attribute should be a Unset.
+ if (options.unset) {
+ Parse._objectEach(attrs, function(unused_value, key) {
+ attrs[key] = new Parse.Op.Unset();
+ });
+ }
+
+ // Apply all the attributes to get the estimated values.
+ var dataToValidate = _.clone(attrs);
+ Parse._objectEach(dataToValidate, function(value, key) {
+ if (value instanceof Parse.Op) {
+ dataToValidate[key] = value._estimate(self.attributes[key],
+ self, key);
+ if (dataToValidate[key] === Parse.Op._UNSET) {
+ delete dataToValidate[key];
+ }
+ }
+ });
+
+ // Run validation.
+ if (!this._validate(attrs, options)) {
+ return false;
+ }
+
+ this._mergeMagicFields(attrs);
+
+ options.changes = {};
+ var escaped = this._escapedAttributes;
+ var prev = this._previousAttributes || {};
+
+ // Update attributes.
+ Parse._arrayEach(_.keys(attrs), function(attr) {
+ var val = attrs[attr];
+
+ // If this is a relation object we need to set the parent correctly,
+ // since the location where it was parsed does not have access to
+ // this object.
+ if (val instanceof Parse.Relation) {
+ val.parent = self;
+ }
+
+ if (!(val instanceof Parse.Op)) {
+ val = new Parse.Op.Set(val);
+ }
+
+ // See if this change will actually have any effect.
+ var isRealChange = true;
+ if (val instanceof Parse.Op.Set &&
+ _.isEqual(self.attributes[attr], val.value)) {
+ isRealChange = false;
+ }
+
+ if (isRealChange) {
+ delete escaped[attr];
+ if (options.silent) {
+ self._silent[attr] = true;
+ } else {
+ options.changes[attr] = true;
+ }
+ }
+
+ var currentChanges = _.last(self._opSetQueue);
+ currentChanges[attr] = val._mergeWithPrevious(currentChanges[attr]);
+ self._rebuildEstimatedDataForKey(attr);
+
+ if (isRealChange) {
+ self.changed[attr] = self.attributes[attr];
+ if (!options.silent) {
+ self._pending[attr] = true;
+ }
+ } else {
+ delete self.changed[attr];
+ delete self._pending[attr];
+ }
+ });
+
+ if (!options.silent) {
+ this.change(options);
+ }
+ return this;
+ },
+
+ /**
+ * Remove an attribute from the model, firing "change"
unless
+ * you choose to silence it. This is a noop if the attribute doesn't
+ * exist.
+ */
+ unset: function(attr, options) {
+ options = options || {};
+ options.unset = true;
+ return this.set(attr, null, options);
+ },
+
+ /**
+ * Atomically increments the value of the given attribute the next time the
+ * object is saved. If no amount is specified, 1 is used by default.
+ *
+ * @param attr {String} The key.
+ * @param amount {Number} The amount to increment by.
+ */
+ increment: function(attr, amount) {
+ if (_.isUndefined(amount) || _.isNull(amount)) {
+ amount = 1;
+ }
+ return this.set(attr, new Parse.Op.Increment(amount));
+ },
+
+ /**
+ * Atomically add an object to the end of the array associated with a given
+ * key.
+ * @param attr {String} The key.
+ * @param item {} The item to add.
+ */
+ add: function(attr, item) {
+ return this.set(attr, new Parse.Op.Add([item]));
+ },
+
+ /**
+ * Atomically add an object to the array associated with a given key, only
+ * if it is not already present in the array. The position of the insert is
+ * not guaranteed.
+ *
+ * @param attr {String} The key.
+ * @param item {} The object to add.
+ */
+ addUnique: function(attr, item) {
+ return this.set(attr, new Parse.Op.AddUnique([item]));
+ },
+
+ /**
+ * Atomically remove all instances of an object from the array associated
+ * with a given key.
+ *
+ * @param attr {String} The key.
+ * @param item {} The object to remove.
+ */
+ remove: function(attr, item) {
+ return this.set(attr, new Parse.Op.Remove([item]));
+ },
+
+ /**
+ * Returns an instance of a subclass of Parse.Op describing what kind of
+ * modification has been performed on this field since the last time it was
+ * saved. For example, after calling object.increment("x"), calling
+ * object.op("x") would return an instance of Parse.Op.Increment.
+ *
+ * @param attr {String} The key.
+ * @returns {Parse.Op} The operation, or undefined if none.
+ */
+ op: function(attr) {
+ return _.last(this._opSetQueue)[attr];
+ },
+
+ /**
+ * Clear all attributes on the model, firing "change"
unless
+ * you choose to silence it.
+ */
+ clear: function(options) {
+ options = options || {};
+ options.unset = true;
+ var keysToClear = _.extend(this.attributes, this._operations);
+ return this.set(keysToClear, options);
+ },
+
+ /**
+ * Returns a JSON-encoded set of operations to be sent with the next save
+ * request.
+ */
+ _getSaveJSON: function() {
+ var json = _.clone(_.first(this._opSetQueue));
+ Parse._objectEach(json, function(op, key) {
+ json[key] = op.toJSON();
+ });
+ return json;
+ },
+
+ /**
+ * Returns true if this object can be serialized for saving.
+ */
+ _canBeSerialized: function() {
+ return Parse.Object._canBeSerializedAsValue(this.attributes);
+ },
+
+ /**
+ * Fetch the model from the server. If the server's representation of the
+ * model differs from its current attributes, they will be overriden,
+ * triggering a "change"
event.
+ *
+ * @param {Object} options A Backbone-style callback object.
+ * Valid options are:+ * object.save();+ * or
+ * object.save(null, options);+ * or
+ * object.save(attrs, options);+ * or
+ * object.save(key, value, options);+ * + * For example,
+ * gameTurn.save({ + * player: "Jake Cutter", + * diceRoll: 2 + * }, { + * success: function(gameTurnAgain) { + * // The save was successful. + * }, + * error: function(gameTurnAgain, error) { + * // The save failed. Error is an instance of Parse.Error. + * } + * });+ * or with promises:
+ * gameTurn.save({ + * player: "Jake Cutter", + * diceRoll: 2 + * }).then(function(gameTurnAgain) { + * // The save was successful. + * }, function(error) { + * // The save failed. Error is an instance of Parse.Error. + * });+ * + * @param {Object} options A Backbone-style callback object. + * Valid options are:
"change"
+ * event. If you specify an attribute name, determine if that attribute
+ * has changed.
+ * @param {String} attr Optional attribute name
+ * @return {Boolean}
+ */
+ hasChanged: function(attr) {
+ if (!arguments.length) {
+ return !_.isEmpty(this.changed);
+ }
+ return this.changed && _.has(this.changed, attr);
+ },
+
+ /**
+ * Returns an object containing all the attributes that have changed, or
+ * false if there are no changed attributes. Useful for determining what
+ * parts of a view need to be updated and/or what attributes need to be
+ * persisted to the server. Unset attributes will be set to undefined.
+ * You can also pass an attributes object to diff against the model,
+ * determining if there *would be* a change.
+ */
+ changedAttributes: function(diff) {
+ if (!diff) {
+ return this.hasChanged() ? _.clone(this.changed) : false;
+ }
+ var changed = {};
+ var old = this._previousAttributes;
+ Parse._objectEach(diff, function(diffVal, attr) {
+ if (!_.isEqual(old[attr], diffVal)) {
+ changed[attr] = diffVal;
+ }
+ });
+ return changed;
+ },
+
+ /**
+ * Gets the previous value of an attribute, recorded at the time the last
+ * "change"
event was fired.
+ * @param {String} attr Name of the attribute to get.
+ */
+ previous: function(attr) {
+ if (!arguments.length || !this._previousAttributes) {
+ return null;
+ }
+ return this._previousAttributes[attr];
+ },
+
+ /**
+ * Gets all of the attributes of the model at the time of the previous
+ * "change"
event.
+ * @return {Object}
+ */
+ previousAttributes: function() {
+ return _.clone(this._previousAttributes);
+ },
+
+ /**
+ * Checks if the model is currently in a valid state. It's only possible to
+ * get into an *invalid* state if you're using silent changes.
+ * @return {Boolean}
+ */
+ isValid: function() {
+ return !this.validate(this.attributes);
+ },
+
+ /**
+ * You should not call this function directly unless you subclass
+ * Parse.Object
, in which case you can override this method
+ * to provide additional validation on set
and
+ * save
. Your implementation should return
+ *
+ * @param {Object} attrs The current data to validate.
+ * @param {Object} options A Backbone-like options object.
+ * @return {} False if the data is valid. An error object otherwise.
+ * @see Parse.Object#set
+ */
+ validate: function(attrs, options) {
+ if (_.has(attrs, "ACL") && !(attrs.ACL instanceof Parse.ACL)) {
+ return new Parse.Error(Parse.Error.OTHER_CAUSE,
+ "ACL must be a Parse.ACL.");
+ }
+ var correct = true;
+ Parse._objectEach(attrs, function(unused_value, key) {
+ if (!(/^[A-Za-z][0-9A-Za-z_]*$/).test(key)) {
+ correct = false;
+ }
+ });
+ if (!correct) {
+ return new Parse.Error(Parse.Error.INVALID_KEY_NAME);
+ }
+ return false;
+ },
+
+ /**
+ * Run validation against a set of incoming attributes, returning `true`
+ * if all is well. If a specific `error` callback has been passed,
+ * call that instead of firing the general `"error"` event.
+ */
+ _validate: function(attrs, options) {
+ if (options.silent || !this.validate) {
+ return true;
+ }
+ attrs = _.extend({}, this.attributes, attrs);
+ var error = this.validate(attrs, options);
+ if (!error) {
+ return true;
+ }
+ if (options && options.error) {
+ options.error(this, error, options);
+ } else {
+ this.trigger('error', this, error, options);
+ }
+ return false;
+ },
+
+ /**
+ * Returns the ACL for this object.
+ * @returns {Parse.ACL} An instance of Parse.ACL.
+ * @see Parse.Object#get
+ */
+ getACL: function() {
+ return this.get("ACL");
+ },
+
+ /**
+ * Sets the ACL to be used for this object.
+ * @param {Parse.ACL} acl An instance of Parse.ACL.
+ * @param {Object} options Optional Backbone-like options object to be
+ * passed in to set.
+ * @return {Boolean} Whether the set passed validation.
+ * @see Parse.Object#set
+ */
+ setACL: function(acl, options) {
+ return this.set("ACL", acl, options);
+ }
+
+ });
+
+ /**
+ * Returns the appropriate subclass for making new instances of the given
+ * className string.
+ */
+ Parse.Object._getSubclass = function(className) {
+ if (!_.isString(className)) {
+ throw "Parse.Object._getSubclass requires a string argument.";
+ }
+ var ObjectClass = Parse.Object._classMap[className];
+ if (!ObjectClass) {
+ ObjectClass = Parse.Object.extend(className);
+ Parse.Object._classMap[className] = ObjectClass;
+ }
+ return ObjectClass;
+ };
+
+ /**
+ * Creates an instance of a subclass of Parse.Object for the given classname.
+ */
+ Parse.Object._create = function(className, attributes, options) {
+ var ObjectClass = Parse.Object._getSubclass(className);
+ return new ObjectClass(attributes, options);
+ };
+
+ /**
+ * Returns a list of object ids given a list of objects.
+ */
+ Parse.Object._toObjectIdArray = function(list, omitObjectsWithData) {
+ if (list.length === 0) {
+ return Parse.Promise.as(list);
+ }
+
+ var error;
+ var className = list[0].className;
+ var objectIds = [];
+ for (var i = 0; i < list.length; i++) {
+ var object = list[i];
+ if (className !== object.className) {
+ error = new Parse.Error(Parse.Error.INVALID_CLASS_NAME,
+ "All objects should be of the same class");
+ return Parse.Promise.error(error);
+ } else if (!object.id) {
+ error = new Parse.Error(Parse.Error.MISSING_OBJECT_ID,
+ "All objects must have an ID");
+ return Parse.Promise.error(error);
+ } else if (omitObjectsWithData && object._hasData) {
+ continue;
+ }
+ objectIds.push(object.id);
+ }
+
+ return Parse.Promise.as(objectIds);
+ };
+
+ /**
+ * Updates a list of objects with fetched results.
+ */
+ Parse.Object._updateWithFetchedResults = function(list, fetched, forceFetch) {
+ var fetchedObjectsById = {};
+ Parse._arrayEach(fetched, function(object, i) {
+ fetchedObjectsById[object.id] = object;
+ });
+
+ for (var i = 0; i < list.length; i++) {
+ var object = list[i];
+ var fetchedObject = fetchedObjectsById[object.id];
+ if (!fetchedObject && forceFetch) {
+ var error = new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
+ "All objects must exist on the server");
+ return Parse.Promise.error(error);
+ }
+
+ object._mergeFromObject(fetchedObject);
+ }
+
+ return Parse.Promise.as(list);
+ };
+
+ /**
+ * Fetches the objects given in list. The forceFetch option will fetch all
+ * objects if true and ignore objects with data if false.
+ */
+ Parse.Object._fetchAll = function(list, forceFetch) {
+ if (list.length === 0) {
+ return Parse.Promise.as(list);
+ }
+
+ var omitObjectsWithData = !forceFetch;
+ return Parse.Object._toObjectIdArray(
+ list,
+ omitObjectsWithData
+ ).then(function(objectIds) {
+ var className = list[0].className;
+ var query = new Parse.Query(className);
+ query.containedIn("objectId", objectIds);
+ query.limit = objectIds.length;
+ return query.find();
+ }).then(function(results) {
+ return Parse.Object._updateWithFetchedResults(
+ list,
+ results,
+ forceFetch
+ );
+ });
+ };
+
+ // Set up a map of className to class so that we can create new instances of
+ // Parse Objects from JSON automatically.
+ Parse.Object._classMap = {};
+
+ Parse.Object._extend = Parse._extend;
+
+ /**
+ * Creates a new subclass of Parse.Object for the given Parse class name.
+ *
+ * Every extension of a Parse class will inherit from the most recent + * previous extension of that class. When a Parse.Object is automatically + * created by parsing JSON, it will use the most recent extension of that + * class.
+ * + *You should call either:
+ * var MyClass = Parse.Object.extend("MyClass", { + * Instance methods, + * initialize: function(attrs, options) { + * this.someInstanceProperty = [], + * Other instance properties + * } + * }, { + * Class properties + * });+ * or, for Backbone compatibility:
+ * var MyClass = Parse.Object.extend({ + * className: "MyClass", + * Instance methods, + * initialize: function(attrs, options) { + * this.someInstanceProperty = [], + * Other instance properties + * } + * }, { + * Class properties + * });+ * + * @param {String} className The name of the Parse class backing this model. + * @param {Object} protoProps Instance properties to add to instances of the + * class returned from this method. + * @param {Object} classProps Class properties to add the class returned from + * this method. + * @return {Class} A new subclass of Parse.Object. + */ + Parse.Object.extend = function(className, protoProps, classProps) { + // Handle the case with only two args. + if (!_.isString(className)) { + if (className && _.has(className, "className")) { + return Parse.Object.extend(className.className, className, protoProps); + } else { + throw new Error( + "Parse.Object.extend's first argument should be the className."); + } + } + + // If someone tries to subclass "User", coerce it to the right type. + if (className === "User" && Parse.User._performUserRewrite) { + className = "_User"; + } + protoProps = protoProps || {}; + protoProps.className = className; + + var NewClassObject = null; + if (_.has(Parse.Object._classMap, className)) { + var OldClassObject = Parse.Object._classMap[className]; + // This new subclass has been told to extend both from "this" and from + // OldClassObject. This is multiple inheritance, which isn't supported. + // For now, let's just pick one. + NewClassObject = OldClassObject._extend(protoProps, classProps); + } else { + NewClassObject = this._extend(protoProps, classProps); + } + // Extending a subclass should reuse the classname automatically. + NewClassObject.extend = function(arg0) { + if (_.isString(arg0) || (arg0 && _.has(arg0, "className"))) { + return Parse.Object.extend.apply(NewClassObject, arguments); + } + var newArguments = [className].concat(Parse._.toArray(arguments)); + return Parse.Object.extend.apply(NewClassObject, newArguments); + }; + + /** + * Creates a reference to a subclass of Parse.Object with the given id. This + * does not exist on Parse.Object, only on subclasses. + * + *
A shortcut for:
+ * var Foo = Parse.Object.extend("Foo"); + * var pointerToFoo = new Foo(); + * pointerToFoo.id = "myObjectId"; + *+ * + * @name createWithoutData + * @param {String} id The ID of the object to create a reference to. + * @return {Parse.Object} A Parse.Object reference. + * @function + * @memberOf Parse.Object + */ + NewClassObject.createWithoutData = function(id) { + var obj = new NewClassObject(); + obj.id = id; + return obj; + }; + + Parse.Object._classMap[className] = NewClassObject; + return NewClassObject; + }; + + Parse.Object._findUnsavedChildren = function(object, children, files) { + Parse._traverse(object, function(object) { + if (object instanceof Parse.Object) { + object._refreshCache(); + if (object.dirty()) { + children.push(object); + } + return; + } + + if (object instanceof Parse.File) { + if (!object.url()) { + files.push(object); + } + return; + } + }); + }; + + Parse.Object._canBeSerializedAsValue = function(object) { + + if (object instanceof Parse.Object) { + return !!object.id; + } + if (object instanceof Parse.File) { + // Don't recurse indefinitely into files. + return true; + } + + var canBeSerializedAsValue = true; + + if (_.isArray(object)) { + Parse._arrayEach(object, function(child) { + if (!Parse.Object._canBeSerializedAsValue(child)) { + canBeSerializedAsValue = false; + } + }); + } else if (_.isObject(object)) { + Parse._objectEach(object, function(child) { + if (!Parse.Object._canBeSerializedAsValue(child)) { + canBeSerializedAsValue = false; + } + }); + } + return canBeSerializedAsValue; + }; + + /** + * @param {Object} object The root object. + * @param {Object} options: The only valid option is useMasterKey. + */ + Parse.Object._deepSaveAsync = function(object, options) { + var unsavedChildren = []; + var unsavedFiles = []; + Parse.Object._findUnsavedChildren(object, unsavedChildren, unsavedFiles); + + var promise = Parse.Promise.as(); + _.each(unsavedFiles, function(file) { + promise = promise.then(function() { + return file.save(options); + }); + }); + + var objects = _.uniq(unsavedChildren); + var remaining = _.uniq(objects); + + return promise.then(function() { + return Parse.Promise._continueWhile(function() { + return remaining.length > 0; + }, function() { + + // Gather up all the objects that can be saved in this batch. + var batch = []; + var newRemaining = []; + Parse._arrayEach(remaining, function(object) { + // Limit batches to 20 objects. + if (batch.length > 20) { + newRemaining.push(object); + return; + } + + if (object._canBeSerialized()) { + batch.push(object); + } else { + newRemaining.push(object); + } + }); + remaining = newRemaining; + + // If we can't save any objects, there must be a circular reference. + if (batch.length === 0) { + return Parse.Promise.error( + new Parse.Error(Parse.Error.OTHER_CAUSE, + "Tried to save a batch with a cycle.")); + } + + // Reserve a spot in every object's save queue. + var readyToStart = Parse.Promise.when(_.map(batch, function(object) { + return object._allPreviousSaves || Parse.Promise.as(); + })); + var batchFinished = new Parse.Promise(); + Parse._arrayEach(batch, function(object) { + object._allPreviousSaves = batchFinished; + }); + + // Save a single batch, whether previous saves succeeded or failed. + return readyToStart._continueWith(function() { + return Parse._request({ + route: "batch", + method: "POST", + useMasterKey: options.useMasterKey, + sessionToken: options.sessionToken, + data: { + requests: _.map(batch, function(object) { + var json = object._getSaveJSON(); + var method = "POST"; + + var path = "/1/classes/" + object.className; + if (object.id) { + path = path + "/" + object.id; + method = "PUT"; + } + + object._startSave(); + + return { + method: method, + path: path, + body: json + }; + }) + } + }).then(function(response, status, xhr) { + var error; + Parse._arrayEach(batch, function(object, i) { + if (response[i].success) { + object._finishSave( + object.parse(response[i].success, status, xhr)); + } else { + error = error || response[i].error; + object._cancelSave(); + } + }); + if (error) { + return Parse.Promise.error( + new Parse.Error(error.code, error.error)); + } + + }).then(function(results) { + batchFinished.resolve(results); + return results; + }, function(error) { + batchFinished.reject(error); + return Parse.Promise.error(error); + }); + }); + }); + }).then(function() { + return object; + }); + }; + +}(this)); + +(function(root) { + root.Parse = root.Parse || {}; + var Parse = root.Parse; + var _ = Parse._; + + /** + * Represents a Role on the Parse server. Roles represent groupings of + * Users for the purposes of granting permissions (e.g. specifying an ACL + * for an Object). Roles are specified by their sets of child users and + * child roles, all of which are granted any permissions that the parent + * role has. + * + *
Roles must have a name (which cannot be changed after creation of the + * role), and must specify an ACL.
+ * @class + * A Parse.Role is a local representation of a role persisted to the Parse + * cloud. + */ + Parse.Role = Parse.Object.extend("_Role", /** @lends Parse.Role.prototype */ { + // Instance Methods + + /** + * Constructs a new ParseRole with the given name and ACL. + * + * @param {String} name The name of the Role to create. + * @param {Parse.ACL} acl The ACL for this role. Roles must have an ACL. + */ + constructor: function(name, acl) { + if (_.isString(name) && (acl instanceof Parse.ACL)) { + Parse.Object.prototype.constructor.call(this, null, null); + this.setName(name); + this.setACL(acl); + } else { + Parse.Object.prototype.constructor.call(this, name, acl); + } + }, + + /** + * Gets the name of the role. You can alternatively call role.get("name") + * + * @return {String} the name of the role. + */ + getName: function() { + return this.get("name"); + }, + + /** + * Sets the name for a role. This value must be set before the role has + * been saved to the server, and cannot be set once the role has been + * saved. + * + *+ * A role's name can only contain alphanumeric characters, _, -, and + * spaces. + *
+ * + *This is equivalent to calling role.set("name", name)
+ * + * @param {String} name The name of the role. + * @param {Object} options Standard options object with success and error + * callbacks. + */ + setName: function(name, options) { + return this.set("name", name, options); + }, + + /** + * Gets the Parse.Relation for the Parse.Users that are direct + * children of this role. These users are granted any privileges that this + * role has been granted (e.g. read or write access through ACLs). You can + * add or remove users from the role through this relation. + * + *This is equivalent to calling role.relation("users")
+ * + * @return {Parse.Relation} the relation for the users belonging to this + * role. + */ + getUsers: function() { + return this.relation("users"); + }, + + /** + * Gets the Parse.Relation for the Parse.Roles that are direct + * children of this role. These roles' users are granted any privileges that + * this role has been granted (e.g. read or write access through ACLs). You + * can add or remove child roles from this role through this relation. + * + *This is equivalent to calling role.relation("roles")
+ * + * @return {Parse.Relation} the relation for the roles belonging to this + * role. + */ + getRoles: function() { + return this.relation("roles"); + }, + + /** + * @ignore + */ + validate: function(attrs, options) { + if ("name" in attrs && attrs.name !== this.getName()) { + var newName = attrs.name; + if (this.id && this.id !== attrs.objectId) { + // Check to see if the objectId being set matches this.id. + // This happens during a fetch -- the id is set before calling fetch. + // Let the name be set in this case. + return new Parse.Error(Parse.Error.OTHER_CAUSE, + "A role's name can only be set before it has been saved."); + } + if (!_.isString(newName)) { + return new Parse.Error(Parse.Error.OTHER_CAUSE, + "A role's name must be a String."); + } + if (!(/^[0-9a-zA-Z\-_ ]+$/).test(newName)) { + return new Parse.Error(Parse.Error.OTHER_CAUSE, + "A role's name can only contain alphanumeric characters, _," + + " -, and spaces."); + } + } + if (Parse.Object.prototype.validate) { + return Parse.Object.prototype.validate.call(this, attrs, options); + } + return false; + } + }); +}(this)); + + +/*global _: false */ +(function(root) { + root.Parse = root.Parse || {}; + var Parse = root.Parse; + var _ = Parse._; + + /** + * Creates a new instance with the given models and options. Typically, you + * will not call this method directly, but will instead make a subclass using + *Parse.Collection.extend
.
+ *
+ * @param {Array} models An array of instances of Parse.Object
.
+ *
+ * @param {Object} options An optional object with Backbone-style options.
+ * Valid options are:Provides a standard collection class for our sets of models, ordered + * or unordered. For more information, see the + * Backbone + * documentation.
+ */ + Parse.Collection = function(models, options) { + options = options || {}; + if (options.comparator) { + this.comparator = options.comparator; + } + if (options.model) { + this.model = options.model; + } + if (options.query) { + this.query = options.query; + } + this._reset(); + this.initialize.apply(this, arguments); + if (models) { + this.reset(models, {silent: true, parse: options.parse}); + } + }; + + // Define the Collection's inheritable methods. + _.extend(Parse.Collection.prototype, Parse.Events, + /** @lends Parse.Collection.prototype */ { + + // The default model for a collection is just a Parse.Object. + // This should be overridden in most cases. + + model: Parse.Object, + + /** + * Initialize is an empty function by default. Override it with your own + * initialization logic. + */ + initialize: function(){}, + + /** + * The JSON representation of a Collection is an array of the + * models' attributes. + */ + toJSON: function() { + return this.map(function(model){ return model.toJSON(); }); + }, + + /** + * Add a model, or list of models to the set. Pass **silent** to avoid + * firing the `add` event for every new model. + * + * @param {Array} models An array of instances ofParse.Object
.
+ *
+ * @param {Object} options An optional object with Backbone-style options.
+ * Valid options are:remove
event for every model removed.
+ *
+ * @param {Array} models The model or list of models to remove from the
+ * collection.
+ * @param {Object} options An optional object with Backbone-style options.
+ * Valid options are: Parse.Collection
. For example,+ * var MyCollection = Parse.Collection.extend({ + * // Instance properties + * + * model: MyClass, + * query: MyQuery, + * + * getFirst: function() { + * return this.at(0); + * } + * }, { + * // Class properties + * + * makeOne: function() { + * return new MyCollection(); + * } + * }); + * + * var collection = new MyCollection(); + *+ * + * @function + * @param {Object} instanceProps Instance properties for the collection. + * @param {Object} classProps Class properies for the collection. + * @return {Class} A new subclass of
Parse.Collection
.
+ */
+ Parse.Collection.extend = Parse._extend;
+
+}(this));
+
+/*global _: false, document: false */
+(function(root) {
+ root.Parse = root.Parse || {};
+ var Parse = root.Parse;
+ var _ = Parse._;
+
+ /**
+ * Creating a Parse.View creates its initial element outside of the DOM,
+ * if an existing element is not provided...
+ * @class
+ *
+ * A fork of Backbone.View, provided for your convenience. If you use this + * class, you must also include jQuery, or another library that provides a + * jQuery-compatible $ function. For more information, see the + * Backbone + * documentation.
+ *Available in the client SDK only.
+ */ + Parse.View = function(options) { + this.cid = _.uniqueId('view'); + this._configure(options || {}); + this._ensureElement(); + this.initialize.apply(this, arguments); + this.delegateEvents(); + }; + + // Cached regex to split keys for `delegate`. + var eventSplitter = /^(\S+)\s*(.*)$/; + + // List of view options to be merged as properties. + + var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', + 'className', 'tagName']; + + // Set up all inheritable **Parse.View** properties and methods. + _.extend(Parse.View.prototype, Parse.Events, + /** @lends Parse.View.prototype */ { + + // The default `tagName` of a View's element is `"div"`. + tagName: 'div', + + /** + * jQuery delegate for element lookup, scoped to DOM elements within the + * current view. This should be prefered to global lookups where possible. + */ + $: function(selector) { + return this.$el.find(selector); + }, + + /** + * Initialize is an empty function by default. Override it with your own + * initialization logic. + */ + initialize: function(){}, + + /** + * The core function that your view should override, in order + * to populate its element (`this.el`), with the appropriate HTML. The + * convention is for **render** to always return `this`. + */ + render: function() { + return this; + }, + + /** + * Remove this view from the DOM. Note that the view isn't present in the + * DOM by default, so calling this method may be a no-op. + */ + remove: function() { + this.$el.remove(); + return this; + }, + + /** + * For small amounts of DOM Elements, where a full-blown template isn't + * needed, use **make** to manufacture elements, one at a time. + *+ * var el = this.make('li', {'class': 'row'}, + * this.model.escape('title'));+ */ + make: function(tagName, attributes, content) { + var el = document.createElement(tagName); + if (attributes) { + Parse.$(el).attr(attributes); + } + if (content) { + Parse.$(el).html(content); + } + return el; + }, + + /** + * Changes the view's element (`this.el` property), including event + * re-delegation. + */ + setElement: function(element, delegate) { + this.$el = Parse.$(element); + this.el = this.$el[0]; + if (delegate !== false) { + this.delegateEvents(); + } + return this; + }, + + /** + * Set callbacks.
this.events
is a hash of
+ * + * *{"event selector": "callback"}* + * + * { + * 'mousedown .title': 'edit', + * 'click .button': 'save' + * 'click .open': function(e) { ... } + * } + *+ * pairs. Callbacks will be bound to the view, with `this` set properly. + * Uses event delegation for efficiency. + * Omitting the selector binds the event to `this.el`. + * This only works for delegate-able events: not `focus`, `blur`, and + * not `change`, `submit`, and `reset` in Internet Explorer. + */ + delegateEvents: function(events) { + events = events || Parse._getValue(this, 'events'); + if (!events) { + return; + } + this.undelegateEvents(); + var self = this; + Parse._objectEach(events, function(method, key) { + if (!_.isFunction(method)) { + method = self[events[key]]; + } + if (!method) { + throw new Error('Event "' + events[key] + '" does not exist'); + } + var match = key.match(eventSplitter); + var eventName = match[1], selector = match[2]; + method = _.bind(method, self); + eventName += '.delegateEvents' + self.cid; + if (selector === '') { + self.$el.bind(eventName, method); + } else { + self.$el.delegate(selector, eventName, method); + } + }); + }, + + /** + * Clears all callbacks previously bound to the view with `delegateEvents`. + * You usually don't need to use this, but may wish to if you have multiple + * Backbone views attached to the same DOM element. + */ + undelegateEvents: function() { + this.$el.unbind('.delegateEvents' + this.cid); + }, + + /** + * Performs the initial configuration of a View with a set of options. + * Keys with special meaning *(model, collection, id, className)*, are + * attached directly to the view. + */ + _configure: function(options) { + if (this.options) { + options = _.extend({}, this.options, options); + } + var self = this; + _.each(viewOptions, function(attr) { + if (options[attr]) { + self[attr] = options[attr]; + } + }); + this.options = options; + }, + + /** + * Ensure that the View has a DOM element to render into. + * If `this.el` is a string, pass it through `$()`, take the first + * matching element, and re-assign it to `el`. Otherwise, create + * an element from the `id`, `className` and `tagName` properties. + */ + _ensureElement: function() { + if (!this.el) { + var attrs = Parse._getValue(this, 'attributes') || {}; + if (this.id) { + attrs.id = this.id; + } + if (this.className) { + attrs['class'] = this.className; + } + this.setElement(this.make(this.tagName, attrs), false); + } else { + this.setElement(this.el, false); + } + } + + }); + + /** + * @function + * @param {Object} instanceProps Instance properties for the view. + * @param {Object} classProps Class properies for the view. + * @return {Class} A new subclass of
Parse.View
.
+ */
+ Parse.View.extend = Parse._extend;
+
+}(this));
+
+(function(root) {
+ root.Parse = root.Parse || {};
+ var Parse = root.Parse;
+ var _ = Parse._;
+
+ /**
+ * @class
+ *
+ * A Parse.User object is a local representation of a user persisted to the + * Parse cloud. This class is a subclass of a Parse.Object, and retains the + * same functionality of a Parse.Object, but also extends it with various + * user specific methods, like authentication, signing up, and validation of + * uniqueness.
+ */ + Parse.User = Parse.Object.extend("_User", /** @lends Parse.User.prototype */ { + // Instance Variables + _isCurrentUser: false, + + + // Instance Methods + + /** + * Merges another object's attributes into this object. + */ + _mergeFromObject: function(other) { + if (other.getSessionToken()) { + this._sessionToken = other.getSessionToken(); + } + Parse.User.__super__._mergeFromObject.call(this, other); + }, + + /** + * Internal method to handle special fields in a _User response. + */ + _mergeMagicFields: function(attrs) { + if (attrs.sessionToken) { + this._sessionToken = attrs.sessionToken; + delete attrs.sessionToken; + } + Parse.User.__super__._mergeMagicFields.call(this, attrs); + }, + + /** + * Removes null values from authData (which exist temporarily for + * unlinking) + */ + _cleanupAuthData: function() { + if (!this.isCurrent()) { + return; + } + var authData = this.get('authData'); + if (!authData) { + return; + } + Parse._objectEach(this.get('authData'), function(value, key) { + if (!authData[key]) { + delete authData[key]; + } + }); + }, + + /** + * Synchronizes authData for all providers. + */ + _synchronizeAllAuthData: function() { + var authData = this.get('authData'); + if (!authData) { + return; + } + + var self = this; + Parse._objectEach(this.get('authData'), function(value, key) { + self._synchronizeAuthData(key); + }); + }, + + /** + * Synchronizes auth data for a provider (e.g. puts the access token in the + * right place to be used by the Facebook SDK). + */ + _synchronizeAuthData: function(provider) { + if (!this.isCurrent()) { + return; + } + var authType; + if (_.isString(provider)) { + authType = provider; + provider = Parse.User._authProviders[authType]; + } else { + authType = provider.getAuthType(); + } + var authData = this.get('authData'); + if (!authData || !provider) { + return; + } + var success = provider.restoreAuthentication(authData[authType]); + if (!success) { + this._unlinkFrom(provider); + } + }, + + _handleSaveResult: function(makeCurrent) { + // Clean up and synchronize the authData object, removing any unset values + if (makeCurrent) { + this._isCurrentUser = true; + } + this._cleanupAuthData(); + this._synchronizeAllAuthData(); + // Don't keep the password around. + delete this._serverData.password; + this._rebuildEstimatedDataForKey("password"); + this._refreshCache(); + if (makeCurrent || this.isCurrent()) { + Parse.User._saveCurrentUser(this); + } + }, + + /** + * Unlike in the Android/iOS SDKs, logInWith is unnecessary, since you can + * call linkWith on the user (even if it doesn't exist yet on the server). + */ + _linkWith: function(provider, options) { + var authType; + if (_.isString(provider)) { + authType = provider; + provider = Parse.User._authProviders[provider]; + } else { + authType = provider.getAuthType(); + } + if (_.has(options, 'authData')) { + var authData = this.get('authData') || {}; + authData[authType] = options.authData; + this.set('authData', authData); + + // Overridden so that the user can be made the current user. + var newOptions = _.clone(options) || {}; + newOptions.success = function(model) { + model._handleSaveResult(true); + if (options.success) { + options.success.apply(this, arguments); + } + }; + return this.save({'authData': authData}, newOptions); + } else { + var self = this; + var promise = new Parse.Promise(); + provider.authenticate({ + success: function(provider, result) { + self._linkWith(provider, { + authData: result, + success: options.success, + error: options.error + }).then(function() { + promise.resolve(self); + }); + }, + error: function(provider, error) { + if (options.error) { + options.error(self, error); + } + promise.reject(error); + } + }); + return promise; + } + }, + + /** + * Unlinks a user from a service. + */ + _unlinkFrom: function(provider, options) { + var authType; + if (_.isString(provider)) { + authType = provider; + provider = Parse.User._authProviders[provider]; + } else { + authType = provider.getAuthType(); + } + var newOptions = _.clone(options); + var self = this; + newOptions.authData = null; + newOptions.success = function(model) { + self._synchronizeAuthData(provider); + if (options.success) { + options.success.apply(this, arguments); + } + }; + return this._linkWith(provider, newOptions); + }, + + /** + * Checks whether a user is linked to a service. + */ + _isLinked: function(provider) { + var authType; + if (_.isString(provider)) { + authType = provider; + } else { + authType = provider.getAuthType(); + } + var authData = this.get('authData') || {}; + return !!authData[authType]; + }, + + /** + * Deauthenticates all providers. + */ + _logOutWithAll: function() { + var authData = this.get('authData'); + if (!authData) { + return; + } + var self = this; + Parse._objectEach(this.get('authData'), function(value, key) { + self._logOutWith(key); + }); + }, + + /** + * Deauthenticates a single provider (e.g. removing access tokens from the + * Facebook SDK). + */ + _logOutWith: function(provider) { + if (!this.isCurrent()) { + return; + } + if (_.isString(provider)) { + provider = Parse.User._authProviders[provider]; + } + if (provider && provider.deauthenticate) { + provider.deauthenticate(); + } + }, + + /** + * Signs up a new user. You should call this instead of save for + * new Parse.Users. This will create a new Parse.User on the server, and + * also persist the session on disk so that you can access the user using + *current
.
+ *
+ * A username and password must be set before calling signUp.
+ * + *Calls options.success or options.error on completion.
+ * + * @param {Object} attrs Extra fields to set on the new user, or null. + * @param {Object} options A Backbone-style options object. + * @return {Parse.Promise} A promise that is fulfilled when the signup + * finishes. + * @see Parse.User.signUp + */ + signUp: function(attrs, options) { + var error; + options = options || {}; + + var username = (attrs && attrs.username) || this.get("username"); + if (!username || (username === "")) { + error = new Parse.Error( + Parse.Error.OTHER_CAUSE, + "Cannot sign up user with an empty name."); + if (options && options.error) { + options.error(this, error); + } + return Parse.Promise.error(error); + } + + var password = (attrs && attrs.password) || this.get("password"); + if (!password || (password === "")) { + error = new Parse.Error( + Parse.Error.OTHER_CAUSE, + "Cannot sign up user with an empty password."); + if (options && options.error) { + options.error(this, error); + } + return Parse.Promise.error(error); + } + + // Overridden so that the user can be made the current user. + var newOptions = _.clone(options); + newOptions.success = function(model) { + model._handleSaveResult(Parse.User._canUseCurrentUser()); + if (options.success) { + options.success.apply(this, arguments); + } + }; + return this.save(attrs, newOptions); + }, + + /** + * Logs in a Parse.User. On success, this saves the session to localStorage, + * so you can retrieve the currently logged in user using + *current
.
+ *
+ * A username and password must be set before calling logIn.
+ * + *Calls options.success or options.error on completion.
+ * + * @param {Object} options A Backbone-style options object. + * @see Parse.User.logIn + * @return {Parse.Promise} A promise that is fulfilled with the user when + * the login is complete. + */ + logIn: function(options) { + if (!Parse.User._canUseCurrentUser()) { + throw new Error( + 'It is not possible to log in on a server environment.' + ); + } + var model = this; + options = options || {}; + var request = Parse._request({ + route: "login", + method: "GET", + useMasterKey: options.useMasterKey, + data: this.toJSON() + }); + return request.then(function(resp, status, xhr) { + var serverAttrs = model.parse(resp, status, xhr); + model._finishFetch(serverAttrs); + model._handleSaveResult(true); + return model; + })._thenRunCallbacks(options, this); + }, + + /** + * @see Parse.Object#save + */ + save: function(arg1, arg2, arg3) { + var i, attrs, current, options, saved; + if (_.isObject(arg1) || _.isNull(arg1) || _.isUndefined(arg1)) { + attrs = arg1; + options = arg2; + } else { + attrs = {}; + attrs[arg1] = arg2; + options = arg3; + } + options = options || {}; + + var newOptions = _.clone(options); + newOptions.success = function(model) { + model._handleSaveResult(false); + if (options.success) { + options.success.apply(this, arguments); + } + }; + return Parse.Object.prototype.save.call(this, attrs, newOptions); + }, + + /** + * @see Parse.Object#fetch + */ + fetch: function(options) { + var newOptions = options ? _.clone(options) : {}; + newOptions.success = function(model) { + model._handleSaveResult(false); + if (options && options.success) { + options.success.apply(this, arguments); + } + }; + return Parse.Object.prototype.fetch.call(this, newOptions); + }, + + /** + * Returns true ifcurrent
would return this user.
+ * @see Parse.User#current
+ */
+ isCurrent: function() {
+ return this._isCurrentUser;
+ },
+
+ /**
+ * Returns get("username").
+ * @return {String}
+ * @see Parse.Object#get
+ */
+ getUsername: function() {
+ return this.get("username");
+ },
+
+ /**
+ * Calls set("username", username, options) and returns the result.
+ * @param {String} username
+ * @param {Object} options A Backbone-style options object.
+ * @return {Boolean}
+ * @see Parse.Object.set
+ */
+ setUsername: function(username, options) {
+ return this.set("username", username, options);
+ },
+
+ /**
+ * Calls set("password", password, options) and returns the result.
+ * @param {String} password
+ * @param {Object} options A Backbone-style options object.
+ * @return {Boolean}
+ * @see Parse.Object.set
+ */
+ setPassword: function(password, options) {
+ return this.set("password", password, options);
+ },
+
+ /**
+ * Returns get("email").
+ * @return {String}
+ * @see Parse.Object#get
+ */
+ getEmail: function() {
+ return this.get("email");
+ },
+
+ /**
+ * Calls set("email", email, options) and returns the result.
+ * @param {String} email
+ * @param {Object} options A Backbone-style options object.
+ * @return {Boolean}
+ * @see Parse.Object.set
+ */
+ setEmail: function(email, options) {
+ return this.set("email", email, options);
+ },
+
+ /**
+ * Checks whether this user is the current user and has been authenticated.
+ * @return (Boolean) whether this user is the current user and is logged in.
+ */
+ authenticated: function() {
+ return !!this._sessionToken &&
+ (Parse.User.current() && Parse.User.current().id === this.id);
+ },
+
+ /**
+ * Returns the session token for this user, if the user has been logged in,
+ * or if it is the result of a query with the master key. Otherwise, returns
+ * undefined.
+ * @return {String} the session token, or undefined
+ */
+ getSessionToken: function() {
+ return this._sessionToken;
+ },
+
+ /**
+ * Request a revocable session token to replace the older style of token.
+ * @param {Object} options A Backbone-style options object.
+ *
+ * @return {Parse.Promise} A promise that is resolved when the replacement
+ * token has been fetched.
+ */
+ _upgradeToRevocableSession: function(options) {
+ options = options || {};
+ if (!Parse.User.current()) {
+ return Parse.Promise.as()._thenRunCallbacks(options);
+ }
+ var currentSession = Parse.User.current().getSessionToken();
+ if (Parse.Session._isRevocable(currentSession)) {
+ return Parse.Promise.as()._thenRunCallbacks(options);
+ }
+ return Parse._request({
+ route: 'upgradeToRevocableSession',
+ method: 'POST',
+ useMasterKey: options.useMasterKey,
+ sessionToken: currentSession
+ }).then(function(result) {
+ var session = new Parse.Session();
+ session._finishFetch(result);
+ var currentUser = Parse.User.current();
+ currentUser._sessionToken = session.getSessionToken();
+ Parse.User._saveCurrentUser(currentUser);
+ })._thenRunCallbacks(options);
+ },
+
+ }, /** @lends Parse.User */ {
+ // Class Variables
+
+ // The currently logged-in user.
+ _currentUser: null,
+
+ // Whether currentUser is known to match the serialized version on disk.
+ // This is useful for saving a localstorage check if you try to load
+ // _currentUser frequently while there is none stored.
+ _currentUserMatchesDisk: false,
+
+ // The localStorage key suffix that the current user is stored under.
+ _CURRENT_USER_KEY: "currentUser",
+
+ // The mapping of auth provider names to actual providers
+ _authProviders: {},
+
+ // Whether to rewrite className User to _User
+ _performUserRewrite: true,
+
+ // Whether to send a Revocable Session header
+ _isRevocableSessionEnabled: false,
+
+ // Whether to enable a memory-unsafe current user in node.js
+ _enableUnsafeCurrentUser: false,
+
+
+ // Class Methods
+
+ /**
+ * Signs up a new user with a username (or email) and password.
+ * This will create a new Parse.User on the server, and also persist the
+ * session in localStorage so that you can access the user using
+ * {@link #current}.
+ *
+ * Calls options.success or options.error on completion.
+ * + * @param {String} username The username (or email) to sign up with. + * @param {String} password The password to sign up with. + * @param {Object} attrs Extra fields to set on the new user. + * @param {Object} options A Backbone-style options object. + * @return {Parse.Promise} A promise that is fulfilled with the user when + * the signup completes. + * @see Parse.User#signUp + */ + signUp: function(username, password, attrs, options) { + attrs = attrs || {}; + attrs.username = username; + attrs.password = password; + var user = Parse.Object._create("_User"); + return user.signUp(attrs, options); + }, + + /** + * Logs in a user with a username (or email) and password. On success, this + * saves the session to disk, so you can retrieve the currently logged in + * user usingcurrent
.
+ *
+ * Calls options.success or options.error on completion.
+ * + * @param {String} username The username (or email) to log in with. + * @param {String} password The password to log in with. + * @param {Object} options A Backbone-style options object. + * @return {Parse.Promise} A promise that is fulfilled with the user when + * the login completes. + * @see Parse.User#logIn + */ + logIn: function(username, password, options) { + var user = Parse.Object._create("_User"); + user._finishFetch({ username: username, password: password }); + return user.logIn(options); + }, + + /** + * Logs in a user with a session token. On success, this saves the session + * to disk, so you can retrieve the currently logged in user using + *current
.
+ *
+ * Calls options.success or options.error on completion.
+ * + * @param {String} sessionToken The sessionToken to log in with. + * @param {Object} options A Backbone-style options object. + * @return {Parse.Promise} A promise that is fulfilled with the user when + * the login completes. + */ + become: function(sessionToken, options) { + if (!Parse.User._canUseCurrentUser()) { + throw new Error( + 'It is not secure to become a user on a node.js server environment.' + ); + } + options = options || {}; + + var user = Parse.Object._create("_User"); + return Parse._request({ + route: "users", + className: "me", + method: "GET", + useMasterKey: options.useMasterKey, + sessionToken: sessionToken + }).then(function(resp, status, xhr) { + var serverAttrs = user.parse(resp, status, xhr); + user._finishFetch(serverAttrs); + user._handleSaveResult(true); + return user; + + })._thenRunCallbacks(options, user); + }, + + /** + * Logs out the currently logged in user session. This will remove the + * session from disk, log out of linked services, and future calls to + *current
will return null
.
+ * @return {Parse.Promise} A promise that is resolved when the session is
+ * destroyed on the server.
+ */
+ logOut: function() {
+ if (!Parse.User._canUseCurrentUser()) {
+ throw new Error(
+ 'There is no current user user on a node.js server environment.'
+ );
+ }
+ return Parse.User._currentAsync().then(function(currentUser) {
+ var promise = Parse.Storage.removeItemAsync(
+ Parse._getParsePath(Parse.User._CURRENT_USER_KEY));
+
+ if (currentUser !== null) {
+ var currentSession = currentUser.getSessionToken();
+ if (Parse.Session._isRevocable(currentSession)) {
+ promise.then(function() {
+ return Parse._request({
+ route: 'logout',
+ method: 'POST',
+ sessionToken: currentSession
+ });
+ });
+ }
+ currentUser._logOutWithAll();
+ currentUser._isCurrentUser = false;
+ }
+ Parse.User._currentUserMatchesDisk = true;
+ Parse.User._currentUser = null;
+
+ return promise;
+ });
+ },
+
+ /**
+ * Requests a password reset email to be sent to the specified email address
+ * associated with the user account. This email allows the user to securely
+ * reset their password on the Parse site.
+ *
+ * Calls options.success or options.error on completion.
+ * + * @param {String} email The email address associated with the user that + * forgot their password. + * @param {Object} options A Backbone-style options object. + */ + requestPasswordReset: function(email, options) { + options = options || {}; + var request = Parse._request({ + route: "requestPasswordReset", + method: "POST", + useMasterKey: options.useMasterKey, + data: { email: email } + }); + return request._thenRunCallbacks(options); + }, + + /** + * Retrieves the currently logged in ParseUser with a valid session, + * either from memory or localStorage, if necessary. + * @return {Parse.Object} The currently logged in Parse.User. + */ + current: function() { + if (!Parse.User._canUseCurrentUser()) { + throw new Error( + 'There is no current user user on a node.js server environment.' + ); + } + if (Parse.Storage.async) { + // We can't return the current user synchronously + Parse.User._currentAsync(); + return Parse.User._currentUser; + } + + if (Parse.User._currentUser) { + return Parse.User._currentUser; + } + + if (Parse.User._currentUserMatchesDisk) { + + return Parse.User._currentUser; + } + + // Load the user from local storage. + Parse.User._currentUserMatchesDisk = true; + + var userData = Parse.Storage.getItem(Parse._getParsePath( + Parse.User._CURRENT_USER_KEY)); + if (!userData) { + + return null; + } + Parse.User._currentUser = Parse.Object._create("_User"); + Parse.User._currentUser._isCurrentUser = true; + + var json = JSON.parse(userData); + Parse.User._currentUser.id = json._id; + delete json._id; + Parse.User._currentUser._sessionToken = json._sessionToken; + delete json._sessionToken; + Parse.User._currentUser._finishFetch(json); + + Parse.User._currentUser._synchronizeAllAuthData(); + Parse.User._currentUser._refreshCache(); + Parse.User._currentUser._opSetQueue = [{}]; + return Parse.User._currentUser; + }, + + /** + * Retrieves the currently logged in ParseUser from asynchronous Storage. + * @return {Parse.Promise} A Promise that is resolved with the currently + * logged in Parse User + */ + _currentAsync: function() { + if (Parse.User._currentUser) { + return Parse.Promise.as(Parse.User._currentUser); + } + + if (Parse.User._currentUserMatchesDisk) { + return Parse.Promise.as(Parse.User._currentUser); + } + + // Load the user from Storage + return Parse.Storage.getItemAsync(Parse._getParsePath( + Parse.User._CURRENT_USER_KEY)).then(function(userData) { + if (!userData) { + return null; + } + Parse.User._currentUser = Parse.Object._create("_User"); + Parse.User._currentUser._isCurrentUser = true; + + var json = JSON.parse(userData); + Parse.User._currentUser.id = json._id; + delete json._id; + Parse.User._currentUser._sessionToken = json._sessionToken; + delete json._sessionToken; + Parse.User._currentUser._finishFetch(json); + + Parse.User._currentUser._synchronizeAllAuthData(); + Parse.User._currentUser._refreshCache(); + Parse.User._currentUser._opSetQueue = [{}]; + return Parse.User._currentUser; + }); + }, + + /** + * Allow someone to define a custom User class without className + * being rewritten to _User. The default behavior is to rewrite + * User to _User for legacy reasons. This allows developers to + * override that behavior. + * + * @param {Boolean} isAllowed Whether or not to allow custom User class + */ + allowCustomUserClass: function(isAllowed) { + this._performUserRewrite = !isAllowed; + }, + + /** + * Allow a legacy application to start using revocable sessions. If the + * current session token is not revocable, a request will be made for a new, + * revocable session. + * It is not necessary to call this method from cloud code unless you are + * handling user signup or login from the server side. In a cloud code call, + * this function will not attempt to upgrade the current token. + * @param {Object} options A Backbone-style options object. + * + * @return {Parse.Promise} A promise that is resolved when the process has + * completed. If a replacement session token is requested, the promise + * will be resolved after a new token has been fetched. + */ + enableRevocableSession: function(options) { + options = options || {}; + Parse.User._isRevocableSessionEnabled = true; + if (Parse.User._canUseCurrentUser() && Parse.User.current()) { + return Parse.User.current()._upgradeToRevocableSession(options); + } + return Parse.Promise.as()._thenRunCallbacks(options); + }, + + /** + * + */ + enableUnsafeCurrentUser: function() { + Parse.User._enableUnsafeCurrentUser = true; + }, + + _canUseCurrentUser: function() { + return !Parse._isNode || Parse.User._enableUnsafeCurrentUser; + }, + + /** + * Persists a user as currentUser to localStorage, and into the singleton. + */ + _saveCurrentUser: function(user) { + if (Parse.User._currentUser !== null && + Parse.User._currentUser !== user) { + Parse.User.logOut(); + } + user._isCurrentUser = true; + Parse.User._currentUser = user; + Parse.User._currentUserMatchesDisk = true; + + var json = user.toJSON(); + json._id = user.id; + json._sessionToken = user._sessionToken; + if (Parse.Storage.async) { + Parse.Storage.setItemAsync( + Parse._getParsePath(Parse.User._CURRENT_USER_KEY), + JSON.stringify(json)); + } else { + Parse.Storage.setItem( + Parse._getParsePath(Parse.User._CURRENT_USER_KEY), + JSON.stringify(json)); + } + }, + + _registerAuthenticationProvider: function(provider) { + Parse.User._authProviders[provider.getAuthType()] = provider; + // Synchronize the current user with the auth provider. + if (Parse.User.current()) { + Parse.User.current()._synchronizeAuthData(provider.getAuthType()); + } + }, + + _logInWith: function(provider, options) { + var user = Parse.Object._create("_User"); + return user._linkWith(provider, options); + } + + }); +}(this)); + + +(function(root) { + root.Parse = root.Parse || {}; + var Parse = root.Parse; + + /** + * @class + * + *A Parse.Session object is a local representation of a revocable session. + * This class is a subclass of a Parse.Object, and retains the same + * functionality of a Parse.Object.
+ */ + Parse.Session = Parse.Object.extend('_Session', + /** @lends Parse.Session.prototype */ + { + /** + * Returns the session token string. + * @return {String} + */ + getSessionToken: function() { + return this._sessionToken; + }, + + /** + * Internal method to handle special fields in a _Session response. + */ + _mergeMagicFields: function(attrs) { + if (attrs.sessionToken) { + this._sessionToken = attrs.sessionToken; + delete attrs.sessionToken; + } + Parse.Session.__super__._mergeMagicFields.call(this, attrs); + }, + }, /** @lends Parse.Session */ { + + // Throw an error when modifying these read-only fields + readOnlyAttributes: { + createdWith: true, + expiresAt: true, + installationId: true, + restricted: true, + sessionToken: true, + user: true + }, + + /** + * Retrieves the Session object for the currently logged in session. + * @return {Parse.Promise} A promise that is resolved with the Parse.Session + * object after it has been fetched. + */ + current: function(options) { + options = options || {}; + + var session = Parse.Object._create('_Session'); + var currentToken = Parse.User.current().getSessionToken(); + return Parse._request({ + route: 'sessions', + className: 'me', + method: 'GET', + useMasterKey: options.useMasterKey, + sessionToken: currentToken + }).then(function(resp, status, xhr) { + var serverAttrs = session.parse(resp, status, xhr); + session._finishFetch(serverAttrs); + return session; + })._thenRunCallbacks(options, session); + }, + + /** + * Determines whether a session token is revocable. + * @return {Boolean} + */ + _isRevocable: function(token) { + return token.indexOf('r:') > -1; + }, + + /** + * Determines whether the current session token is revocable. + * This method is useful for migrating Express.js or Node.js web apps to + * use revocable sessions. If you are migrating an app that uses the Parse + * SDK in the browser only, please use Parse.User.enableRevocableSession() + * instead, so that sessions can be automatically upgraded. + * @return {Boolean} + */ + isCurrentSessionRevocable: function() { + if (Parse.User.current() !== null) { + return Parse.Session._isRevocable( + Parse.User.current().getSessionToken() + ); + } + } + }); +})(this); + +// Parse.Query is a way to create a list of Parse.Objects. +(function(root) { + root.Parse = root.Parse || {}; + var Parse = root.Parse; + var _ = Parse._; + + /** + * Creates a new parse Parse.Query for the given Parse.Object subclass. + * @param objectClass - + * An instance of a subclass of Parse.Object, or a Parse className string. + * @class + * + *Parse.Query defines a query that is used to fetch Parse.Objects. The
+ * most common use case is finding all objects that match a query through the
+ * find
method. For example, this sample code fetches all objects
+ * of class MyClass
. It calls a different function depending on
+ * whether the fetch succeeded or not.
+ *
+ *
+ * var query = new Parse.Query(MyClass); + * query.find({ + * success: function(results) { + * // results is an array of Parse.Object. + * }, + * + * error: function(error) { + * // error is an instance of Parse.Error. + * } + * });+ * + *
A Parse.Query can also be used to retrieve a single object whose id is
+ * known, through the get method. For example, this sample code fetches an
+ * object of class MyClass
and id myId
. It calls a
+ * different function depending on whether the fetch succeeded or not.
+ *
+ *
+ * var query = new Parse.Query(MyClass); + * query.get(myId, { + * success: function(object) { + * // object is an instance of Parse.Object. + * }, + * + * error: function(object, error) { + * // error is an instance of Parse.Error. + * } + * });+ * + *
A Parse.Query can also be used to count the number of objects that match
+ * the query without retrieving all of those objects. For example, this
+ * sample code counts the number of objects of the class MyClass
+ *
+ * var query = new Parse.Query(MyClass); + * query.count({ + * success: function(number) { + * // There are number instances of MyClass. + * }, + * + * error: function(error) { + * // error is an instance of Parse.Error. + * } + * });+ */ + Parse.Query = function(objectClass) { + if (_.isString(objectClass)) { + objectClass = Parse.Object._getSubclass(objectClass); + } + + this.objectClass = objectClass; + + this.className = objectClass.prototype.className; + + this._where = {}; + this._include = []; + this._limit = -1; // negative limit means, do not send a limit + this._skip = 0; + this._extraOptions = {}; + }; + + /** + * Constructs a Parse.Query that is the OR of the passed in queries. For + * example: + *
var compoundQuery = Parse.Query.or(query1, query2, query3);+ * + * will create a compoundQuery that is an or of the query1, query2, and + * query3. + * @param {...Parse.Query} var_args The list of queries to OR. + * @return {Parse.Query} The query that is the OR of the passed in queries. + */ + Parse.Query.or = function() { + var queries = _.toArray(arguments); + var className = null; + Parse._arrayEach(queries, function(q) { + if (_.isNull(className)) { + className = q.className; + } + + if (className !== q.className) { + throw "All queries must be for the same class"; + } + }); + var query = new Parse.Query(className); + query._orQuery(queries); + return query; + }; + + Parse.Query.prototype = { + /** + * Constructs a Parse.Object whose id is already known by fetching data from + * the server. Either options.success or options.error is called when the + * find completes. + * + * @param {String} objectId The id of the object to be fetched. + * @param {Object} options A Backbone-style options object. + * Valid options are:
Parse.Object
+ * with which to start this Collection.
+ * @param {Object} options An optional object with Backbone-style options.
+ * Valid options are:
+ *
+ * FB.init()
. Parse.FacebookUtils will invoke FB.init() for you
+ * with these arguments.
+ *
+ * @param {Object} options Facebook options argument as described here:
+ *
+ * FB.init(). The status flag will be coerced to 'false' because it
+ * interferes with Parse Facebook integration. Call FB.getLoginStatus()
+ * explicitly if this behavior is required by your application.
+ */
+ init: function(options) {
+ if (typeof(FB) === 'undefined') {
+ throw "The Facebook JavaScript SDK must be loaded before calling init.";
+ }
+ initOptions = _.clone(options) || {};
+ if (initOptions.status && typeof(console) !== "undefined") {
+ var warn = console.warn || console.log || function() {};
+ warn.call(console, "The 'status' flag passed into" +
+ " FB.init, when set to true, can interfere with Parse Facebook" +
+ " integration, so it has been suppressed. Please call" +
+ " FB.getLoginStatus() explicitly if you require this behavior.");
+ }
+ initOptions.status = false;
+ FB.init(initOptions);
+ Parse.User._registerAuthenticationProvider(provider);
+ initialized = true;
+ },
+
+ /**
+ * Gets whether the user has their account linked to Facebook.
+ *
+ * @param {Parse.User} user User to check for a facebook link.
+ * The user must be logged in on this device.
+ * @return {Boolean} true
if the user has their account
+ * linked to Facebook.
+ */
+ isLinked: function(user) {
+ return user._isLinked("facebook");
+ },
+
+ /**
+ * Logs in a user using Facebook. This method delegates to the Facebook
+ * SDK to authenticate the user, and then automatically logs in (or
+ * creates, in the case where it is a new user) a Parse.User.
+ *
+ * @param {String, Object} permissions The permissions required for Facebook
+ * log in. This is a comma-separated string of permissions.
+ * Alternatively, supply a Facebook authData object as described in our
+ * REST API docs if you want to handle getting facebook auth tokens
+ * yourself.
+ * @param {Object} options Standard options object with success and error
+ * callbacks.
+ */
+ logIn: function(permissions, options) {
+ if (!permissions || _.isString(permissions)) {
+ if (!initialized) {
+ throw "You must initialize FacebookUtils before calling logIn.";
+ }
+ requestedPermissions = permissions;
+ return Parse.User._logInWith("facebook", options);
+ } else {
+ var newOptions = _.clone(options) || {};
+ newOptions.authData = permissions;
+ return Parse.User._logInWith("facebook", newOptions);
+ }
+ },
+
+ /**
+ * Links Facebook to an existing PFUser. This method delegates to the
+ * Facebook SDK to authenticate the user, and then automatically links
+ * the account to the Parse.User.
+ *
+ * @param {Parse.User} user User to link to Facebook. This must be the
+ * current user.
+ * @param {String, Object} permissions The permissions required for Facebook
+ * log in. This is a comma-separated string of permissions.
+ * Alternatively, supply a Facebook authData object as described in our
+ * REST API docs if you want to handle getting facebook auth tokens
+ * yourself.
+ * @param {Object} options Standard options object with success and error
+ * callbacks.
+ */
+ link: function(user, permissions, options) {
+ if (!permissions || _.isString(permissions)) {
+ if (!initialized) {
+ throw "You must initialize FacebookUtils before calling link.";
+ }
+ requestedPermissions = permissions;
+ return user._linkWith("facebook", options);
+ } else {
+ var newOptions = _.clone(options) || {};
+ newOptions.authData = permissions;
+ return user._linkWith("facebook", newOptions);
+ }
+ },
+
+ /**
+ * Unlinks the Parse.User from a Facebook account.
+ *
+ * @param {Parse.User} user User to unlink from Facebook. This must be the
+ * current user.
+ * @param {Object} options Standard options object with success and error
+ * callbacks.
+ */
+ unlink: function(user, options) {
+ if (!initialized) {
+ throw "You must initialize FacebookUtils before calling unlink.";
+ }
+ return user._unlinkFrom("facebook", options);
+ }
+ };
+
+}(this));
+
+/*global _: false, document: false, window: false, navigator: false */
+(function(root) {
+ root.Parse = root.Parse || {};
+ var Parse = root.Parse;
+ var _ = Parse._;
+
+ /**
+ * History serves as a global router (per frame) to handle hashchange
+ * events or pushState, match the appropriate route, and trigger
+ * callbacks. You shouldn't ever have to create one of these yourself
+ * — you should use the reference to Parse.history
+ * that will be created for you automatically if you make use of
+ * Routers with routes.
+ * @class
+ *
+ * A fork of Backbone.History, provided for your convenience. If you + * use this class, you must also include jQuery, or another library + * that provides a jQuery-compatible $ function. For more information, + * see the + * Backbone documentation.
+ *Available in the client SDK only.
+ */ + Parse.History = function() { + this.handlers = []; + _.bindAll(this, 'checkUrl'); + }; + + // Cached regex for cleaning leading hashes and slashes . + var routeStripper = /^[#\/]/; + + // Cached regex for detecting MSIE. + var isExplorer = /msie [\w.]+/; + + // Has the history handling already been started? + Parse.History.started = false; + + // Set up all inheritable **Parse.History** properties and methods. + _.extend(Parse.History.prototype, Parse.Events, + /** @lends Parse.History.prototype */ { + + // The default interval to poll for hash changes, if necessary, is + // twenty times a second. + interval: 50, + + // Gets the true hash value. Cannot use location.hash directly due to bug + // in Firefox where location.hash will always be decoded. + getHash: function(windowOverride) { + var loc = windowOverride ? windowOverride.location : window.location; + var match = loc.href.match(/#(.*)$/); + return match ? match[1] : ''; + }, + + // Get the cross-browser normalized URL fragment, either from the URL, + // the hash, or the override. + getFragment: function(fragment, forcePushState) { + if (Parse._isNullOrUndefined(fragment)) { + if (this._hasPushState || forcePushState) { + fragment = window.location.pathname; + var search = window.location.search; + if (search) { + fragment += search; + } + } else { + fragment = this.getHash(); + } + } + if (!fragment.indexOf(this.options.root)) { + fragment = fragment.substr(this.options.root.length); + } + return fragment.replace(routeStripper, ''); + }, + + /** + * Start the hash change handling, returning `true` if the current + * URL matches an existing route, and `false` otherwise. + */ + start: function(options) { + if (Parse.History.started) { + throw new Error("Parse.history has already been started"); + } + Parse.History.started = true; + + // Figure out the initial configuration. Do we need an iframe? + // Is pushState desired ... is it available? + this.options = _.extend({}, {root: '/'}, this.options, options); + this._wantsHashChange = this.options.hashChange !== false; + this._wantsPushState = !!this.options.pushState; + this._hasPushState = !!(this.options.pushState && + window.history && + window.history.pushState); + var fragment = this.getFragment(); + var docMode = document.documentMode; + var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && + (!docMode || docMode <= 7)); + + if (oldIE) { + this.iframe = Parse.$('') + .hide().appendTo('body')[0].contentWindow; + this.navigate(fragment); + } + + // Depending on whether we're using pushState or hashes, and whether + // 'onhashchange' is supported, determine how we check the URL state. + if (this._hasPushState) { + Parse.$(window).bind('popstate', this.checkUrl); + } else if (this._wantsHashChange && + ('onhashchange' in window) && + !oldIE) { + Parse.$(window).bind('hashchange', this.checkUrl); + } else if (this._wantsHashChange) { + this._checkUrlInterval = window.setInterval(this.checkUrl, + this.interval); + } + + // Determine if we need to change the base url, for a pushState link + // opened by a non-pushState browser. + this.fragment = fragment; + var loc = window.location; + var atRoot = loc.pathname === this.options.root; + + // If we've started off with a route from a `pushState`-enabled browser, + // but we're currently in a browser that doesn't support it... + if (this._wantsHashChange && + this._wantsPushState && + !this._hasPushState && + !atRoot) { + this.fragment = this.getFragment(null, true); + window.location.replace(this.options.root + '#' + this.fragment); + // Return immediately as browser will do redirect to new url + return true; + + // Or if we've started out with a hash-based route, but we're currently + // in a browser where it could be `pushState`-based instead... + } else if (this._wantsPushState && + this._hasPushState && + atRoot && + loc.hash) { + this.fragment = this.getHash().replace(routeStripper, ''); + window.history.replaceState({}, document.title, + loc.protocol + '//' + loc.host + this.options.root + this.fragment); + } + + if (!this.options.silent) { + return this.loadUrl(); + } + }, + + // Disable Parse.history, perhaps temporarily. Not useful in a real app, + // but possibly useful for unit testing Routers. + stop: function() { + Parse.$(window).unbind('popstate', this.checkUrl) + .unbind('hashchange', this.checkUrl); + window.clearInterval(this._checkUrlInterval); + Parse.History.started = false; + }, + + // Add a route to be tested when the fragment changes. Routes added later + // may override previous routes. + route: function(route, callback) { + this.handlers.unshift({route: route, callback: callback}); + }, + + // Checks the current URL to see if it has changed, and if it has, + // calls `loadUrl`, normalizing across the hidden iframe. + checkUrl: function(e) { + var current = this.getFragment(); + if (current === this.fragment && this.iframe) { + current = this.getFragment(this.getHash(this.iframe)); + } + if (current === this.fragment) { + return false; + } + if (this.iframe) { + this.navigate(current); + } + if (!this.loadUrl()) { + this.loadUrl(this.getHash()); + } + }, + + // Attempt to load the current URL fragment. If a route succeeds with a + // match, returns `true`. If no defined routes matches the fragment, + // returns `false`. + loadUrl: function(fragmentOverride) { + var fragment = this.fragment = this.getFragment(fragmentOverride); + var matched = _.any(this.handlers, function(handler) { + if (handler.route.test(fragment)) { + handler.callback(fragment); + return true; + } + }); + return matched; + }, + + // Save a fragment into the hash history, or replace the URL state if the + // 'replace' option is passed. You are responsible for properly URL-encoding + // the fragment in advance. + // + // The options object can contain `trigger: true` if you wish to have the + // route callback be fired (not usually desirable), or `replace: true`, if + // you wish to modify the current URL without adding an entry to the + // history. + navigate: function(fragment, options) { + if (!Parse.History.started) { + return false; + } + if (!options || options === true) { + options = {trigger: options}; + } + var frag = (fragment || '').replace(routeStripper, ''); + if (this.fragment === frag) { + return; + } + + // If pushState is available, we use it to set the fragment as a real URL. + if (this._hasPushState) { + if (frag.indexOf(this.options.root) !== 0) { + frag = this.options.root + frag; + } + this.fragment = frag; + var replaceOrPush = options.replace ? 'replaceState' : 'pushState'; + window.history[replaceOrPush]({}, document.title, frag); + + // If hash changes haven't been explicitly disabled, update the hash + // fragment to store history. + } else if (this._wantsHashChange) { + this.fragment = frag; + this._updateHash(window.location, frag, options.replace); + if (this.iframe && + (frag !== this.getFragment(this.getHash(this.iframe)))) { + // Opening and closing the iframe tricks IE7 and earlier + // to push a history entry on hash-tag change. + // When replace is true, we don't want this. + if (!options.replace) { + this.iframe.document.open().close(); + } + this._updateHash(this.iframe.location, frag, options.replace); + } + + // If you've told us that you explicitly don't want fallback hashchange- + // based history, then `navigate` becomes a page refresh. + } else { + window.location.assign(this.options.root + fragment); + } + if (options.trigger) { + this.loadUrl(fragment); + } + }, + + // Update the hash location, either replacing the current entry, or adding + // a new one to the browser history. + _updateHash: function(location, fragment, replace) { + if (replace) { + var s = location.toString().replace(/(javascript:|#).*$/, ''); + location.replace(s + '#' + fragment); + } else { + location.hash = fragment; + } + } + }); +}(this)); + +/*global _: false*/ +(function(root) { + root.Parse = root.Parse || {}; + var Parse = root.Parse; + var _ = Parse._; + + /** + * Routers map faux-URLs to actions, and fire events when routes are + * matched. Creating a new one sets its `routes` hash, if not set statically. + * @class + * + *A fork of Backbone.Router, provided for your convenience. + * For more information, see the + * Backbone + * documentation.
+ *Available in the client SDK only.
+ */ + Parse.Router = function(options) { + options = options || {}; + if (options.routes) { + this.routes = options.routes; + } + this._bindRoutes(); + this.initialize.apply(this, arguments); + }; + + // Cached regular expressions for matching named param parts and splatted + // parts of route strings. + var namedParam = /:\w+/g; + var splatParam = /\*\w+/g; + var escapeRegExp = /[\-\[\]{}()+?.,\\\^\$\|#\s]/g; + + // Set up all inheritable **Parse.Router** properties and methods. + _.extend(Parse.Router.prototype, Parse.Events, + /** @lends Parse.Router.prototype */ { + + /** + * Initialize is an empty function by default. Override it with your own + * initialization logic. + */ + initialize: function(){}, + + /** + * Manually bind a single named route to a callback. For example: + * + *this.route('search/:query/p:num', 'search', function(query, num) { + * ... + * });+ */ + route: function(route, name, callback) { + Parse.history = Parse.history || new Parse.History(); + if (!_.isRegExp(route)) { + route = this._routeToRegExp(route); + } + if (!callback) { + callback = this[name]; + } + Parse.history.route(route, _.bind(function(fragment) { + var args = this._extractParameters(route, fragment); + if (callback) { + callback.apply(this, args); + } + this.trigger.apply(this, ['route:' + name].concat(args)); + Parse.history.trigger('route', this, name, args); + }, this)); + return this; + }, + + /** + * Whenever you reach a point in your application that you'd + * like to save as a URL, call navigate in order to update the + * URL. If you wish to also call the route function, set the + * trigger option to true. To update the URL without creating + * an entry in the browser's history, set the replace option + * to true. + */ + navigate: function(fragment, options) { + Parse.history.navigate(fragment, options); + }, + + // Bind all defined routes to `Parse.history`. We have to reverse the + // order of the routes here to support behavior where the most general + // routes can be defined at the bottom of the route map. + _bindRoutes: function() { + if (!this.routes) { + return; + } + var routes = []; + for (var route in this.routes) { + if (this.routes.hasOwnProperty(route)) { + routes.unshift([route, this.routes[route]]); + } + } + for (var i = 0, l = routes.length; i < l; i++) { + this.route(routes[i][0], routes[i][1], this[routes[i][1]]); + } + }, + + // Convert a route string into a regular expression, suitable for matching + // against the current location hash. + _routeToRegExp: function(route) { + route = route.replace(escapeRegExp, '\\$&') + .replace(namedParam, '([^\/]+)') + .replace(splatParam, '(.*?)'); + return new RegExp('^' + route + '$'); + }, + + // Given a route, and a URL fragment that it matches, return the array of + // extracted parameters. + _extractParameters: function(route, fragment) { + return route.exec(fragment).slice(1); + } + }); + + /** + * @function + * @param {Object} instanceProps Instance properties for the router. + * @param {Object} classProps Class properies for the router. + * @return {Class} A new subclass of
Parse.Router
.
+ */
+ Parse.Router.extend = Parse._extend;
+}(this));
+(function(root) {
+ root.Parse = root.Parse || {};
+ var Parse = root.Parse;
+ var _ = Parse._;
+
+ /**
+ * @namespace Contains functions for calling and declaring
+ * cloud functions.
+ * + * Some functions are only available from Cloud Code. + *
+ */ + Parse.Cloud = Parse.Cloud || {}; + + _.extend(Parse.Cloud, /** @lends Parse.Cloud */ { + /** + * Makes a call to a cloud function. + * @param {String} name The function name. + * @param {Object} data The parameters to send to the cloud function. + * @param {Object} options A Backbone-style options object + * options.success, if set, should be a function to handle a successful + * call to a cloud function. options.error should be a function that + * handles an error running the cloud function. Both functions are + * optional. Both functions take a single argument. + * @return {Parse.Promise} A promise that will be resolved with the result + * of the function. + */ + run: function(name, data, options) { + options = options || {}; + + var request = Parse._request({ + route: "functions", + className: name, + method: 'POST', + useMasterKey: options.useMasterKey, + sessionToken: options.sessionToken, + data: Parse._encode(data, null, true) + }); + + return request.then(function(resp) { + return Parse._decode(null, resp).result; + })._thenRunCallbacks(options); + } + }); +}(this)); + +(function(root) { + root.Parse = root.Parse || {}; + var Parse = root.Parse; + + Parse.Installation = Parse.Object.extend("_Installation"); + + /** + * Contains functions to deal with Push in Parse + * @name Parse.Push + * @namespace + */ + Parse.Push = Parse.Push || {}; + + /** + * Sends a push notification. + * @param {Object} data - The data of the push notification. Valid fields + * are: + *