├── .gitignore ├── README.md ├── cloud ├── cloudinary.coffee ├── cloudinary.js ├── cloudinary │ ├── config.coffee │ ├── config.js │ ├── core.coffee │ ├── core.js │ ├── lib │ │ ├── crypto │ │ │ └── sha1.js │ │ └── underscore.js │ └── version.js └── cloudinary_config.js.sample └── sample ├── README.md └── cloud └── main.js /.gitignore: -------------------------------------------------------------------------------- 1 | sample/cloud/cloudinary_config.js 2 | sample/config 3 | sample/public 4 | .project 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | :warning: Following [Parse's end of service notification](http://blog.parse.com/announcements/moving-on/), this repo is deprecated and not supported. The issues section won't be available. 2 | 3 | 4 | # Cloudinary Parse Module 5 | 6 | ## What is Cloudinary? 7 | 8 | Cloudinary is a cloud-based service that provides an end-to-end image management solution including uploads, storage, manipulations, optimizations and delivery. 9 | 10 | With Cloudinary you can easily upload images to the cloud, automatically perform smart image manipulations without installing any complex software. 11 | All your images are then seamlessly delivered through a fast CDN, optimized and using industry best practices. 12 | Cloudinary offers comprehensive APIs and administration capabilities and is easy to integrate with new and existing web and mobile applications. 13 | 14 | ## About the Cloudinary Parse Module 15 | 16 | The Cloudinary Parse Module simplifies the integration of your Parse based applications with the Cloudinary service. The module allows you to use Cloudinary's rich set of cloud-based image management features. Securely upload images to the cloud directly from your mobile apps. The image identifiers are automatically assigned to your Parse backend model. 17 | 18 | The image identifiers can then be used to easily deliver your images responsively to your mobile users while applying any of Cloudinary's image manipulation capabilities, including smart cropping, various image effects, watermarks, overlays and many others. 19 | 20 | 21 | ## Files 22 | 23 | * `cloud/cloudinary.js` - The cloudinary library entrypoint. In order to load cloudinary library you must `require('cloud/cloudinary')` the result of this expression is the cloudinary namespace. See `sample/cloud/main.js` for sample usage. 24 | 25 | ### Installation 26 | 27 | Installing the Cloud Module is done by copying all files and folders under [https://github.com/cloudinary/cloudinary_parse/tree/master/cloud](https://github.com/cloudinary/cloudinary_parse/tree/master/cloud) to the `cloud` folder of the specific Cloud Code project that uses the module. In the case of the included sample Cloud Code project, it should be copied to the `cloud` folder of a local copy of: [https://github.com/cloudinary/cloudinary_parse/tree/master/sample](https://github.com/cloudinary/cloudinary_parse/tree/master/sample) 28 | 29 | 30 | ## Configuration 31 | 32 | Supplying cloudinary configuration can be specified either by providing `cloudinary_config.js` in the `cloud` directory, or by directly calling `cloudinary.config` with a configuration object (See comment in `sample/cloud/main.js` for sample usage). 33 | 34 | The configuration is based on the Cloud Name, API Key and API Secret credentials of your Cloudinary account. They can be obtained from the dashboard of [Cloudinary's Management Console](https://cloudinary.com/console). 35 | 36 | If you haven't done so already, you can [sign-up to a free Cloudinary account](https://cloudinary.com/users/register/free). 37 | 38 | ## Functions 39 | 40 | ### sign_upload_request 41 | 42 | Creates a signed request that can be used to construct an HTML form or passed to Cloudinary front-end libraries to initiate a secure image upload. 43 | 44 | * Uploading of images to Cloudinary is performed directly from web or mobile applications. 45 | * In order to initiate an authenticated upload request from the mobile apps, the server-side, Parse in this case, needs to create a signature for securing each upload request. 46 | * The `sign_upload_request` method generates a signature of given upload parameters, including image manipulation ones, using the API Secret parameter of the Cloudinary account. 47 | 48 | ### beforeSaveFactory 49 | When given an `object_name` and a `field_name`, creates a beforeSave function that verifies updates to `field_name` are only done with a signed cloudinary identifier. The beforeSave function also removes the signatures when saving to the database. 50 | 51 | * Images uploaded directly from web and mobile applications are assigned unique Cloudinary identifiers that should be stored in the Parse backend model for any manipulation and displaying using Cloudinary in web and mobile applications. 52 | * The `beforeSaveFactory` function allows verifying a given image identifier and storing correctly in a Parse model (database) record. 53 | 54 | ### url 55 | builds a cloudinary URL given the image identifier and transformation parameters. 56 | As a reference you can check the [nodejs documentation](http://cloudinary.com/documentation/node_image_manipulation). Alternatively, you can build Cloudinary image manipulation and delivery URLs from your mobile applications. 57 | 58 | * Cloudinary manipulates images on-the-fly and delivers them via a fast CDN using dynamic URLs. 59 | * The URLs are based on the identifiers of uploaded image. Usually such URLs are built on the client-side web and mobile applications using the identifiers received from the Parse backend. 60 | * However, you might want to generate such URLs on the Parse server side and return them to client apps. 61 | * The `url` function takes care of building the image manipulation and delivery URLs. 62 | * Below are sample manipulation and delivery URL of a previously uploaded image with the public ID of 'couple': 63 | * Original image uploaded to Cloudinary: [http://res.cloudinary.com/demo/image/upload/couple.jpg](http://res.cloudinary.com/demo/image/upload/couple.jpg) 64 | * On-the-fly manipulation URL of a 150x150 face detection based cropped thumbnail to a circular shape with the Sepia effect applied. [http://res.cloudinary.com/demo/image/upload/c_thumb,w_150,h_150,g_faces,r_max,e_sepia/couple.jpg](http://res.cloudinary.com/demo/image/upload/c_thumb,w_150,h_150,g_faces,r_max,e_sepia/couple.jpg) 65 | 66 | ## Usage Example 67 | 68 | ```` 69 | cloudinary = require("cloud/cloudinary"); 70 | 71 | cloudinary.beforeSaveFactory("Photo", "cloudinaryIdentifier"); 72 | 73 | Parse.Cloud.define("sign_cloudinary_upload_request", function(request, response) { 74 | if (!request.user || !request.user.authenticated()) { 75 | response.error("Needs an authenticated user"); 76 | return; 77 | } 78 | response.success( 79 | cloudinary.sign_upload_request({tags: request.user.getUsername(), eager: {crop: "fill", width: 150, height: 100, gravity: "face"}}) 80 | ); 81 | }); 82 | 83 | ```` 84 | 85 | ## Sample projects 86 | 87 | See [Cloudinary's sample Parse Cloud Code project](https://github.com/cloudinary/cloudinary_parse/tree/master/sample) 88 | 89 | See [Cloudinary's sample Android application](https://github.com/cloudinary/cloudinary_android_parse_sample) that uses Cloudinary's Parse Cloud Module through the sample Parse Cloud Code mentioned above. 90 | 91 | ## Additional resources 92 | 93 | Additional resources are available at: 94 | 95 | * [Website](http://cloudinary.com) 96 | * [Image transformations documentation](http://cloudinary.com/documentation/image_transformations) 97 | * [Documentation](http://cloudinary.com/documentation) 98 | * [Knowledge Base](http://support.cloudinary.com/forums) 99 | * [iOS SDK](https://github.com/cloudinary/cloudinary_ios) 100 | * [Android SDK](https://github.com/cloudinary/cloudinary_android) 101 | 102 | ## Support 103 | 104 | You can [open an issue through GitHub](https://github.com/cloudinary/cloudinary_parse/issues). 105 | 106 | Contact us [http://cloudinary.com/contact](http://cloudinary.com/contact) 107 | 108 | Stay tuned for updates, tips and tutorials: [Blog](http://cloudinary.com/blog), [Twitter](https://twitter.com/cloudinary), [Facebook](http://www.facebook.com/Cloudinary). 109 | 110 | 111 | # License 112 | Released under the MIT license. 113 | 114 | * [underscore.js](http://underscorejs.org/) is also released under the MIT license. 115 | * [crypto-js](https://code.google.com/p/crypto-js/) is released under the New BSD license. 116 | 117 | -------------------------------------------------------------------------------- /cloud/cloudinary.coffee: -------------------------------------------------------------------------------- 1 | GLOBAL = Parse.Cloudinary ?= {} 2 | GLOBAL.get_cloudinary_path = -> 3 | return GLOBAL.PREFIX if GLOBAL.PREFIX? 4 | prefixes = ['cloud', ''] 5 | cloudinary_path = "cloudinary" 6 | require_test_file = "version" 7 | 8 | for prefix in prefixes 9 | try 10 | require [prefix, cloudinary_path, require_test_file].join("/") 11 | GLOBAL.PREFIX = [prefix, cloudinary_path, ""].join("/") 12 | return GLOBAL.PREFIX 13 | catch e 14 | throw e unless e.message.match(/Module \S+ not found/) 15 | throw new Error("Couldn't find cloudinary in: " + prefices.join(", ")) 16 | 17 | GLOBAL.require = (file) -> 18 | path = GLOBAL.get_cloudinary_path() 19 | require path + file 20 | 21 | _ = GLOBAL.require 'lib/underscore.js' 22 | 23 | _.extend exports, GLOBAL.require('core.js') 24 | exports.config = GLOBAL.require('config.js') 25 | exports.initialize = (cloud_name, api_key, api_secret) -> 26 | exports.config(cloud_name: cloud_name, api_key: api_key, api_secret: api_secret) 27 | exports.version = Parse.Cloudinary.VERSION 28 | 29 | ### 30 | This factory creates a beforeSave filter that verifies that a given 31 | cloudinaryIdentifier field in your object is a valid (has correct signature) 32 | 33 | @note This function allows changing of other fields without validation 34 | ### 35 | exports.beforeSaveFactory = (object_name, field_name) -> 36 | Parse.Cloud.beforeSave object_name, (request, response) -> 37 | verify_upload = (previous_value) -> 38 | if identifier is previous_value or exports.verify_upload(identifier) 39 | 40 | # Remove signature and store 41 | request.object.set field_name, exports.remove_signature(identifier) 42 | response.success() 43 | else 44 | response.error "Bad signature" 45 | identifier = request.object.get(field_name) 46 | (new Parse.Query(object_name)).get request.object.id, 47 | success: (previous) -> 48 | verify_upload previous.get(field_name) 49 | 50 | error: (object, error) -> 51 | return verify_upload(new Parse.Object) if error.code is Parse.Error.OBJECT_NOT_FOUND 52 | response.error error 53 | -------------------------------------------------------------------------------- /cloud/cloudinary.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | (function() { 3 | var GLOBAL, _; 4 | 5 | GLOBAL = Parse.Cloudinary != null ? Parse.Cloudinary : Parse.Cloudinary = {}; 6 | 7 | GLOBAL.get_cloudinary_path = function() { 8 | var cloudinary_path, e, prefix, prefixes, require_test_file, _i, _len; 9 | if (GLOBAL.PREFIX != null) { 10 | return GLOBAL.PREFIX; 11 | } 12 | prefixes = ['cloud', '']; 13 | cloudinary_path = "cloudinary"; 14 | require_test_file = "version"; 15 | for (_i = 0, _len = prefixes.length; _i < _len; _i++) { 16 | prefix = prefixes[_i]; 17 | try { 18 | require([prefix, cloudinary_path, require_test_file].join("/")); 19 | GLOBAL.PREFIX = [prefix, cloudinary_path, ""].join("/"); 20 | return GLOBAL.PREFIX; 21 | } catch (_error) { 22 | e = _error; 23 | if (!e.message.match(/Module \S+ not found/)) { 24 | throw e; 25 | } 26 | } 27 | } 28 | throw new Error("Couldn't find cloudinary in: " + prefices.join(", ")); 29 | }; 30 | 31 | GLOBAL.require = function(file) { 32 | var path; 33 | path = GLOBAL.get_cloudinary_path(); 34 | return require(path + file); 35 | }; 36 | 37 | _ = GLOBAL.require('lib/underscore.js'); 38 | 39 | _.extend(exports, GLOBAL.require('core.js')); 40 | 41 | exports.config = GLOBAL.require('config.js'); 42 | 43 | exports.initialize = function(cloud_name, api_key, api_secret) { 44 | return exports.config({ 45 | cloud_name: cloud_name, 46 | api_key: api_key, 47 | api_secret: api_secret 48 | }); 49 | }; 50 | 51 | exports.version = Parse.Cloudinary.VERSION; 52 | 53 | /* 54 | This factory creates a beforeSave filter that verifies that a given 55 | cloudinaryIdentifier field in your object is a valid (has correct signature) 56 | 57 | @note This function allows changing of other fields without validation 58 | */ 59 | 60 | 61 | exports.beforeSaveFactory = function(object_name, field_name) { 62 | return Parse.Cloud.beforeSave(object_name, function(request, response) { 63 | var identifier, verify_upload; 64 | verify_upload = function(previous_value) { 65 | if (identifier === previous_value || exports.verify_upload(identifier)) { 66 | request.object.set(field_name, exports.remove_signature(identifier)); 67 | return response.success(); 68 | } else { 69 | return response.error("Bad signature"); 70 | } 71 | }; 72 | identifier = request.object.get(field_name); 73 | return (new Parse.Query(object_name)).get(request.object.id, { 74 | success: function(previous) { 75 | return verify_upload(previous.get(field_name)); 76 | }, 77 | error: function(object, error) { 78 | if (error.code === Parse.Error.OBJECT_NOT_FOUND) { 79 | return verify_upload(new Parse.Object); 80 | } 81 | return response.error(error); 82 | } 83 | }); 84 | }); 85 | }; 86 | 87 | }).call(this); 88 | -------------------------------------------------------------------------------- /cloud/cloudinary/config.coffee: -------------------------------------------------------------------------------- 1 | GLOBAL = Parse.Cloudinary ?= {} 2 | _ = GLOBAL.require("lib/underscore") 3 | 4 | cloudinary_config = undefined 5 | module.exports = (new_config, new_value) -> 6 | if cloudinary_config == undefined || new_config == true 7 | try 8 | cloudinary_config = require('cloud/cloudinary_config').config 9 | catch err 10 | console.log("Couldn't find configuration file at 'cloud/cloudinary_config.js'") 11 | cloudinary_config = {} 12 | if not _.isUndefined(new_value) 13 | cloudinary_config[new_config] = new_value 14 | else if _.isString(new_config) 15 | return cloudinary_config[new_config] 16 | else if _.isObject(new_config) 17 | cloudinary_config = new_config 18 | cloudinary_config 19 | 20 | -------------------------------------------------------------------------------- /cloud/cloudinary/config.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | (function() { 3 | var GLOBAL, cloudinary_config, _; 4 | 5 | GLOBAL = Parse.Cloudinary != null ? Parse.Cloudinary : Parse.Cloudinary = {}; 6 | 7 | _ = GLOBAL.require("lib/underscore"); 8 | 9 | cloudinary_config = void 0; 10 | 11 | module.exports = function(new_config, new_value) { 12 | var err; 13 | if (cloudinary_config === void 0 || new_config === true) { 14 | try { 15 | cloudinary_config = require('cloud/cloudinary_config').config; 16 | } catch (_error) { 17 | err = _error; 18 | console.log("Couldn't find configuration file at 'cloud/cloudinary_config.js'"); 19 | cloudinary_config = {}; 20 | } 21 | } 22 | if (!_.isUndefined(new_value)) { 23 | cloudinary_config[new_config] = new_value; 24 | } else if (_.isString(new_config)) { 25 | return cloudinary_config[new_config]; 26 | } else if (_.isObject(new_config)) { 27 | cloudinary_config = new_config; 28 | } 29 | return cloudinary_config; 30 | }; 31 | 32 | }).call(this); 33 | -------------------------------------------------------------------------------- /cloud/cloudinary/core.coffee: -------------------------------------------------------------------------------- 1 | GLOBAL = Parse.Cloudinary ?= {} 2 | _ = GLOBAL.require("lib/underscore") 3 | sha1 = GLOBAL.require("lib/crypto/sha1") 4 | config = GLOBAL.require("config") 5 | 6 | exports.CF_SHARED_CDN = "d3jpl91pxevbkh.cloudfront.net" 7 | exports.OLD_AKAMAI_SHARED_CDN = "cloudinary-a.akamaihd.net" 8 | exports.AKAMAI_SHARED_CDN = "res.cloudinary.com" 9 | exports.SHARED_CDN = exports.AKAMAI_SHARED_CDN 10 | 11 | exports.sign_upload_request = (params) -> 12 | params = build_upload_params(params) 13 | params.signature = sign_request(params) 14 | params.api_key = config().api_key 15 | if !params.api_key? 16 | throw "Must supply api_key" 17 | params 18 | 19 | identifier_pattern = /// 20 | ^(?:([^/]+)/)?? 21 | (?:([^/]+)/)?? 22 | (?:v(\d+)/)? 23 | (?:([^#/]+?) 24 | (?:\.([^.#/]+))?) 25 | (?:#([^/]+))?$ 26 | /// 27 | 28 | exports.verify_upload = (identifier) -> 29 | [match, resource_type, image_type, version, public_id, format, signature] = 30 | identifier.match(identifier_pattern) || [] 31 | expected_signature = sign_request(public_id: public_id, version: version) 32 | expected_signature == signature 33 | 34 | exports.remove_signature = (identifier) -> 35 | identifier.replace /#.*$/, '' 36 | 37 | sign_request = (params) -> 38 | for k, v of params when not present(v) 39 | delete params[k] 40 | 41 | api_secret = config().api_secret 42 | if !api_secret? 43 | throw "Must supply api_secret" 44 | 45 | api_sign_request(params, api_secret) 46 | 47 | get_api_url = (action = 'upload', options = {}) -> 48 | cloudinary = options["upload_prefix"] ? config().upload_prefix ? "https://api.cloudinary.com" 49 | cloud_name = options["cloud_name"] ? config().cloud_name ? throw("Must supply cloud_name") 50 | resource_type = options["resource_type"] ? "image" 51 | return [cloudinary, "v1_1", cloud_name, resource_type, action].join("/") 52 | 53 | api_sign_request = (params_to_sign, api_secret) -> 54 | to_sign = _.sortBy("#{k}=#{build_array(v).join(",")}" for k, v of params_to_sign when v, _.identity).join("&") 55 | sha1.hex_sha1(to_sign + api_secret) 56 | 57 | build_eager = (transformations) -> 58 | (for transformation in build_array(transformations) 59 | transformation = _.clone(transformation) 60 | _.filter([generate_transformation_string(transformation), transformation.format], present).join("/") 61 | ).join("|") 62 | 63 | build_custom_headers = (headers) -> 64 | if !headers? 65 | return undefined 66 | else if _.isArray(headers) 67 | ; 68 | else if _.isObject(headers) 69 | headers = [k + ": " + v for k, v of headers] 70 | else 71 | return headers 72 | return headers.join("\n") 73 | 74 | build_upload_params = (options) -> 75 | options ?= {} 76 | 77 | params = 78 | timestamp: timestamp(), 79 | transformation: generate_transformation_string(options), 80 | public_id: options.public_id, 81 | callback: options.callback, 82 | format: options.format, 83 | backup: options.backup, 84 | faces: options.faces, 85 | exif: options.exif, 86 | image_metadata: options.image_metadata, 87 | colors: options.colors, 88 | type: options.type, 89 | eager: build_eager(options.eager), 90 | use_filename: options.use_filename, 91 | unique_filename: options.unique_filename, 92 | discard_original_filename: options.discard_original_filename, 93 | notification_url: options.notification_url, 94 | eager_notification_url: options.eager_notification_url, 95 | eager_async: options.eager_async, 96 | invalidate: options.invalidate, 97 | proxy: options.proxy, 98 | folder: options.folder, 99 | overwrite: options.overwrite, 100 | allowed_formats: options.allowed_formats && build_array(options.allowed_formats).join(","), 101 | moderation: options.moderation 102 | updateable_resource_params(options, params) 103 | 104 | encode_double_array = encode_double_array = (arg) -> 105 | build_array(arg).map((e) -> build_array(e).join(",")).join("|") 106 | 107 | encode_key_value = encode_key_value = (arg) -> 108 | if _.isObject(arg) 109 | pairs = for k, v of arg 110 | "#{k}=#{v}" 111 | pairs.join("|") 112 | else 113 | arg 114 | 115 | build_eager = build_eager = (transformations) -> 116 | (for transformation in build_array(transformations) 117 | transformation = _.clone(transformation) 118 | _.filter([generate_transformation_string(transformation), transformation.format], present).join("/") 119 | ).join("|") 120 | 121 | build_custom_headers = build_custom_headers = (headers) -> 122 | if !headers? 123 | return undefined 124 | else if _.isArray(headers) 125 | ; 126 | else if _.isObject(headers) 127 | headers = [k + ": " + v for k, v of headers] 128 | else 129 | return headers 130 | return headers.join("\n") 131 | 132 | present = present = (value) -> 133 | not _.isUndefined(value) and ("" + value).length > 0 134 | 135 | generate_transformation_string = generate_transformation_string = (options) -> 136 | if _.isArray(options) 137 | result = for base_transformation in options 138 | generate_transformation_string(_.clone(base_transformation)) 139 | return result.join("/") 140 | 141 | width = options["width"] 142 | height = options["height"] 143 | size = option_consume(options, "size") 144 | [options["width"], options["height"]] = [width, height] = size.split("x") if size 145 | 146 | has_layer = options.overlay or options.underlay 147 | crop = option_consume(options, "crop") 148 | angle = build_array(option_consume(options, "angle")).join(".") 149 | no_html_sizes = has_layer or present(angle) or crop == "fit" or crop == "limit" 150 | 151 | delete options["width"] if width and (no_html_sizes or parseFloat(width) < 1) 152 | delete options["height"] if height and (no_html_sizes or parseFloat(height) < 1) 153 | background = option_consume(options, "background") 154 | background = background and background.replace(/^#/, "rgb:") 155 | color = option_consume(options, "color") 156 | color = color and color.replace(/^#/, "rgb:") 157 | base_transformations = build_array(option_consume(options, "transformation", [])) 158 | named_transformation = [] 159 | if _.filter(base_transformations, _.isObject).length > 0 160 | base_transformations = _.map(base_transformations, (base_transformation) -> 161 | if _.isObject(base_transformation) 162 | generate_transformation_string(_.clone(base_transformation)) 163 | else 164 | generate_transformation_string(transformation: base_transformation) 165 | ) 166 | else 167 | named_transformation = base_transformations.join(".") 168 | base_transformations = [] 169 | effect = option_consume(options, "effect") 170 | effect = effect.join(":") if _.isArray(effect) 171 | 172 | border = option_consume(options, "border") 173 | if _.isObject(border) 174 | border = "#{border.width ? 2}px_solid_#{(border.color ? "black").replace(/^#/, 'rgb:')}" 175 | flags = build_array(option_consume(options, "flags")).join(".") 176 | 177 | params = 178 | c: crop 179 | t: named_transformation 180 | w: width 181 | h: height 182 | b: background 183 | co: color 184 | e: effect 185 | a: angle 186 | bo: border 187 | fl: flags 188 | 189 | simple_params = 190 | x: "x" 191 | y: "y" 192 | radius: "r" 193 | gravity: "g" 194 | quality: "q" 195 | prefix: "p" 196 | default_image: "d" 197 | underlay: "u" 198 | overlay: "l" 199 | fetch_format: "f" 200 | density: "dn" 201 | page: "pg" 202 | color_space: "cs" 203 | delay: "dl" 204 | opacity: "o" 205 | 206 | for param, short of simple_params 207 | params[short] = option_consume(options, param) 208 | 209 | params = _.sortBy([key, value] for key, value of params, (key, value) -> key) 210 | 211 | params.push [option_consume(options, "raw_transformation")] 212 | transformation = (param.join("_") for param in params when present(_.last(param))).join(",") 213 | 214 | base_transformations.push transformation 215 | _.filter(base_transformations, present).join "/" 216 | 217 | exports.url = (public_id, options = {}) -> 218 | type = option_consume(options, "type", "upload") 219 | options.fetch_format ?= option_consume(options, "format") if type is "fetch" 220 | transformation = generate_transformation_string(options) 221 | resource_type = option_consume(options, "resource_type", "image") 222 | version = option_consume(options, "version") 223 | format = option_consume(options, "format") 224 | cloud_name = option_consume(options, "cloud_name", config().cloud_name) 225 | throw "Unknown cloud_name" unless cloud_name 226 | private_cdn = option_consume(options, "private_cdn", config().private_cdn) 227 | secure_distribution = option_consume(options, "secure_distribution", config().secure_distribution) 228 | secure = option_consume(options, "secure", config().secure) 229 | cdn_subdomain = option_consume(options, "cdn_subdomain", config().cdn_subdomain) 230 | cname = option_consume(options, "cname", config().cname) 231 | shorten = option_consume(options, "shorten", config().shorten) 232 | sign_url = option_consume(options, "sign_url", config().sign_url) 233 | api_secret = option_consume(options, "api_secret", config().api_secret) 234 | 235 | parsed_identifier = /^(image|raw)\/([a-z0-9_]+)\/v(\d+)\/([^#]+)$/.exec(public_id) 236 | if parsed_identifier 237 | resource_type = parsed_identifier[1] 238 | type = parsed_identifier[2] 239 | version = parsed_identifier[3] 240 | public_id = parsed_identifier[4] 241 | 242 | if public_id.match(/^https?:/) 243 | return public_id if type is "upload" or type is "asset" 244 | public_id = encodeURIComponent(public_id).replace(/%3A/g, ":").replace(/%2F/g, "/") 245 | else 246 | public_id = encodeURIComponent(decodeURIComponent(public_id)).replace(/%3A/g, ":").replace(/%2F/g, "/") 247 | public_id += "." + format if format 248 | 249 | shared_domain = !private_cdn 250 | if secure 251 | if !secure_distribution || secure_distribution == exports.OLD_AKAMAI_SHARED_CDN 252 | secure_distribution = (if private_cdn then "#{cloud_name}-res.cloudinary.com" else exports.SHARED_CDN) 253 | shared_domain ||= secure_distribution == exports.SHARED_CDN 254 | prefix = "https://#{secure_distribution}" 255 | else 256 | subdomain = (if cdn_subdomain then "a#{(crc32(public_id) % 5) + 1}." else "") 257 | host = cname ? "#{if private_cdn then "#{cloud_name}-" else ""}res.cloudinary.com" 258 | prefix = "http://#{subdomain}#{host}" 259 | prefix += "/#{cloud_name}" if shared_domain 260 | 261 | if shorten && resource_type == "image" && type == "upload" 262 | resource_type = "iu" 263 | type = undefined 264 | 265 | version ?= 1 if public_id.search("/") >= 0 && !public_id.match(/^v[0-9]+/) && !public_id.match(/^https?:\//) 266 | 267 | rest = [transformation, (if version then "v" + version else ""), public_id ].filter((part) -> part != "" and part != null).join("/") 268 | if sign_url 269 | signature = sha1.b64_sha1(rest + api_secret).replace(/\//g,'_').replace(/\+/g,'-').substring(0, 8) 270 | rest = "s--#{signature}--/" + rest 271 | 272 | url = [ prefix, resource_type, type, rest ].join("/") 273 | url.replace(/([^:])\/+/g, "$1/") 274 | 275 | 276 | updateable_resource_params = updateable_resource_params = (options, params = {}) -> 277 | params.tags = build_array(options.tags).join(",") if options.tags? 278 | params.context = encode_key_value(options.context) if options.context? 279 | params.face_coordinates = encode_double_array(options.face_coordinates) if options.face_coordinates? 280 | params.headers = build_custom_headers(options.headers) if options.headers? 281 | params.ocr = options.ocr if options.ocr? 282 | params.raw_convert = options.raw_convert if options.raw_convert? 283 | params.categorization = options.categorization if options.categorization? 284 | params.detection = options.detection if options.detection? 285 | params.similarity_search = options.similarity_search if options.similarity_search? 286 | params.auto_tagging = options.auto_tagging if options.auto_tagging? 287 | params 288 | 289 | 290 | timestamp = -> 291 | Math.floor(new Date().getTime()/1000) 292 | 293 | option_consume = (options, option_name, default_value) -> 294 | result = options[option_name] 295 | delete options[option_name] 296 | 297 | if result? then result else default_value 298 | 299 | build_array = (arg) -> 300 | if !arg? 301 | [] 302 | else if _.isArray(arg) 303 | arg 304 | else 305 | [arg] 306 | 307 | # http://kevin.vanzonneveld.net 308 | # + original by: Webtoolkit.info (http://www.webtoolkit.info/) 309 | # + improved by: T0bsn 310 | # + improved by: http://stackoverflow.com/questions/2647935/javascript-crc32-function-and-php-crc32-not-matching 311 | # - depends on: utf8_encode 312 | # * example 1: crc32('Kevin van Zonneveld'); 313 | # * returns 1: 1249991249 314 | crc32 = (str) -> 315 | str = utf8_encode(str) 316 | table = "00000000 77073096 EE0E612C 990951BA 076DC419 706AF48F E963A535 9E6495A3 0EDB8832 79DCB8A4 E0D5E91E 97D2D988 09B64C2B 7EB17CBD E7B82D07 90BF1D91 1DB71064 6AB020F2 F3B97148 84BE41DE 1ADAD47D 6DDDE4EB F4D4B551 83D385C7 136C9856 646BA8C0 FD62F97A 8A65C9EC 14015C4F 63066CD9 FA0F3D63 8D080DF5 3B6E20C8 4C69105E D56041E4 A2677172 3C03E4D1 4B04D447 D20D85FD A50AB56B 35B5A8FA 42B2986C DBBBC9D6 ACBCF940 32D86CE3 45DF5C75 DCD60DCF ABD13D59 26D930AC 51DE003A C8D75180 BFD06116 21B4F4B5 56B3C423 CFBA9599 B8BDA50F 2802B89E 5F058808 C60CD9B2 B10BE924 2F6F7C87 58684C11 C1611DAB B6662D3D 76DC4190 01DB7106 98D220BC EFD5102A 71B18589 06B6B51F 9FBFE4A5 E8B8D433 7807C9A2 0F00F934 9609A88E E10E9818 7F6A0DBB 086D3D2D 91646C97 E6635C01 6B6B51F4 1C6C6162 856530D8 F262004E 6C0695ED 1B01A57B 8208F4C1 F50FC457 65B0D9C6 12B7E950 8BBEB8EA FCB9887C 62DD1DDF 15DA2D49 8CD37CF3 FBD44C65 4DB26158 3AB551CE A3BC0074 D4BB30E2 4ADFA541 3DD895D7 A4D1C46D D3D6F4FB 4369E96A 346ED9FC AD678846 DA60B8D0 44042D73 33031DE5 AA0A4C5F DD0D7CC9 5005713C 270241AA BE0B1010 C90C2086 5768B525 206F85B3 B966D409 CE61E49F 5EDEF90E 29D9C998 B0D09822 C7D7A8B4 59B33D17 2EB40D81 B7BD5C3B C0BA6CAD EDB88320 9ABFB3B6 03B6E20C 74B1D29A EAD54739 9DD277AF 04DB2615 73DC1683 E3630B12 94643B84 0D6D6A3E 7A6A5AA8 E40ECF0B 9309FF9D 0A00AE27 7D079EB1 F00F9344 8708A3D2 1E01F268 6906C2FE F762575D 806567CB 196C3671 6E6B06E7 FED41B76 89D32BE0 10DA7A5A 67DD4ACC F9B9DF6F 8EBEEFF9 17B7BE43 60B08ED5 D6D6A3E8 A1D1937E 38D8C2C4 4FDFF252 D1BB67F1 A6BC5767 3FB506DD 48B2364B D80D2BDA AF0A1B4C 36034AF6 41047A60 DF60EFC3 A867DF55 316E8EEF 4669BE79 CB61B38C BC66831A 256FD2A0 5268E236 CC0C7795 BB0B4703 220216B9 5505262F C5BA3BBE B2BD0B28 2BB45A92 5CB36A04 C2D7FFA7 B5D0CF31 2CD99E8B 5BDEAE1D 9B64C2B0 EC63F226 756AA39C 026D930A 9C0906A9 EB0E363F 72076785 05005713 95BF4A82 E2B87A14 7BB12BAE 0CB61B38 92D28E9B E5D5BE0D 7CDCEFB7 0BDBDF21 86D3D2D4 F1D4E242 68DDB3F8 1FDA836E 81BE16CD F6B9265B 6FB077E1 18B74777 88085AE6 FF0F6A70 66063BCA 11010B5C 8F659EFF F862AE69 616BFFD3 166CCF45 A00AE278 D70DD2EE 4E048354 3903B3C2 A7672661 D06016F7 4969474D 3E6E77DB AED16A4A D9D65ADC 40DF0B66 37D83BF0 A9BCAE53 DEBB9EC5 47B2CF7F 30B5FFE9 BDBDF21C CABAC28A 53B39330 24B4A3A6 BAD03605 CDD70693 54DE5729 23D967BF B3667A2E C4614AB8 5D681B02 2A6F2B94 B40BBE37 C30C8EA1 5A05DF1B 2D02EF8D" 317 | crc = 0 318 | x = 0 319 | y = 0 320 | crc = crc ^ (-1) 321 | i = 0 322 | iTop = str.length 323 | 324 | while i < iTop 325 | y = (crc ^ str.charCodeAt(i)) & 0xFF 326 | x = "0x" + table.substr(y * 9, 8) 327 | crc = (crc >>> 8) ^ x 328 | i++ 329 | crc = crc ^ (-1) 330 | crc += 4294967296 if crc < 0 331 | crc 332 | -------------------------------------------------------------------------------- /cloud/cloudinary/core.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | (function() { 3 | var GLOBAL, api_sign_request, build_array, build_custom_headers, build_eager, build_upload_params, config, crc32, encode_double_array, encode_key_value, generate_transformation_string, get_api_url, identifier_pattern, option_consume, present, sha1, sign_request, timestamp, updateable_resource_params, _; 4 | 5 | GLOBAL = Parse.Cloudinary != null ? Parse.Cloudinary : Parse.Cloudinary = {}; 6 | 7 | _ = GLOBAL.require("lib/underscore"); 8 | 9 | sha1 = GLOBAL.require("lib/crypto/sha1"); 10 | 11 | config = GLOBAL.require("config"); 12 | 13 | exports.CF_SHARED_CDN = "d3jpl91pxevbkh.cloudfront.net"; 14 | 15 | exports.OLD_AKAMAI_SHARED_CDN = "cloudinary-a.akamaihd.net"; 16 | 17 | exports.AKAMAI_SHARED_CDN = "res.cloudinary.com"; 18 | 19 | exports.SHARED_CDN = exports.AKAMAI_SHARED_CDN; 20 | 21 | exports.sign_upload_request = function(params) { 22 | params = build_upload_params(params); 23 | params.signature = sign_request(params); 24 | params.api_key = config().api_key; 25 | if (params.api_key == null) { 26 | throw "Must supply api_key"; 27 | } 28 | return params; 29 | }; 30 | 31 | identifier_pattern = /^(?:([^\/]+)\/)??(?:([^\/]+)\/)??(?:v(\d+)\/)?(?:([^#\/]+?)(?:\.([^.#\/]+))?)(?:#([^\/]+))?$/; 32 | 33 | exports.verify_upload = function(identifier) { 34 | var expected_signature, format, image_type, match, public_id, resource_type, signature, version, _ref; 35 | _ref = identifier.match(identifier_pattern) || [], match = _ref[0], resource_type = _ref[1], image_type = _ref[2], version = _ref[3], public_id = _ref[4], format = _ref[5], signature = _ref[6]; 36 | expected_signature = sign_request({ 37 | public_id: public_id, 38 | version: version 39 | }); 40 | return expected_signature === signature; 41 | }; 42 | 43 | exports.remove_signature = function(identifier) { 44 | return identifier.replace(/#.*$/, ''); 45 | }; 46 | 47 | sign_request = function(params) { 48 | var api_secret, k, v; 49 | for (k in params) { 50 | v = params[k]; 51 | if (!present(v)) { 52 | delete params[k]; 53 | } 54 | } 55 | api_secret = config().api_secret; 56 | if (api_secret == null) { 57 | throw "Must supply api_secret"; 58 | } 59 | return api_sign_request(params, api_secret); 60 | }; 61 | 62 | get_api_url = function(action, options) { 63 | var cloud_name, cloudinary, resource_type, _ref, _ref1, _ref2, _ref3; 64 | if (action == null) { 65 | action = 'upload'; 66 | } 67 | if (options == null) { 68 | options = {}; 69 | } 70 | cloudinary = (_ref = (_ref1 = options["upload_prefix"]) != null ? _ref1 : config().upload_prefix) != null ? _ref : "https://api.cloudinary.com"; 71 | cloud_name = (function() { 72 | var _ref3; 73 | if ((_ref2 = (_ref3 = options["cloud_name"]) != null ? _ref3 : config().cloud_name) != null) { 74 | return _ref2; 75 | } else { 76 | throw "Must supply cloud_name"; 77 | } 78 | })(); 79 | resource_type = (_ref3 = options["resource_type"]) != null ? _ref3 : "image"; 80 | return [cloudinary, "v1_1", cloud_name, resource_type, action].join("/"); 81 | }; 82 | 83 | api_sign_request = function(params_to_sign, api_secret) { 84 | var k, to_sign, v; 85 | to_sign = _.sortBy((function() { 86 | var _results; 87 | _results = []; 88 | for (k in params_to_sign) { 89 | v = params_to_sign[k]; 90 | if (v) { 91 | _results.push("" + k + "=" + (build_array(v).join(","))); 92 | } 93 | } 94 | return _results; 95 | })(), _.identity).join("&"); 96 | return sha1.hex_sha1(to_sign + api_secret); 97 | }; 98 | 99 | build_eager = function(transformations) { 100 | var transformation; 101 | return ((function() { 102 | var _i, _len, _ref, _results; 103 | _ref = build_array(transformations); 104 | _results = []; 105 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 106 | transformation = _ref[_i]; 107 | transformation = _.clone(transformation); 108 | _results.push(_.filter([generate_transformation_string(transformation), transformation.format], present).join("/")); 109 | } 110 | return _results; 111 | })()).join("|"); 112 | }; 113 | 114 | build_custom_headers = function(headers) { 115 | var k, v; 116 | if (headers == null) { 117 | return void 0; 118 | } else if (_.isArray(headers)) { 119 | 120 | } else if (_.isObject(headers)) { 121 | headers = [ 122 | (function() { 123 | var _results; 124 | _results = []; 125 | for (k in headers) { 126 | v = headers[k]; 127 | _results.push(k + ": " + v); 128 | } 129 | return _results; 130 | })() 131 | ]; 132 | } else { 133 | return headers; 134 | } 135 | return headers.join("\n"); 136 | }; 137 | 138 | build_upload_params = function(options) { 139 | var params; 140 | if (options == null) { 141 | options = {}; 142 | } 143 | params = { 144 | timestamp: timestamp(), 145 | transformation: generate_transformation_string(options), 146 | public_id: options.public_id, 147 | callback: options.callback, 148 | format: options.format, 149 | backup: options.backup, 150 | faces: options.faces, 151 | exif: options.exif, 152 | image_metadata: options.image_metadata, 153 | colors: options.colors, 154 | type: options.type, 155 | eager: build_eager(options.eager), 156 | use_filename: options.use_filename, 157 | unique_filename: options.unique_filename, 158 | discard_original_filename: options.discard_original_filename, 159 | notification_url: options.notification_url, 160 | eager_notification_url: options.eager_notification_url, 161 | eager_async: options.eager_async, 162 | invalidate: options.invalidate, 163 | proxy: options.proxy, 164 | folder: options.folder, 165 | overwrite: options.overwrite, 166 | allowed_formats: options.allowed_formats && build_array(options.allowed_formats).join(","), 167 | moderation: options.moderation 168 | }; 169 | return updateable_resource_params(options, params); 170 | }; 171 | 172 | encode_double_array = encode_double_array = function(arg) { 173 | return build_array(arg).map(function(e) { 174 | return build_array(e).join(","); 175 | }).join("|"); 176 | }; 177 | 178 | encode_key_value = encode_key_value = function(arg) { 179 | var k, pairs, v; 180 | if (_.isObject(arg)) { 181 | pairs = (function() { 182 | var _results; 183 | _results = []; 184 | for (k in arg) { 185 | v = arg[k]; 186 | _results.push("" + k + "=" + v); 187 | } 188 | return _results; 189 | })(); 190 | return pairs.join("|"); 191 | } else { 192 | return arg; 193 | } 194 | }; 195 | 196 | build_eager = build_eager = function(transformations) { 197 | var transformation; 198 | return ((function() { 199 | var _i, _len, _ref, _results; 200 | _ref = build_array(transformations); 201 | _results = []; 202 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 203 | transformation = _ref[_i]; 204 | transformation = _.clone(transformation); 205 | _results.push(_.filter([generate_transformation_string(transformation), transformation.format], present).join("/")); 206 | } 207 | return _results; 208 | })()).join("|"); 209 | }; 210 | 211 | build_custom_headers = build_custom_headers = function(headers) { 212 | var k, v; 213 | if (headers == null) { 214 | return void 0; 215 | } else if (_.isArray(headers)) { 216 | 217 | } else if (_.isObject(headers)) { 218 | headers = [ 219 | (function() { 220 | var _results; 221 | _results = []; 222 | for (k in headers) { 223 | v = headers[k]; 224 | _results.push(k + ": " + v); 225 | } 226 | return _results; 227 | })() 228 | ]; 229 | } else { 230 | return headers; 231 | } 232 | return headers.join("\n"); 233 | }; 234 | 235 | present = present = function(value) { 236 | return !_.isUndefined(value) && ("" + value).length > 0; 237 | }; 238 | 239 | generate_transformation_string = generate_transformation_string = function(options) { 240 | var angle, background, base_transformation, base_transformations, border, color, crop, effect, flags, has_layer, height, key, named_transformation, no_html_sizes, param, params, result, short, simple_params, size, transformation, value, width, _ref, _ref1, _ref2, _ref3; 241 | if (_.isArray(options)) { 242 | result = (function() { 243 | var _i, _len, _results; 244 | _results = []; 245 | for (_i = 0, _len = options.length; _i < _len; _i++) { 246 | base_transformation = options[_i]; 247 | _results.push(generate_transformation_string(_.clone(base_transformation))); 248 | } 249 | return _results; 250 | })(); 251 | return result.join("/"); 252 | } 253 | width = options["width"]; 254 | height = options["height"]; 255 | size = option_consume(options, "size"); 256 | if (size) { 257 | _ref1 = (_ref = size.split("x"), width = _ref[0], height = _ref[1], _ref), options["width"] = _ref1[0], options["height"] = _ref1[1]; 258 | } 259 | has_layer = options.overlay || options.underlay; 260 | crop = option_consume(options, "crop"); 261 | angle = build_array(option_consume(options, "angle")).join("."); 262 | no_html_sizes = has_layer || present(angle) || crop === "fit" || crop === "limit"; 263 | if (width && (no_html_sizes || parseFloat(width) < 1)) { 264 | delete options["width"]; 265 | } 266 | if (height && (no_html_sizes || parseFloat(height) < 1)) { 267 | delete options["height"]; 268 | } 269 | background = option_consume(options, "background"); 270 | background = background && background.replace(/^#/, "rgb:"); 271 | color = option_consume(options, "color"); 272 | color = color && color.replace(/^#/, "rgb:"); 273 | base_transformations = build_array(option_consume(options, "transformation", [])); 274 | named_transformation = []; 275 | if (_.filter(base_transformations, _.isObject).length > 0) { 276 | base_transformations = _.map(base_transformations, function(base_transformation) { 277 | if (_.isObject(base_transformation)) { 278 | return generate_transformation_string(_.clone(base_transformation)); 279 | } else { 280 | return generate_transformation_string({ 281 | transformation: base_transformation 282 | }); 283 | } 284 | }); 285 | } else { 286 | named_transformation = base_transformations.join("."); 287 | base_transformations = []; 288 | } 289 | effect = option_consume(options, "effect"); 290 | if (_.isArray(effect)) { 291 | effect = effect.join(":"); 292 | } 293 | border = option_consume(options, "border"); 294 | if (_.isObject(border)) { 295 | border = "" + ((_ref2 = border.width) != null ? _ref2 : 2) + "px_solid_" + (((_ref3 = border.color) != null ? _ref3 : "black").replace(/^#/, 'rgb:')); 296 | } 297 | flags = build_array(option_consume(options, "flags")).join("."); 298 | params = { 299 | c: crop, 300 | t: named_transformation, 301 | w: width, 302 | h: height, 303 | b: background, 304 | co: color, 305 | e: effect, 306 | a: angle, 307 | bo: border, 308 | fl: flags 309 | }; 310 | simple_params = { 311 | x: "x", 312 | y: "y", 313 | radius: "r", 314 | gravity: "g", 315 | quality: "q", 316 | prefix: "p", 317 | default_image: "d", 318 | underlay: "u", 319 | overlay: "l", 320 | fetch_format: "f", 321 | density: "dn", 322 | page: "pg", 323 | color_space: "cs", 324 | delay: "dl", 325 | opacity: "o" 326 | }; 327 | for (param in simple_params) { 328 | short = simple_params[param]; 329 | params[short] = option_consume(options, param); 330 | } 331 | params = _.sortBy((function() { 332 | var _results; 333 | _results = []; 334 | for (key in params) { 335 | value = params[key]; 336 | _results.push([key, value]); 337 | } 338 | return _results; 339 | })(), function(key, value) { 340 | return key; 341 | }); 342 | params.push([option_consume(options, "raw_transformation")]); 343 | transformation = ((function() { 344 | var _i, _len, _results; 345 | _results = []; 346 | for (_i = 0, _len = params.length; _i < _len; _i++) { 347 | param = params[_i]; 348 | if (present(_.last(param))) { 349 | _results.push(param.join("_")); 350 | } 351 | } 352 | return _results; 353 | })()).join(","); 354 | base_transformations.push(transformation); 355 | return _.filter(base_transformations, present).join("/"); 356 | }; 357 | 358 | exports.url = function(public_id, options) { 359 | var api_secret, cdn_subdomain, cloud_name, cname, format, host, parsed_identifier, prefix, private_cdn, resource_type, rest, secure, secure_distribution, shared_domain, shorten, sign_url, signature, subdomain, transformation, type, url, version; 360 | if (options == null) { 361 | options = {}; 362 | } 363 | type = option_consume(options, "type", "upload"); 364 | if (type === "fetch") { 365 | if (options.fetch_format == null) { 366 | options.fetch_format = option_consume(options, "format"); 367 | } 368 | } 369 | transformation = generate_transformation_string(options); 370 | resource_type = option_consume(options, "resource_type", "image"); 371 | version = option_consume(options, "version"); 372 | format = option_consume(options, "format"); 373 | cloud_name = option_consume(options, "cloud_name", config().cloud_name); 374 | if (!cloud_name) { 375 | throw "Unknown cloud_name"; 376 | } 377 | private_cdn = option_consume(options, "private_cdn", config().private_cdn); 378 | secure_distribution = option_consume(options, "secure_distribution", config().secure_distribution); 379 | secure = option_consume(options, "secure", config().secure); 380 | cdn_subdomain = option_consume(options, "cdn_subdomain", config().cdn_subdomain); 381 | cname = option_consume(options, "cname", config().cname); 382 | shorten = option_consume(options, "shorten", config().shorten); 383 | sign_url = option_consume(options, "sign_url", config().sign_url); 384 | api_secret = option_consume(options, "api_secret", config().api_secret); 385 | parsed_identifier = /^(image|raw)\/([a-z0-9_]+)\/v(\d+)\/([^#]+)$/.exec(public_id); 386 | if (parsed_identifier) { 387 | resource_type = parsed_identifier[1]; 388 | type = parsed_identifier[2]; 389 | version = parsed_identifier[3]; 390 | public_id = parsed_identifier[4]; 391 | } 392 | if (public_id.match(/^https?:/)) { 393 | if (type === "upload" || type === "asset") { 394 | return public_id; 395 | } 396 | public_id = encodeURIComponent(public_id).replace(/%3A/g, ":").replace(/%2F/g, "/"); 397 | } else { 398 | public_id = encodeURIComponent(decodeURIComponent(public_id)).replace(/%3A/g, ":").replace(/%2F/g, "/"); 399 | if (format) { 400 | public_id += "." + format; 401 | } 402 | } 403 | shared_domain = !private_cdn; 404 | if (secure) { 405 | if (!secure_distribution || secure_distribution === exports.OLD_AKAMAI_SHARED_CDN) { 406 | secure_distribution = (private_cdn ? "" + cloud_name + "-res.cloudinary.com" : exports.SHARED_CDN); 407 | } 408 | shared_domain || (shared_domain = secure_distribution === exports.SHARED_CDN); 409 | prefix = "https://" + secure_distribution; 410 | } else { 411 | subdomain = (cdn_subdomain ? "a" + ((crc32(public_id) % 5) + 1) + "." : ""); 412 | host = cname != null ? cname : "" + (private_cdn ? "" + cloud_name + "-" : "") + "res.cloudinary.com"; 413 | prefix = "http://" + subdomain + host; 414 | } 415 | if (shared_domain) { 416 | prefix += "/" + cloud_name; 417 | } 418 | if (shorten && resource_type === "image" && type === "upload") { 419 | resource_type = "iu"; 420 | type = void 0; 421 | } 422 | if (public_id.search("/") >= 0 && !public_id.match(/^v[0-9]+/) && !public_id.match(/^https?:\//)) { 423 | if (version == null) { 424 | version = 1; 425 | } 426 | } 427 | rest = [transformation, (version ? "v" + version : ""), public_id].filter(function(part) { 428 | return part !== "" && part !== null; 429 | }).join("/"); 430 | if (sign_url) { 431 | signature = sha1.b64_sha1(rest + api_secret).replace(/\//g, '_').replace(/\+/g, '-').substring(0, 8); 432 | rest = ("s--" + signature + "--/") + rest; 433 | } 434 | url = [prefix, resource_type, type, rest].join("/"); 435 | return url.replace(/([^:])\/+/g, "$1/"); 436 | }; 437 | 438 | updateable_resource_params = updateable_resource_params = function(options, params) { 439 | if (params == null) { 440 | params = {}; 441 | } 442 | if (options.tags != null) { 443 | params.tags = build_array(options.tags).join(","); 444 | } 445 | if (options.context != null) { 446 | params.context = encode_key_value(options.context); 447 | } 448 | if (options.face_coordinates != null) { 449 | params.face_coordinates = encode_double_array(options.face_coordinates); 450 | } 451 | if (options.headers != null) { 452 | params.headers = build_custom_headers(options.headers); 453 | } 454 | if (options.ocr != null) { 455 | params.ocr = options.ocr; 456 | } 457 | if (options.raw_convert != null) { 458 | params.raw_convert = options.raw_convert; 459 | } 460 | if (options.categorization != null) { 461 | params.categorization = options.categorization; 462 | } 463 | if (options.detection != null) { 464 | params.detection = options.detection; 465 | } 466 | if (options.similarity_search != null) { 467 | params.similarity_search = options.similarity_search; 468 | } 469 | if (options.auto_tagging != null) { 470 | params.auto_tagging = options.auto_tagging; 471 | } 472 | return params; 473 | }; 474 | 475 | timestamp = function() { 476 | return Math.floor(new Date().getTime() / 1000); 477 | }; 478 | 479 | option_consume = function(options, option_name, default_value) { 480 | var result; 481 | result = options[option_name]; 482 | delete options[option_name]; 483 | if (result != null) { 484 | return result; 485 | } else { 486 | return default_value; 487 | } 488 | }; 489 | 490 | build_array = function(arg) { 491 | if (arg == null) { 492 | return []; 493 | } else if (_.isArray(arg)) { 494 | return arg; 495 | } else { 496 | return [arg]; 497 | } 498 | }; 499 | 500 | crc32 = function(str) { 501 | var crc, i, iTop, table, x, y; 502 | str = utf8_encode(str); 503 | table = "00000000 77073096 EE0E612C 990951BA 076DC419 706AF48F E963A535 9E6495A3 0EDB8832 79DCB8A4 E0D5E91E 97D2D988 09B64C2B 7EB17CBD E7B82D07 90BF1D91 1DB71064 6AB020F2 F3B97148 84BE41DE 1ADAD47D 6DDDE4EB F4D4B551 83D385C7 136C9856 646BA8C0 FD62F97A 8A65C9EC 14015C4F 63066CD9 FA0F3D63 8D080DF5 3B6E20C8 4C69105E D56041E4 A2677172 3C03E4D1 4B04D447 D20D85FD A50AB56B 35B5A8FA 42B2986C DBBBC9D6 ACBCF940 32D86CE3 45DF5C75 DCD60DCF ABD13D59 26D930AC 51DE003A C8D75180 BFD06116 21B4F4B5 56B3C423 CFBA9599 B8BDA50F 2802B89E 5F058808 C60CD9B2 B10BE924 2F6F7C87 58684C11 C1611DAB B6662D3D 76DC4190 01DB7106 98D220BC EFD5102A 71B18589 06B6B51F 9FBFE4A5 E8B8D433 7807C9A2 0F00F934 9609A88E E10E9818 7F6A0DBB 086D3D2D 91646C97 E6635C01 6B6B51F4 1C6C6162 856530D8 F262004E 6C0695ED 1B01A57B 8208F4C1 F50FC457 65B0D9C6 12B7E950 8BBEB8EA FCB9887C 62DD1DDF 15DA2D49 8CD37CF3 FBD44C65 4DB26158 3AB551CE A3BC0074 D4BB30E2 4ADFA541 3DD895D7 A4D1C46D D3D6F4FB 4369E96A 346ED9FC AD678846 DA60B8D0 44042D73 33031DE5 AA0A4C5F DD0D7CC9 5005713C 270241AA BE0B1010 C90C2086 5768B525 206F85B3 B966D409 CE61E49F 5EDEF90E 29D9C998 B0D09822 C7D7A8B4 59B33D17 2EB40D81 B7BD5C3B C0BA6CAD EDB88320 9ABFB3B6 03B6E20C 74B1D29A EAD54739 9DD277AF 04DB2615 73DC1683 E3630B12 94643B84 0D6D6A3E 7A6A5AA8 E40ECF0B 9309FF9D 0A00AE27 7D079EB1 F00F9344 8708A3D2 1E01F268 6906C2FE F762575D 806567CB 196C3671 6E6B06E7 FED41B76 89D32BE0 10DA7A5A 67DD4ACC F9B9DF6F 8EBEEFF9 17B7BE43 60B08ED5 D6D6A3E8 A1D1937E 38D8C2C4 4FDFF252 D1BB67F1 A6BC5767 3FB506DD 48B2364B D80D2BDA AF0A1B4C 36034AF6 41047A60 DF60EFC3 A867DF55 316E8EEF 4669BE79 CB61B38C BC66831A 256FD2A0 5268E236 CC0C7795 BB0B4703 220216B9 5505262F C5BA3BBE B2BD0B28 2BB45A92 5CB36A04 C2D7FFA7 B5D0CF31 2CD99E8B 5BDEAE1D 9B64C2B0 EC63F226 756AA39C 026D930A 9C0906A9 EB0E363F 72076785 05005713 95BF4A82 E2B87A14 7BB12BAE 0CB61B38 92D28E9B E5D5BE0D 7CDCEFB7 0BDBDF21 86D3D2D4 F1D4E242 68DDB3F8 1FDA836E 81BE16CD F6B9265B 6FB077E1 18B74777 88085AE6 FF0F6A70 66063BCA 11010B5C 8F659EFF F862AE69 616BFFD3 166CCF45 A00AE278 D70DD2EE 4E048354 3903B3C2 A7672661 D06016F7 4969474D 3E6E77DB AED16A4A D9D65ADC 40DF0B66 37D83BF0 A9BCAE53 DEBB9EC5 47B2CF7F 30B5FFE9 BDBDF21C CABAC28A 53B39330 24B4A3A6 BAD03605 CDD70693 54DE5729 23D967BF B3667A2E C4614AB8 5D681B02 2A6F2B94 B40BBE37 C30C8EA1 5A05DF1B 2D02EF8D"; 504 | crc = 0; 505 | x = 0; 506 | y = 0; 507 | crc = crc ^ (-1); 508 | i = 0; 509 | iTop = str.length; 510 | while (i < iTop) { 511 | y = (crc ^ str.charCodeAt(i)) & 0xFF; 512 | x = "0x" + table.substr(y * 9, 8); 513 | crc = (crc >>> 8) ^ x; 514 | i++; 515 | } 516 | crc = crc ^ (-1); 517 | if (crc < 0) { 518 | crc += 4294967296; 519 | } 520 | return crc; 521 | }; 522 | 523 | }).call(this); 524 | -------------------------------------------------------------------------------- /cloud/cloudinary/lib/crypto/sha1.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined 3 | * in FIPS PUB 180-1 4 | * Version 2.1a Copyright Paul Johnston 2000 - 2002. 5 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet 6 | * Distributed under the BSD License 7 | * See http://pajhome.org.uk/crypt/md5 for details. 8 | */ 9 | (typeof define !== "function" ? function($){ $(require, exports, module); } : define)(function(require, exports, module, undefined) { 10 | 11 | exports.hex_sha1 = hex_sha1; 12 | exports.b64_sha1 = b64_sha1; 13 | exports.str_sha1 = str_sha1; 14 | exports.hex_hmac_sha1 = hex_hmac_sha1; 15 | exports.b64_hmac_sha1 = b64_hmac_sha1; 16 | exports.str_hmac_sha1 = str_hmac_sha1; 17 | 18 | /* 19 | * Configurable variables. You may need to tweak these to be compatible with 20 | * the server-side, but the defaults work in most cases. 21 | */ 22 | var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ 23 | var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */ 24 | var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ 25 | 26 | /* 27 | * These are the functions you'll usually want to call 28 | * They take string arguments and return either hex or base-64 encoded strings 29 | */ 30 | function hex_sha1(s){return binb2hex(core_sha1(str2binb(s),s.length * chrsz));} 31 | function b64_sha1(s){return binb2b64(core_sha1(str2binb(s),s.length * chrsz));} 32 | function str_sha1(s){return binb2str(core_sha1(str2binb(s),s.length * chrsz));} 33 | function hex_hmac_sha1(key, data){ return binb2hex(core_hmac_sha1(key, data));} 34 | function b64_hmac_sha1(key, data){ return binb2b64(core_hmac_sha1(key, data));} 35 | function str_hmac_sha1(key, data){ return binb2str(core_hmac_sha1(key, data));} 36 | 37 | /* 38 | * Perform a simple self-test to see if the VM is working 39 | */ 40 | function sha1_vm_test() 41 | { 42 | return hex_sha1("abc") == "a9993e364706816aba3e25717850c26c9cd0d89d"; 43 | } 44 | 45 | /* 46 | * Calculate the SHA-1 of an array of big-endian words, and a bit length 47 | */ 48 | function core_sha1(x, len) 49 | { 50 | /* append padding */ 51 | x[len >> 5] |= 0x80 << (24 - len % 32); 52 | x[((len + 64 >> 9) << 4) + 15] = len; 53 | 54 | var w = Array(80); 55 | var a = 1732584193; 56 | var b = -271733879; 57 | var c = -1732584194; 58 | var d = 271733878; 59 | var e = -1009589776; 60 | 61 | for(var i = 0; i < x.length; i += 16) 62 | { 63 | var olda = a; 64 | var oldb = b; 65 | var oldc = c; 66 | var oldd = d; 67 | var olde = e; 68 | 69 | for(var j = 0; j < 80; j++) 70 | { 71 | if(j < 16) w[j] = x[i + j]; 72 | else w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1); 73 | var t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)), 74 | safe_add(safe_add(e, w[j]), sha1_kt(j))); 75 | e = d; 76 | d = c; 77 | c = rol(b, 30); 78 | b = a; 79 | a = t; 80 | } 81 | 82 | a = safe_add(a, olda); 83 | b = safe_add(b, oldb); 84 | c = safe_add(c, oldc); 85 | d = safe_add(d, oldd); 86 | e = safe_add(e, olde); 87 | } 88 | return Array(a, b, c, d, e); 89 | 90 | } 91 | 92 | /* 93 | * Perform the appropriate triplet combination function for the current 94 | * iteration 95 | */ 96 | function sha1_ft(t, b, c, d) 97 | { 98 | if(t < 20) return (b & c) | ((~b) & d); 99 | if(t < 40) return b ^ c ^ d; 100 | if(t < 60) return (b & c) | (b & d) | (c & d); 101 | return b ^ c ^ d; 102 | } 103 | 104 | /* 105 | * Determine the appropriate additive constant for the current iteration 106 | */ 107 | function sha1_kt(t) 108 | { 109 | return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 : 110 | (t < 60) ? -1894007588 : -899497514; 111 | } 112 | 113 | /* 114 | * Calculate the HMAC-SHA1 of a key and some data 115 | */ 116 | function core_hmac_sha1(key, data) 117 | { 118 | var bkey = str2binb(key); 119 | if(bkey.length > 16) bkey = core_sha1(bkey, key.length * chrsz); 120 | 121 | var ipad = Array(16), opad = Array(16); 122 | for(var i = 0; i < 16; i++) 123 | { 124 | ipad[i] = bkey[i] ^ 0x36363636; 125 | opad[i] = bkey[i] ^ 0x5C5C5C5C; 126 | } 127 | 128 | var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * chrsz); 129 | return core_sha1(opad.concat(hash), 512 + 160); 130 | } 131 | 132 | /* 133 | * Add integers, wrapping at 2^32. This uses 16-bit operations internally 134 | * to work around bugs in some JS interpreters. 135 | */ 136 | function safe_add(x, y) 137 | { 138 | var lsw = (x & 0xFFFF) + (y & 0xFFFF); 139 | var msw = (x >> 16) + (y >> 16) + (lsw >> 16); 140 | return (msw << 16) | (lsw & 0xFFFF); 141 | } 142 | 143 | /* 144 | * Bitwise rotate a 32-bit number to the left. 145 | */ 146 | function rol(num, cnt) 147 | { 148 | return (num << cnt) | (num >>> (32 - cnt)); 149 | } 150 | 151 | /* 152 | * Convert an 8-bit or 16-bit string to an array of big-endian words 153 | * In 8-bit function, characters >255 have their hi-byte silently ignored. 154 | */ 155 | function str2binb(str) 156 | { 157 | var bin = Array(); 158 | var mask = (1 << chrsz) - 1; 159 | for(var i = 0; i < str.length * chrsz; i += chrsz) 160 | bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (32 - chrsz - i%32); 161 | return bin; 162 | } 163 | 164 | /* 165 | * Convert an array of big-endian words to a string 166 | */ 167 | function binb2str(bin) 168 | { 169 | var str = ""; 170 | var mask = (1 << chrsz) - 1; 171 | for(var i = 0; i < bin.length * 32; i += chrsz) 172 | str += String.fromCharCode((bin[i>>5] >>> (32 - chrsz - i%32)) & mask); 173 | return str; 174 | } 175 | 176 | /* 177 | * Convert an array of big-endian words to a hex string. 178 | */ 179 | function binb2hex(binarray) 180 | { 181 | var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; 182 | var str = ""; 183 | for(var i = 0; i < binarray.length * 4; i++) 184 | { 185 | str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) + 186 | hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF); 187 | } 188 | return str; 189 | } 190 | 191 | /* 192 | * Convert an array of big-endian words to a base-64 string 193 | */ 194 | function binb2b64(binarray) 195 | { 196 | var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 197 | var str = ""; 198 | for(var i = 0; i < binarray.length * 4; i += 3) 199 | { 200 | var triplet = (((binarray[i >> 2] >> 8 * (3 - i %4)) & 0xFF) << 16) 201 | | (((binarray[i+1 >> 2] >> 8 * (3 - (i+1)%4)) & 0xFF) << 8 ) 202 | | ((binarray[i+2 >> 2] >> 8 * (3 - (i+2)%4)) & 0xFF); 203 | for(var j = 0; j < 4; j++) 204 | { 205 | if(i * 8 + j * 6 > binarray.length * 32) str += b64pad; 206 | else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); 207 | } 208 | } 209 | return str; 210 | } 211 | 212 | }); 213 | -------------------------------------------------------------------------------- /cloud/cloudinary/lib/underscore.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.4.4 2 | // http://underscorejs.org 3 | // (c) 2009-2013 Jeremy Ashkenas, DocumentCloud Inc. 4 | // Underscore may be freely distributed under the MIT license. 5 | 6 | (function() { 7 | 8 | // Baseline setup 9 | // -------------- 10 | 11 | // Establish the root object, `window` in the browser, or `global` on the server. 12 | var root = this; 13 | 14 | // Save the previous value of the `_` variable. 15 | var previousUnderscore = root._; 16 | 17 | // Establish the object that gets returned to break out of a loop iteration. 18 | var breaker = {}; 19 | 20 | // Save bytes in the minified (but not gzipped) version: 21 | var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; 22 | 23 | // Create quick reference variables for speed access to core prototypes. 24 | var push = ArrayProto.push, 25 | slice = ArrayProto.slice, 26 | concat = ArrayProto.concat, 27 | toString = ObjProto.toString, 28 | hasOwnProperty = ObjProto.hasOwnProperty; 29 | 30 | // All **ECMAScript 5** native function implementations that we hope to use 31 | // are declared here. 32 | var 33 | nativeForEach = ArrayProto.forEach, 34 | nativeMap = ArrayProto.map, 35 | nativeReduce = ArrayProto.reduce, 36 | nativeReduceRight = ArrayProto.reduceRight, 37 | nativeFilter = ArrayProto.filter, 38 | nativeEvery = ArrayProto.every, 39 | nativeSome = ArrayProto.some, 40 | nativeIndexOf = ArrayProto.indexOf, 41 | nativeLastIndexOf = ArrayProto.lastIndexOf, 42 | nativeIsArray = Array.isArray, 43 | nativeKeys = Object.keys, 44 | nativeBind = FuncProto.bind; 45 | 46 | // Create a safe reference to the Underscore object for use below. 47 | var _ = function(obj) { 48 | if (obj instanceof _) return obj; 49 | if (!(this instanceof _)) return new _(obj); 50 | this._wrapped = obj; 51 | }; 52 | 53 | // Export the Underscore object for **Node.js**, with 54 | // backwards-compatibility for the old `require()` API. If we're in 55 | // the browser, add `_` as a global object via a string identifier, 56 | // for Closure Compiler "advanced" mode. 57 | if (typeof exports !== 'undefined') { 58 | if (typeof module !== 'undefined' && module.exports) { 59 | exports = module.exports = _; 60 | } 61 | exports._ = _; 62 | } else { 63 | root._ = _; 64 | } 65 | 66 | // Current version. 67 | _.VERSION = '1.4.4'; 68 | 69 | // Collection Functions 70 | // -------------------- 71 | 72 | // The cornerstone, an `each` implementation, aka `forEach`. 73 | // Handles objects with the built-in `forEach`, arrays, and raw objects. 74 | // Delegates to **ECMAScript 5**'s native `forEach` if available. 75 | var each = _.each = _.forEach = function(obj, iterator, context) { 76 | if (obj == null) return; 77 | if (nativeForEach && obj.forEach === nativeForEach) { 78 | obj.forEach(iterator, context); 79 | } else if (obj.length === +obj.length) { 80 | for (var i = 0, l = obj.length; i < l; i++) { 81 | if (iterator.call(context, obj[i], i, obj) === breaker) return; 82 | } 83 | } else { 84 | for (var key in obj) { 85 | if (_.has(obj, key)) { 86 | if (iterator.call(context, obj[key], key, obj) === breaker) return; 87 | } 88 | } 89 | } 90 | }; 91 | 92 | // Return the results of applying the iterator to each element. 93 | // Delegates to **ECMAScript 5**'s native `map` if available. 94 | _.map = _.collect = function(obj, iterator, context) { 95 | var results = []; 96 | if (obj == null) return results; 97 | if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); 98 | each(obj, function(value, index, list) { 99 | results[results.length] = iterator.call(context, value, index, list); 100 | }); 101 | return results; 102 | }; 103 | 104 | var reduceError = 'Reduce of empty array with no initial value'; 105 | 106 | // **Reduce** builds up a single result from a list of values, aka `inject`, 107 | // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. 108 | _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { 109 | var initial = arguments.length > 2; 110 | if (obj == null) obj = []; 111 | if (nativeReduce && obj.reduce === nativeReduce) { 112 | if (context) iterator = _.bind(iterator, context); 113 | return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); 114 | } 115 | each(obj, function(value, index, list) { 116 | if (!initial) { 117 | memo = value; 118 | initial = true; 119 | } else { 120 | memo = iterator.call(context, memo, value, index, list); 121 | } 122 | }); 123 | if (!initial) throw new TypeError(reduceError); 124 | return memo; 125 | }; 126 | 127 | // The right-associative version of reduce, also known as `foldr`. 128 | // Delegates to **ECMAScript 5**'s native `reduceRight` if available. 129 | _.reduceRight = _.foldr = function(obj, iterator, memo, context) { 130 | var initial = arguments.length > 2; 131 | if (obj == null) obj = []; 132 | if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { 133 | if (context) iterator = _.bind(iterator, context); 134 | return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); 135 | } 136 | var length = obj.length; 137 | if (length !== +length) { 138 | var keys = _.keys(obj); 139 | length = keys.length; 140 | } 141 | each(obj, function(value, index, list) { 142 | index = keys ? keys[--length] : --length; 143 | if (!initial) { 144 | memo = obj[index]; 145 | initial = true; 146 | } else { 147 | memo = iterator.call(context, memo, obj[index], index, list); 148 | } 149 | }); 150 | if (!initial) throw new TypeError(reduceError); 151 | return memo; 152 | }; 153 | 154 | // Return the first value which passes a truth test. Aliased as `detect`. 155 | _.find = _.detect = function(obj, iterator, context) { 156 | var result; 157 | any(obj, function(value, index, list) { 158 | if (iterator.call(context, value, index, list)) { 159 | result = value; 160 | return true; 161 | } 162 | }); 163 | return result; 164 | }; 165 | 166 | // Return all the elements that pass a truth test. 167 | // Delegates to **ECMAScript 5**'s native `filter` if available. 168 | // Aliased as `select`. 169 | _.filter = _.select = function(obj, iterator, context) { 170 | var results = []; 171 | if (obj == null) return results; 172 | if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); 173 | each(obj, function(value, index, list) { 174 | if (iterator.call(context, value, index, list)) results[results.length] = value; 175 | }); 176 | return results; 177 | }; 178 | 179 | // Return all the elements for which a truth test fails. 180 | _.reject = function(obj, iterator, context) { 181 | return _.filter(obj, function(value, index, list) { 182 | return !iterator.call(context, value, index, list); 183 | }, context); 184 | }; 185 | 186 | // Determine whether all of the elements match a truth test. 187 | // Delegates to **ECMAScript 5**'s native `every` if available. 188 | // Aliased as `all`. 189 | _.every = _.all = function(obj, iterator, context) { 190 | iterator || (iterator = _.identity); 191 | var result = true; 192 | if (obj == null) return result; 193 | if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); 194 | each(obj, function(value, index, list) { 195 | if (!(result = result && iterator.call(context, value, index, list))) return breaker; 196 | }); 197 | return !!result; 198 | }; 199 | 200 | // Determine if at least one element in the object matches a truth test. 201 | // Delegates to **ECMAScript 5**'s native `some` if available. 202 | // Aliased as `any`. 203 | var any = _.some = _.any = function(obj, iterator, context) { 204 | iterator || (iterator = _.identity); 205 | var result = false; 206 | if (obj == null) return result; 207 | if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); 208 | each(obj, function(value, index, list) { 209 | if (result || (result = iterator.call(context, value, index, list))) return breaker; 210 | }); 211 | return !!result; 212 | }; 213 | 214 | // Determine if the array or object contains a given value (using `===`). 215 | // Aliased as `include`. 216 | _.contains = _.include = function(obj, target) { 217 | if (obj == null) return false; 218 | if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; 219 | return any(obj, function(value) { 220 | return value === target; 221 | }); 222 | }; 223 | 224 | // Invoke a method (with arguments) on every item in a collection. 225 | _.invoke = function(obj, method) { 226 | var args = slice.call(arguments, 2); 227 | var isFunc = _.isFunction(method); 228 | return _.map(obj, function(value) { 229 | return (isFunc ? method : value[method]).apply(value, args); 230 | }); 231 | }; 232 | 233 | // Convenience version of a common use case of `map`: fetching a property. 234 | _.pluck = function(obj, key) { 235 | return _.map(obj, function(value){ return value[key]; }); 236 | }; 237 | 238 | // Convenience version of a common use case of `filter`: selecting only objects 239 | // containing specific `key:value` pairs. 240 | _.where = function(obj, attrs, first) { 241 | if (_.isEmpty(attrs)) return first ? null : []; 242 | return _[first ? 'find' : 'filter'](obj, function(value) { 243 | for (var key in attrs) { 244 | if (attrs[key] !== value[key]) return false; 245 | } 246 | return true; 247 | }); 248 | }; 249 | 250 | // Convenience version of a common use case of `find`: getting the first object 251 | // containing specific `key:value` pairs. 252 | _.findWhere = function(obj, attrs) { 253 | return _.where(obj, attrs, true); 254 | }; 255 | 256 | // Return the maximum element or (element-based computation). 257 | // Can't optimize arrays of integers longer than 65,535 elements. 258 | // See: https://bugs.webkit.org/show_bug.cgi?id=80797 259 | _.max = function(obj, iterator, context) { 260 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { 261 | return Math.max.apply(Math, obj); 262 | } 263 | if (!iterator && _.isEmpty(obj)) return -Infinity; 264 | var result = {computed : -Infinity, value: -Infinity}; 265 | each(obj, function(value, index, list) { 266 | var computed = iterator ? iterator.call(context, value, index, list) : value; 267 | computed >= result.computed && (result = {value : value, computed : computed}); 268 | }); 269 | return result.value; 270 | }; 271 | 272 | // Return the minimum element (or element-based computation). 273 | _.min = function(obj, iterator, context) { 274 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { 275 | return Math.min.apply(Math, obj); 276 | } 277 | if (!iterator && _.isEmpty(obj)) return Infinity; 278 | var result = {computed : Infinity, value: Infinity}; 279 | each(obj, function(value, index, list) { 280 | var computed = iterator ? iterator.call(context, value, index, list) : value; 281 | computed < result.computed && (result = {value : value, computed : computed}); 282 | }); 283 | return result.value; 284 | }; 285 | 286 | // Shuffle an array. 287 | _.shuffle = function(obj) { 288 | var rand; 289 | var index = 0; 290 | var shuffled = []; 291 | each(obj, function(value) { 292 | rand = _.random(index++); 293 | shuffled[index - 1] = shuffled[rand]; 294 | shuffled[rand] = value; 295 | }); 296 | return shuffled; 297 | }; 298 | 299 | // An internal function to generate lookup iterators. 300 | var lookupIterator = function(value) { 301 | return _.isFunction(value) ? value : function(obj){ return obj[value]; }; 302 | }; 303 | 304 | // Sort the object's values by a criterion produced by an iterator. 305 | _.sortBy = function(obj, value, context) { 306 | var iterator = lookupIterator(value); 307 | return _.pluck(_.map(obj, function(value, index, list) { 308 | return { 309 | value : value, 310 | index : index, 311 | criteria : iterator.call(context, value, index, list) 312 | }; 313 | }).sort(function(left, right) { 314 | var a = left.criteria; 315 | var b = right.criteria; 316 | if (a !== b) { 317 | if (a > b || a === void 0) return 1; 318 | if (a < b || b === void 0) return -1; 319 | } 320 | return left.index < right.index ? -1 : 1; 321 | }), 'value'); 322 | }; 323 | 324 | // An internal function used for aggregate "group by" operations. 325 | var group = function(obj, value, context, behavior) { 326 | var result = {}; 327 | var iterator = lookupIterator(value || _.identity); 328 | each(obj, function(value, index) { 329 | var key = iterator.call(context, value, index, obj); 330 | behavior(result, key, value); 331 | }); 332 | return result; 333 | }; 334 | 335 | // Groups the object's values by a criterion. Pass either a string attribute 336 | // to group by, or a function that returns the criterion. 337 | _.groupBy = function(obj, value, context) { 338 | return group(obj, value, context, function(result, key, value) { 339 | (_.has(result, key) ? result[key] : (result[key] = [])).push(value); 340 | }); 341 | }; 342 | 343 | // Counts instances of an object that group by a certain criterion. Pass 344 | // either a string attribute to count by, or a function that returns the 345 | // criterion. 346 | _.countBy = function(obj, value, context) { 347 | return group(obj, value, context, function(result, key) { 348 | if (!_.has(result, key)) result[key] = 0; 349 | result[key]++; 350 | }); 351 | }; 352 | 353 | // Use a comparator function to figure out the smallest index at which 354 | // an object should be inserted so as to maintain order. Uses binary search. 355 | _.sortedIndex = function(array, obj, iterator, context) { 356 | iterator = iterator == null ? _.identity : lookupIterator(iterator); 357 | var value = iterator.call(context, obj); 358 | var low = 0, high = array.length; 359 | while (low < high) { 360 | var mid = (low + high) >>> 1; 361 | iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; 362 | } 363 | return low; 364 | }; 365 | 366 | // Safely convert anything iterable into a real, live array. 367 | _.toArray = function(obj) { 368 | if (!obj) return []; 369 | if (_.isArray(obj)) return slice.call(obj); 370 | if (obj.length === +obj.length) return _.map(obj, _.identity); 371 | return _.values(obj); 372 | }; 373 | 374 | // Return the number of elements in an object. 375 | _.size = function(obj) { 376 | if (obj == null) return 0; 377 | return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; 378 | }; 379 | 380 | // Array Functions 381 | // --------------- 382 | 383 | // Get the first element of an array. Passing **n** will return the first N 384 | // values in the array. Aliased as `head` and `take`. The **guard** check 385 | // allows it to work with `_.map`. 386 | _.first = _.head = _.take = function(array, n, guard) { 387 | if (array == null) return void 0; 388 | return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; 389 | }; 390 | 391 | // Returns everything but the last entry of the array. Especially useful on 392 | // the arguments object. Passing **n** will return all the values in 393 | // the array, excluding the last N. The **guard** check allows it to work with 394 | // `_.map`. 395 | _.initial = function(array, n, guard) { 396 | return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); 397 | }; 398 | 399 | // Get the last element of an array. Passing **n** will return the last N 400 | // values in the array. The **guard** check allows it to work with `_.map`. 401 | _.last = function(array, n, guard) { 402 | if (array == null) return void 0; 403 | if ((n != null) && !guard) { 404 | return slice.call(array, Math.max(array.length - n, 0)); 405 | } else { 406 | return array[array.length - 1]; 407 | } 408 | }; 409 | 410 | // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. 411 | // Especially useful on the arguments object. Passing an **n** will return 412 | // the rest N values in the array. The **guard** 413 | // check allows it to work with `_.map`. 414 | _.rest = _.tail = _.drop = function(array, n, guard) { 415 | return slice.call(array, (n == null) || guard ? 1 : n); 416 | }; 417 | 418 | // Trim out all falsy values from an array. 419 | _.compact = function(array) { 420 | return _.filter(array, _.identity); 421 | }; 422 | 423 | // Internal implementation of a recursive `flatten` function. 424 | var flatten = function(input, shallow, output) { 425 | each(input, function(value) { 426 | if (_.isArray(value)) { 427 | shallow ? push.apply(output, value) : flatten(value, shallow, output); 428 | } else { 429 | output.push(value); 430 | } 431 | }); 432 | return output; 433 | }; 434 | 435 | // Return a completely flattened version of an array. 436 | _.flatten = function(array, shallow) { 437 | return flatten(array, shallow, []); 438 | }; 439 | 440 | // Return a version of the array that does not contain the specified value(s). 441 | _.without = function(array) { 442 | return _.difference(array, slice.call(arguments, 1)); 443 | }; 444 | 445 | // Produce a duplicate-free version of the array. If the array has already 446 | // been sorted, you have the option of using a faster algorithm. 447 | // Aliased as `unique`. 448 | _.uniq = _.unique = function(array, isSorted, iterator, context) { 449 | if (_.isFunction(isSorted)) { 450 | context = iterator; 451 | iterator = isSorted; 452 | isSorted = false; 453 | } 454 | var initial = iterator ? _.map(array, iterator, context) : array; 455 | var results = []; 456 | var seen = []; 457 | each(initial, function(value, index) { 458 | if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { 459 | seen.push(value); 460 | results.push(array[index]); 461 | } 462 | }); 463 | return results; 464 | }; 465 | 466 | // Produce an array that contains the union: each distinct element from all of 467 | // the passed-in arrays. 468 | _.union = function() { 469 | return _.uniq(concat.apply(ArrayProto, arguments)); 470 | }; 471 | 472 | // Produce an array that contains every item shared between all the 473 | // passed-in arrays. 474 | _.intersection = function(array) { 475 | var rest = slice.call(arguments, 1); 476 | return _.filter(_.uniq(array), function(item) { 477 | return _.every(rest, function(other) { 478 | return _.indexOf(other, item) >= 0; 479 | }); 480 | }); 481 | }; 482 | 483 | // Take the difference between one array and a number of other arrays. 484 | // Only the elements present in just the first array will remain. 485 | _.difference = function(array) { 486 | var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); 487 | return _.filter(array, function(value){ return !_.contains(rest, value); }); 488 | }; 489 | 490 | // Zip together multiple lists into a single array -- elements that share 491 | // an index go together. 492 | _.zip = function() { 493 | var args = slice.call(arguments); 494 | var length = _.max(_.pluck(args, 'length')); 495 | var results = new Array(length); 496 | for (var i = 0; i < length; i++) { 497 | results[i] = _.pluck(args, "" + i); 498 | } 499 | return results; 500 | }; 501 | 502 | // Converts lists into objects. Pass either a single array of `[key, value]` 503 | // pairs, or two parallel arrays of the same length -- one of keys, and one of 504 | // the corresponding values. 505 | _.object = function(list, values) { 506 | if (list == null) return {}; 507 | var result = {}; 508 | for (var i = 0, l = list.length; i < l; i++) { 509 | if (values) { 510 | result[list[i]] = values[i]; 511 | } else { 512 | result[list[i][0]] = list[i][1]; 513 | } 514 | } 515 | return result; 516 | }; 517 | 518 | // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), 519 | // we need this function. Return the position of the first occurrence of an 520 | // item in an array, or -1 if the item is not included in the array. 521 | // Delegates to **ECMAScript 5**'s native `indexOf` if available. 522 | // If the array is large and already in sort order, pass `true` 523 | // for **isSorted** to use binary search. 524 | _.indexOf = function(array, item, isSorted) { 525 | if (array == null) return -1; 526 | var i = 0, l = array.length; 527 | if (isSorted) { 528 | if (typeof isSorted == 'number') { 529 | i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted); 530 | } else { 531 | i = _.sortedIndex(array, item); 532 | return array[i] === item ? i : -1; 533 | } 534 | } 535 | if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); 536 | for (; i < l; i++) if (array[i] === item) return i; 537 | return -1; 538 | }; 539 | 540 | // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. 541 | _.lastIndexOf = function(array, item, from) { 542 | if (array == null) return -1; 543 | var hasIndex = from != null; 544 | if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { 545 | return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); 546 | } 547 | var i = (hasIndex ? from : array.length); 548 | while (i--) if (array[i] === item) return i; 549 | return -1; 550 | }; 551 | 552 | // Generate an integer Array containing an arithmetic progression. A port of 553 | // the native Python `range()` function. See 554 | // [the Python documentation](http://docs.python.org/library/functions.html#range). 555 | _.range = function(start, stop, step) { 556 | if (arguments.length <= 1) { 557 | stop = start || 0; 558 | start = 0; 559 | } 560 | step = arguments[2] || 1; 561 | 562 | var len = Math.max(Math.ceil((stop - start) / step), 0); 563 | var idx = 0; 564 | var range = new Array(len); 565 | 566 | while(idx < len) { 567 | range[idx++] = start; 568 | start += step; 569 | } 570 | 571 | return range; 572 | }; 573 | 574 | // Function (ahem) Functions 575 | // ------------------ 576 | 577 | // Create a function bound to a given object (assigning `this`, and arguments, 578 | // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if 579 | // available. 580 | _.bind = function(func, context) { 581 | if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); 582 | var args = slice.call(arguments, 2); 583 | return function() { 584 | return func.apply(context, args.concat(slice.call(arguments))); 585 | }; 586 | }; 587 | 588 | // Partially apply a function by creating a version that has had some of its 589 | // arguments pre-filled, without changing its dynamic `this` context. 590 | _.partial = function(func) { 591 | var args = slice.call(arguments, 1); 592 | return function() { 593 | return func.apply(this, args.concat(slice.call(arguments))); 594 | }; 595 | }; 596 | 597 | // Bind all of an object's methods to that object. Useful for ensuring that 598 | // all callbacks defined on an object belong to it. 599 | _.bindAll = function(obj) { 600 | var funcs = slice.call(arguments, 1); 601 | if (funcs.length === 0) funcs = _.functions(obj); 602 | each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); 603 | return obj; 604 | }; 605 | 606 | // Memoize an expensive function by storing its results. 607 | _.memoize = function(func, hasher) { 608 | var memo = {}; 609 | hasher || (hasher = _.identity); 610 | return function() { 611 | var key = hasher.apply(this, arguments); 612 | return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); 613 | }; 614 | }; 615 | 616 | // Delays a function for the given number of milliseconds, and then calls 617 | // it with the arguments supplied. 618 | _.delay = function(func, wait) { 619 | var args = slice.call(arguments, 2); 620 | return setTimeout(function(){ return func.apply(null, args); }, wait); 621 | }; 622 | 623 | // Defers a function, scheduling it to run after the current call stack has 624 | // cleared. 625 | _.defer = function(func) { 626 | return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); 627 | }; 628 | 629 | // Returns a function, that, when invoked, will only be triggered at most once 630 | // during a given window of time. 631 | _.throttle = function(func, wait) { 632 | var context, args, timeout, result; 633 | var previous = 0; 634 | var later = function() { 635 | previous = new Date; 636 | timeout = null; 637 | result = func.apply(context, args); 638 | }; 639 | return function() { 640 | var now = new Date; 641 | var remaining = wait - (now - previous); 642 | context = this; 643 | args = arguments; 644 | if (remaining <= 0) { 645 | clearTimeout(timeout); 646 | timeout = null; 647 | previous = now; 648 | result = func.apply(context, args); 649 | } else if (!timeout) { 650 | timeout = setTimeout(later, remaining); 651 | } 652 | return result; 653 | }; 654 | }; 655 | 656 | // Returns a function, that, as long as it continues to be invoked, will not 657 | // be triggered. The function will be called after it stops being called for 658 | // N milliseconds. If `immediate` is passed, trigger the function on the 659 | // leading edge, instead of the trailing. 660 | _.debounce = function(func, wait, immediate) { 661 | var timeout, result; 662 | return function() { 663 | var context = this, args = arguments; 664 | var later = function() { 665 | timeout = null; 666 | if (!immediate) result = func.apply(context, args); 667 | }; 668 | var callNow = immediate && !timeout; 669 | clearTimeout(timeout); 670 | timeout = setTimeout(later, wait); 671 | if (callNow) result = func.apply(context, args); 672 | return result; 673 | }; 674 | }; 675 | 676 | // Returns a function that will be executed at most one time, no matter how 677 | // often you call it. Useful for lazy initialization. 678 | _.once = function(func) { 679 | var ran = false, memo; 680 | return function() { 681 | if (ran) return memo; 682 | ran = true; 683 | memo = func.apply(this, arguments); 684 | func = null; 685 | return memo; 686 | }; 687 | }; 688 | 689 | // Returns the first function passed as an argument to the second, 690 | // allowing you to adjust arguments, run code before and after, and 691 | // conditionally execute the original function. 692 | _.wrap = function(func, wrapper) { 693 | return function() { 694 | var args = [func]; 695 | push.apply(args, arguments); 696 | return wrapper.apply(this, args); 697 | }; 698 | }; 699 | 700 | // Returns a function that is the composition of a list of functions, each 701 | // consuming the return value of the function that follows. 702 | _.compose = function() { 703 | var funcs = arguments; 704 | return function() { 705 | var args = arguments; 706 | for (var i = funcs.length - 1; i >= 0; i--) { 707 | args = [funcs[i].apply(this, args)]; 708 | } 709 | return args[0]; 710 | }; 711 | }; 712 | 713 | // Returns a function that will only be executed after being called N times. 714 | _.after = function(times, func) { 715 | if (times <= 0) return func(); 716 | return function() { 717 | if (--times < 1) { 718 | return func.apply(this, arguments); 719 | } 720 | }; 721 | }; 722 | 723 | // Object Functions 724 | // ---------------- 725 | 726 | // Retrieve the names of an object's properties. 727 | // Delegates to **ECMAScript 5**'s native `Object.keys` 728 | _.keys = nativeKeys || function(obj) { 729 | if (obj !== Object(obj)) throw new TypeError('Invalid object'); 730 | var keys = []; 731 | for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; 732 | return keys; 733 | }; 734 | 735 | // Retrieve the values of an object's properties. 736 | _.values = function(obj) { 737 | var values = []; 738 | for (var key in obj) if (_.has(obj, key)) values.push(obj[key]); 739 | return values; 740 | }; 741 | 742 | // Convert an object into a list of `[key, value]` pairs. 743 | _.pairs = function(obj) { 744 | var pairs = []; 745 | for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]); 746 | return pairs; 747 | }; 748 | 749 | // Invert the keys and values of an object. The values must be serializable. 750 | _.invert = function(obj) { 751 | var result = {}; 752 | for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key; 753 | return result; 754 | }; 755 | 756 | // Return a sorted list of the function names available on the object. 757 | // Aliased as `methods` 758 | _.functions = _.methods = function(obj) { 759 | var names = []; 760 | for (var key in obj) { 761 | if (_.isFunction(obj[key])) names.push(key); 762 | } 763 | return names.sort(); 764 | }; 765 | 766 | // Extend a given object with all the properties in passed-in object(s). 767 | _.extend = function(obj) { 768 | each(slice.call(arguments, 1), function(source) { 769 | if (source) { 770 | for (var prop in source) { 771 | obj[prop] = source[prop]; 772 | } 773 | } 774 | }); 775 | return obj; 776 | }; 777 | 778 | // Return a copy of the object only containing the whitelisted properties. 779 | _.pick = function(obj) { 780 | var copy = {}; 781 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); 782 | each(keys, function(key) { 783 | if (key in obj) copy[key] = obj[key]; 784 | }); 785 | return copy; 786 | }; 787 | 788 | // Return a copy of the object without the blacklisted properties. 789 | _.omit = function(obj) { 790 | var copy = {}; 791 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); 792 | for (var key in obj) { 793 | if (!_.contains(keys, key)) copy[key] = obj[key]; 794 | } 795 | return copy; 796 | }; 797 | 798 | // Fill in a given object with default properties. 799 | _.defaults = function(obj) { 800 | each(slice.call(arguments, 1), function(source) { 801 | if (source) { 802 | for (var prop in source) { 803 | if (obj[prop] == null) obj[prop] = source[prop]; 804 | } 805 | } 806 | }); 807 | return obj; 808 | }; 809 | 810 | // Create a (shallow-cloned) duplicate of an object. 811 | _.clone = function(obj) { 812 | if (!_.isObject(obj)) return obj; 813 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj); 814 | }; 815 | 816 | // Invokes interceptor with the obj, and then returns obj. 817 | // The primary purpose of this method is to "tap into" a method chain, in 818 | // order to perform operations on intermediate results within the chain. 819 | _.tap = function(obj, interceptor) { 820 | interceptor(obj); 821 | return obj; 822 | }; 823 | 824 | // Internal recursive comparison function for `isEqual`. 825 | var eq = function(a, b, aStack, bStack) { 826 | // Identical objects are equal. `0 === -0`, but they aren't identical. 827 | // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. 828 | if (a === b) return a !== 0 || 1 / a == 1 / b; 829 | // A strict comparison is necessary because `null == undefined`. 830 | if (a == null || b == null) return a === b; 831 | // Unwrap any wrapped objects. 832 | if (a instanceof _) a = a._wrapped; 833 | if (b instanceof _) b = b._wrapped; 834 | // Compare `[[Class]]` names. 835 | var className = toString.call(a); 836 | if (className != toString.call(b)) return false; 837 | switch (className) { 838 | // Strings, numbers, dates, and booleans are compared by value. 839 | case '[object String]': 840 | // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is 841 | // equivalent to `new String("5")`. 842 | return a == String(b); 843 | case '[object Number]': 844 | // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for 845 | // other numeric values. 846 | return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); 847 | case '[object Date]': 848 | case '[object Boolean]': 849 | // Coerce dates and booleans to numeric primitive values. Dates are compared by their 850 | // millisecond representations. Note that invalid dates with millisecond representations 851 | // of `NaN` are not equivalent. 852 | return +a == +b; 853 | // RegExps are compared by their source patterns and flags. 854 | case '[object RegExp]': 855 | return a.source == b.source && 856 | a.global == b.global && 857 | a.multiline == b.multiline && 858 | a.ignoreCase == b.ignoreCase; 859 | } 860 | if (typeof a != 'object' || typeof b != 'object') return false; 861 | // Assume equality for cyclic structures. The algorithm for detecting cyclic 862 | // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. 863 | var length = aStack.length; 864 | while (length--) { 865 | // Linear search. Performance is inversely proportional to the number of 866 | // unique nested structures. 867 | if (aStack[length] == a) return bStack[length] == b; 868 | } 869 | // Add the first object to the stack of traversed objects. 870 | aStack.push(a); 871 | bStack.push(b); 872 | var size = 0, result = true; 873 | // Recursively compare objects and arrays. 874 | if (className == '[object Array]') { 875 | // Compare array lengths to determine if a deep comparison is necessary. 876 | size = a.length; 877 | result = size == b.length; 878 | if (result) { 879 | // Deep compare the contents, ignoring non-numeric properties. 880 | while (size--) { 881 | if (!(result = eq(a[size], b[size], aStack, bStack))) break; 882 | } 883 | } 884 | } else { 885 | // Objects with different constructors are not equivalent, but `Object`s 886 | // from different frames are. 887 | var aCtor = a.constructor, bCtor = b.constructor; 888 | if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && 889 | _.isFunction(bCtor) && (bCtor instanceof bCtor))) { 890 | return false; 891 | } 892 | // Deep compare objects. 893 | for (var key in a) { 894 | if (_.has(a, key)) { 895 | // Count the expected number of properties. 896 | size++; 897 | // Deep compare each member. 898 | if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; 899 | } 900 | } 901 | // Ensure that both objects contain the same number of properties. 902 | if (result) { 903 | for (key in b) { 904 | if (_.has(b, key) && !(size--)) break; 905 | } 906 | result = !size; 907 | } 908 | } 909 | // Remove the first object from the stack of traversed objects. 910 | aStack.pop(); 911 | bStack.pop(); 912 | return result; 913 | }; 914 | 915 | // Perform a deep comparison to check if two objects are equal. 916 | _.isEqual = function(a, b) { 917 | return eq(a, b, [], []); 918 | }; 919 | 920 | // Is a given array, string, or object empty? 921 | // An "empty" object has no enumerable own-properties. 922 | _.isEmpty = function(obj) { 923 | if (obj == null) return true; 924 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; 925 | for (var key in obj) if (_.has(obj, key)) return false; 926 | return true; 927 | }; 928 | 929 | // Is a given value a DOM element? 930 | _.isElement = function(obj) { 931 | return !!(obj && obj.nodeType === 1); 932 | }; 933 | 934 | // Is a given value an array? 935 | // Delegates to ECMA5's native Array.isArray 936 | _.isArray = nativeIsArray || function(obj) { 937 | return toString.call(obj) == '[object Array]'; 938 | }; 939 | 940 | // Is a given variable an object? 941 | _.isObject = function(obj) { 942 | return obj === Object(obj); 943 | }; 944 | 945 | // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. 946 | each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { 947 | _['is' + name] = function(obj) { 948 | return toString.call(obj) == '[object ' + name + ']'; 949 | }; 950 | }); 951 | 952 | // Define a fallback version of the method in browsers (ahem, IE), where 953 | // there isn't any inspectable "Arguments" type. 954 | if (!_.isArguments(arguments)) { 955 | _.isArguments = function(obj) { 956 | return !!(obj && _.has(obj, 'callee')); 957 | }; 958 | } 959 | 960 | // Optimize `isFunction` if appropriate. 961 | if (typeof (/./) !== 'function') { 962 | _.isFunction = function(obj) { 963 | return typeof obj === 'function'; 964 | }; 965 | } 966 | 967 | // Is a given object a finite number? 968 | _.isFinite = function(obj) { 969 | return isFinite(obj) && !isNaN(parseFloat(obj)); 970 | }; 971 | 972 | // Is the given value `NaN`? (NaN is the only number which does not equal itself). 973 | _.isNaN = function(obj) { 974 | return _.isNumber(obj) && obj != +obj; 975 | }; 976 | 977 | // Is a given value a boolean? 978 | _.isBoolean = function(obj) { 979 | return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; 980 | }; 981 | 982 | // Is a given value equal to null? 983 | _.isNull = function(obj) { 984 | return obj === null; 985 | }; 986 | 987 | // Is a given variable undefined? 988 | _.isUndefined = function(obj) { 989 | return obj === void 0; 990 | }; 991 | 992 | // Shortcut function for checking if an object has a given property directly 993 | // on itself (in other words, not on a prototype). 994 | _.has = function(obj, key) { 995 | return hasOwnProperty.call(obj, key); 996 | }; 997 | 998 | // Utility Functions 999 | // ----------------- 1000 | 1001 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its 1002 | // previous owner. Returns a reference to the Underscore object. 1003 | _.noConflict = function() { 1004 | root._ = previousUnderscore; 1005 | return this; 1006 | }; 1007 | 1008 | // Keep the identity function around for default iterators. 1009 | _.identity = function(value) { 1010 | return value; 1011 | }; 1012 | 1013 | // Run a function **n** times. 1014 | _.times = function(n, iterator, context) { 1015 | var accum = Array(n); 1016 | for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i); 1017 | return accum; 1018 | }; 1019 | 1020 | // Return a random integer between min and max (inclusive). 1021 | _.random = function(min, max) { 1022 | if (max == null) { 1023 | max = min; 1024 | min = 0; 1025 | } 1026 | return min + Math.floor(Math.random() * (max - min + 1)); 1027 | }; 1028 | 1029 | // List of HTML entities for escaping. 1030 | var entityMap = { 1031 | escape: { 1032 | '&': '&', 1033 | '<': '<', 1034 | '>': '>', 1035 | '"': '"', 1036 | "'": ''', 1037 | '/': '/' 1038 | } 1039 | }; 1040 | entityMap.unescape = _.invert(entityMap.escape); 1041 | 1042 | // Regexes containing the keys and values listed immediately above. 1043 | var entityRegexes = { 1044 | escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), 1045 | unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') 1046 | }; 1047 | 1048 | // Functions for escaping and unescaping strings to/from HTML interpolation. 1049 | _.each(['escape', 'unescape'], function(method) { 1050 | _[method] = function(string) { 1051 | if (string == null) return ''; 1052 | return ('' + string).replace(entityRegexes[method], function(match) { 1053 | return entityMap[method][match]; 1054 | }); 1055 | }; 1056 | }); 1057 | 1058 | // If the value of the named property is a function then invoke it; 1059 | // otherwise, return it. 1060 | _.result = function(object, property) { 1061 | if (object == null) return null; 1062 | var value = object[property]; 1063 | return _.isFunction(value) ? value.call(object) : value; 1064 | }; 1065 | 1066 | // Add your own custom functions to the Underscore object. 1067 | _.mixin = function(obj) { 1068 | each(_.functions(obj), function(name){ 1069 | var func = _[name] = obj[name]; 1070 | _.prototype[name] = function() { 1071 | var args = [this._wrapped]; 1072 | push.apply(args, arguments); 1073 | return result.call(this, func.apply(_, args)); 1074 | }; 1075 | }); 1076 | }; 1077 | 1078 | // Generate a unique integer id (unique within the entire client session). 1079 | // Useful for temporary DOM ids. 1080 | var idCounter = 0; 1081 | _.uniqueId = function(prefix) { 1082 | var id = ++idCounter + ''; 1083 | return prefix ? prefix + id : id; 1084 | }; 1085 | 1086 | // By default, Underscore uses ERB-style template delimiters, change the 1087 | // following template settings to use alternative delimiters. 1088 | _.templateSettings = { 1089 | evaluate : /<%([\s\S]+?)%>/g, 1090 | interpolate : /<%=([\s\S]+?)%>/g, 1091 | escape : /<%-([\s\S]+?)%>/g 1092 | }; 1093 | 1094 | // When customizing `templateSettings`, if you don't want to define an 1095 | // interpolation, evaluation or escaping regex, we need one that is 1096 | // guaranteed not to match. 1097 | var noMatch = /(.)^/; 1098 | 1099 | // Certain characters need to be escaped so that they can be put into a 1100 | // string literal. 1101 | var escapes = { 1102 | "'": "'", 1103 | '\\': '\\', 1104 | '\r': 'r', 1105 | '\n': 'n', 1106 | '\t': 't', 1107 | '\u2028': 'u2028', 1108 | '\u2029': 'u2029' 1109 | }; 1110 | 1111 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; 1112 | 1113 | // JavaScript micro-templating, similar to John Resig's implementation. 1114 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 1115 | // and correctly escapes quotes within interpolated code. 1116 | _.template = function(text, data, settings) { 1117 | var render; 1118 | settings = _.defaults({}, settings, _.templateSettings); 1119 | 1120 | // Combine delimiters into one regular expression via alternation. 1121 | var matcher = new RegExp([ 1122 | (settings.escape || noMatch).source, 1123 | (settings.interpolate || noMatch).source, 1124 | (settings.evaluate || noMatch).source 1125 | ].join('|') + '|$', 'g'); 1126 | 1127 | // Compile the template source, escaping string literals appropriately. 1128 | var index = 0; 1129 | var source = "__p+='"; 1130 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { 1131 | source += text.slice(index, offset) 1132 | .replace(escaper, function(match) { return '\\' + escapes[match]; }); 1133 | 1134 | if (escape) { 1135 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; 1136 | } 1137 | if (interpolate) { 1138 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; 1139 | } 1140 | if (evaluate) { 1141 | source += "';\n" + evaluate + "\n__p+='"; 1142 | } 1143 | index = offset + match.length; 1144 | return match; 1145 | }); 1146 | source += "';\n"; 1147 | 1148 | // If a variable is not specified, place data values in local scope. 1149 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; 1150 | 1151 | source = "var __t,__p='',__j=Array.prototype.join," + 1152 | "print=function(){__p+=__j.call(arguments,'');};\n" + 1153 | source + "return __p;\n"; 1154 | 1155 | try { 1156 | render = new Function(settings.variable || 'obj', '_', source); 1157 | } catch (e) { 1158 | e.source = source; 1159 | throw e; 1160 | } 1161 | 1162 | if (data) return render(data, _); 1163 | var template = function(data) { 1164 | return render.call(this, data, _); 1165 | }; 1166 | 1167 | // Provide the compiled function source as a convenience for precompilation. 1168 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; 1169 | 1170 | return template; 1171 | }; 1172 | 1173 | // Add a "chain" function, which will delegate to the wrapper. 1174 | _.chain = function(obj) { 1175 | return _(obj).chain(); 1176 | }; 1177 | 1178 | // OOP 1179 | // --------------- 1180 | // If Underscore is called as a function, it returns a wrapped object that 1181 | // can be used OO-style. This wrapper holds altered versions of all the 1182 | // underscore functions. Wrapped objects may be chained. 1183 | 1184 | // Helper function to continue chaining intermediate results. 1185 | var result = function(obj) { 1186 | return this._chain ? _(obj).chain() : obj; 1187 | }; 1188 | 1189 | // Add all of the Underscore functions to the wrapper object. 1190 | _.mixin(_); 1191 | 1192 | // Add all mutator Array functions to the wrapper. 1193 | each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { 1194 | var method = ArrayProto[name]; 1195 | _.prototype[name] = function() { 1196 | var obj = this._wrapped; 1197 | method.apply(obj, arguments); 1198 | if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; 1199 | return result.call(this, obj); 1200 | }; 1201 | }); 1202 | 1203 | // Add all accessor Array functions to the wrapper. 1204 | each(['concat', 'join', 'slice'], function(name) { 1205 | var method = ArrayProto[name]; 1206 | _.prototype[name] = function() { 1207 | return result.call(this, method.apply(this._wrapped, arguments)); 1208 | }; 1209 | }); 1210 | 1211 | _.extend(_.prototype, { 1212 | 1213 | // Start chaining a wrapped Underscore object. 1214 | chain: function() { 1215 | this._chain = true; 1216 | return this; 1217 | }, 1218 | 1219 | // Extracts the result from a wrapped and chained object. 1220 | value: function() { 1221 | return this._wrapped; 1222 | } 1223 | 1224 | }); 1225 | 1226 | }).call(this); 1227 | -------------------------------------------------------------------------------- /cloud/cloudinary/version.js: -------------------------------------------------------------------------------- 1 | Parse.Cloudinary.VERSION = "1.0.0"; 2 | -------------------------------------------------------------------------------- /cloud/cloudinary_config.js.sample: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | cloud_name: 'my_cloud_name', 3 | api_key: 'my_api_key', 4 | api_secret: 'my_api_secret' 5 | }; 6 | -------------------------------------------------------------------------------- /sample/README.md: -------------------------------------------------------------------------------- 1 | # Cloudinary Parse Sample Project 2 | This package contains a sample intergration with the Cloudinary Parse Cloud Module. 3 | The cloudinary library provides a mechanism for storing cloudinary credentials, signing upload requests and creating Cloudinary URLs for image thumbnails. 4 | The sample code includes simple Cloud Functions that signs upload requests for authenticated users and return a Cloudinary URL for an image thumbnail. 5 | 6 | ## Files 7 | 8 | * `cloud/main.js` - The entry point to the sample project. Contains: 9 | * A sample cloud function (`sign_cloudinary_upload_request`) that returns an object with all required parameters to initiate a direct upload to Cloudinary. 10 | The function requires an authenticated Parse user, embeds the username into the tags field and eagerly creates a thumbnail. 11 | The returned data can be used to construct an HTML form or passed to Cloudinary front-end libraries to initiate an image upload. 12 | * Initialize the beforeSave factory (`beforeSaveFactory`), which creates a beforeSave function that verifies updates to `field_name` are only done with a signed cloudinary identifier. 13 | The beforeSave function also removes the signatures when saving to the database. 14 | * A sample cloud function that enables you to get a thumbnail url for Cloudinary of a the Photo entity given its objectId. 15 | 16 | * `cloud/cloudinary_config.js` holds cloudinary configuration as examplified in `cloud/cloudinary_config.js.sample` 17 | 18 | ## Setup the sample project 19 | 20 | * [Signup or login to Parse](https://parse.com/#signup) 21 | * [Create a new app](https://parse.com/apps/new) 22 | * Install the Cloud Code Command Line Tool (See [Cloud Code Guide](https://parse.com/docs/cloud_code_guide#started) for more info) 23 | * Create a new project with `parse new` 24 | * Copy the files in this package to the new project folder 25 | * Copy the files from the Cloudinary Cloud Module to the new project folder 26 | * Create a Cloudinary configuration file (`cloudinary_config.js`) 27 | * Deploy your code using `parse deploy` 28 | 29 | You're now ready to go 30 | 31 | # Sample usage with cURL 32 | ## Signup 33 | 34 | curl -X POST \ 35 | -H "X-Parse-Application-Id: PARSE_APP_ID" \ 36 | -H "X-Parse-REST-API-Key: PARSE_REST_KEY" \ 37 | -H "Content-Type: application/json" \ 38 | -d '{"username":"MY_USER","password":"MY_PASS"}' \ 39 | https://api.parse.com/1/users 40 | 41 | ## Login 42 | 43 | curl -X GET \ 44 | -H "X-Parse-Application-Id: PARSE_APP_ID" \ 45 | -H "X-Parse-REST-API-Key: PARSE_REST_KEY" \ 46 | -G \ 47 | --data-urlencode 'username=MY_USER' \ 48 | --data-urlencode 'password=MY_PASS' \ 49 | https://api.parse.com/1/login 50 | 51 | Response: 52 | 53 | { 54 | "username":"MY_USER", 55 | "createdAt":"2013-04-21T12:55:41.891Z", 56 | "updatedAt":"2013-04-21T12:55:41.891Z", 57 | "objectId":"USER_ID", 58 | "sessionToken":"SESSION_TOKEN" 59 | } 60 | 61 | 62 | ## Get signature 63 | 64 | Use `sessionToken` from login response 65 | 66 | curl -X POST \ 67 | -H "X-Parse-Application-Id: PARSE_APP_ID" \ 68 | -H "X-Parse-REST-API-Key: PARSE_REST_KEY" \ 69 | -H "X-Parse-Session-Token: SESSION_TOKEN" \ 70 | -H "Content-Type: application/json" \ 71 | -d '{}' \ 72 | https://api.parse.com/1/functions/sign_cloudinary_upload_request 73 | 74 | Resposne: 75 | 76 | { 77 | "result": { 78 | "timestamp":1366555048, 79 | "tags":"MY_USER", 80 | "eager":"c_fill,g_face,h_100,w_150" 81 | "signature":"CLOUDINARY_SIGNATURE", 82 | "api_key":"CLOUDINARY_API_KEY" 83 | } 84 | } 85 | 86 | ## Upload image to Cloudinary using obtained signature 87 | 88 | Note - Uploading to Cloudinary can be done using Cloudinary's client libraries including the 89 | [iOS](https://github.com/cloudinary/cloudinary_ios) and [Android](https://github.com/cloudinary/cloudinary_android) SDKs. 90 | Alternatively, with cURL, using the response from `sign_cloudinary_upload_request`: 91 | 92 | curl -X POST \ 93 | -F timestamp=1366555048 \ 94 | -F tags=MY_USER \ 95 | -F signature=CLOUDINARY_SIGNATURE \ 96 | -F api_key="CLOUDINARY_API_KEY" \ 97 | -F file=@sample.jpg \ 98 | http://api.cloudinary.com/v1_1/my_cloud_name/image/upload 99 | 100 | Response: 101 | 102 | { 103 | "public_id":"k3vmeifbepxddbzjuop9", 104 | "version":1366555348, 105 | "signature":"CLOUDINARY_RESULT_SIGNATURE", 106 | "width":453, 107 | "height":604, 108 | "format":"jpg", 109 | "resource_type":"image", 110 | "created_at":"2013-04-21T15:31:06Z", 111 | "tags":["MY_USER"], 112 | "bytes":52534, 113 | "type":"upload", 114 | "url":"http://res.cloudinary.com/my_cloud_name/image/upload/v1366555348/k3vmeifbepxddbzjuop9.jpg", 115 | "secure_url":"https://res.cloudinary.com/my_cloud_name/image/upload/v1366555348/k3vmeifbepxddbzjuop9.jpg" 116 | } 117 | 118 | ## Create Photo record using the uploaded image 119 | 120 | Using the response from Cloudinary: 121 | 122 | curl -X POST \ 123 | -H "X-Parse-Application-Id: PARSE_APP_ID" \ 124 | -H "X-Parse-REST-API-Key: PARSE_REST_KEY" \ 125 | -H "X-Parse-Session-Token: SESSION_TOKEN" \ 126 | -H "Content-Type: application/json" \ 127 | -d '{"cloudinaryIdentifier":"image/upload/v1366555348/k3vmeifbepxddbzjuop9.jpg#CLOUDINARY_RESULT_SIGNATURE"}' \ 128 | https://api.parse.com/1/classes/Photo 129 | 130 | Response: 131 | 132 | { 133 | "cloudinaryIdentifier":"image/upload/v1366555348/k3vmeifbepxddbzjuop9.jpg", 134 | "createdAt":"2013-04-21T15:31:06Z", 135 | "objectId":"PHOTO_ID" 136 | } 137 | 138 | ## Generate a Cloudinary URL of a thumbnail of the Photo 139 | 140 | Using the objectId returned when creating the Photo record: 141 | 142 | curl -X POST \ 143 | -H "X-Parse-Application-Id: PARSE_APP_ID" \ 144 | -H "X-Parse-REST-API-Key: PARSE_REST_KEY" \ 145 | -H "X-Parse-Session-Token: SESSION_TOKEN" \ 146 | -H "Content-Type: application/json" \ 147 | -d '{"objectId":"PHOTO_ID"}' \ 148 | https://api.parse.com/1/functions/photo_thumbnail_url 149 | 150 | Response: 151 | 152 | { 153 | "result": { 154 | "url":"http://res.cloudinary.com/my_cloud_name/image/upload/c_fill,h_100,w_150/v1366555348/k3vmeifbepxddbzjuop9.jpg" 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /sample/cloud/main.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Sample Cloudinary Parse Cloud Code that uses the Cloudinary Parse Module 3 | */ 4 | cloudinary = require("cloud/cloudinary"); 5 | 6 | /* 7 | Configuration sample: 8 | cloudinary.config({ 9 | cloud_name: 'my_cloud_name', 10 | api_key: 'my_api_key', 11 | api_secret: 'my_api_secret', 12 | }); 13 | */ 14 | 15 | /// The following lines install a beforeSave filter for the given field within the given object 16 | var OBJECT_NAME = "Photo"; 17 | var CLOUDINARY_IDENTIFIER_FIELD_NAME = "cloudinaryIdentifier"; 18 | /// You can either use and modify the example beforeSaveFactory in this file, or use the one from the library: 19 | // beforeSaveFactory(object_name, field_name); 20 | cloudinary.beforeSaveFactory(OBJECT_NAME, CLOUDINARY_IDENTIFIER_FIELD_NAME); 21 | 22 | /** 23 | * The following declaration exposes a cloud code function that enables you 24 | * to sign a direct-upload request from your app. 25 | * @note This function assumes no extra parameters are needed for the upload. 26 | * @note This function embeds the username in the cloudinary tags field and eagerly creates a thumbnail. 27 | */ 28 | Parse.Cloud.define("sign_cloudinary_upload_request", function(request, response) { 29 | if (!request.user || !request.user.authenticated()) { 30 | response.error("Needs an authenticated user"); 31 | return; 32 | } 33 | response.success( 34 | cloudinary.sign_upload_request({tags: request.user.getUsername(), eager: {crop: "fill", width: 150, height: 100, gravity: "face"}}) 35 | ); 36 | }); 37 | 38 | /** 39 | * The following declaration exposes a cloud code function that enables you to get a 40 | * thumbnail url for Cloudinary of a the Photo entity. 41 | * Cloud-based image manipulation URLs can also be generated on the mobile apps based 42 | * on the identifier returned when uploading a object using the beforeSaveFactory above. 43 | */ 44 | Parse.Cloud.define("photo_thumbnail_url", function(request, response) { 45 | if (!request.user || !request.user.authenticated()) { 46 | response.error("Needs an authenticated user"); 47 | return; 48 | } 49 | 50 | var query = new Parse.Query(OBJECT_NAME); 51 | query.get(request.params.objectId, { 52 | success: function(result) { 53 | response.success({ 54 | url: cloudinary.url(result.get(CLOUDINARY_IDENTIFIER_FIELD_NAME), {crop: "fill", width: 150, height: 100, gravity: "face"}) 55 | }); 56 | }, 57 | error: function() { 58 | response.error("image lookup failed"); 59 | } 60 | }); 61 | }); 62 | 63 | 64 | --------------------------------------------------------------------------------