From 2a519a47c930739e06577888dbc08d09f2d71206 Mon Sep 17 00:00:00 2001 From: Alon Hammerman Date: Tue, 29 Oct 2013 00:41:15 -0500 Subject: [PATCH 01/24] sample/PhotoAlbum - fix parsing for cloudinary created_at (ISO-8601) --- samples/PhotoAlbum/main.php | 53 ++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/samples/PhotoAlbum/main.php b/samples/PhotoAlbum/main.php index bcf228b7..17736301 100644 --- a/samples/PhotoAlbum/main.php +++ b/samples/PhotoAlbum/main.php @@ -9,14 +9,14 @@ include 'settings.php'; // Global settings - if (array_key_exists('REQUEST_SCHEME', $_SERVER)) { + if (array_key_exists('REQUEST_SCHEME', $_SERVER)) { $cors_location = $_SERVER["REQUEST_SCHEME"] . "://" . $_SERVER["SERVER_NAME"] . - dirname($_SERVER["SCRIPT_NAME"]) . "/lib/cloudinary_cors.html"; + dirname($_SERVER["SCRIPT_NAME"]) . "/lib/cloudinary_cors.html"; } else { - $cors_location = "http://" . $_SERVER["HTTP_HOST"] . "/lib/cloudinary_cors.html"; + $cors_location = "http://" . $_SERVER["HTTP_HOST"] . "/lib/cloudinary_cors.html"; } - - $thumbs_params = array("format" => "jpg", "height" => 150, "width" => 150, + + $thumbs_params = array("format" => "jpg", "height" => 150, "width" => 150, "class" => "thumbnail inline"); // Helper functions @@ -30,31 +30,36 @@ function array_to_table($array) { $saved_error_reporting = error_reporting(0); echo ""; foreach ($array as $key => $value) { - if ($key != 'class') { - if ($key == 'url' || $key == 'secure_url') { - $display_value = '"' . $value . '"'; - } else { - $display_value = json_encode($value); - } - echo ""; - } + if ($key != 'class') { + if ($key == 'url' || $key == 'secure_url') { + $display_value = '"' . $value . '"'; + } else { + $display_value = json_encode($value); + } + echo ""; + } } echo "
" . $key . ":" . $display_value . "
" . $key . ":" . $display_value . "
"; error_reporting($saved_error_reporting); } function create_photo_model($options = array()) { - $photo = \R::dispense('photo'); - - # Add metadata we want to keep: - $photo->created_at = \R::isoDateTime(); - foreach ( $options as $key => $value ) { - if ($key != 'tags') { - $photo->{$key} = $value; - } - } - $id = \R::store($photo); - } + $photo = \R::dispense('photo'); + + foreach ( $options as $key => $value ) { + if ($key != 'tags') { + $photo->{$key} = $value; + } + } + + # Add metadata we want to keep: + $photo->moderated = false; + $photo->created_at = (array_key_exists('created_at', $photo) ? + DateTime::createFromFormat(DateTime::ISO8601, $photo->created_at) : + \R::isoDateTime()); + + $id = \R::store($photo); + } } ?> From dae0ba1b8a5c1684e08dfd035c73f3254f771683 Mon Sep 17 00:00:00 2001 From: Alon Hammerman Date: Sat, 23 Nov 2013 15:51:34 -0600 Subject: [PATCH 02/24] php framework - Use DIRECTORY_SEPARATOR --- src/Api.php | 2 +- src/Uploader.php | 2 +- tests/ApiTest.php | 8 ++++---- tests/CloudinaryTest.php | 4 ++-- tests/UploaderTest.php | 8 ++++---- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Api.php b/src/Api.php index 4082023a..bb9574a4 100644 --- a/src/Api.php +++ b/src/Api.php @@ -152,7 +152,7 @@ protected function call_api($method, $uri, $params, &$options) { curl_setopt($ch, CURLOPT_TIMEOUT, 60); curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); curl_setopt($ch, CURLOPT_USERPWD, "{$api_key}:{$api_secret}"); - curl_setopt($ch, CURLOPT_CAINFO,realpath(dirname(__FILE__))."/cacert.pem"); + curl_setopt($ch, CURLOPT_CAINFO,realpath(dirname(__FILE__)).DIRECTORY_SEPARATOR."cacert.pem"); $response = $this->execute($ch); $curl_error = NULL; if(curl_errno($ch)) diff --git a/src/Uploader.php b/src/Uploader.php index 6b431f7f..cc14a106 100644 --- a/src/Uploader.php +++ b/src/Uploader.php @@ -183,7 +183,7 @@ public static function call_api($action, $params, $options = array(), $file = NU curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_TIMEOUT, 60); curl_setopt($ch, CURLOPT_POSTFIELDS, $post_params); - curl_setopt($ch, CURLOPT_CAINFO,realpath(dirname(__FILE__))."/cacert.pem"); + curl_setopt($ch, CURLOPT_CAINFO,realpath(dirname(__FILE__)).DIRECTORY_SEPARATOR."cacert.pem"); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $curl_error = NULL; diff --git a/tests/ApiTest.php b/tests/ApiTest.php index bd5b3a33..141bfe1f 100644 --- a/tests/ApiTest.php +++ b/tests/ApiTest.php @@ -1,8 +1,8 @@ "test123", "secure_distribution" => NULL, "private_cdn" => FALSE)); diff --git a/tests/UploaderTest.php b/tests/UploaderTest.php index 782d600e..ace39e15 100644 --- a/tests/UploaderTest.php +++ b/tests/UploaderTest.php @@ -1,8 +1,8 @@ Date: Wed, 20 Nov 2013 19:58:51 -0600 Subject: [PATCH 03/24] PhotoAlbum (non cake) - use cloudinary_js_config --- samples/PhotoAlbum/upload.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/samples/PhotoAlbum/upload.php b/samples/PhotoAlbum/upload.php index a37f659b..dd831f6a 100644 --- a/samples/PhotoAlbum/upload.php +++ b/samples/PhotoAlbum/upload.php @@ -18,9 +18,7 @@ - + From ee21e45298e2c4e05e29ae5dc9d1e79e975a27ac Mon Sep 17 00:00:00 2001 From: Alon Hammerman Date: Tue, 5 Nov 2013 18:27:09 -0600 Subject: [PATCH 04/24] cake - Add freshly baked PhotoAlbumCake app and CloudinaryCake plugin --- .../CloudinaryCake/Config/Schema/empty | 0 .../CloudinaryCake/Console/Command/Task/empty | 0 .../CloudinaryCakeAppController.php | 7 + .../CloudinaryCake/Controller/Component/empty | 0 cake_plugin/CloudinaryCake/Lib/empty | 0 .../CloudinaryCake/Model/Behavior/empty | 0 .../Model/CloudinaryCakeAppModel.php | 7 + .../CloudinaryCake/Model/Datasource/empty | 0 .../Test/Case/Controller/Component/empty | 0 .../Test/Case/Model/Behavior/empty | 0 .../Test/Case/View/Helper/empty | 0 cake_plugin/CloudinaryCake/Test/Fixture/empty | 0 cake_plugin/CloudinaryCake/Vendor/empty | 0 cake_plugin/CloudinaryCake/View/Helper/empty | 0 cake_plugin/CloudinaryCake/webroot/empty | 0 samples/PhotoAlbumCake/.gitignore | 1 + samples/PhotoAlbumCake/.htaccess | 5 + .../PhotoAlbumCake/Config/Schema/db_acl.php | 62 ++ .../PhotoAlbumCake/Config/Schema/db_acl.sql | 41 + samples/PhotoAlbumCake/Config/Schema/i18n.php | 43 + samples/PhotoAlbumCake/Config/Schema/i18n.sql | 27 + .../PhotoAlbumCake/Config/Schema/sessions.php | 38 + .../PhotoAlbumCake/Config/Schema/sessions.sql | 17 + samples/PhotoAlbumCake/Config/acl.ini.php | 60 ++ samples/PhotoAlbumCake/Config/acl.php | 126 +++ samples/PhotoAlbumCake/Config/bootstrap.php | 102 +++ samples/PhotoAlbumCake/Config/core.php | 368 +++++++++ .../Config/database.php.default | 72 ++ .../PhotoAlbumCake/Config/email.php.default | 85 ++ samples/PhotoAlbumCake/Config/routes.php | 37 + .../Console/Command/AppShell.php | 23 + samples/PhotoAlbumCake/Console/cake | 42 + samples/PhotoAlbumCake/Console/cake.bat | 31 + samples/PhotoAlbumCake/Console/cake.php | 37 + .../Controller/AppController.php | 27 + .../Controller/PagesController.php | 70 ++ samples/PhotoAlbumCake/Model/AppModel.php | 26 + .../View/Emails/html/default.ctp | 26 + .../View/Emails/text/default.ctp | 20 + .../PhotoAlbumCake/View/Errors/error400.ctp | 23 + .../PhotoAlbumCake/View/Errors/error500.ctp | 20 + .../PhotoAlbumCake/View/Helper/AppHelper.php | 26 + .../View/Layouts/Emails/html/default.ctp | 21 + .../View/Layouts/Emails/text/default.ctp | 22 + samples/PhotoAlbumCake/View/Layouts/ajax.ctp | 11 + .../PhotoAlbumCake/View/Layouts/default.ctp | 53 ++ samples/PhotoAlbumCake/View/Layouts/error.ctp | 53 ++ samples/PhotoAlbumCake/View/Layouts/flash.ctp | 29 + .../View/Layouts/js/default.ctp | 2 + .../View/Layouts/rss/default.ctp | 14 + .../View/Layouts/xml/default.ctp | 1 + samples/PhotoAlbumCake/View/Pages/home.ctp | 226 ++++++ samples/PhotoAlbumCake/index.php | 10 + samples/PhotoAlbumCake/webroot/.htaccess | 6 + .../webroot/css/cake.generic.css | 742 ++++++++++++++++++ samples/PhotoAlbumCake/webroot/favicon.ico | Bin 0 -> 372 bytes .../PhotoAlbumCake/webroot/img/cake.icon.png | Bin 0 -> 943 bytes .../PhotoAlbumCake/webroot/img/cake.power.gif | Bin 0 -> 201 bytes .../webroot/img/test-error-icon.png | Bin 0 -> 3358 bytes .../webroot/img/test-fail-icon.png | Bin 0 -> 496 bytes .../webroot/img/test-pass-icon.png | Bin 0 -> 783 bytes .../webroot/img/test-skip-icon.png | Bin 0 -> 1207 bytes samples/PhotoAlbumCake/webroot/index.php | 101 +++ 63 files changed, 2760 insertions(+) create mode 100644 cake_plugin/CloudinaryCake/Config/Schema/empty create mode 100644 cake_plugin/CloudinaryCake/Console/Command/Task/empty create mode 100644 cake_plugin/CloudinaryCake/Controller/CloudinaryCakeAppController.php create mode 100644 cake_plugin/CloudinaryCake/Controller/Component/empty create mode 100644 cake_plugin/CloudinaryCake/Lib/empty create mode 100644 cake_plugin/CloudinaryCake/Model/Behavior/empty create mode 100644 cake_plugin/CloudinaryCake/Model/CloudinaryCakeAppModel.php create mode 100644 cake_plugin/CloudinaryCake/Model/Datasource/empty create mode 100644 cake_plugin/CloudinaryCake/Test/Case/Controller/Component/empty create mode 100644 cake_plugin/CloudinaryCake/Test/Case/Model/Behavior/empty create mode 100644 cake_plugin/CloudinaryCake/Test/Case/View/Helper/empty create mode 100644 cake_plugin/CloudinaryCake/Test/Fixture/empty create mode 100644 cake_plugin/CloudinaryCake/Vendor/empty create mode 100644 cake_plugin/CloudinaryCake/View/Helper/empty create mode 100644 cake_plugin/CloudinaryCake/webroot/empty create mode 100644 samples/PhotoAlbumCake/.gitignore create mode 100644 samples/PhotoAlbumCake/.htaccess create mode 100644 samples/PhotoAlbumCake/Config/Schema/db_acl.php create mode 100644 samples/PhotoAlbumCake/Config/Schema/db_acl.sql create mode 100644 samples/PhotoAlbumCake/Config/Schema/i18n.php create mode 100644 samples/PhotoAlbumCake/Config/Schema/i18n.sql create mode 100644 samples/PhotoAlbumCake/Config/Schema/sessions.php create mode 100644 samples/PhotoAlbumCake/Config/Schema/sessions.sql create mode 100644 samples/PhotoAlbumCake/Config/acl.ini.php create mode 100644 samples/PhotoAlbumCake/Config/acl.php create mode 100644 samples/PhotoAlbumCake/Config/bootstrap.php create mode 100644 samples/PhotoAlbumCake/Config/core.php create mode 100644 samples/PhotoAlbumCake/Config/database.php.default create mode 100644 samples/PhotoAlbumCake/Config/email.php.default create mode 100644 samples/PhotoAlbumCake/Config/routes.php create mode 100644 samples/PhotoAlbumCake/Console/Command/AppShell.php create mode 100644 samples/PhotoAlbumCake/Console/cake create mode 100644 samples/PhotoAlbumCake/Console/cake.bat create mode 100644 samples/PhotoAlbumCake/Console/cake.php create mode 100644 samples/PhotoAlbumCake/Controller/AppController.php create mode 100644 samples/PhotoAlbumCake/Controller/PagesController.php create mode 100644 samples/PhotoAlbumCake/Model/AppModel.php create mode 100644 samples/PhotoAlbumCake/View/Emails/html/default.ctp create mode 100644 samples/PhotoAlbumCake/View/Emails/text/default.ctp create mode 100644 samples/PhotoAlbumCake/View/Errors/error400.ctp create mode 100644 samples/PhotoAlbumCake/View/Errors/error500.ctp create mode 100644 samples/PhotoAlbumCake/View/Helper/AppHelper.php create mode 100644 samples/PhotoAlbumCake/View/Layouts/Emails/html/default.ctp create mode 100644 samples/PhotoAlbumCake/View/Layouts/Emails/text/default.ctp create mode 100644 samples/PhotoAlbumCake/View/Layouts/ajax.ctp create mode 100644 samples/PhotoAlbumCake/View/Layouts/default.ctp create mode 100644 samples/PhotoAlbumCake/View/Layouts/error.ctp create mode 100644 samples/PhotoAlbumCake/View/Layouts/flash.ctp create mode 100644 samples/PhotoAlbumCake/View/Layouts/js/default.ctp create mode 100644 samples/PhotoAlbumCake/View/Layouts/rss/default.ctp create mode 100644 samples/PhotoAlbumCake/View/Layouts/xml/default.ctp create mode 100644 samples/PhotoAlbumCake/View/Pages/home.ctp create mode 100644 samples/PhotoAlbumCake/index.php create mode 100644 samples/PhotoAlbumCake/webroot/.htaccess create mode 100644 samples/PhotoAlbumCake/webroot/css/cake.generic.css create mode 100644 samples/PhotoAlbumCake/webroot/favicon.ico create mode 100644 samples/PhotoAlbumCake/webroot/img/cake.icon.png create mode 100644 samples/PhotoAlbumCake/webroot/img/cake.power.gif create mode 100644 samples/PhotoAlbumCake/webroot/img/test-error-icon.png create mode 100644 samples/PhotoAlbumCake/webroot/img/test-fail-icon.png create mode 100644 samples/PhotoAlbumCake/webroot/img/test-pass-icon.png create mode 100644 samples/PhotoAlbumCake/webroot/img/test-skip-icon.png create mode 100644 samples/PhotoAlbumCake/webroot/index.php diff --git a/cake_plugin/CloudinaryCake/Config/Schema/empty b/cake_plugin/CloudinaryCake/Config/Schema/empty new file mode 100644 index 00000000..e69de29b diff --git a/cake_plugin/CloudinaryCake/Console/Command/Task/empty b/cake_plugin/CloudinaryCake/Console/Command/Task/empty new file mode 100644 index 00000000..e69de29b diff --git a/cake_plugin/CloudinaryCake/Controller/CloudinaryCakeAppController.php b/cake_plugin/CloudinaryCake/Controller/CloudinaryCakeAppController.php new file mode 100644 index 00000000..903eba94 --- /dev/null +++ b/cake_plugin/CloudinaryCake/Controller/CloudinaryCakeAppController.php @@ -0,0 +1,7 @@ + + RewriteEngine on + RewriteRule ^$ webroot/ [L] + RewriteRule (.*) webroot/$1 [L] + \ No newline at end of file diff --git a/samples/PhotoAlbumCake/Config/Schema/db_acl.php b/samples/PhotoAlbumCake/Config/Schema/db_acl.php new file mode 100644 index 00000000..dcdac464 --- /dev/null +++ b/samples/PhotoAlbumCake/Config/Schema/db_acl.php @@ -0,0 +1,62 @@ + array('type' => 'integer', 'null' => false, 'default' => null, 'length' => 10, 'key' => 'primary'), + 'parent_id' => array('type' => 'integer', 'null' => true, 'default' => null, 'length' => 10), + 'model' => array('type' => 'string', 'null' => true), + 'foreign_key' => array('type' => 'integer', 'null' => true, 'default' => null, 'length' => 10), + 'alias' => array('type' => 'string', 'null' => true), + 'lft' => array('type' => 'integer', 'null' => true, 'default' => null, 'length' => 10), + 'rght' => array('type' => 'integer', 'null' => true, 'default' => null, 'length' => 10), + 'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1)) + ); + + public $aros = array( + 'id' => array('type' => 'integer', 'null' => false, 'default' => null, 'length' => 10, 'key' => 'primary'), + 'parent_id' => array('type' => 'integer', 'null' => true, 'default' => null, 'length' => 10), + 'model' => array('type' => 'string', 'null' => true), + 'foreign_key' => array('type' => 'integer', 'null' => true, 'default' => null, 'length' => 10), + 'alias' => array('type' => 'string', 'null' => true), + 'lft' => array('type' => 'integer', 'null' => true, 'default' => null, 'length' => 10), + 'rght' => array('type' => 'integer', 'null' => true, 'default' => null, 'length' => 10), + 'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1)) + ); + + public $aros_acos = array( + 'id' => array('type' => 'integer', 'null' => false, 'default' => null, 'length' => 10, 'key' => 'primary'), + 'aro_id' => array('type' => 'integer', 'null' => false, 'length' => 10, 'key' => 'index'), + 'aco_id' => array('type' => 'integer', 'null' => false, 'length' => 10), + '_create' => array('type' => 'string', 'null' => false, 'default' => '0', 'length' => 2), + '_read' => array('type' => 'string', 'null' => false, 'default' => '0', 'length' => 2), + '_update' => array('type' => 'string', 'null' => false, 'default' => '0', 'length' => 2), + '_delete' => array('type' => 'string', 'null' => false, 'default' => '0', 'length' => 2), + 'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1), 'ARO_ACO_KEY' => array('column' => array('aro_id', 'aco_id'), 'unique' => 1)) + ); + +} diff --git a/samples/PhotoAlbumCake/Config/Schema/db_acl.sql b/samples/PhotoAlbumCake/Config/Schema/db_acl.sql new file mode 100644 index 00000000..274780e2 --- /dev/null +++ b/samples/PhotoAlbumCake/Config/Schema/db_acl.sql @@ -0,0 +1,41 @@ +# $Id$ +# +# Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) +# +# Licensed under The MIT License +# For full copyright and license information, please see the LICENSE.txt +# Redistributions of files must retain the above copyright notice. +# MIT License (http://www.opensource.org/licenses/mit-license.php) + +CREATE TABLE acos ( + id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, + parent_id INTEGER(10) DEFAULT NULL, + model VARCHAR(255) DEFAULT '', + foreign_key INTEGER(10) UNSIGNED DEFAULT NULL, + alias VARCHAR(255) DEFAULT '', + lft INTEGER(10) DEFAULT NULL, + rght INTEGER(10) DEFAULT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE aros_acos ( + id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, + aro_id INTEGER(10) UNSIGNED NOT NULL, + aco_id INTEGER(10) UNSIGNED NOT NULL, + _create CHAR(2) NOT NULL DEFAULT 0, + _read CHAR(2) NOT NULL DEFAULT 0, + _update CHAR(2) NOT NULL DEFAULT 0, + _delete CHAR(2) NOT NULL DEFAULT 0, + PRIMARY KEY(id) +); + +CREATE TABLE aros ( + id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, + parent_id INTEGER(10) DEFAULT NULL, + model VARCHAR(255) DEFAULT '', + foreign_key INTEGER(10) UNSIGNED DEFAULT NULL, + alias VARCHAR(255) DEFAULT '', + lft INTEGER(10) DEFAULT NULL, + rght INTEGER(10) DEFAULT NULL, + PRIMARY KEY (id) +); \ No newline at end of file diff --git a/samples/PhotoAlbumCake/Config/Schema/i18n.php b/samples/PhotoAlbumCake/Config/Schema/i18n.php new file mode 100644 index 00000000..372be84d --- /dev/null +++ b/samples/PhotoAlbumCake/Config/Schema/i18n.php @@ -0,0 +1,43 @@ + array('type' => 'integer', 'null' => false, 'default' => null, 'length' => 10, 'key' => 'primary'), + 'locale' => array('type' => 'string', 'null' => false, 'length' => 6, 'key' => 'index'), + 'model' => array('type' => 'string', 'null' => false, 'key' => 'index'), + 'foreign_key' => array('type' => 'integer', 'null' => false, 'length' => 10, 'key' => 'index'), + 'field' => array('type' => 'string', 'null' => false, 'key' => 'index'), + 'content' => array('type' => 'text', 'null' => true, 'default' => null), + 'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1), 'locale' => array('column' => 'locale', 'unique' => 0), 'model' => array('column' => 'model', 'unique' => 0), 'row_id' => array('column' => 'foreign_key', 'unique' => 0), 'field' => array('column' => 'field', 'unique' => 0)) + ); + +} diff --git a/samples/PhotoAlbumCake/Config/Schema/i18n.sql b/samples/PhotoAlbumCake/Config/Schema/i18n.sql new file mode 100644 index 00000000..66a42bd1 --- /dev/null +++ b/samples/PhotoAlbumCake/Config/Schema/i18n.sql @@ -0,0 +1,27 @@ +# $Id$ +# +# Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) +# +# Licensed under The MIT License +# For full copyright and license information, please see the LICENSE.txt +# Redistributions of files must retain the above copyright notice. +# MIT License (http://www.opensource.org/licenses/mit-license.php) + +CREATE TABLE i18n ( + id int(10) NOT NULL auto_increment, + locale varchar(6) NOT NULL, + model varchar(255) NOT NULL, + foreign_key int(10) NOT NULL, + field varchar(255) NOT NULL, + content mediumtext, + PRIMARY KEY (id), +# UNIQUE INDEX I18N_LOCALE_FIELD(locale, model, foreign_key, field), +# INDEX I18N_LOCALE_ROW(locale, model, foreign_key), +# INDEX I18N_LOCALE_MODEL(locale, model), +# INDEX I18N_FIELD(model, foreign_key, field), +# INDEX I18N_ROW(model, foreign_key), + INDEX locale (locale), + INDEX model (model), + INDEX row_id (foreign_key), + INDEX field (field) +); \ No newline at end of file diff --git a/samples/PhotoAlbumCake/Config/Schema/sessions.php b/samples/PhotoAlbumCake/Config/Schema/sessions.php new file mode 100644 index 00000000..d62a9603 --- /dev/null +++ b/samples/PhotoAlbumCake/Config/Schema/sessions.php @@ -0,0 +1,38 @@ + array('type' => 'string', 'null' => false, 'key' => 'primary'), + 'data' => array('type' => 'text', 'null' => true, 'default' => null), + 'expires' => array('type' => 'integer', 'null' => true, 'default' => null), + 'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1)) + ); + +} diff --git a/samples/PhotoAlbumCake/Config/Schema/sessions.sql b/samples/PhotoAlbumCake/Config/Schema/sessions.sql new file mode 100644 index 00000000..76845bdc --- /dev/null +++ b/samples/PhotoAlbumCake/Config/Schema/sessions.sql @@ -0,0 +1,17 @@ +# $Id$ +# +# Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) +# 1785 E. Sahara Avenue, Suite 490-204 +# Las Vegas, Nevada 89104 +# +# Licensed under The MIT License +# For full copyright and license information, please see the LICENSE.txt +# Redistributions of files must retain the above copyright notice. +# MIT License (http://www.opensource.org/licenses/mit-license.php) + +CREATE TABLE cake_sessions ( + id varchar(255) NOT NULL default '', + data text, + expires int(11) default NULL, + PRIMARY KEY (id) +); \ No newline at end of file diff --git a/samples/PhotoAlbumCake/Config/acl.ini.php b/samples/PhotoAlbumCake/Config/acl.ini.php new file mode 100644 index 00000000..8c094167 --- /dev/null +++ b/samples/PhotoAlbumCake/Config/acl.ini.php @@ -0,0 +1,60 @@ +; +;/** +; * ACL Configuration +; * +; * +; * PHP 5 +; * +; * @link http://cakephp.org CakePHP(tm) Project +; * @package app.Config +; * @since CakePHP(tm) v 0.10.0.1076 +; */ + +; acl.ini.php - CakePHP ACL Configuration +; --------------------------------------------------------------------- +; Use this file to specify user permissions. +; aco = access control object (something in your application) +; aro = access request object (something requesting access) +; +; User records are added as follows: +; +; [uid] +; groups = group1, group2, group3 +; allow = aco1, aco2, aco3 +; deny = aco4, aco5, aco6 +; +; Group records are added in a similar manner: +; +; [gid] +; allow = aco1, aco2, aco3 +; deny = aco4, aco5, aco6 +; +; The allow, deny, and groups sections are all optional. +; NOTE: groups names *cannot* ever be the same as usernames! +; +; ACL permissions are checked in the following order: +; 1. Check for user denies (and DENY if specified) +; 2. Check for user allows (and ALLOW if specified) +; 3. Gather user's groups +; 4. Check group denies (and DENY if specified) +; 5. Check group allows (and ALLOW if specified) +; 6. If no aro, aco, or group information is found, DENY +; +; --------------------------------------------------------------------- + +;------------------------------------- +;Users +;------------------------------------- + +[username-goes-here] +groups = group1, group2 +deny = aco1, aco2 +allow = aco3, aco4 + +;------------------------------------- +;Groups +;------------------------------------- + +[groupname-goes-here] +deny = aco5, aco6 +allow = aco7, aco8 diff --git a/samples/PhotoAlbumCake/Config/acl.php b/samples/PhotoAlbumCake/Config/acl.php new file mode 100644 index 00000000..2829e69c --- /dev/null +++ b/samples/PhotoAlbumCake/Config/acl.php @@ -0,0 +1,126 @@ +Auth->authorize = array('Actions' => array('actionPath' => 'controllers/'),...) + * + * Now, when a user (i.e. jeff) authenticates successfully and requests a controller action (i.e. /invoices/delete) + * that is not allowed by default (e.g. via $this->Auth->allow('edit') in the Invoices controller) then AuthComponent + * will ask the configured ACL interface if access is granted. Under the assumptions 1. and 2. this will be + * done via a call to Acl->check() with + * + * array('User' => array('username' => 'jeff', 'group_id' => 4, ...)) + * + * as ARO and + * + * '/controllers/invoices/delete' + * + * as ACO. + * + * If the configured map looks like + * + * $config['map'] = array( + * 'User' => 'User/username', + * 'Role' => 'User/group_id', + * ); + * + * then PhpAcl will lookup if we defined a role like User/jeff. If that role is not found, PhpAcl will try to + * find a definition for Role/4. If the definition isn't found then a default role (Role/default) will be used to + * check rules for the given ACO. The search can be expanded by defining aliases in the alias configuration. + * E.g. if you want to use a more readable name than Role/4 in your definitions you can define an alias like + * + * $config['alias'] = array( + * 'Role/4' => 'Role/editor', + * ); + * + * In the roles configuration you can define roles on the lhs and inherited roles on the rhs: + * + * $config['roles'] = array( + * 'Role/admin' => null, + * 'Role/accountant' => null, + * 'Role/editor' => null, + * 'Role/manager' => 'Role/editor, Role/accountant', + * 'User/jeff' => 'Role/manager', + * ); + * + * In this example manager inherits all rules from editor and accountant. Role/admin doesn't inherit from any role. + * Lets define some rules: + * + * $config['rules'] = array( + * 'allow' => array( + * '*' => 'Role/admin', + * 'controllers/users/(dashboard|profile)' => 'Role/default', + * 'controllers/invoices/*' => 'Role/accountant', + * 'controllers/articles/*' => 'Role/editor', + * 'controllers/users/*' => 'Role/manager', + * 'controllers/invoices/delete' => 'Role/manager', + * ), + * 'deny' => array( + * 'controllers/invoices/delete' => 'Role/accountant, User/jeff', + * 'controllers/articles/(delete|publish)' => 'Role/editor', + * ), + * ); + * + * Ok, so as jeff inherits from Role/manager he's matched every rule that references User/jeff, Role/manager, + * Role/editor, Role/accountant and Role/default. However, for jeff, rules for User/jeff are more specific than + * rules for Role/manager, rules for Role/manager are more specific than rules for Role/editor and so on. + * This is important when allow and deny rules match for a role. E.g. Role/accountant is allowed + * controllers/invoices/* but at the same time controllers/invoices/delete is denied. But there is a more + * specific rule defined for Role/manager which is allowed controllers/invoices/delete. However, the most specific + * rule denies access to the delete action explicitly for User/jeff, so he'll be denied access to the resource. + * + * If we would remove the role definition for User/jeff, then jeff would be granted access as he would be resolved + * to Role/manager and Role/manager has an allow rule. + */ + +/** + * The role map defines how to resolve the user record from your application + * to the roles you defined in the roles configuration. + */ +$config['map'] = array( + 'User' => 'User/username', + 'Role' => 'User/group_id', +); + +/** + * define aliases to map your model information to + * the roles defined in your role configuration. + */ +$config['alias'] = array( + 'Role/4' => 'Role/editor', +); + +/** + * role configuration + */ +$config['roles'] = array( + 'Role/admin' => null, +); + +/** + * rule configuration + */ +$config['rules'] = array( + 'allow' => array( + '*' => 'Role/admin', + ), + 'deny' => array(), +); diff --git a/samples/PhotoAlbumCake/Config/bootstrap.php b/samples/PhotoAlbumCake/Config/bootstrap.php new file mode 100644 index 00000000..cc65b6d2 --- /dev/null +++ b/samples/PhotoAlbumCake/Config/bootstrap.php @@ -0,0 +1,102 @@ + 'File')); + +/** + * The settings below can be used to set additional paths to models, views and controllers. + * + * App::build(array( + * 'Model' => array('/path/to/models/', '/next/path/to/models/'), + * 'Model/Behavior' => array('/path/to/behaviors/', '/next/path/to/behaviors/'), + * 'Model/Datasource' => array('/path/to/datasources/', '/next/path/to/datasources/'), + * 'Model/Datasource/Database' => array('/path/to/databases/', '/next/path/to/database/'), + * 'Model/Datasource/Session' => array('/path/to/sessions/', '/next/path/to/sessions/'), + * 'Controller' => array('/path/to/controllers/', '/next/path/to/controllers/'), + * 'Controller/Component' => array('/path/to/components/', '/next/path/to/components/'), + * 'Controller/Component/Auth' => array('/path/to/auths/', '/next/path/to/auths/'), + * 'Controller/Component/Acl' => array('/path/to/acls/', '/next/path/to/acls/'), + * 'View' => array('/path/to/views/', '/next/path/to/views/'), + * 'View/Helper' => array('/path/to/helpers/', '/next/path/to/helpers/'), + * 'Console' => array('/path/to/consoles/', '/next/path/to/consoles/'), + * 'Console/Command' => array('/path/to/commands/', '/next/path/to/commands/'), + * 'Console/Command/Task' => array('/path/to/tasks/', '/next/path/to/tasks/'), + * 'Lib' => array('/path/to/libs/', '/next/path/to/libs/'), + * 'Locale' => array('/path/to/locales/', '/next/path/to/locales/'), + * 'Vendor' => array('/path/to/vendors/', '/next/path/to/vendors/'), + * 'Plugin' => array('/path/to/plugins/', '/next/path/to/plugins/'), + * )); + * + */ + +/** + * Custom Inflector rules, can be set to correctly pluralize or singularize table, model, controller names or whatever other + * string is passed to the inflection functions + * + * Inflector::rules('singular', array('rules' => array(), 'irregular' => array(), 'uninflected' => array())); + * Inflector::rules('plural', array('rules' => array(), 'irregular' => array(), 'uninflected' => array())); + * + */ + +/** + * Plugins need to be loaded manually, you can either load them one by one or all of them in a single call + * Uncomment one of the lines below, as you need. make sure you read the documentation on CakePlugin to use more + * advanced ways of loading plugins + * + * CakePlugin::loadAll(); // Loads all plugins at once + * CakePlugin::load('DebugKit'); //Loads a single plugin named DebugKit + * + */ + +/** + * You can attach event listeners to the request lifecycle as Dispatcher Filter . By Default CakePHP bundles two filters: + * + * - AssetDispatcher filter will serve your asset files (css, images, js, etc) from your themes and plugins + * - CacheDispatcher filter will read the Cache.check configure variable and try to serve cached content generated from controllers + * + * Feel free to remove or add filters as you see fit for your application. A few examples: + * + * Configure::write('Dispatcher.filters', array( + * 'MyCacheFilter', // will use MyCacheFilter class from the Routing/Filter package in your app. + * 'MyPlugin.MyFilter', // will use MyFilter class from the Routing/Filter package in MyPlugin plugin. + * array('callable' => $aFunction, 'on' => 'before', 'priority' => 9), // A valid PHP callback type to be called on beforeDispatch + * array('callable' => $anotherMethod, 'on' => 'after'), // A valid PHP callback type to be called on afterDispatch + * + * )); + */ +Configure::write('Dispatcher.filters', array( + 'AssetDispatcher', + 'CacheDispatcher' +)); + +/** + * Configures default file logging options + */ +App::uses('CakeLog', 'Log'); +CakeLog::config('debug', array( + 'engine' => 'File', + 'types' => array('notice', 'info', 'debug'), + 'file' => 'debug', +)); +CakeLog::config('error', array( + 'engine' => 'File', + 'types' => array('warning', 'error', 'critical', 'alert', 'emergency'), + 'file' => 'error', +)); + +CakePlugin::load('CloudinaryCake', array('bootstrap' => false, 'routes' => false)); diff --git a/samples/PhotoAlbumCake/Config/core.php b/samples/PhotoAlbumCake/Config/core.php new file mode 100644 index 00000000..b527ed01 --- /dev/null +++ b/samples/PhotoAlbumCake/Config/core.php @@ -0,0 +1,368 @@ + 0 + * and log errors with CakeLog when debug = 0. + * + * Options: + * + * - `handler` - callback - The callback to handle errors. You can set this to any callable type, + * including anonymous functions. + * Make sure you add App::uses('MyHandler', 'Error'); when using a custom handler class + * - `level` - integer - The level of errors you are interested in capturing. + * - `trace` - boolean - Include stack traces for errors in log files. + * + * @see ErrorHandler for more information on error handling and configuration. + */ + Configure::write('Error', array( + 'handler' => 'ErrorHandler::handleError', + 'level' => E_ALL & ~E_DEPRECATED, + 'trace' => true + )); + +/** + * Configure the Exception handler used for uncaught exceptions. By default, + * ErrorHandler::handleException() is used. It will display a HTML page for the exception, and + * while debug > 0, framework errors like Missing Controller will be displayed. When debug = 0, + * framework errors will be coerced into generic HTTP errors. + * + * Options: + * + * - `handler` - callback - The callback to handle exceptions. You can set this to any callback type, + * including anonymous functions. + * Make sure you add App::uses('MyHandler', 'Error'); when using a custom handler class + * - `renderer` - string - The class responsible for rendering uncaught exceptions. If you choose a custom class you + * should place the file for that class in app/Lib/Error. This class needs to implement a render method. + * - `log` - boolean - Should Exceptions be logged? + * - `skipLog` - array - list of exceptions to skip for logging. Exceptions that + * extend one of the listed exceptions will also be skipped for logging. + * Example: `'skipLog' => array('NotFoundException', 'UnauthorizedException')` + * + * @see ErrorHandler for more information on exception handling and configuration. + */ + Configure::write('Exception', array( + 'handler' => 'ErrorHandler::handleException', + 'renderer' => 'ExceptionRenderer', + 'log' => true + )); + +/** + * Application wide charset encoding + */ + Configure::write('App.encoding', 'UTF-8'); + +/** + * To configure CakePHP *not* to use mod_rewrite and to + * use CakePHP pretty URLs, remove these .htaccess + * files: + * + * /.htaccess + * /app/.htaccess + * /app/webroot/.htaccess + * + * And uncomment the App.baseUrl below. But keep in mind + * that plugin assets such as images, CSS and JavaScript files + * will not work without URL rewriting! + * To work around this issue you should either symlink or copy + * the plugin assets into you app's webroot directory. This is + * recommended even when you are using mod_rewrite. Handling static + * assets through the Dispatcher is incredibly inefficient and + * included primarily as a development convenience - and + * thus not recommended for production applications. + */ + //Configure::write('App.baseUrl', env('SCRIPT_NAME')); + +/** + * To configure CakePHP to use a particular domain URL + * for any URL generation inside the application, set the following + * configuration variable to the http(s) address to your domain. This + * will override the automatic detection of full base URL and can be + * useful when generating links from the CLI (e.g. sending emails) + */ + //Configure::write('App.fullBaseUrl', 'http://example.com'); + +/** + * Web path to the public images directory under webroot. + * If not set defaults to 'img/' + */ + //Configure::write('App.imageBaseUrl', 'img/'); + +/** + * Web path to the CSS files directory under webroot. + * If not set defaults to 'css/' + */ + //Configure::write('App.cssBaseUrl', 'css/'); + +/** + * Web path to the js files directory under webroot. + * If not set defaults to 'js/' + */ + //Configure::write('App.jsBaseUrl', 'js/'); + +/** + * Uncomment the define below to use CakePHP prefix routes. + * + * The value of the define determines the names of the routes + * and their associated controller actions: + * + * Set to an array of prefixes you want to use in your application. Use for + * admin or other prefixed routes. + * + * Routing.prefixes = array('admin', 'manager'); + * + * Enables: + * `admin_index()` and `/admin/controller/index` + * `manager_index()` and `/manager/controller/index` + * + */ + //Configure::write('Routing.prefixes', array('admin')); + +/** + * Turn off all caching application-wide. + * + */ + //Configure::write('Cache.disable', true); + +/** + * Enable cache checking. + * + * If set to true, for view caching you must still use the controller + * public $cacheAction inside your controllers to define caching settings. + * You can either set it controller-wide by setting public $cacheAction = true, + * or in each action using $this->cacheAction = true. + * + */ + //Configure::write('Cache.check', true); + +/** + * Enable cache view prefixes. + * + * If set it will be prepended to the cache name for view file caching. This is + * helpful if you deploy the same application via multiple subdomains and languages, + * for instance. Each version can then have its own view cache namespace. + * Note: The final cache file name will then be `prefix_cachefilename`. + */ + //Configure::write('Cache.viewPrefix', 'prefix'); + +/** + * Session configuration. + * + * Contains an array of settings to use for session configuration. The defaults key is + * used to define a default preset to use for sessions, any settings declared here will override + * the settings of the default config. + * + * ## Options + * + * - `Session.cookie` - The name of the cookie to use. Defaults to 'CAKEPHP' + * - `Session.timeout` - The number of minutes you want sessions to live for. This timeout is handled by CakePHP + * - `Session.cookieTimeout` - The number of minutes you want session cookies to live for. + * - `Session.checkAgent` - Do you want the user agent to be checked when starting sessions? You might want to set the + * value to false, when dealing with older versions of IE, Chrome Frame or certain web-browsing devices and AJAX + * - `Session.defaults` - The default configuration set to use as a basis for your session. + * There are four builtins: php, cake, cache, database. + * - `Session.handler` - Can be used to enable a custom session handler. Expects an array of callables, + * that can be used with `session_save_handler`. Using this option will automatically add `session.save_handler` + * to the ini array. + * - `Session.autoRegenerate` - Enabling this setting, turns on automatic renewal of sessions, and + * sessionids that change frequently. See CakeSession::$requestCountdown. + * - `Session.ini` - An associative array of additional ini values to set. + * + * The built in defaults are: + * + * - 'php' - Uses settings defined in your php.ini. + * - 'cake' - Saves session files in CakePHP's /tmp directory. + * - 'database' - Uses CakePHP's database sessions. + * - 'cache' - Use the Cache class to save sessions. + * + * To define a custom session handler, save it at /app/Model/Datasource/Session/.php. + * Make sure the class implements `CakeSessionHandlerInterface` and set Session.handler to + * + * To use database sessions, run the app/Config/Schema/sessions.php schema using + * the cake shell command: cake schema create Sessions + * + */ + Configure::write('Session', array( + 'defaults' => 'php' + )); + +/** + * A random string used in security hashing methods. + */ Configure::write('Security.salt', '3d8ecf91b9472e0940533565724835090e8b4dcc'); + +/** + * A random numeric string (digits only) used to encrypt/decrypt strings. + */ Configure::write('Security.cipherSeed', '343938626362333332343965613865'); + +/** + * Apply timestamps with the last modified time to static assets (js, css, images). + * Will append a query string parameter containing the time the file was modified. This is + * useful for invalidating browser caches. + * + * Set to `true` to apply timestamps when debug > 0. Set to 'force' to always enable + * timestamping regardless of debug value. + */ + //Configure::write('Asset.timestamp', true); + +/** + * Compress CSS output by removing comments, whitespace, repeating tags, etc. + * This requires a/var/cache directory to be writable by the web server for caching. + * and /vendors/csspp/csspp.php + * + * To use, prefix the CSS link URL with '/ccss/' instead of '/css/' or use HtmlHelper::css(). + */ + //Configure::write('Asset.filter.css', 'css.php'); + +/** + * Plug in your own custom JavaScript compressor by dropping a script in your webroot to handle the + * output, and setting the config below to the name of the script. + * + * To use, prefix your JavaScript link URLs with '/cjs/' instead of '/js/' or use JsHelper::link(). + */ + //Configure::write('Asset.filter.js', 'custom_javascript_output_filter.php'); + +/** + * The class name and database used in CakePHP's + * access control lists. + */ + Configure::write('Acl.classname', 'DbAcl'); + Configure::write('Acl.database', 'default'); + +/** + * Uncomment this line and correct your server timezone to fix + * any date & time related errors. + */ + //date_default_timezone_set('UTC'); + +/** + * + * Cache Engine Configuration + * Default settings provided below + * + * File storage engine. + * + * Cache::config('default', array( + * 'engine' => 'File', //[required] + * 'duration' => 3600, //[optional] + * 'probability' => 100, //[optional] + * 'path' => CACHE, //[optional] use system tmp directory - remember to use absolute path + * 'prefix' => 'cake_', //[optional] prefix every cache file with this string + * 'lock' => false, //[optional] use file locking + * 'serialize' => true, //[optional] + * 'mask' => 0664, //[optional] + * )); + * + * APC (http://pecl.php.net/package/APC) + * + * Cache::config('default', array( + * 'engine' => 'Apc', //[required] + * 'duration' => 3600, //[optional] + * 'probability' => 100, //[optional] + * 'prefix' => Inflector::slug(APP_DIR) . '_', //[optional] prefix every cache file with this string + * )); + * + * Xcache (http://xcache.lighttpd.net/) + * + * Cache::config('default', array( + * 'engine' => 'Xcache', //[required] + * 'duration' => 3600, //[optional] + * 'probability' => 100, //[optional] + * 'prefix' => Inflector::slug(APP_DIR) . '_', //[optional] prefix every cache file with this string + * 'user' => 'user', //user from xcache.admin.user settings + * 'password' => 'password', //plaintext password (xcache.admin.pass) + * )); + * + * Memcache (http://www.danga.com/memcached/) + * + * Cache::config('default', array( + * 'engine' => 'Memcache', //[required] + * 'duration' => 3600, //[optional] + * 'probability' => 100, //[optional] + * 'prefix' => Inflector::slug(APP_DIR) . '_', //[optional] prefix every cache file with this string + * 'servers' => array( + * '127.0.0.1:11211' // localhost, default port 11211 + * ), //[optional] + * 'persistent' => true, // [optional] set this to false for non-persistent connections + * 'compress' => false, // [optional] compress data in Memcache (slower, but uses less memory) + * )); + * + * Wincache (http://php.net/wincache) + * + * Cache::config('default', array( + * 'engine' => 'Wincache', //[required] + * 'duration' => 3600, //[optional] + * 'probability' => 100, //[optional] + * 'prefix' => Inflector::slug(APP_DIR) . '_', //[optional] prefix every cache file with this string + * )); + */ + +/** + * Configure the cache handlers that CakePHP will use for internal + * metadata like class maps, and model schema. + * + * By default File is used, but for improved performance you should use APC. + * + * Note: 'default' and other application caches should be configured in app/Config/bootstrap.php. + * Please check the comments in bootstrap.php for more info on the cache engines available + * and their settings. + */ +$engine = 'File'; + +// In development mode, caches should expire quickly. +$duration = '+999 days'; +if (Configure::read('debug') > 0) { + $duration = '+10 seconds'; +} + +// Prefix each application on the same server with a different string, to avoid Memcache and APC conflicts. +$prefix = '._'; + +/** + * Configure the cache used for general framework caching. Path information, + * object listings, and translation cache files are stored with this configuration. + */ +Cache::config('_cake_core_', array( + 'engine' => $engine, + 'prefix' => $prefix . 'cake_core_', + 'path' => CACHE . 'persistent' . DS, + 'serialize' => ($engine === 'File'), + 'duration' => $duration +)); + +/** + * Configure the cache for model and datasource caches. This cache configuration + * is used to store schema descriptions, and table listings in connections. + */ +Cache::config('_cake_model_', array( + 'engine' => $engine, + 'prefix' => $prefix . 'cake_model_', + 'path' => CACHE . 'models' . DS, + 'serialize' => ($engine === 'File'), + 'duration' => $duration +)); diff --git a/samples/PhotoAlbumCake/Config/database.php.default b/samples/PhotoAlbumCake/Config/database.php.default new file mode 100644 index 00000000..a124e9dd --- /dev/null +++ b/samples/PhotoAlbumCake/Config/database.php.default @@ -0,0 +1,72 @@ + The name of a supported datasource; valid options are as follows: + * Database/Mysql - MySQL 4 & 5, + * Database/Sqlite - SQLite (PHP5 only), + * Database/Postgres - PostgreSQL 7 and higher, + * Database/Sqlserver - Microsoft SQL Server 2005 and higher + * + * You can add custom database datasources (or override existing datasources) by adding the + * appropriate file to app/Model/Datasource/Database. Datasources should be named 'MyDatasource.php', + * + * + * persistent => true / false + * Determines whether or not the database should use a persistent connection + * + * host => + * the host you connect to the database. To add a socket or port number, use 'port' => # + * + * prefix => + * Uses the given prefix for all the tables in this database. This setting can be overridden + * on a per-table basis with the Model::$tablePrefix property. + * + * schema => + * For Postgres/Sqlserver specifies which schema you would like to use the tables in. Postgres defaults to 'public'. For Sqlserver, it defaults to empty and use + * the connected user's default schema (typically 'dbo'). + * + * encoding => + * For MySQL, Postgres specifies the character encoding to use when connecting to the + * database. Uses database default not specified. + * + * unix_socket => + * For MySQL to connect via socket specify the `unix_socket` parameter instead of `host` and `port` + */ +class DATABASE_CONFIG { + + public $default = array( + 'datasource' => 'Database/Mysql', + 'persistent' => false, + 'host' => 'localhost', + 'login' => 'user', + 'password' => 'password', + 'database' => 'database_name', + 'prefix' => '', + //'encoding' => 'utf8', + ); + + public $test = array( + 'datasource' => 'Database/Mysql', + 'persistent' => false, + 'host' => 'localhost', + 'login' => 'user', + 'password' => 'password', + 'database' => 'test_database_name', + 'prefix' => '', + //'encoding' => 'utf8', + ); +} diff --git a/samples/PhotoAlbumCake/Config/email.php.default b/samples/PhotoAlbumCake/Config/email.php.default new file mode 100644 index 00000000..749ac2bd --- /dev/null +++ b/samples/PhotoAlbumCake/Config/email.php.default @@ -0,0 +1,85 @@ + The name of a supported transport; valid options are as follows: + * Mail - Send using PHP mail function + * Smtp - Send using SMTP + * Debug - Do not send the email, just return the result + * + * You can add custom transports (or override existing transports) by adding the + * appropriate file to app/Network/Email. Transports should be named 'YourTransport.php', + * where 'Your' is the name of the transport. + * + * from => + * The origin email. See CakeEmail::from() about the valid values + * + */ +class EmailConfig { + + public $default = array( + 'transport' => 'Mail', + 'from' => 'you@localhost', + //'charset' => 'utf-8', + //'headerCharset' => 'utf-8', + ); + + public $smtp = array( + 'transport' => 'Smtp', + 'from' => array('site@localhost' => 'My Site'), + 'host' => 'localhost', + 'port' => 25, + 'timeout' => 30, + 'username' => 'user', + 'password' => 'secret', + 'client' => null, + 'log' => false, + //'charset' => 'utf-8', + //'headerCharset' => 'utf-8', + ); + + public $fast = array( + 'from' => 'you@localhost', + 'sender' => null, + 'to' => null, + 'cc' => null, + 'bcc' => null, + 'replyTo' => null, + 'readReceipt' => null, + 'returnPath' => null, + 'messageId' => true, + 'subject' => null, + 'message' => null, + 'headers' => null, + 'viewRender' => null, + 'template' => false, + 'layout' => false, + 'viewVars' => null, + 'attachments' => null, + 'emailFormat' => null, + 'transport' => 'Smtp', + 'host' => 'localhost', + 'port' => 25, + 'timeout' => 30, + 'username' => 'user', + 'password' => 'secret', + 'client' => null, + 'log' => true, + //'charset' => 'utf-8', + //'headerCharset' => 'utf-8', + ); + +} diff --git a/samples/PhotoAlbumCake/Config/routes.php b/samples/PhotoAlbumCake/Config/routes.php new file mode 100644 index 00000000..d42ac984 --- /dev/null +++ b/samples/PhotoAlbumCake/Config/routes.php @@ -0,0 +1,37 @@ + 'pages', 'action' => 'display', 'home')); +/** + * ...and connect the rest of 'Pages' controller's URLs. + */ + Router::connect('/pages/*', array('controller' => 'pages', 'action' => 'display')); + +/** + * Load all plugin routes. See the CakePlugin documentation on + * how to customize the loading of plugin routes. + */ + CakePlugin::routes(); + +/** + * Load the CakePHP default routes. Only remove this if you do not want to use + * the built-in default routes. + */ + require CAKE . 'Config' . DS . 'routes.php'; diff --git a/samples/PhotoAlbumCake/Console/Command/AppShell.php b/samples/PhotoAlbumCake/Console/Command/AppShell.php new file mode 100644 index 00000000..ac6ae94c --- /dev/null +++ b/samples/PhotoAlbumCake/Console/Command/AppShell.php @@ -0,0 +1,23 @@ +redirect('/'); + } + $page = $subpage = $title_for_layout = null; + + if (!empty($path[0])) { + $page = $path[0]; + } + if (!empty($path[1])) { + $subpage = $path[1]; + } + if (!empty($path[$count - 1])) { + $title_for_layout = Inflector::humanize($path[$count - 1]); + } + $this->set(compact('page', 'subpage', 'title_for_layout')); + + try { + $this->render(implode('/', $path)); + } catch (MissingViewException $e) { + if (Configure::read('debug')) { + throw $e; + } + throw new NotFoundException(); + } + } +} diff --git a/samples/PhotoAlbumCake/Model/AppModel.php b/samples/PhotoAlbumCake/Model/AppModel.php new file mode 100644 index 00000000..e1beb384 --- /dev/null +++ b/samples/PhotoAlbumCake/Model/AppModel.php @@ -0,0 +1,26 @@ + + ' . $line . "

\n"; +endforeach; +?> \ No newline at end of file diff --git a/samples/PhotoAlbumCake/View/Emails/text/default.ctp b/samples/PhotoAlbumCake/View/Emails/text/default.ctp new file mode 100644 index 00000000..a09ddee3 --- /dev/null +++ b/samples/PhotoAlbumCake/View/Emails/text/default.ctp @@ -0,0 +1,20 @@ + + \ No newline at end of file diff --git a/samples/PhotoAlbumCake/View/Errors/error400.ctp b/samples/PhotoAlbumCake/View/Errors/error400.ctp new file mode 100644 index 00000000..34ba7bd7 --- /dev/null +++ b/samples/PhotoAlbumCake/View/Errors/error400.ctp @@ -0,0 +1,23 @@ + +

+

+ : + '{$url}'" + ); ?> +

+ 0): + echo $this->element('exception_stack_trace'); +endif; +?> diff --git a/samples/PhotoAlbumCake/View/Errors/error500.ctp b/samples/PhotoAlbumCake/View/Errors/error500.ctp new file mode 100644 index 00000000..9d0161d2 --- /dev/null +++ b/samples/PhotoAlbumCake/View/Errors/error500.ctp @@ -0,0 +1,20 @@ + +

+

+ : + +

+ 0): + echo $this->element('exception_stack_trace'); +endif; +?> diff --git a/samples/PhotoAlbumCake/View/Helper/AppHelper.php b/samples/PhotoAlbumCake/View/Helper/AppHelper.php new file mode 100644 index 00000000..7b734247 --- /dev/null +++ b/samples/PhotoAlbumCake/View/Helper/AppHelper.php @@ -0,0 +1,26 @@ + + + + + <?php echo $title_for_layout; ?> + + + fetch('content'); ?> + +

This email was sent using the CakePHP Framework

+ + \ No newline at end of file diff --git a/samples/PhotoAlbumCake/View/Layouts/Emails/text/default.ctp b/samples/PhotoAlbumCake/View/Layouts/Emails/text/default.ctp new file mode 100644 index 00000000..a84762ea --- /dev/null +++ b/samples/PhotoAlbumCake/View/Layouts/Emails/text/default.ctp @@ -0,0 +1,22 @@ + +fetch('content'); ?> + +This email was sent using the CakePHP Framework, http://cakephp.org. diff --git a/samples/PhotoAlbumCake/View/Layouts/ajax.ctp b/samples/PhotoAlbumCake/View/Layouts/ajax.ctp new file mode 100644 index 00000000..8b06e4dc --- /dev/null +++ b/samples/PhotoAlbumCake/View/Layouts/ajax.ctp @@ -0,0 +1,11 @@ + +fetch('content'); ?> diff --git a/samples/PhotoAlbumCake/View/Layouts/default.ctp b/samples/PhotoAlbumCake/View/Layouts/default.ctp new file mode 100644 index 00000000..98a67ab1 --- /dev/null +++ b/samples/PhotoAlbumCake/View/Layouts/default.ctp @@ -0,0 +1,53 @@ + + + + + Html->charset(); ?> + + <?php echo $cakeDescription ?>: + <?php echo $title_for_layout; ?> + + Html->meta('icon'); + + echo $this->Html->css('cake.generic'); + + echo $this->fetch('meta'); + echo $this->fetch('css'); + echo $this->fetch('script'); + ?> + + +
+ +
+ + Session->flash(); ?> + + fetch('content'); ?> +
+ +
+ element('sql_dump'); ?> + + diff --git a/samples/PhotoAlbumCake/View/Layouts/error.ctp b/samples/PhotoAlbumCake/View/Layouts/error.ctp new file mode 100644 index 00000000..98a67ab1 --- /dev/null +++ b/samples/PhotoAlbumCake/View/Layouts/error.ctp @@ -0,0 +1,53 @@ + + + + + Html->charset(); ?> + + <?php echo $cakeDescription ?>: + <?php echo $title_for_layout; ?> + + Html->meta('icon'); + + echo $this->Html->css('cake.generic'); + + echo $this->fetch('meta'); + echo $this->fetch('css'); + echo $this->fetch('script'); + ?> + + +
+ +
+ + Session->flash(); ?> + + fetch('content'); ?> +
+ +
+ element('sql_dump'); ?> + + diff --git a/samples/PhotoAlbumCake/View/Layouts/flash.ctp b/samples/PhotoAlbumCake/View/Layouts/flash.ctp new file mode 100644 index 00000000..72047d40 --- /dev/null +++ b/samples/PhotoAlbumCake/View/Layouts/flash.ctp @@ -0,0 +1,29 @@ + + + + +Html->charset(); ?> +<?php echo $page_title; ?> + + + + + + + +

+ + \ No newline at end of file diff --git a/samples/PhotoAlbumCake/View/Layouts/js/default.ctp b/samples/PhotoAlbumCake/View/Layouts/js/default.ctp new file mode 100644 index 00000000..7239b5da --- /dev/null +++ b/samples/PhotoAlbumCake/View/Layouts/js/default.ctp @@ -0,0 +1,2 @@ + + diff --git a/samples/PhotoAlbumCake/View/Layouts/rss/default.ctp b/samples/PhotoAlbumCake/View/Layouts/rss/default.ctp new file mode 100644 index 00000000..077de612 --- /dev/null +++ b/samples/PhotoAlbumCake/View/Layouts/rss/default.ctp @@ -0,0 +1,14 @@ +Rss->document( + $this->Rss->channel( + array(), $channel, $this->fetch('content') + ) +); +?> diff --git a/samples/PhotoAlbumCake/View/Layouts/xml/default.ctp b/samples/PhotoAlbumCake/View/Layouts/xml/default.ctp new file mode 100644 index 00000000..fbd5ee0c --- /dev/null +++ b/samples/PhotoAlbumCake/View/Layouts/xml/default.ctp @@ -0,0 +1 @@ +fetch('content'); ?> diff --git a/samples/PhotoAlbumCake/View/Pages/home.ctp b/samples/PhotoAlbumCake/View/Pages/home.ctp new file mode 100644 index 00000000..0e4ccb0f --- /dev/null +++ b/samples/PhotoAlbumCake/View/Pages/home.ctp @@ -0,0 +1,226 @@ + +

+

+ +

+ 0): + Debugger::checkSecurityKeys(); +endif; +?> + +

+ + 1) Help me configure it + 2) I don't / can't use URL rewriting +

+ +

+=')): + echo ''; + echo __d('cake_dev', 'Your version of PHP is 5.2.8 or higher.'); + echo ''; + else: + echo ''; + echo __d('cake_dev', 'Your version of PHP is too low. You need PHP 5.2.8 or higher to use CakePHP.'); + echo ''; + endif; +?> +

+

+ '; + echo __d('cake_dev', 'Your tmp directory is writable.'); + echo ''; + else: + echo ''; + echo __d('cake_dev', 'Your tmp directory is NOT writable.'); + echo ''; + endif; + ?> +

+

+ '; + echo __d('cake_dev', 'The %s is being used for core caching. To change the config edit %s', ''. $settings['engine'] . 'Engine', 'APP/Config/core.php'); + echo ''; + else: + echo ''; + echo __d('cake_dev', 'Your cache is NOT working. Please check the settings in %s', 'APP/Config/core.php'); + echo ''; + endif; + ?> +

+

+ '; + echo __d('cake_dev', 'Your database configuration file is present.'); + $filePresent = true; + echo ''; + else: + echo ''; + echo __d('cake_dev', 'Your database configuration file is NOT present.'); + echo '
'; + echo __d('cake_dev', 'Rename %s to %s', 'APP/Config/database.php.default', 'APP/Config/database.php'); + echo '
'; + endif; + ?> +

+getMessage(); + if (method_exists($connectionError, 'getAttributes')): + $attributes = $connectionError->getAttributes(); + if (isset($errorMsg['message'])): + $errorMsg .= '
' . $attributes['message']; + endif; + endif; + } +?> +

+ isConnected()): + echo ''; + echo __d('cake_dev', 'CakePHP is able to connect to the database.'); + echo ''; + else: + echo ''; + echo __d('cake_dev', 'CakePHP is NOT able to connect to the database.'); + echo '

'; + echo $errorMsg; + echo '
'; + endif; + ?> +

+ +'; + echo __d('cake_dev', 'PCRE has not been compiled with Unicode support.'); + echo '
'; + echo __d('cake_dev', 'Recompile PCRE with Unicode support by adding --enable-unicode-properties when configuring'); + echo '

'; + endif; +?> + +

+ '; + echo __d('cake_dev', 'DebugKit plugin is present'); + echo ''; + else: + echo ''; + echo __d('cake_dev', 'DebugKit is not installed. It will help you inspect and debug different aspects of your application.'); + echo '
'; + echo __d('cake_dev', 'You can install it from %s', $this->Html->link('github', 'https://github.com/cakephp/debug_kit')); + echo '
'; + endif; + ?> +

+ +

+

+ +To change its layout, edit: %s.
+You can also add some CSS styles for your pages at: %s.', + 'APP/View/Pages/home.ctp', 'APP/View/Layouts/default.ctp', 'APP/webroot/css'); +?> +

+ +

+

+ Html->link( + sprintf('%s %s', __d('cake_dev', 'New'), __d('cake_dev', 'CakePHP 2.0 Docs')), + 'http://book.cakephp.org/2.0/en/', + array('target' => '_blank', 'escape' => false) + ); + ?> +

+

+ Html->link( + __d('cake_dev', 'The 15 min Blog Tutorial'), + 'http://book.cakephp.org/2.0/en/tutorials-and-examples/blog/blog.html', + array('target' => '_blank', 'escape' => false) + ); + ?> +

+ +

+

+

    +
  • + Html->link('DebugKit', 'https://github.com/cakephp/debug_kit') ?>: + +
  • +
  • + Html->link('Localized', 'https://github.com/cakephp/localized') ?>: + +
  • +
+

+ +

+

+ +

+

+ +

+ + diff --git a/samples/PhotoAlbumCake/index.php b/samples/PhotoAlbumCake/index.php new file mode 100644 index 00000000..b598dbbe --- /dev/null +++ b/samples/PhotoAlbumCake/index.php @@ -0,0 +1,10 @@ + + RewriteEngine On + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + diff --git a/samples/PhotoAlbumCake/webroot/css/cake.generic.css b/samples/PhotoAlbumCake/webroot/css/cake.generic.css new file mode 100644 index 00000000..9bdf5307 --- /dev/null +++ b/samples/PhotoAlbumCake/webroot/css/cake.generic.css @@ -0,0 +1,742 @@ +@charset "utf-8"; +/** + * + * Generic CSS for CakePHP + * + * CakePHP(tm) : Rapid Development Framework (http://cakephp.org) + * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * + * Licensed under The MIT License + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * @link http://cakephp.org CakePHP(tm) Project + * @package app.webroot.css + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +* { + margin:0; + padding:0; +} + +/** General Style Info **/ +body { + background: #003d4c; + color: #fff; + font-family:'lucida grande',verdana,helvetica,arial,sans-serif; + font-size:90%; + margin: 0; +} +a { + color: #003d4c; + text-decoration: underline; + font-weight: bold; +} +a:hover { + color: #367889; + text-decoration:none; +} +a img { + border:none; +} +h1, h2, h3, h4 { + font-weight: normal; + margin-bottom:0.5em; +} +h1 { + background:#fff; + color: #003d4c; + font-size: 100%; +} +h2 { + background:#fff; + color: #e32; + font-family:'Gill Sans','lucida grande', helvetica, arial, sans-serif; + font-size: 190%; +} +h3 { + color: #2c6877; + font-family:'Gill Sans','lucida grande', helvetica, arial, sans-serif; + font-size: 165%; +} +h4 { + color: #993; + font-weight: normal; +} +ul, li { + margin: 0 12px; +} +p { + margin: 0 0 1em 0; +} + +/** Layout **/ +#container { + text-align: left; +} + +#header{ + padding: 10px 20px; +} +#header h1 { + line-height:20px; + background: #003d4c url('../img/cake.icon.png') no-repeat left; + color: #fff; + padding: 0px 30px; +} +#header h1 a { + color: #fff; + background: #003d4c; + font-weight: normal; + text-decoration: none; +} +#header h1 a:hover { + color: #fff; + background: #003d4c; + text-decoration: underline; +} +#content{ + background: #fff; + clear: both; + color: #333; + padding: 10px 20px 40px 20px; + overflow: auto; +} +#footer { + clear: both; + padding: 6px 10px; + text-align: right; +} +#header a, #footer a { + color: #fff; +} + +/** containers **/ +div.form, +div.index, +div.view { + float:right; + width:76%; + border-left:1px solid #666; + padding:10px 2%; +} +div.actions { + float:left; + width:16%; + padding:10px 1.5%; +} +div.actions h3 { + padding-top:0; + color:#777; +} + + +/** Tables **/ +table { + border-right:0; + clear: both; + color: #333; + margin-bottom: 10px; + width: 100%; +} +th { + border:0; + border-bottom:2px solid #555; + text-align: left; + padding:4px; +} +th a { + display: block; + padding: 2px 4px; + text-decoration: none; +} +th a.asc:after { + content: ' ⇣'; +} +th a.desc:after { + content: ' ⇡'; +} +table tr td { + padding: 6px; + text-align: left; + vertical-align: top; + border-bottom:1px solid #ddd; +} +table tr:nth-child(even) { + background: #f9f9f9; +} +td.actions { + text-align: center; + white-space: nowrap; +} +table td.actions a { + margin: 0px 6px; + padding:2px 5px; +} + +/* SQL log */ +.cake-sql-log { + background: #fff; +} +.cake-sql-log td { + padding: 4px 8px; + text-align: left; + font-family: Monaco, Consolas, "Courier New", monospaced; +} +.cake-sql-log caption { + color:#fff; +} + +/** Paging **/ +.paging { + background:#fff; + color: #ccc; + margin-top: 1em; + clear:both; +} +.paging .current, +.paging .disabled, +.paging a { + text-decoration: none; + padding: 5px 8px; + display: inline-block +} +.paging > span { + display: inline-block; + border: 1px solid #ccc; + border-left: 0; +} +.paging > span:hover { + background: #efefef; +} +.paging .prev { + border-left: 1px solid #ccc; + -moz-border-radius: 4px 0 0 4px; + -webkit-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} +.paging .next { + -moz-border-radius: 0 4px 4px 0; + -webkit-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} +.paging .disabled { + color: #ddd; +} +.paging .disabled:hover { + background: transparent; +} +.paging .current { + background: #efefef; + color: #c73e14; +} + +/** Scaffold View **/ +dl { + line-height: 2em; + margin: 0em 0em; + width: 60%; +} +dl dd:nth-child(4n+2), +dl dt:nth-child(4n+1) { + background: #f4f4f4; +} + +dt { + font-weight: bold; + padding-left: 4px; + vertical-align: top; + width: 10em; +} +dd { + margin-left: 10em; + margin-top: -2em; + vertical-align: top; +} + +/** Forms **/ +form { + clear: both; + margin-right: 20px; + padding: 0; + width: 95%; +} +fieldset { + border: none; + margin-bottom: 1em; + padding: 16px 10px; +} +fieldset legend { + color: #e32; + font-size: 160%; + font-weight: bold; +} +fieldset fieldset { + margin-top: 0; + padding: 10px 0 0; +} +fieldset fieldset legend { + font-size: 120%; + font-weight: normal; +} +fieldset fieldset div { + clear: left; + margin: 0 20px; +} +form div { + clear: both; + margin-bottom: 1em; + padding: .5em; + vertical-align: text-top; +} +form .input { + color: #444; +} +form .required { + font-weight: bold; +} +form .required label:after { + color: #e32; + content: '*'; + display:inline; +} +form div.submit { + border: 0; + clear: both; + margin-top: 10px; +} +label { + display: block; + font-size: 110%; + margin-bottom:3px; +} +input, textarea { + clear: both; + font-size: 140%; + font-family: "frutiger linotype", "lucida grande", "verdana", sans-serif; + padding: 1%; + width:98%; +} +select { + clear: both; + font-size: 120%; + vertical-align: text-bottom; +} +select[multiple=multiple] { + width: 100%; +} +option { + font-size: 120%; + padding: 0 3px; +} +input[type=checkbox] { + clear: left; + float: left; + margin: 0px 6px 7px 2px; + width: auto; +} +div.checkbox label { + display: inline; +} +input[type=radio] { + float:left; + width:auto; + margin: 6px 0; + padding: 0; + line-height: 26px; +} +.radio label { + margin: 0 0 6px 20px; + line-height: 26px; +} +input[type=submit] { + display: inline; + font-size: 110%; + width: auto; +} +form .submit input[type=submit] { + background:#62af56; + background-image: -webkit-gradient(linear, left top, left bottom, from(#76BF6B), to(#3B8230)); + background-image: -webkit-linear-gradient(top, #76BF6B, #3B8230); + background-image: -moz-linear-gradient(top, #76BF6B, #3B8230); + border-color: #2d6324; + color: #fff; + text-shadow: rgba(0, 0, 0, 0.5) 0px -1px 0px; + padding: 8px 10px; +} +form .submit input[type=submit]:hover { + background: #5BA150; +} +/* Form errors */ +form .error { + background: #FFDACC; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + border-radius: 4px; + font-weight: normal; +} +form .error-message { + -moz-border-radius: none; + -webkit-border-radius: none; + border-radius: none; + border: none; + background: none; + margin: 0; + padding-left: 4px; + padding-right: 0; +} +form .error, +form .error-message { + color: #9E2424; + -webkit-box-shadow: none; + -moz-box-shadow: none; + -ms-box-shadow: none; + -o-box-shadow: none; + box-shadow: none; + text-shadow: none; +} + +/** Notices and Errors **/ +.message { + clear: both; + color: #fff; + font-size: 140%; + font-weight: bold; + margin: 0 0 1em 0; + padding: 5px; +} + +.success, +.message, +.cake-error, +.cake-debug, +.notice, +p.error, +.error-message { + background: #ffcc00; + background-repeat: repeat-x; + background-image: -moz-linear-gradient(top, #ffcc00, #E6B800); + background-image: -ms-linear-gradient(top, #ffcc00, #E6B800); + background-image: -webkit-gradient(linear, left top, left bottom, from(#ffcc00), to(#E6B800)); + background-image: -webkit-linear-gradient(top, #ffcc00, #E6B800); + background-image: -o-linear-gradient(top, #ffcc00, #E6B800); + background-image: linear-gradient(top, #ffcc00, #E6B800); + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + border: 1px solid rgba(0, 0, 0, 0.2); + margin-bottom: 18px; + padding: 7px 14px; + color: #404040; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); +} +.success, +.message, +.cake-error, +p.error, +.error-message { + clear: both; + color: #fff; + background: #c43c35; + border: 1px solid rgba(0, 0, 0, 0.5); + background-repeat: repeat-x; + background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -webkit-gradient(linear, left top, left bottom, from(#ee5f5b), to(#c43c35)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); + background-image: linear-gradient(top, #ee5f5b, #c43c35); + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.3); +} +.success { + clear: both; + color: #fff; + border: 1px solid rgba(0, 0, 0, 0.5); + background: #3B8230; + background-repeat: repeat-x; + background-image: -webkit-gradient(linear, left top, left bottom, from(#76BF6B), to(#3B8230)); + background-image: -webkit-linear-gradient(top, #76BF6B, #3B8230); + background-image: -moz-linear-gradient(top, #76BF6B, #3B8230); + background-image: -ms-linear-gradient(top, #76BF6B, #3B8230); + background-image: -o-linear-gradient(top, #76BF6B, #3B8230); + background-image: linear-gradient(top, #76BF6B, #3B8230); + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.3); +} +p.error { + font-family: Monaco, Consolas, Courier, monospace; + font-size: 120%; + padding: 0.8em; + margin: 1em 0; +} +p.error em { + font-weight: normal; + line-height: 140%; +} +.notice { + color: #000; + display: block; + font-size: 120%; + padding: 0.8em; + margin: 1em 0; +} +.success { + color: #fff; +} + +/** Actions **/ +.actions ul { + margin: 0; + padding: 0; +} +.actions li { + margin:0 0 0.5em 0; + list-style-type: none; + white-space: nowrap; + padding: 0; +} +.actions ul li a { + font-weight: normal; + display: block; + clear: both; +} + +/* Buttons and button links */ +input[type=submit], +.actions ul li a, +.actions a { + font-weight:normal; + padding: 4px 8px; + background: #dcdcdc; + background-image: -webkit-gradient(linear, left top, left bottom, from(#fefefe), to(#dcdcdc)); + background-image: -webkit-linear-gradient(top, #fefefe, #dcdcdc); + background-image: -moz-linear-gradient(top, #fefefe, #dcdcdc); + background-image: -ms-linear-gradient(top, #fefefe, #dcdcdc); + background-image: -o-linear-gradient(top, #fefefe, #dcdcdc); + background-image: linear-gradient(top, #fefefe, #dcdcdc); + color:#333; + border:1px solid #bbb; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + text-decoration: none; + text-shadow: #fff 0px 1px 0px; + min-width: 0; + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3), 0px 1px 1px rgba(0, 0, 0, 0.2); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3), 0px 1px 1px rgba(0, 0, 0, 0.2); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3), 0px 1px 1px rgba(0, 0, 0, 0.2); + -webkit-user-select: none; + user-select: none; +} +.actions ul li a:hover, +.actions a:hover { + background: #ededed; + border-color: #acacac; + text-decoration: none; +} +input[type=submit]:active, +.actions ul li a:active, +.actions a:active { + background: #eee; + background-image: -webkit-gradient(linear, left top, left bottom, from(#dfdfdf), to(#eee)); + background-image: -webkit-linear-gradient(top, #dfdfdf, #eee); + background-image: -moz-linear-gradient(top, #dfdfdf, #eee); + background-image: -ms-linear-gradient(top, #dfdfdf, #eee); + background-image: -o-linear-gradient(top, #dfdfdf, #eee); + background-image: linear-gradient(top, #dfdfdf, #eee); + text-shadow: #eee 0px 1px 0px; + -moz-box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.3); + -webkit-box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.3); + box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.3); + border-color: #aaa; + text-decoration: none; +} + +/** Related **/ +.related { + clear: both; + display: block; +} + +/** Debugging **/ +pre { + color: #000; + background: #f0f0f0; + padding: 15px; + -moz-box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); + -webkit-box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); + box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); +} +.cake-debug-output { + padding: 0; + position: relative; +} +.cake-debug-output > span { + position: absolute; + top: 5px; + right: 5px; + background: rgba(255, 255, 255, 0.3); + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + border-radius: 4px; + padding: 5px 6px; + color: #000; + display: block; + float: left; + -moz-box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.25), 0 1px 0 rgba(255, 255, 255, 0.5); + -webkit-box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.25), 0 1px 0 rgba(255, 255, 255, 0.5); + box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.25), 0 1px 0 rgba(255, 255, 255, 0.5); + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.8); +} +.cake-debug, +.cake-error { + font-size: 16px; + line-height: 20px; + clear: both; +} +.cake-error > a { + text-shadow: none; +} +.cake-error { + white-space: normal; +} +.cake-stack-trace { + background: rgba(255, 255, 255, 0.7); + color: #333; + margin: 10px 0 5px 0; + padding: 10px 10px 0 10px; + font-size: 120%; + line-height: 140%; + overflow: auto; + position: relative; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + border-radius: 4px; +} +.cake-stack-trace a { + text-shadow: none; + background: rgba(255, 255, 255, 0.7); + padding: 5px; + -moz-border-radius: 10px; + -webkit-border-radius: 10px; + border-radius: 10px; + margin: 0px 4px 10px 2px; + font-family: sans-serif; + font-size: 14px; + line-height: 14px; + display: inline-block; + text-decoration: none; + -moz-box-shadow: inset 0px 1px 0 rgba(0, 0, 0, 0.3); + -webkit-box-shadow: inset 0px 1px 0 rgba(0, 0, 0, 0.3); + box-shadow: inset 0px 1px 0 rgba(0, 0, 0, 0.3); +} +.cake-code-dump pre { + position: relative; + overflow: auto; +} +.cake-context { + margin-bottom: 10px; +} +.cake-stack-trace pre { + color: #000; + background-color: #F0F0F0; + margin: 0px 0 10px 0; + padding: 1em; + overflow: auto; + text-shadow: none; +} +.cake-stack-trace li { + padding: 10px 5px 0px; + margin: 0 0 4px 0; + font-family: monospace; + border: 1px solid #bbb; + -moz-border-radius: 4px; + -wekbkit-border-radius: 4px; + border-radius: 4px; + background: #dcdcdc; + background-image: -webkit-gradient(linear, left top, left bottom, from(#fefefe), to(#dcdcdc)); + background-image: -webkit-linear-gradient(top, #fefefe, #dcdcdc); + background-image: -moz-linear-gradient(top, #fefefe, #dcdcdc); + background-image: -ms-linear-gradient(top, #fefefe, #dcdcdc); + background-image: -o-linear-gradient(top, #fefefe, #dcdcdc); + background-image: linear-gradient(top, #fefefe, #dcdcdc); +} +/* excerpt */ +.cake-code-dump pre, +.cake-code-dump pre code { + clear: both; + font-size: 12px; + line-height: 15px; + margin: 4px 2px; + padding: 4px; + overflow: auto; +} +.cake-code-dump .code-highlight { + display: block; + background-color: rgba(255, 255, 0, 0.5); +} +.code-coverage-results div.code-line { + padding-left:5px; + display:block; + margin-left:10px; +} +.code-coverage-results div.uncovered span.content { + background:#ecc; +} +.code-coverage-results div.covered span.content { + background:#cec; +} +.code-coverage-results div.ignored span.content { + color:#aaa; +} +.code-coverage-results span.line-num { + color:#666; + display:block; + float:left; + width:20px; + text-align:right; + margin-right:5px; +} +.code-coverage-results span.line-num strong { + color:#666; +} +.code-coverage-results div.start { + border:1px solid #aaa; + border-width:1px 1px 0px 1px; + margin-top:30px; + padding-top:5px; +} +.code-coverage-results div.end { + border:1px solid #aaa; + border-width:0px 1px 1px 1px; + margin-bottom:30px; + padding-bottom:5px; +} +.code-coverage-results div.realstart { + margin-top:0px; +} +.code-coverage-results p.note { + color:#bbb; + padding:5px; + margin:5px 0 10px; + font-size:10px; +} +.code-coverage-results span.result-bad { + color: #a00; +} +.code-coverage-results span.result-ok { + color: #fa0; +} +.code-coverage-results span.result-good { + color: #0a0; +} + +/** Elements **/ +#url-rewriting-warning { + display:none; +} diff --git a/samples/PhotoAlbumCake/webroot/favicon.ico b/samples/PhotoAlbumCake/webroot/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b36e81f2f35133dede48dc18f78d3e1a3353f7bc GIT binary patch literal 372 zcmV-)0gL{LP)Ce4&o}{bgKX)=r-B_#-Qs`WM)Da5Prgl zsZ%T@FChJjuHh1`0m$SsVlk8KBKw^@522Z3S}fqzqUy4nLhVp$W)@MMy#WF!pk3U7 zB65kQi)kZ#rBYK5tRifZI?>XjFU4<@lsph9Z1>blC@{qW13mof`13?K&cOa z9q)cr7> zIGsQFGn3| zCzs2iP$-yfVPOGVTU&6sT(-5fwHb2tVsLP9#{Vr9Ct?R7q(rf?v2A5#W$OI=e1YUJ zQ1YRnA&iWSQ1XYAm__>aYb6XIhMiYVD+-z8_pYi6+CsH{*^m;vOjqvbr=H&DFkeqxHQBh$Scsoy0Glw(T zsaSG*ok62V;~yXYNgP*DUw;o98^+0@vGFb{HC+As}XJ=;xg=B7N_;-mKbHH{|lXs_o+aPcs5~J?s%^P2Odb)Uz z$GvY6^!N9(C2-h?28B$qx7%_yHnt2eU%nQ0qThbl6a_+b)EirjBgQ`g1_07Fr&6R? RzIgxu002ovPDHLkV1mdlwUYn< literal 0 HcmV?d00001 diff --git a/samples/PhotoAlbumCake/webroot/img/cake.power.gif b/samples/PhotoAlbumCake/webroot/img/cake.power.gif new file mode 100644 index 0000000000000000000000000000000000000000..8f8d570a2e24d86f0ad7730ee8f2435fd49f152c GIT binary patch literal 201 zcmV;)05<&ZTq0L2I(c1A@d@rg`ENj#vn zcl`yi#iKX*jb2F7vd0WQgUq5Tw}Jp}g+ZnCeBY3dYNI+m71%bHRfx4UCkD2th(Q*@ zmd5r+MJNYn7MP)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z0006>Nklb9w)H??3Zq{to|uwe}MC z?e+bSKTh?^`{wD^wWPb{@}CHTKzw?6`$N5PyyB2n?^xO1cT3;J75nFbCC72*cXxMX zxm>nuPu5Jmal8UN_lgUheaAk$l{jyl3&KrcBcIP#g%B8HC>Dz>KUn6j_Z4&V8GCzs z-A1EvJqQBvqUM>*;2yA1EEd0ylNIZ(i;!35`Jv=;2ffT?2Hm9ejw-*-|nVOoS zP$-bg<ZSNe~2d zIvrfsrBEnvbaVtD0RtdB&%Ev&{`8k5nAgpm!>J!T7(dIo(HhhYPE122Ukjo z)|xnuiK1xS*%(6!=07%mmpr3`|W=94{ zl7!J{gb;!>P0?DTlp>B}thHEcQAz=P&I}}wOVjjGmP3*xD5d^b*-V_DpQDs|1-u(S z2jBtm%*+h=eE!N=P)c!fasm(lgNzq|0@nKdKEvVguj1iwnEipLzl$$o0oV~jSl{>U o#KeU4ecuWpEU@#dehEJZ0P;*FSb&o|IsgCw07*qoM6N<$f;Xi=v;Y7A literal 0 HcmV?d00001 diff --git a/samples/PhotoAlbumCake/webroot/img/test-fail-icon.png b/samples/PhotoAlbumCake/webroot/img/test-fail-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f9d2f147ec4ef406186c967c17aa9d592a09336e GIT binary patch literal 496 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbL!WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8r2#%6u9q1Y_A@XXWnehT!0_XL%T5M{>kJI%85mYDFg#fm z@qmG069dC728KUx-uyQ(To^b!NZ6$XaAf}9Vfq#lclKVe|Fz`$^pf#C)( z?|%b>GYkxe7#Qw~i+_GseHzG+ka#R5b(MkPp}6=pPR{@T|KFMBG8O1x^O7LHU?7(a zz#tGgw;O20JWm(LkcwMA=Z`Zr83?dk2y7BE6=YUe|*dabSZF*mIx>v0bsK zhImj64Tcwn`A_+lH49lnMl*u*gGmJ>51|J|OF=yr*B8yJW~ec3o5lk`2g|wzshC-w ziSCMq2n+=S*%-Br(g+${U@~D`FhT&sxB*oN&;*cc$WcTT9wVtcsdKUucOmbP;||0x z#q3tLa6o9^vHKPIF?*Owqq5D^MpaVDQnH801Nwb#BL&(K7w$YlkeuS0|3?^KwXL|W zZgJvS!cr?^E>%Hd`MA%ASbAg$QJRhgw-~dTt8se0&H5u6>~Hz*7HSB4*!uq4vq;NjRN`wbRrY=k ztZO7Hf9`nx3i*;j@S2CmwzWRi?KpG$@ix}{S-*Q_F&XAfUVh5qiW8;W{?l*PWU~|& zXJF+?I7gWJwdv?FLs}6xsv$ajvf*#NUOnZcOK`I#;=J!d&L`LN;INb9hb~B}{sAr8 BX72z1 literal 0 HcmV?d00001 diff --git a/samples/PhotoAlbumCake/webroot/img/test-skip-icon.png b/samples/PhotoAlbumCake/webroot/img/test-skip-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..749771c9895a1abbb843f894540a43a5dc426b2c GIT binary patch literal 1207 zcmcgr`BM@I7-iGaX-3y=+tk$2=@1heYqQ2Fr&dQR+w#8F+EyJ=3j{$FBt!*70YwQk z&qPo`NyPiUZ$&My$ooDs54`YP_Mg~q=6i3xpWeLp=A+ozJW)A+`8)ssP_ce$=^$sd z|5jN+js`b>c9fGQ%E}Gp2=_suJ&|C5xi{PktY_`-=?iuMdwPc;I>BZDfC9koxs%l! zEKAYA?)3C@cTc8#*X_a+xXL52WNcR59{1Y|%sC@xAdC&dt3FBE?v&eJ(VET#C1aPfAbX9c(2(>>t!J=b9>Y}4{Vv&J?_v2@ z{Zeit+2+s~*}>tpx2alAc$vPpzT>hM@4;GBTU{^zhonf02G`{6!jDK79JiR5fMQk5Qsz~iA0Kwj3krEQBhIR(a{tNg-WH;XtbD^nAq4@I-MRD z7Z)EN&tNd#zkknUGFdEELPA1fV&aDnACi)il9Q8DQc_Y=Q`6GY($mvFe*Bn`k-=uO zGcz+e91fSu&C1Hk&d$!s$>H&Md_F%nH#aXYPaqI{`t&J3KVK*m78DeSM54mN!lI(0 z;^N|xl9JNW(z3F$^78VEii*n0%BrfW>gwv6nwr|$+Pb>B`uh5YhK9z*#-^sG=H}*> zmX_Am*0#2`_V)IUj*iaG&aSSm?(S}}SlrXo)7#tI*Vp&?^XD&LzV!F^4-52q~$`a_L84UdB=6VyZ=u9nnig?j|*#Wx)G#h*aePfzjUYo@M;4}Fb&*iuQMNlT)P zH1r_DmZyA5)S=yZ1P7iME0&D<)aAv^j SzbA!qhX89U8%v>u2jO4XOSDb^ literal 0 HcmV?d00001 diff --git a/samples/PhotoAlbumCake/webroot/index.php b/samples/PhotoAlbumCake/webroot/index.php new file mode 100644 index 00000000..5ad4986f --- /dev/null +++ b/samples/PhotoAlbumCake/webroot/index.php @@ -0,0 +1,101 @@ +dispatch( + new CakeRequest(), + new CakeResponse() +); From dbf12581ff774ce5aa1131e1b61af4ac3587dfc2 Mon Sep 17 00:00:00 2001 From: Alon Hammerman Date: Fri, 22 Nov 2013 02:17:02 -0600 Subject: [PATCH 05/24] php framework - cloudinary_url supports cloudinary identifier as source --- src/Cloudinary.php | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Cloudinary.php b/src/Cloudinary.php index 12cf1111..85ec18ae 100644 --- a/src/Cloudinary.php +++ b/src/Cloudinary.php @@ -5,7 +5,7 @@ class Cloudinary { const OLD_AKAMAI_SHARED_CDN = "cloudinary-a.akamaihd.net"; const AKAMAI_SHARED_CDN = "res.cloudinary.com"; const SHARED_CDN = "res.cloudinary.com"; - + private static $config = NULL; public static $JS_CONFIG_PARAMS = array("api_key", "cloud_name", "private_cdn", "secure_distribution", "cdn_subdomain"); @@ -153,6 +153,7 @@ public static function generate_transformation_string(&$options=array()) { // Warning: $options are being destructively updated! public static function cloudinary_url($source, &$options=array()) { + $source = self::check_identifier_source($source, $options); $type = Cloudinary::option_consume($options, "type", "upload"); if ($type == "fetch" && !isset($options["fetch_format"])) { @@ -210,6 +211,25 @@ public static function cloudinary_url($source, &$options=array()) { $type, $transformation, $version ? "v" . $version : "", $source))); } + // [/][/][v/][.][#] + // Warning: $options are being destructively updated! + public static function check_identifier_source($source, &$options=array()) { + $IDENTIFIER_RE = "~" . + "^(?:([^/]+)/)??(?:([^/]+)/)??(?:v(\\d+)/)?" . + "(?:([^#/]+?)(?:\\.([^.#/]+))?)(?:#([^/]+))?$" . "~"; + $matches = array(); + if (strstr(':', $source) !== false || !preg_match($IDENTIFIER_RE, $source, $matches)) { + return $source; + } + $optionNames = array('resource_type', 'type', 'version', 'public_id', 'format'); + foreach ($optionNames as $index => $optionName) { + if ($matches[$index+1]) { + $options[$optionName] = $matches[$index+1]; + } + } + return Cloudinary::option_consume($options, 'public_id'); + } + // Based on http://stackoverflow.com/a/1734255/526985 private static function smart_escape($str) { $revert = array('%21'=>'!', '%2A'=>'*', '%27'=>"'", '%28'=>'(', '%29'=>')', '%3A'=>':', '%2F'=>'/'); From ca200e3a6f934db224b1f6153eee1e2964e21002 Mon Sep 17 00:00:00 2001 From: Alon Hammerman Date: Fri, 22 Nov 2013 20:55:30 -0600 Subject: [PATCH 06/24] php framework - add extended_identifier to PreloadedFile --- src/Uploader.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Uploader.php b/src/Uploader.php index cc14a106..25bd9e4d 100644 --- a/src/Uploader.php +++ b/src/Uploader.php @@ -283,7 +283,10 @@ public function identifier() { return "v" . $this->version . "/" . $this->filename; } - + public function extended_identifier() { + return $this->resource_type . "/" . $this->type . "/" . $this->identifier(); + } + public function __toString() { return $this->resource_type . "/" . $this->type . "/v" . $this->version . "/" . $this->filename . "#" . $this->signature; } From ca5aba6d186d6c1eade20ac0d02dd103e4a63a49 Mon Sep 17 00:00:00 2001 From: Alon Hammerman Date: Tue, 19 Nov 2013 22:53:07 -0600 Subject: [PATCH 07/24] cake_plugin - add CloudinaryBehavior --- .../CloudinaryCake/Lib/CloudinaryField.php | 51 +++++++ .../Model/Behavior/CloudinaryBehavior.php | 137 ++++++++++++++++++ src/Uploader.php | 2 - 3 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 cake_plugin/CloudinaryCake/Lib/CloudinaryField.php create mode 100644 cake_plugin/CloudinaryCake/Model/Behavior/CloudinaryBehavior.php diff --git a/cake_plugin/CloudinaryCake/Lib/CloudinaryField.php b/cake_plugin/CloudinaryCake/Lib/CloudinaryField.php new file mode 100644 index 00000000..d9ab8883 --- /dev/null +++ b/cake_plugin/CloudinaryCake/Lib/CloudinaryField.php @@ -0,0 +1,51 @@ +identifier = $identifier; + } + + public function __toString() { + return explode("#", $this->identifier)[0]; + } + + public function url($options = array()) { + if (!$this->identifier) { + // TODO: Error? + return; + } + return cloudinary_url($this->identifier, $options); + } + + public function upload($file, $options = array()) { + $options['return_error'] = false; + $ret = \Cloudinary\Uploader::upload($file, $options); + $preloaded = new \Cloudinary\PreloadedFile(\Cloudinary::signed_preloaded_image($ret)); + if ($this->verifyUpload && !$preloaded.is_valid()) { + throw new \Exception("Error! Couldn't verify cloudinary response!"); + } + $this->identifier = $preloaded->identifier(); + } + + public function delete() { + $options['return_error'] = false; + $ret = \Cloudinary\Uploader::destroy($this->identifier); + unset($this->identifier); + } + + public function verify() { + $preloaded = new \Cloudinary\PreloadedFile($this->identifier); + return $preloaded->is_valid(); + } +} diff --git a/cake_plugin/CloudinaryCake/Model/Behavior/CloudinaryBehavior.php b/cake_plugin/CloudinaryCake/Model/Behavior/CloudinaryBehavior.php new file mode 100644 index 00000000..3d11f58f --- /dev/null +++ b/cake_plugin/CloudinaryCake/Model/Behavior/CloudinaryBehavior.php @@ -0,0 +1,137 @@ +settings[$Model->alias])) { + $this->settings[$Model->alias] = array(/* default values */); + } + $this->settings[$Model->alias] = array_merge( + $this->settings[$Model->alias], (array)$settings); + $Model->Cloudinary = array(); + } + + public function cleanup(Model $Model) { + error_log("CloudinaryBehavior::cleanup(): "); + } + + /// Callbacks /// + public function afterFind(Model $Model, $results, $primary = false) { + error_log("CloudinaryBehavior::afterFind()"); + + $fieldNames = $this->relevantFields($Model); + if (!$fieldNames) { + return $results; + } + + foreach ($results as &$result) { + foreach ($fieldNames as $fieldName) { + $this->updateCloudinaryField($Model, $fieldName, $result); + } + } + return $results; + } + + public function beforeSave(Model $Model, $options = array()) { + foreach ($this->relevantFields($Model, $options) as $fieldName) { + $this->saveCloudinaryField($Model, $fieldName); + } + return true; + } + + public function beforeValidate(Model $Model, $options = array()) { + error_log("CloudinaryBehavior::beforeValidate()"); + foreach ($this->relevantFields($Model, $options) as $fieldName) { + $field = @$Model->data[$Model->alias][$fieldName]; + if (is_string($field) && $field) { + if (!(new CloudinaryField($field))->verify()) { + $Model->invalidate($fieldName, "Bad cloudinary signature!"); + error_log("CloudinaryBehavior::beforeValidate(): Error in field " . $fieldName . " with data: " . $field); + return false; + } + } + } + return true; + } + + /// Methods + private function createCloudinaryField(Model $Model, $fieldName, $source=NULL) { + error_log("CloudinaryBehavior::createCloudinaryField(): "); + $source = $source ? $source : $Model->data; + return new CloudinaryField(isset($source[$Model->alias][$fieldName]) ? + $source[$Model->alias][$fieldName] : ""); + } + + private function updateCloudinaryField(Model $Model, $fieldName, &$data=NULL) { + $source =& $data ? $data : $Model->data; + if (isset($source[$Model->alias][$fieldName]) && $source[$Model->alias][$fieldName] instanceof CloudinaryField) { + error_log("CloudinaryBehavior::updateCloudinaryField - not updating again field '" . $fieldName . "' of " . $Model); + return; + } + error_log("CloudinaryBehavior::updateCloudinaryField - updating field '" . $fieldName . "' of " . $Model->alias); + $source[$Model->alias][$fieldName] = $this->createCloudinaryField($Model, $fieldName, $source); + } + + private function saveCloudinaryField(Model $Model, $fieldName) { + $field = @$Model->data[$Model->alias][$fieldName]; + $ret = NULL; + if ($field instanceof CloudinaryField) { + return; + } elseif (!$field) { + $ret = new CloudinaryField(); + } elseif (is_string($field)) { + $ret = new CloudinaryField($field); + // $ret->verify(); - Validate only in beforeValidate + } elseif (is_array($field) && isset($field['tmp_name'])) { + $ret = new CloudinaryField(); + $ret->upload($field['tmp_name']); + } else { + // TODO - handle file object? + throw new \Exception("Couldn't save cloudinary field '" . $Model->alias . ":" . $fieldName . + "' - unknown input: " . gettype($field)); + } + $Model->data[$Model->alias][$fieldName] = $ret; + } + + private function relevantFields(Model $Model, $options = array()) { + $cloudinaryFields = $this->settings[$Model->alias]['fields']; + if (!(isset($options['fieldList']) && $options['fieldList'])) { + return $cloudinaryFields; + } + return array_intersect($cloudinaryFields, $options['fieldList']); + } +} + +/* +Simplest usage: + Model: + class Photo extends AppModel { + public $actsAs = array('CloudinaryCake.Cloudinary' => array('fields' => array('cloudinaryIdentifier'))); + } + + Controller: + Find: + $photo = $this->Photo->find('first', $options); // returns CloudinaryField in + + Modify: + $photo['Photo']['cloudinaryIdentifier'].upload($new_file); + + Delete: + $photo['Photo']['cloudinaryIdentifier'].delete($new_file); + + Save: + Photo->save($photo); + + Save (from form): + Photo->save($this->request->data); // should work with file upload or identifier + + * Validate identifier upon save + * +*/ diff --git a/src/Uploader.php b/src/Uploader.php index 25bd9e4d..5d470249 100644 --- a/src/Uploader.php +++ b/src/Uploader.php @@ -252,11 +252,9 @@ public function __construct($file_info) { $this->version = $matches[3]; $this->filename = $matches[4]; $this->signature = $matches[5]; - $public_id_and_format = $this->split_format($this->filename); $this->public_id = $public_id_and_format[0]; $this->format = $public_id_and_format[1]; - } else { throw new \InvalidArgumentException("Invalid preloaded file info"); } From b9ec83ebc6f0b3f161af1dab4387724bf6f8d4b8 Mon Sep 17 00:00:00 2001 From: Alon Hammerman Date: Sat, 23 Nov 2013 18:12:18 -0600 Subject: [PATCH 08/24] cake_plugin - CloudinaryBehavior change model's defaults in order for Model::create() to return a CloudinaryField in apropriate fields Note: Uses some monkey patching evil! --- .../Model/Behavior/CloudinaryBehavior.php | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/cake_plugin/CloudinaryCake/Model/Behavior/CloudinaryBehavior.php b/cake_plugin/CloudinaryCake/Model/Behavior/CloudinaryBehavior.php index 3d11f58f..6371763a 100644 --- a/cake_plugin/CloudinaryCake/Model/Behavior/CloudinaryBehavior.php +++ b/cake_plugin/CloudinaryCake/Model/Behavior/CloudinaryBehavior.php @@ -4,6 +4,11 @@ App::uses('CloudinaryField', 'CloudinaryCake.Lib'); class CloudinaryBehavior extends ModelBehavior { + public $settingsDefaults = array( + "fields" => array(), + "changeModelDefaults" => true + ); + public function __construct() { error_log("CloudinaryBehavior::__construct)"); } @@ -11,11 +16,14 @@ public function __construct() { public function setup(Model $Model, $settings = array()) { error_log("CloudinaryBehavior::setup(): "); if (!isset($this->settings[$Model->alias])) { - $this->settings[$Model->alias] = array(/* default values */); + $this->settings[$Model->alias] = $this->settingsDefaults; } $this->settings[$Model->alias] = array_merge( - $this->settings[$Model->alias], (array)$settings); - $Model->Cloudinary = array(); + $this->settings[$Model->alias], (array)$settings + ); + if ($this->settings[$Model->alias]['changeModelDefaults']) { + $this->changeModelDefaults($Model); + } } public function cleanup(Model $Model) { @@ -107,6 +115,31 @@ private function relevantFields(Model $Model, $options = array()) { } return array_intersect($cloudinaryFields, $options['fieldList']); } + + private static function modifyPropertyUsingForce($instance, $property, $newValue) { + if (version_compare(PHP_VERSION, '5.3.0') >= 0) { + $myClassReflection = new ReflectionClass(get_class($instance)); + $secret = $myClassReflection->getProperty($property); + $secret->setAccessible(true); + $secret->setValue($instance, $newValue); + } + } + + private function changeModelDefaults(Model $Model) { + $schema = $Model->schema(); + foreach ($this->relevantFields($Model) as $fieldName) { + $schema[$fieldName] = new CloneOnAccessArray($schema[$fieldName]); + $schema[$fieldName]['default'] = new CloudinaryField(); + } + self::modifyPropertyUsingForce($Model, "_schema", $schema); + } +} + +class CloneOnAccessArray extends ArrayObject { + public function offsetGet($offset) { + $ret = parent::offsetGet($offset); + return (is_object($ret)) ? clone $ret : $ret; + } } /* From 358399b2d3f39fe6d8cfbd0c7b7ac0160d18f330 Mon Sep 17 00:00:00 2001 From: Alon Hammerman Date: Fri, 22 Nov 2013 21:00:39 -0600 Subject: [PATCH 09/24] cake_plugin - use extended_identifier (from future 1.0.8) php framework --- cake_plugin/CloudinaryCake/Lib/CloudinaryField.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cake_plugin/CloudinaryCake/Lib/CloudinaryField.php b/cake_plugin/CloudinaryCake/Lib/CloudinaryField.php index d9ab8883..56d6727f 100644 --- a/cake_plugin/CloudinaryCake/Lib/CloudinaryField.php +++ b/cake_plugin/CloudinaryCake/Lib/CloudinaryField.php @@ -35,7 +35,7 @@ public function upload($file, $options = array()) { if ($this->verifyUpload && !$preloaded.is_valid()) { throw new \Exception("Error! Couldn't verify cloudinary response!"); } - $this->identifier = $preloaded->identifier(); + $this->identifier = $preloaded->extended_identifier(); } public function delete() { From ed01cfdada44ae9ec827542ff79fdddf7f0a9ca4 Mon Sep 17 00:00:00 2001 From: Alon Hammerman Date: Thu, 21 Nov 2013 16:46:13 -0600 Subject: [PATCH 10/24] cake_plugin - add CloudinaryHelper --- .../Model/Behavior/CloudinaryBehavior.php | 7 ++- .../View/Helper/CloudinaryHelper.php | 61 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 cake_plugin/CloudinaryCake/View/Helper/CloudinaryHelper.php diff --git a/cake_plugin/CloudinaryCake/Model/Behavior/CloudinaryBehavior.php b/cake_plugin/CloudinaryCake/Model/Behavior/CloudinaryBehavior.php index 6371763a..5cc0b53d 100644 --- a/cake_plugin/CloudinaryCake/Model/Behavior/CloudinaryBehavior.php +++ b/cake_plugin/CloudinaryCake/Model/Behavior/CloudinaryBehavior.php @@ -69,7 +69,12 @@ public function beforeValidate(Model $Model, $options = array()) { return true; } - /// Methods + /// Public Methods /// + public function cloudinaryFields(Model $Model) { + return $this->settings[$Model->alias]['fields']; + } + + /// Private Methods /// private function createCloudinaryField(Model $Model, $fieldName, $source=NULL) { error_log("CloudinaryBehavior::createCloudinaryField(): "); $source = $source ? $source : $Model->data; diff --git a/cake_plugin/CloudinaryCake/View/Helper/CloudinaryHelper.php b/cake_plugin/CloudinaryCake/View/Helper/CloudinaryHelper.php new file mode 100644 index 00000000..238fa50d --- /dev/null +++ b/cake_plugin/CloudinaryCake/View/Helper/CloudinaryHelper.php @@ -0,0 +1,61 @@ +cloudinaryFunctions)) { + return call_user_func_array($name, $args); + } + return parent::__call($name, $args); + } + + /// Automatically detect cloudinary fields on models that have declared them. + public function input($fieldName, $options = array()) { + $this->setEntity($fieldName); + $model = $this->_getModel($this->model()); + $fieldKey = $this->field(); + if ($model->hasMethod('cloudinaryFields') && in_array($fieldKey, $model->cloudinaryFields())) { + $options['type'] = 'file'; + } + return parent::input($fieldName, $options); + } + + public function cloudinary_includes($options = array()) { + foreach ($this->cloudinaryJSIncludes as $include) { + echo $this->Html->script($include, $options); + } + } + + /// Called for input() when type => direct_upload + public function direct_upload() { + $modelKey = $this->model(); + $fieldKey = $this->field(); + return \cl_image_upload_tag("data[" . $modelKey . "][" . $fieldKey . "]"); + } +} From daa260c9d3a950ab9017c094fa9ff9cff1fabbb1 Mon Sep 17 00:00:00 2001 From: Alon Hammerman Date: Sat, 23 Nov 2013 22:59:35 -0600 Subject: [PATCH 11/24] CloudinaryField - move from cake_plugin to src --- .../Config/IncludeCloudinary.php | 11 +++++++ .../Model/Behavior/CloudinaryBehavior.php | 18 +---------- .../View/Helper/CloudinaryHelper.php | 4 +-- src/Cloudinary.php | 12 +++++--- .../Lib => src}/CloudinaryField.php | 30 ++++++++++--------- 5 files changed, 38 insertions(+), 37 deletions(-) create mode 100644 cake_plugin/CloudinaryCake/Config/IncludeCloudinary.php rename {cake_plugin/CloudinaryCake/Lib => src}/CloudinaryField.php (56%) diff --git a/cake_plugin/CloudinaryCake/Config/IncludeCloudinary.php b/cake_plugin/CloudinaryCake/Config/IncludeCloudinary.php new file mode 100644 index 00000000..bd30b7f3 --- /dev/null +++ b/cake_plugin/CloudinaryCake/Config/IncludeCloudinary.php @@ -0,0 +1,11 @@ + true ); - public function __construct() { - error_log("CloudinaryBehavior::__construct)"); - } - public function setup(Model $Model, $settings = array()) { - error_log("CloudinaryBehavior::setup(): "); if (!isset($this->settings[$Model->alias])) { $this->settings[$Model->alias] = $this->settingsDefaults; } @@ -26,14 +21,8 @@ public function setup(Model $Model, $settings = array()) { } } - public function cleanup(Model $Model) { - error_log("CloudinaryBehavior::cleanup(): "); - } - /// Callbacks /// public function afterFind(Model $Model, $results, $primary = false) { - error_log("CloudinaryBehavior::afterFind()"); - $fieldNames = $this->relevantFields($Model); if (!$fieldNames) { return $results; @@ -55,13 +44,11 @@ public function beforeSave(Model $Model, $options = array()) { } public function beforeValidate(Model $Model, $options = array()) { - error_log("CloudinaryBehavior::beforeValidate()"); foreach ($this->relevantFields($Model, $options) as $fieldName) { $field = @$Model->data[$Model->alias][$fieldName]; if (is_string($field) && $field) { if (!(new CloudinaryField($field))->verify()) { $Model->invalidate($fieldName, "Bad cloudinary signature!"); - error_log("CloudinaryBehavior::beforeValidate(): Error in field " . $fieldName . " with data: " . $field); return false; } } @@ -76,7 +63,6 @@ public function cloudinaryFields(Model $Model) { /// Private Methods /// private function createCloudinaryField(Model $Model, $fieldName, $source=NULL) { - error_log("CloudinaryBehavior::createCloudinaryField(): "); $source = $source ? $source : $Model->data; return new CloudinaryField(isset($source[$Model->alias][$fieldName]) ? $source[$Model->alias][$fieldName] : ""); @@ -85,10 +71,8 @@ private function createCloudinaryField(Model $Model, $fieldName, $source=NULL) { private function updateCloudinaryField(Model $Model, $fieldName, &$data=NULL) { $source =& $data ? $data : $Model->data; if (isset($source[$Model->alias][$fieldName]) && $source[$Model->alias][$fieldName] instanceof CloudinaryField) { - error_log("CloudinaryBehavior::updateCloudinaryField - not updating again field '" . $fieldName . "' of " . $Model); return; } - error_log("CloudinaryBehavior::updateCloudinaryField - updating field '" . $fieldName . "' of " . $Model->alias); $source[$Model->alias][$fieldName] = $this->createCloudinaryField($Model, $fieldName, $source); } diff --git a/cake_plugin/CloudinaryCake/View/Helper/CloudinaryHelper.php b/cake_plugin/CloudinaryCake/View/Helper/CloudinaryHelper.php index 238fa50d..21e3e271 100644 --- a/cake_plugin/CloudinaryCake/View/Helper/CloudinaryHelper.php +++ b/cake_plugin/CloudinaryCake/View/Helper/CloudinaryHelper.php @@ -1,7 +1,7 @@ setEntity($fieldName); $model = $this->_getModel($this->model()); $fieldKey = $this->field(); - if ($model->hasMethod('cloudinaryFields') && in_array($fieldKey, $model->cloudinaryFields())) { + if (!@$options['type'] && $model->hasMethod('cloudinaryFields') && in_array($fieldKey, $model->cloudinaryFields())) { $options['type'] = 'file'; } return parent::input($fieldName, $options); diff --git a/src/Cloudinary.php b/src/Cloudinary.php index 85ec18ae..0150ef70 100644 --- a/src/Cloudinary.php +++ b/src/Cloudinary.php @@ -153,7 +153,7 @@ public static function generate_transformation_string(&$options=array()) { // Warning: $options are being destructively updated! public static function cloudinary_url($source, &$options=array()) { - $source = self::check_identifier_source($source, $options); + $source = self::check_cloudinary_field($source, $options); $type = Cloudinary::option_consume($options, "type", "upload"); if ($type == "fetch" && !isset($options["fetch_format"])) { @@ -213,17 +213,21 @@ public static function cloudinary_url($source, &$options=array()) { // [/][/][v/][.][#] // Warning: $options are being destructively updated! - public static function check_identifier_source($source, &$options=array()) { + public static function check_cloudinary_field($source, &$options=array()) { $IDENTIFIER_RE = "~" . "^(?:([^/]+)/)??(?:([^/]+)/)??(?:v(\\d+)/)?" . "(?:([^#/]+?)(?:\\.([^.#/]+))?)(?:#([^/]+))?$" . "~"; $matches = array(); - if (strstr(':', $source) !== false || !preg_match($IDENTIFIER_RE, $source, $matches)) { + if (!(is_object($source) && method_exists($source, 'identifier') && $source->identifier())) { + return $source; + } + $identifier = $source->identifier(); + if (strstr(':', $identifier) !== false || !preg_match($IDENTIFIER_RE, $identifier, $matches)) { return $source; } $optionNames = array('resource_type', 'type', 'version', 'public_id', 'format'); foreach ($optionNames as $index => $optionName) { - if ($matches[$index+1]) { + if (@$matches[$index+1]) { $options[$optionName] = $matches[$index+1]; } } diff --git a/cake_plugin/CloudinaryCake/Lib/CloudinaryField.php b/src/CloudinaryField.php similarity index 56% rename from cake_plugin/CloudinaryCake/Lib/CloudinaryField.php rename to src/CloudinaryField.php index 56d6727f..e758c4b9 100644 --- a/cake_plugin/CloudinaryCake/Lib/CloudinaryField.php +++ b/src/CloudinaryField.php @@ -3,29 +3,31 @@ * Manages access to a cloudinary image as a field */ -require_once '../../../src/Cloudinary.php'; -require_once '../../../src/Uploader.php'; +require_once 'Cloudinary.php'; +require_once 'Uploader.php'; -class CloudinaryField extends Object { - private $identifier = NULL; - private $autoSave = false; +class CloudinaryField { + private $_identifier = NULL; private $verifyUpload = false; public function __construct($identifier = "") { - error_log("CloudinaryField::__construct - " . $identifier); - $this->identifier = $identifier; + $this->_identifier = $identifier; } public function __toString() { - return explode("#", $this->identifier)[0]; + return explode("#", $this->identifier())[0]; + } + + public function identifier() { + return $this->_identifier; } public function url($options = array()) { - if (!$this->identifier) { + if (!$this->_identifier) { // TODO: Error? return; } - return cloudinary_url($this->identifier, $options); + return cloudinary_url($this, $options); } public function upload($file, $options = array()) { @@ -35,17 +37,17 @@ public function upload($file, $options = array()) { if ($this->verifyUpload && !$preloaded.is_valid()) { throw new \Exception("Error! Couldn't verify cloudinary response!"); } - $this->identifier = $preloaded->extended_identifier(); + $this->_identifier = $preloaded->extended_identifier(); } public function delete() { $options['return_error'] = false; - $ret = \Cloudinary\Uploader::destroy($this->identifier); - unset($this->identifier); + $ret = \Cloudinary\Uploader::destroy($this->_identifier); + unset($this->_identifier); } public function verify() { - $preloaded = new \Cloudinary\PreloadedFile($this->identifier); + $preloaded = new \Cloudinary\PreloadedFile($this->_identifier); return $preloaded->is_valid(); } } From c0beff94f2903adf9b8d471a81e29c75acc31282 Mon Sep 17 00:00:00 2001 From: Alon Hammerman Date: Mon, 25 Nov 2013 00:53:53 -0600 Subject: [PATCH 12/24] tests - add CloudinaryFieldTest [incomplete] --- tests/CloudinaryFieldTest.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 tests/CloudinaryFieldTest.php diff --git a/tests/CloudinaryFieldTest.php b/tests/CloudinaryFieldTest.php new file mode 100644 index 00000000..0cecd337 --- /dev/null +++ b/tests/CloudinaryFieldTest.php @@ -0,0 +1,23 @@ +"test123", "secure_distribution" => NULL, "private_cdn" => FALSE)); + } + + public function test_cloudinary_url_from_cloudinary_field() { + // should use cloud_name from config + $result = Cloudinary::cloudinary_url(new CloudinaryField("test")); + $this->assertEquals("http://res.cloudinary.com/test123/image/upload/test", $result); + + // should ignore signature + $result = Cloudinary::cloudinary_url(new CloudinaryField("test#signature")); + $this->assertEquals("http://res.cloudinary.com/test123/image/upload/test", $result); + + $result = Cloudinary::cloudinary_url(new CloudinaryField("rss/imgt/v123/test.jpg")); + $this->assertEquals("http://res.cloudinary.com/test123/rss/imgt/v123/test.jpg", $result); + } +} + // [/][/][v/][.][#] From 7b0651c5bcf998f6c776107f43fcffe516bb7692 Mon Sep 17 00:00:00 2001 From: Alon Hammerman Date: Tue, 5 Nov 2013 23:49:29 -0600 Subject: [PATCH 13/24] PhotoAlbumCake - Setup private config --- .gitignore | 2 +- samples/PhotoAlbumCake/.gitignore | 1 + samples/PhotoAlbumCake/Config/bootstrap.php | 9 +++++++++ samples/PhotoAlbumCake/Config/private.php.default | 9 +++++++++ 4 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 samples/PhotoAlbumCake/Config/private.php.default diff --git a/.gitignore b/.gitignore index 3a4edf69..cb4728dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -.project +samples/PhotoAlbumCake/Config/private.php diff --git a/samples/PhotoAlbumCake/.gitignore b/samples/PhotoAlbumCake/.gitignore index a9a5aecf..e83b140c 100644 --- a/samples/PhotoAlbumCake/.gitignore +++ b/samples/PhotoAlbumCake/.gitignore @@ -1 +1,2 @@ tmp +Config/database.php diff --git a/samples/PhotoAlbumCake/Config/bootstrap.php b/samples/PhotoAlbumCake/Config/bootstrap.php index cc65b6d2..2d780096 100644 --- a/samples/PhotoAlbumCake/Config/bootstrap.php +++ b/samples/PhotoAlbumCake/Config/bootstrap.php @@ -43,6 +43,7 @@ * )); * */ +App::build(array('Plugin' => APP . '..' . DS . '..' . DS . 'cake_plugin' . DS)); /** * Custom Inflector rules, can be set to correctly pluralize or singularize table, model, controller names or whatever other @@ -100,3 +101,11 @@ )); CakePlugin::load('CloudinaryCake', array('bootstrap' => false, 'routes' => false)); +Configure::load('CloudinaryCake.IncludeCloudinary'); +try { + Configure::load('private'); + \Cloudinary::config(Configure::read('cloudinary')); +} catch (Exception $e) { + $result = Configure::configured('default'); + $this->assertTrue($result); +} diff --git a/samples/PhotoAlbumCake/Config/private.php.default b/samples/PhotoAlbumCake/Config/private.php.default new file mode 100644 index 00000000..637f3431 --- /dev/null +++ b/samples/PhotoAlbumCake/Config/private.php.default @@ -0,0 +1,9 @@ + array( + "cloud_name" => "CLOUD_NAME", + "api_key" => "API_KEY", + "api_secret" => "API_SECRET" + ) +); + From 59d3ae46ba9eae662061e5a76d40316b843ba48e Mon Sep 17 00:00:00 2001 From: Alon Hammerman Date: Wed, 6 Nov 2013 00:29:08 -0600 Subject: [PATCH 14/24] PhotoAlbumCake - bake all photo --- .../Controller/PhotosController.php | 103 ++++++++++++++++++ samples/PhotoAlbumCake/Model/photo.php | 9 ++ samples/PhotoAlbumCake/View/Photos/add.ctp | 18 +++ samples/PhotoAlbumCake/View/Photos/edit.ctp | 20 ++++ samples/PhotoAlbumCake/View/Photos/index.ctp | 46 ++++++++ samples/PhotoAlbumCake/View/Photos/view.ctp | 39 +++++++ 6 files changed, 235 insertions(+) create mode 100644 samples/PhotoAlbumCake/Controller/PhotosController.php create mode 100644 samples/PhotoAlbumCake/Model/photo.php create mode 100644 samples/PhotoAlbumCake/View/Photos/add.ctp create mode 100644 samples/PhotoAlbumCake/View/Photos/edit.ctp create mode 100644 samples/PhotoAlbumCake/View/Photos/index.ctp create mode 100644 samples/PhotoAlbumCake/View/Photos/view.ctp diff --git a/samples/PhotoAlbumCake/Controller/PhotosController.php b/samples/PhotoAlbumCake/Controller/PhotosController.php new file mode 100644 index 00000000..0ddbd3fa --- /dev/null +++ b/samples/PhotoAlbumCake/Controller/PhotosController.php @@ -0,0 +1,103 @@ +Photo->recursive = 0; + $this->set('photos', $this->Paginator->paginate()); + } + +/** + * view method + * + * @throws NotFoundException + * @param string $id + * @return void + */ + public function view($id = null) { + if (!$this->Photo->exists($id)) { + throw new NotFoundException(__('Invalid photo')); + } + $options = array('conditions' => array('Photo.' . $this->Photo->primaryKey => $id)); + $this->set('photo', $this->Photo->find('first', $options)); + } + +/** + * add method + * + * @return void + */ + public function add() { + if ($this->request->is('post')) { + $this->Photo->create(); + if ($this->Photo->save($this->request->data)) { + $this->Session->setFlash(__('The photo has been saved.')); + return $this->redirect(array('action' => 'index')); + } else { + $this->Session->setFlash(__('The photo could not be saved. Please, try again.')); + } + } + } + +/** + * edit method + * + * @throws NotFoundException + * @param string $id + * @return void + */ + public function edit($id = null) { + if (!$this->Photo->exists($id)) { + throw new NotFoundException(__('Invalid photo')); + } + if ($this->request->is(array('post', 'put'))) { + if ($this->Photo->save($this->request->data)) { + $this->Session->setFlash(__('The photo has been saved.')); + return $this->redirect(array('action' => 'index')); + } else { + $this->Session->setFlash(__('The photo could not be saved. Please, try again.')); + } + } else { + $options = array('conditions' => array('Photo.' . $this->Photo->primaryKey => $id)); + $this->request->data = $this->Photo->find('first', $options); + } + } + +/** + * delete method + * + * @throws NotFoundException + * @param string $id + * @return void + */ + public function delete($id = null) { + $this->Photo->id = $id; + if (!$this->Photo->exists()) { + throw new NotFoundException(__('Invalid photo')); + } + $this->request->onlyAllow('post', 'delete'); + if ($this->Photo->delete()) { + $this->Session->setFlash(__('The photo has been deleted.')); + } else { + $this->Session->setFlash(__('The photo could not be deleted. Please, try again.')); + } + return $this->redirect(array('action' => 'index')); + }} diff --git a/samples/PhotoAlbumCake/Model/photo.php b/samples/PhotoAlbumCake/Model/photo.php new file mode 100644 index 00000000..129e61b9 --- /dev/null +++ b/samples/PhotoAlbumCake/Model/photo.php @@ -0,0 +1,9 @@ + +Form->create('Photo'); ?> +
+ + Form->input('cloudinaryIdentifier'); + echo $this->Form->input('moderated'); + ?> +
+Form->end(__('Submit')); ?> + +
+

+
    + +
  • Html->link(__('List Photos'), array('action' => 'index')); ?>
  • +
+
diff --git a/samples/PhotoAlbumCake/View/Photos/edit.ctp b/samples/PhotoAlbumCake/View/Photos/edit.ctp new file mode 100644 index 00000000..44cc3cb2 --- /dev/null +++ b/samples/PhotoAlbumCake/View/Photos/edit.ctp @@ -0,0 +1,20 @@ +
+Form->create('Photo'); ?> +
+ + Form->input('id'); + echo $this->Form->input('cloudinaryIdentifier'); + echo $this->Form->input('moderated'); + ?> +
+Form->end(__('Submit')); ?> +
+
+

+
    + +
  • Form->postLink(__('Delete'), array('action' => 'delete', $this->Form->value('Photo.id')), null, __('Are you sure you want to delete # %s?', $this->Form->value('Photo.id'))); ?>
  • +
  • Html->link(__('List Photos'), array('action' => 'index')); ?>
  • +
+
diff --git a/samples/PhotoAlbumCake/View/Photos/index.ctp b/samples/PhotoAlbumCake/View/Photos/index.ctp new file mode 100644 index 00000000..d6648638 --- /dev/null +++ b/samples/PhotoAlbumCake/View/Photos/index.ctp @@ -0,0 +1,46 @@ +
+

+ + + + + + + + + + + + + + + + + + + +
Paginator->sort('id'); ?>Paginator->sort('cloudinaryIdentifier'); ?>Paginator->sort('moderated'); ?>Paginator->sort('created'); ?>Paginator->sort('updated'); ?>
      + Html->link(__('View'), array('action' => 'view', $photo['Photo']['id'])); ?> + Html->link(__('Edit'), array('action' => 'edit', $photo['Photo']['id'])); ?> + Form->postLink(__('Delete'), array('action' => 'delete', $photo['Photo']['id']), null, __('Are you sure you want to delete # %s?', $photo['Photo']['id'])); ?> +
+

+ Paginator->counter(array( + 'format' => __('Page {:page} of {:pages}, showing {:current} records out of {:count} total, starting on record {:start}, ending on {:end}') + )); + ?>

+
+ Paginator->prev('< ' . __('previous'), array(), null, array('class' => 'prev disabled')); + echo $this->Paginator->numbers(array('separator' => '')); + echo $this->Paginator->next(__('next') . ' >', array(), null, array('class' => 'next disabled')); + ?> +
+
+
+

+
    +
  • Html->link(__('New Photo'), array('action' => 'add')); ?>
  • +
+
diff --git a/samples/PhotoAlbumCake/View/Photos/view.ctp b/samples/PhotoAlbumCake/View/Photos/view.ctp new file mode 100644 index 00000000..8fbbbfd9 --- /dev/null +++ b/samples/PhotoAlbumCake/View/Photos/view.ctp @@ -0,0 +1,39 @@ +
+

+
+
+
+ +   +
+
+
+ +   +
+
+
+ +   +
+
+
+ +   +
+
+
+ +   +
+
+
+
+

+
    +
  • Html->link(__('Edit Photo'), array('action' => 'edit', $photo['Photo']['id'])); ?>
  • +
  • Form->postLink(__('Delete Photo'), array('action' => 'delete', $photo['Photo']['id']), null, __('Are you sure you want to delete # %s?', $photo['Photo']['id'])); ?>
  • +
  • Html->link(__('List Photos'), array('action' => 'index')); ?>
  • +
  • Html->link(__('New Photo'), array('action' => 'add')); ?>
  • +
+
From e3f96646c08d0677a3df0c3b6e7632d8a8ef9c33 Mon Sep 17 00:00:00 2001 From: Alon Hammerman Date: Wed, 20 Nov 2013 18:06:29 -0600 Subject: [PATCH 15/24] PhotoAlbumCake - Add Cloudinary support to photo admin actions --- .../Controller/PhotosController.php | 1 + samples/PhotoAlbumCake/Model/photo.php | 4 +- samples/PhotoAlbumCake/View/Photos/add.ctp | 2 +- samples/PhotoAlbumCake/View/Photos/edit.ctp | 23 +- samples/PhotoAlbumCake/View/Photos/index.ctp | 12 +- .../webroot/js/canvas-to-blob.min.js | 1 + .../webroot/js/jquery.cloudinary.js | 443 ++++++ .../webroot/js/jquery.fileupload-image.js | 299 ++++ .../webroot/js/jquery.fileupload-process.js | 164 ++ .../webroot/js/jquery.fileupload-validate.js | 116 ++ .../webroot/js/jquery.fileupload.js | 1315 +++++++++++++++++ .../webroot/js/jquery.iframe-transport.js | 205 +++ .../webroot/js/jquery.ui.widget.js | 530 +++++++ .../webroot/js/load-image.min.js | 1 + 14 files changed, 3106 insertions(+), 10 deletions(-) create mode 100644 samples/PhotoAlbumCake/webroot/js/canvas-to-blob.min.js create mode 100644 samples/PhotoAlbumCake/webroot/js/jquery.cloudinary.js create mode 100644 samples/PhotoAlbumCake/webroot/js/jquery.fileupload-image.js create mode 100644 samples/PhotoAlbumCake/webroot/js/jquery.fileupload-process.js create mode 100644 samples/PhotoAlbumCake/webroot/js/jquery.fileupload-validate.js create mode 100644 samples/PhotoAlbumCake/webroot/js/jquery.fileupload.js create mode 100644 samples/PhotoAlbumCake/webroot/js/jquery.iframe-transport.js create mode 100644 samples/PhotoAlbumCake/webroot/js/jquery.ui.widget.js create mode 100644 samples/PhotoAlbumCake/webroot/js/load-image.min.js diff --git a/samples/PhotoAlbumCake/Controller/PhotosController.php b/samples/PhotoAlbumCake/Controller/PhotosController.php index 0ddbd3fa..d9876012 100644 --- a/samples/PhotoAlbumCake/Controller/PhotosController.php +++ b/samples/PhotoAlbumCake/Controller/PhotosController.php @@ -14,6 +14,7 @@ class PhotosController extends AppController { * @var array */ public $components = array('Paginator'); + public $helpers = array('Html', 'Form', 'CloudinaryCake.Cloudinary'); /** * index method diff --git a/samples/PhotoAlbumCake/Model/photo.php b/samples/PhotoAlbumCake/Model/photo.php index 129e61b9..c6ed8252 100644 --- a/samples/PhotoAlbumCake/Model/photo.php +++ b/samples/PhotoAlbumCake/Model/photo.php @@ -4,6 +4,6 @@ * photo Model * */ -class photo extends AppModel { - +class Photo extends AppModel { + public $actsAs = array('CloudinaryCake.Cloudinary' => array('fields' => array('cloudinaryIdentifier'))); } diff --git a/samples/PhotoAlbumCake/View/Photos/add.ctp b/samples/PhotoAlbumCake/View/Photos/add.ctp index c05fe56d..5068f9a9 100644 --- a/samples/PhotoAlbumCake/View/Photos/add.ctp +++ b/samples/PhotoAlbumCake/View/Photos/add.ctp @@ -1,5 +1,5 @@
-Form->create('Photo'); ?> +Form->create('Photo', array('type' => 'file')); ?>
Html->script('//code.jquery.com/jquery-1.10.1.min.js'); + # Include cloudinary_js dependencies (requires jQuery) + echo $this->Cloudinary->cloudinary_includes(); + # Setup cloudinary_js using the current cloudinary_php configuration + echo cloudinary_js_config(); +?>
-Form->create('Photo'); ?> +Cloudinary->create('Photo', array('type' => 'file')); ?>
Form->input('id'); - echo $this->Form->input('cloudinaryIdentifier'); - echo $this->Form->input('moderated'); + echo $this->Cloudinary->input('id'); + # Backend upload: + echo $this->Cloudinary->input('cloudinaryIdentifier'); + # Direct upload: + #echo $this->Cloudinary->input('cloudinaryIdentifier', array("type" => "direct_upload")); + echo $this->Cloudinary->input('moderated'); ?>
-Form->end(__('Submit')); ?> +Cloudinary->end(__('Submit')); ?>

    -
  • Form->postLink(__('Delete'), array('action' => 'delete', $this->Form->value('Photo.id')), null, __('Are you sure you want to delete # %s?', $this->Form->value('Photo.id'))); ?>
  • +
  • Cloudinary->postLink(__('Delete'), array('action' => 'delete', $this->Cloudinary->value('Photo.id')), null, __('Are you sure you want to delete # %s?', $this->Cloudinary->value('Photo.id'))); ?>
  • Html->link(__('List Photos'), array('action' => 'index')); ?>
diff --git a/samples/PhotoAlbumCake/View/Photos/index.ctp b/samples/PhotoAlbumCake/View/Photos/index.ctp index d6648638..62a3b410 100644 --- a/samples/PhotoAlbumCake/View/Photos/index.ctp +++ b/samples/PhotoAlbumCake/View/Photos/index.ctp @@ -4,6 +4,7 @@ Paginator->sort('id'); ?> Paginator->sort('cloudinaryIdentifier'); ?> + Thumbnail Paginator->sort('moderated'); ?> Paginator->sort('created'); ?> Paginator->sort('updated'); ?> @@ -12,7 +13,16 @@   -   + url()) { + echo ''; + $close_tag = ''; + } + echo h($photo['Photo']['cloudinaryIdentifier']); + echo @$close_tag; + ?> + Cloudinary->cl_image_tag($photo['Photo']['cloudinaryIdentifier'], + array("width" => 60, "height" => 60, "crop" => "thumb", "gravity" => "face")); ?>        diff --git a/samples/PhotoAlbumCake/webroot/js/canvas-to-blob.min.js b/samples/PhotoAlbumCake/webroot/js/canvas-to-blob.min.js new file mode 100644 index 00000000..d6bb8ca9 --- /dev/null +++ b/samples/PhotoAlbumCake/webroot/js/canvas-to-blob.min.js @@ -0,0 +1 @@ +(function(a){"use strict";var b=a.HTMLCanvasElement&&a.HTMLCanvasElement.prototype,c=a.Blob&&function(){try{return Boolean(new Blob)}catch(a){return!1}}(),d=c&&a.Uint8Array&&function(){try{return(new Blob([new Uint8Array(100)])).size===100}catch(a){return!1}}(),e=a.BlobBuilder||a.WebKitBlobBuilder||a.MozBlobBuilder||a.MSBlobBuilder,f=(c||e)&&a.atob&&a.ArrayBuffer&&a.Uint8Array&&function(a){var b,f,g,h,i,j;a.split(",")[0].indexOf("base64")>=0?b=atob(a.split(",")[1]):b=decodeURIComponent(a.split(",")[1]),f=new ArrayBuffer(b.length),g=new Uint8Array(f);for(h=0;h 127 && c1 < 2048) { + enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128); + } else { + enc = String.fromCharCode((c1 >> 12) | 224, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128); + } + if (enc !== null) { + if (end > start) { + utftext += string.slice(start, end); + } + utftext += enc; + start = end = n + 1; + } + } + + if (end > start) { + utftext += string.slice(start, stringl); + } + + return utftext; + } + + function crc32 (str) { + // http://kevin.vanzonneveld.net + // + original by: Webtoolkit.info (http://www.webtoolkit.info/) + // + improved by: T0bsn + // + improved by: http://stackoverflow.com/questions/2647935/javascript-crc32-function-and-php-crc32-not-matching + // - depends on: utf8_encode + // * example 1: crc32('Kevin van Zonneveld'); + // * returns 1: 1249991249 + str = utf8_encode(str); + var 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"; + + var crc = 0; + var x = 0; + var y = 0; + + crc = crc ^ (-1); + for (var i = 0, iTop = str.length; i < iTop; i++) { + y = (crc ^ str.charCodeAt(i)) & 0xFF; + x = "0x" + table.substr(y * 9, 8); + crc = (crc >>> 8) ^ x; + } + + crc = crc ^ (-1); + //convert to unsigned 32-bit int if needed + if (crc < 0) {crc += 4294967296} + return crc; + } + + function option_consume(options, option_name, default_value) { + var result = options[option_name]; + delete options[option_name]; + return typeof(result) == 'undefined' ? default_value : result; + } + function build_array(arg) { + if (!arg) { + return []; + } else if ($.isArray(arg)) { + return arg; + } else { + return [arg]; + } + } + function present(value) { + return typeof value != 'undefined' && ("" + value).length > 0; + } + function generate_transformation_string(options) { + var width = options['width']; + var height = options['height']; + var size = option_consume(options, 'size'); + if (size) { + var split_size = size.split("x"); + options['width'] = width = split_size[0]; + options['height'] = height = split_size[1]; + } + var has_layer = options.overlay || options.underlay; + + var crop = option_consume(options, 'crop'); + var angle = build_array(option_consume(options, 'angle')).join("."); + + var no_html_sizes = has_layer || present(angle) || crop == "fit" || crop == "limit" || crop == "lfill"; + + if (width && (no_html_sizes || parseFloat(width) < 1)) delete options['width']; + if (height && (no_html_sizes || parseFloat(height) < 1)) delete options['height']; + if (!crop && !has_layer) width = height = undefined; + + var background = option_consume(options, 'background'); + background = background && background.replace(/^#/, 'rgb:'); + var color = option_consume(options, 'color'); + color = color && color.replace(/^#/, 'rgb:'); + + var base_transformations = build_array(option_consume(options, 'transformation', [])); + var named_transformation = []; + if ($.grep(base_transformations, function(bs) {return typeof(bs) == 'object';}).length > 0) { + base_transformations = $.map(base_transformations, function(base_transformation) { + return typeof(base_transformation) == 'object' ? generate_transformation_string($.extend({}, base_transformation)) : generate_transformation_string({transformation: base_transformation}); + }); + } else { + named_transformation = $.grep(base_transformations, function() { return this != null && this != ""}).join("."); + base_transformations = []; + } + var effect = option_consume(options, "effect"); + if ($.isArray(effect)) effect = effect.join(":"); + + var border = option_consume(options, "border") + if ($.isPlainObject(border)) { + var border_width = "" + (border.width || 2); + var border_color = (border.color || "black").replace(/^#/, 'rgb:'); + border = border_width + "px_solid_" + border_color; + } + + var flags = build_array(option_consume(options, 'flags')).join("."); + + var params = [['c', crop], ['t', named_transformation], ['w', width], ['h', height], ['b', background], ['co', color], ['e', effect], ['a', angle], ['bo', border], ['fl', flags]]; + var simple_params = { + x: 'x', + y: 'y', + radius: 'r', + gravity: 'g', + quality: 'q', + prefix: 'p', + default_image: 'd', + underlay: 'u', + overlay: 'l', + fetch_format: 'f', + density: 'dn', + page: 'pg', + color_space: 'cl', + delay: 'dl', + opacity: 'o' + }; + for (var param in simple_params) { + params.push([simple_params[param], option_consume(options, param)]); + } + params.sort(function(a, b){return a[0]b[0] ? 1 : 0);}); + params.push([option_consume(options, 'raw_transformation')]); + var transformation = $.map($.grep(params, function(param) { + var value = param[param.length-1]; + return present(value); + }), function(param) { + return param.join("_"); + }).join(","); + base_transformations.push(transformation); + return $.grep(base_transformations, present).join("/"); + } + var dummyImg = undefined; + function absolutize(url) { + if (!dummyImg) dummyImg = document.createElement("img"); + dummyImg.src = url; + url = dummyImg.src; + dummyImg.src = null; + return url; + } + function cloudinary_url(public_id, options) { + options = options || {}; + var type = option_consume(options, 'type', 'upload'); + if (type == 'fetch') { + options.fetch_format = options.fetch_format || option_consume(options, 'format'); + } + var transformation = generate_transformation_string(options); + var resource_type = option_consume(options, 'resource_type', "image"); + var version = option_consume(options, 'version'); + var format = option_consume(options, 'format'); + var cloud_name = option_consume(options, 'cloud_name', $.cloudinary.config().cloud_name); + if (!cloud_name) throw "Unknown cloud_name"; + var private_cdn = option_consume(options, 'private_cdn', $.cloudinary.config().private_cdn); + var secure_distribution = option_consume(options, 'secure_distribution', $.cloudinary.config().secure_distribution); + var cname = option_consume(options, 'cname', $.cloudinary.config().cname); + var cdn_subdomain = option_consume(options, 'cdn_subdomain', $.cloudinary.config().cdn_subdomain); + var shorten = option_consume(options, 'shorten', $.cloudinary.config().shorten); + var secure = option_consume(options, 'secure', window.location.protocol == 'https:'); + + if (type == 'fetch') { + public_id = absolutize(public_id); + } + + if (public_id.match(/^https?:/)) { + if (type == "upload" || type == "asset") return public_id; + public_id = encodeURIComponent(public_id).replace(/%3A/g, ":").replace(/%2F/g, "/"); + } else { + // Make sure public_id is URI encoded. + public_id = encodeURIComponent(decodeURIComponent(public_id)).replace(/%3A/g, ":").replace(/%2F/g, "/"); + if (format) { + public_id = public_id.replace(/\.(jpg|png|gif|webp)$/, '') + "." + format; + } + } + + var prefix = window.location.protocol == 'file:' ? "file://" : (secure ? 'https://' : 'http://'); + if (cloud_name.match(/^\//) && !secure) { + prefix = "/res" + cloud_name; + } else { + var shared_domain = !private_cdn; + if (secure) { + if (!secure_distribution || secure_distribution == OLD_AKAMAI_SHARED_CDN) { + secure_distribution = private_cdn ? cloud_name + "-res.cloudinary.com" : SHARED_CDN; + } + shared_domain = shared_domain || secure_distribution == SHARED_CDN; + prefix += secure_distribution; + } else { + var subdomain = cdn_subdomain ? "a" + ((crc32(public_id) % 5) + 1) + "." : ""; + host = cname || (private_cdn ? cloud_name + "-res.cloudinary.com" : "res.cloudinary.com" ); + prefix += subdomain + host; + } + if (shared_domain) prefix += "/" + cloud_name; + } + if (shorten && resource_type == "image" && type == "upload") { + resource_type = "iu"; + type = undefined; + } + if (public_id.search("/") >= 0 && !public_id.match(/^v[0-9]+/) && !public_id.match(/^https?:\//) && !present(version)) { + version = 1; + } + + var url = [prefix, resource_type, type, transformation, version ? "v" + version : "", + public_id].join("/").replace(/([^:])\/+/g, '$1/'); + return url; + } + function html_only_attributes(options) { + var width = option_consume(options, 'html_width'); + var height = option_consume(options, 'html_height'); + if (width) options['width'] = width; + if (height) options['height'] = height; + } + var cloudinary_config = undefined; + $.cloudinary = { + CF_SHARED_CDN: CF_SHARED_CDN, + OLD_AKAMAI_SHARED_CDN: OLD_AKAMAI_SHARED_CDN, + AKAMAI_SHARED_CDN: AKAMAI_SHARED_CDN, + SHARED_CDN: SHARED_CDN, + config: function(new_config, new_value) { + if (!cloudinary_config) { + cloudinary_config = {}; + $('meta[name^="cloudinary_"]').each(function() { + cloudinary_config[$(this).attr('name').replace("cloudinary_", '')] = $(this).attr('content'); + }); + } + if (typeof(new_value) != 'undefined') { + cloudinary_config[new_config] = new_value; + } else if (typeof(new_config) == 'string') { + return cloudinary_config[new_config]; + } else if (new_config) { + cloudinary_config = new_config; + } + return cloudinary_config; + }, + url: function(public_id, options) { + options = $.extend({}, options); + return cloudinary_url(public_id, options); + }, + url_internal: cloudinary_url, + transformation_string: function(options) { + options = $.extend({}, options); + return generate_transformation_string(options); + }, + image: function(public_id, options) { + options = $.extend({}, options); + var url = cloudinary_url(public_id, options); + html_only_attributes(options); + return $('').attr(options).attr('src', url); + }, + facebook_profile_image: function(public_id, options) { + return $.cloudinary.image(public_id, $.extend({type: 'facebook'}, options)); + }, + twitter_profile_image: function(public_id, options) { + return $.cloudinary.image(public_id, $.extend({type: 'twitter'}, options)); + }, + twitter_name_profile_image: function(public_id, options) { + return $.cloudinary.image(public_id, $.extend({type: 'twitter_name'}, options)); + }, + gravatar_image: function(public_id, options) { + return $.cloudinary.image(public_id, $.extend({type: 'gravatar'}, options)); + }, + fetch_image: function(public_id, options) { + return $.cloudinary.image(public_id, $.extend({type: 'fetch'}, options)); + }, + sprite_css: function(public_id, options) { + options = $.extend({type: 'sprite'}, options); + if (!public_id.match(/.css$/)) options.format = 'css'; + return $.cloudinary.url(public_id, options); + } + }; + $.fn.cloudinary = function(options) { + this.filter('img').each(function() { + var img_options = $.extend({width: $(this).attr('width'), height: $(this).attr('height'), + src: $(this).attr('src')}, + $.extend($(this).data(), options)); + var public_id = option_consume(img_options, 'source', option_consume(img_options, 'src')); + var url = cloudinary_url(public_id, img_options); + html_only_attributes(img_options); + $(this).attr({src: url, width: img_options['width'], height: img_options['height']}); + }); + return this; + }; + var webp = null; + $.fn.webpify = function(options, webp_options) { + var that = this; + options = options || {}; + webp_options = webp_options || options; + if (!webp) { + var webp = $.Deferred(); + var webp_canary = new Image(); + webp_canary.onerror = webp.reject; + webp_canary.onload = webp.resolve; + webp_canary.src = 'data:image/webp;base64,UklGRjIAAABXRUJQVlA4ICYAAACyAgCdASoBAAEALmk0mk0iIiIiIgBoSygABc6zbAAA/v56QAAAAA=='; + } + $(function() { + webp.done(function() { + $(that).cloudinary($.extend({}, $.extend(webp_options, {format: 'webp'}))); + }).fail(function() { + $(that).cloudinary(options); + }); + }); + } + $.fn.fetchify = function(options) { + return this.cloudinary($.extend(options, {'type': 'fetch'})); + }; +})( jQuery ); + +(function( $ ) { + if (!$.fn.fileupload) { + return; + } + function getInitialValue(el) { + var $el = $(el); + if (!$el.data('cloudinaryField')) { return; } + var widget_parts = $el.prevUntil("[for=" + $el.attr('id') + "]"); + return widget_parts.filter('.initial-value'); + } + function handleDone(e, data) { + if (data.result.error) return; + data.result.path = ["v", data.result.version, "/", data.result.public_id, + data.result.format ? "." + data.result.format : ""].join(""); + + if (data.cloudinaryField && data.form.length > 0) { + var upload_info = [data.result.resource_type, data.result.type, data.result.path].join("/") + "#" + data.result.signature; + var field = $(data.form).find('input[name="' + data.cloudinaryField + '"]'); + if (field.length > 0) { + field.val(upload_info); + } else { + $('').attr({type: "hidden", name: data.cloudinaryField}).val(upload_info).appendTo(data.form); + } + var initial_value = getInitialValue(this); + if (initial_value.length == 1) { + initial_value.find('a').attr('disabled', false).attr('href', data.result.url).text(data.result.public_id); + } + } + $(e.target).trigger('cloudinarydone', data); + } + function handleStart(e, data) { + $(e.target).trigger('cloudinarystart'); + var initial_value = getInitialValue(this); + if (initial_value.length != 1) { return; } + initial_value.find('a').attr('disabled', true).text('Uploading...'); + } + $.fn.cloudinary_fileupload = function(options) { + var initializing = !this.data('blueimpFileupload'); + options = $.extend({ + maxFileSize: 20000000, + dataType: 'json', + headers: {"X-Requested-With": "XMLHttpRequest"} + }, options); + this.fileupload(options); + + if (initializing) { + this.bind("fileuploaddone", handleDone); + this.bind("fileuploadstart", handleStart); + this.bind("fileuploadstop", function(e){ + $(e.target).trigger('cloudinarystop'); + }); + this.bind("fileuploadprogress", function(e,data){ + $(e.target).trigger('cloudinaryprogress',data); + }); + this.bind("fileuploadprogressall", function(e,data){ + $(e.target).trigger('cloudinaryprogressall',data); + }); + this.bind("fileuploadfail", function(e,data){ + $(e.target).trigger('cloudinaryfail',data); + }); + this.bind("fileuploadalways", function(e,data){ + $(e.target).trigger('cloudinaryalways',data); + }); + + if (!this.fileupload('option').url) { + var upload_url = "https://api.cloudinary.com/v1_1/" + $.cloudinary.config().cloud_name + "/upload"; + this.fileupload('option', 'url', upload_url); + } + } + return this; + }; + + $.fn.cloudinary_upload_url = function(remote_url) { + this.fileupload('option', 'formData').file = remote_url; + this.fileupload('add', { files: [ remote_url ] }); + delete(this.fileupload('option', 'formData').file); + } + + $(function() { + $("input.cloudinary-fileupload[type=file]").cloudinary_fileupload(); + }); +})( jQuery ); diff --git a/samples/PhotoAlbumCake/webroot/js/jquery.fileupload-image.js b/samples/PhotoAlbumCake/webroot/js/jquery.fileupload-image.js new file mode 100644 index 00000000..5e181748 --- /dev/null +++ b/samples/PhotoAlbumCake/webroot/js/jquery.fileupload-image.js @@ -0,0 +1,299 @@ +/* + * jQuery File Upload Image Preview & Resize Plugin 1.2.2 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/*jslint nomen: true, unparam: true, regexp: true */ +/*global define, window, document, DataView, Blob, Uint8Array */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'load-image', + 'load-image-meta', + 'load-image-exif', + 'load-image-ios', + 'canvas-to-blob', + './jquery.fileupload-process' + ], factory); + } else { + // Browser globals: + factory( + window.jQuery, + window.loadImage + ); + } +}(function ($, loadImage) { + 'use strict'; + + // Prepend to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.unshift( + { + action: 'loadImageMetaData', + // Always trigger this action, + // even if the previous action was rejected: + always: true, + disableImageHead: '@', + disableExif: '@', + disableExifThumbnail: '@', + disableExifSub: '@', + disableExifGps: '@', + disabled: '@disableImageMetaDataLoad' + }, + { + action: 'loadImage', + // Use the action as prefix for the "@" options: + prefix: true, + fileTypes: '@', + maxFileSize: '@', + noRevoke: '@', + disabled: '@disableImageLoad' + }, + { + action: 'resizeImage', + // Use "image" as prefix for the "@" options: + prefix: 'image', + maxWidth: '@', + maxHeight: '@', + minWidth: '@', + minHeight: '@', + crop: '@', + disabled: '@disableImageResize' + }, + { + action: 'saveImage', + disabled: '@disableImageResize' + }, + { + action: 'saveImageMetaData', + disabled: '@disableImageMetaDataSave' + }, + { + action: 'resizeImage', + // Always trigger this action, + // even if the previous action was rejected: + always: true, + // Use "preview" as prefix for the "@" options: + prefix: 'preview', + maxWidth: '@', + maxHeight: '@', + minWidth: '@', + minHeight: '@', + crop: '@', + orientation: '@', + thumbnail: '@', + canvas: '@', + disabled: '@disableImagePreview' + }, + { + action: 'setImage', + name: '@imagePreviewName', + disabled: '@disableImagePreview' + } + ); + + // The File Upload Resize plugin extends the fileupload widget + // with image resize functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // The regular expression for the types of images to load: + // matched against the file type: + loadImageFileTypes: /^image\/(gif|jpeg|png)$/, + // The maximum file size of images to load: + loadImageMaxFileSize: 10000000, // 10MB + // The maximum width of resized images: + imageMaxWidth: 1920, + // The maximum height of resized images: + imageMaxHeight: 1080, + // Define if resized images should be cropped or only scaled: + imageCrop: false, + // Disable the resize image functionality by default: + disableImageResize: true, + // The maximum width of the preview images: + previewMaxWidth: 80, + // The maximum height of the preview images: + previewMaxHeight: 80, + // Defines the preview orientation (1-8) or takes the orientation + // value from Exif data if set to true: + previewOrientation: true, + // Create the preview using the Exif data thumbnail: + previewThumbnail: true, + // Define if preview images should be cropped or only scaled: + previewCrop: false, + // Define if preview images should be resized as canvas elements: + previewCanvas: true + }, + + processActions: { + + // Loads the image given via data.files and data.index + // as img element if the browser supports canvas. + // Accepts the options fileTypes (regular expression) + // and maxFileSize (integer) to limit the files to load: + loadImage: function (data, options) { + if (options.disabled) { + return data; + } + var that = this, + file = data.files[data.index], + dfd = $.Deferred(); + if (($.type(options.maxFileSize) === 'number' && + file.size > options.maxFileSize) || + (options.fileTypes && + !options.fileTypes.test(file.type)) || + !loadImage( + file, + function (img) { + if (!img.src) { + return dfd.rejectWith(that, [data]); + } + data.img = img; + dfd.resolveWith(that, [data]); + }, + options + )) { + dfd.rejectWith(that, [data]); + } + return dfd.promise(); + }, + + // Resizes the image given as data.canvas or data.img + // and updates data.canvas or data.img with the resized image. + // Accepts the options maxWidth, maxHeight, minWidth, + // minHeight, canvas and crop: + resizeImage: function (data, options) { + if (options.disabled) { + return data; + } + var that = this, + dfd = $.Deferred(), + resolve = function (newImg) { + data[newImg.getContext ? 'canvas' : 'img'] = newImg; + dfd.resolveWith(that, [data]); + }, + thumbnail, + img, + newImg; + options = $.extend({canvas: true}, options); + if (data.exif) { + if (options.orientation === true) { + options.orientation = data.exif.get('Orientation'); + } + if (options.thumbnail) { + thumbnail = data.exif.get('Thumbnail'); + if (thumbnail) { + loadImage(thumbnail, resolve, options); + return dfd.promise(); + } + } + } + img = (options.canvas && data.canvas) || data.img; + if (img) { + newImg = loadImage.scale(img, options); + if (newImg.width !== img.width || + newImg.height !== img.height) { + resolve(newImg); + return dfd.promise(); + } + } + return data; + }, + + // Saves the processed image given as data.canvas + // inplace at data.index of data.files: + saveImage: function (data, options) { + if (!data.canvas || options.disabled) { + return data; + } + var that = this, + file = data.files[data.index], + name = file.name, + dfd = $.Deferred(), + callback = function (blob) { + if (!blob.name) { + if (file.type === blob.type) { + blob.name = file.name; + } else if (file.name) { + blob.name = file.name.replace( + /\..+$/, + '.' + blob.type.substr(6) + ); + } + } + // Store the created blob at the position + // of the original file in the files list: + data.files[data.index] = blob; + dfd.resolveWith(that, [data]); + }; + // Use canvas.mozGetAsFile directly, to retain the filename, as + // Gecko doesn't support the filename option for FormData.append: + if (data.canvas.mozGetAsFile) { + callback(data.canvas.mozGetAsFile( + (/^image\/(jpeg|png)$/.test(file.type) && name) || + ((name && name.replace(/\..+$/, '')) || + 'blob') + '.png', + file.type + )); + } else if (data.canvas.toBlob) { + data.canvas.toBlob(callback, file.type); + } else { + return data; + } + return dfd.promise(); + }, + + loadImageMetaData: function (data, options) { + if (options.disabled) { + return data; + } + var that = this, + dfd = $.Deferred(); + loadImage.parseMetaData(data.files[data.index], function (result) { + $.extend(data, result); + dfd.resolveWith(that, [data]); + }, options); + return dfd.promise(); + }, + + saveImageMetaData: function (data, options) { + if (!(data.imageHead && data.canvas && + data.canvas.toBlob && !options.disabled)) { + return data; + } + var file = data.files[data.index], + blob = new Blob([ + data.imageHead, + // Resized images always have a head size of 20 bytes, + // including the JPEG marker and a minimal JFIF header: + this._blobSlice.call(file, 20) + ], {type: file.type}); + blob.name = file.name; + data.files[data.index] = blob; + return data; + }, + + // Sets the resized version of the image as a property of the + // file object, must be called after "saveImage": + setImage: function (data, options) { + var img = data.canvas || data.img; + if (img && !options.disabled) { + data.files[data.index][options.name || 'preview'] = img; + } + return data; + } + + } + + }); + +})); diff --git a/samples/PhotoAlbumCake/webroot/js/jquery.fileupload-process.js b/samples/PhotoAlbumCake/webroot/js/jquery.fileupload-process.js new file mode 100644 index 00000000..87042c3d --- /dev/null +++ b/samples/PhotoAlbumCake/webroot/js/jquery.fileupload-process.js @@ -0,0 +1,164 @@ +/* + * jQuery File Upload Processing Plugin 1.2.2 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2012, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/*jslint nomen: true, unparam: true */ +/*global define, window */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + './jquery.fileupload' + ], factory); + } else { + // Browser globals: + factory( + window.jQuery + ); + } +}(function ($) { + 'use strict'; + + var originalAdd = $.blueimp.fileupload.prototype.options.add; + + // The File Upload Processing plugin extends the fileupload widget + // with file processing functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // The list of processing actions: + processQueue: [ + /* + { + action: 'log', + type: 'debug' + } + */ + ], + add: function (e, data) { + var $this = $(this); + data.process(function () { + return $this.fileupload('process', data); + }); + originalAdd.call(this, e, data); + } + }, + + processActions: { + /* + log: function (data, options) { + console[options.type]( + 'Processing "' + data.files[data.index].name + '"' + ); + } + */ + }, + + _processFile: function (data) { + var that = this, + dfd = $.Deferred().resolveWith(that, [data]), + chain = dfd.promise(); + this._trigger('process', null, data); + $.each(data.processQueue, function (i, settings) { + var func = function (data) { + return that.processActions[settings.action].call( + that, + data, + settings + ); + }; + chain = chain.pipe(func, settings.always && func); + }); + chain + .done(function () { + that._trigger('processdone', null, data); + that._trigger('processalways', null, data); + }) + .fail(function () { + that._trigger('processfail', null, data); + that._trigger('processalways', null, data); + }); + return chain; + }, + + // Replaces the settings of each processQueue item that + // are strings starting with an "@", using the remaining + // substring as key for the option map, + // e.g. "@autoUpload" is replaced with options.autoUpload: + _transformProcessQueue: function (options) { + var processQueue = []; + $.each(options.processQueue, function () { + var settings = {}, + action = this.action, + prefix = this.prefix === true ? action : this.prefix; + $.each(this, function (key, value) { + if ($.type(value) === 'string' && + value.charAt(0) === '@') { + settings[key] = options[ + value.slice(1) || (prefix ? prefix + + key.charAt(0).toUpperCase() + key.slice(1) : key) + ]; + } else { + settings[key] = value; + } + + }); + processQueue.push(settings); + }); + options.processQueue = processQueue; + }, + + // Returns the number of files currently in the processsing queue: + processing: function () { + return this._processing; + }, + + // Processes the files given as files property of the data parameter, + // returns a Promise object that allows to bind callbacks: + process: function (data) { + var that = this, + options = $.extend({}, this.options, data); + if (options.processQueue && options.processQueue.length) { + this._transformProcessQueue(options); + if (this._processing === 0) { + this._trigger('processstart'); + } + $.each(data.files, function (index) { + var opts = index ? $.extend({}, options) : options, + func = function () { + return that._processFile(opts); + }; + opts.index = index; + that._processing += 1; + that._processingQueue = that._processingQueue.pipe(func, func) + .always(function () { + that._processing -= 1; + if (that._processing === 0) { + that._trigger('processstop'); + } + }); + }); + } + return this._processingQueue; + }, + + _create: function () { + this._super(); + this._processing = 0; + this._processingQueue = $.Deferred().resolveWith(this) + .promise(); + } + + }); + +})); diff --git a/samples/PhotoAlbumCake/webroot/js/jquery.fileupload-validate.js b/samples/PhotoAlbumCake/webroot/js/jquery.fileupload-validate.js new file mode 100644 index 00000000..1cc4f1eb --- /dev/null +++ b/samples/PhotoAlbumCake/webroot/js/jquery.fileupload-validate.js @@ -0,0 +1,116 @@ +/* + * jQuery File Upload Validation Plugin 1.1 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/*jslint nomen: true, unparam: true, regexp: true */ +/*global define, window */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + './jquery.fileupload-process' + ], factory); + } else { + // Browser globals: + factory( + window.jQuery + ); + } +}(function ($) { + 'use strict'; + + // Append to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.push( + { + action: 'validate', + // Always trigger this action, + // even if the previous action was rejected: + always: true, + // Options taken from the global options map: + acceptFileTypes: '@', + maxFileSize: '@', + minFileSize: '@', + maxNumberOfFiles: '@', + disabled: '@disableValidation' + } + ); + + // The File Upload Validation plugin extends the fileupload widget + // with file validation functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + /* + // The regular expression for allowed file types, matches + // against either file type or file name: + acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i, + // The maximum allowed file size in bytes: + maxFileSize: 10000000, // 10 MB + // The minimum allowed file size in bytes: + minFileSize: undefined, // No minimal file size + // The limit of files to be uploaded: + maxNumberOfFiles: 10, + */ + + // Function returning the current number of files, + // has to be overriden for maxNumberOfFiles validation: + getNumberOfFiles: $.noop, + + // Error and info messages: + messages: { + maxNumberOfFiles: 'Maximum number of files exceeded', + acceptFileTypes: 'File type not allowed', + maxFileSize: 'File is too large', + minFileSize: 'File is too small' + } + }, + + processActions: { + + validate: function (data, options) { + if (options.disabled) { + return data; + } + var dfd = $.Deferred(), + settings = this.options, + file = data.files[data.index], + numberOfFiles = settings.getNumberOfFiles(); + if (numberOfFiles && $.type(options.maxNumberOfFiles) === 'number' && + numberOfFiles + data.files.length > options.maxNumberOfFiles) { + file.error = settings.i18n('maxNumberOfFiles'); + } else if (options.acceptFileTypes && + !(options.acceptFileTypes.test(file.type) || + options.acceptFileTypes.test(file.name))) { + file.error = settings.i18n('acceptFileTypes'); + } else if (options.maxFileSize && file.size > options.maxFileSize) { + file.error = settings.i18n('maxFileSize'); + } else if ($.type(file.size) === 'number' && + file.size < options.minFileSize) { + file.error = settings.i18n('minFileSize'); + } else { + delete file.error; + } + if (file.error || data.files.error) { + data.files.error = true; + dfd.rejectWith(this, [data]); + } else { + dfd.resolveWith(this, [data]); + } + return dfd.promise(); + } + + } + + }); + +})); diff --git a/samples/PhotoAlbumCake/webroot/js/jquery.fileupload.js b/samples/PhotoAlbumCake/webroot/js/jquery.fileupload.js new file mode 100644 index 00000000..921f5aed --- /dev/null +++ b/samples/PhotoAlbumCake/webroot/js/jquery.fileupload.js @@ -0,0 +1,1315 @@ +/* + * jQuery File Upload Plugin 5.31.6 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/*jslint nomen: true, unparam: true, regexp: true */ +/*global define, window, document, location, File, Blob, FormData */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'jquery.ui.widget' + ], factory); + } else { + // Browser globals: + factory(window.jQuery); + } +}(function ($) { + 'use strict'; + + // The FileReader API is not actually used, but works as feature detection, + // as e.g. Safari supports XHR file uploads via the FormData API, + // but not non-multipart XHR file uploads: + $.support.xhrFileUpload = !!(window.XMLHttpRequestUpload && window.FileReader); + $.support.xhrFormDataFileUpload = !!window.FormData; + + // Detect support for Blob slicing (required for chunked uploads): + $.support.blobSlice = window.Blob && (Blob.prototype.slice || + Blob.prototype.webkitSlice || Blob.prototype.mozSlice); + + // The fileupload widget listens for change events on file input fields defined + // via fileInput setting and paste or drop events of the given dropZone. + // In addition to the default jQuery Widget methods, the fileupload widget + // exposes the "add" and "send" methods, to add or directly send files using + // the fileupload API. + // By default, files added via file input selection, paste, drag & drop or + // "add" method are uploaded immediately, but it is possible to override + // the "add" callback option to queue file uploads. + $.widget('blueimp.fileupload', { + + options: { + // The drop target element(s), by the default the complete document. + // Set to null to disable drag & drop support: + dropZone: $(document), + // The paste target element(s), by the default the complete document. + // Set to null to disable paste support: + pasteZone: $(document), + // The file input field(s), that are listened to for change events. + // If undefined, it is set to the file input fields inside + // of the widget element on plugin initialization. + // Set to null to disable the change listener. + fileInput: undefined, + // By default, the file input field is replaced with a clone after + // each input field change event. This is required for iframe transport + // queues and allows change events to be fired for the same file + // selection, but can be disabled by setting the following option to false: + replaceFileInput: true, + // The parameter name for the file form data (the request argument name). + // If undefined or empty, the name property of the file input field is + // used, or "files[]" if the file input name property is also empty, + // can be a string or an array of strings: + paramName: undefined, + // By default, each file of a selection is uploaded using an individual + // request for XHR type uploads. Set to false to upload file + // selections in one request each: + singleFileUploads: true, + // To limit the number of files uploaded with one XHR request, + // set the following option to an integer greater than 0: + limitMultiFileUploads: undefined, + // Set the following option to true to issue all file upload requests + // in a sequential order: + sequentialUploads: false, + // To limit the number of concurrent uploads, + // set the following option to an integer greater than 0: + limitConcurrentUploads: undefined, + // Set the following option to true to force iframe transport uploads: + forceIframeTransport: false, + // Set the following option to the location of a redirect url on the + // origin server, for cross-domain iframe transport uploads: + redirect: undefined, + // The parameter name for the redirect url, sent as part of the form + // data and set to 'redirect' if this option is empty: + redirectParamName: undefined, + // Set the following option to the location of a postMessage window, + // to enable postMessage transport uploads: + postMessage: undefined, + // By default, XHR file uploads are sent as multipart/form-data. + // The iframe transport is always using multipart/form-data. + // Set to false to enable non-multipart XHR uploads: + multipart: true, + // To upload large files in smaller chunks, set the following option + // to a preferred maximum chunk size. If set to 0, null or undefined, + // or the browser does not support the required Blob API, files will + // be uploaded as a whole. + maxChunkSize: undefined, + // When a non-multipart upload or a chunked multipart upload has been + // aborted, this option can be used to resume the upload by setting + // it to the size of the already uploaded bytes. This option is most + // useful when modifying the options object inside of the "add" or + // "send" callbacks, as the options are cloned for each file upload. + uploadedBytes: undefined, + // By default, failed (abort or error) file uploads are removed from the + // global progress calculation. Set the following option to false to + // prevent recalculating the global progress data: + recalculateProgress: true, + // Interval in milliseconds to calculate and trigger progress events: + progressInterval: 100, + // Interval in milliseconds to calculate progress bitrate: + bitrateInterval: 500, + // By default, uploads are started automatically when adding files: + autoUpload: true, + + // Error and info messages: + messages: { + uploadedBytes: 'Uploaded bytes exceed file size' + }, + + // Translation function, gets the message key to be translated + // and an object with context specific data as arguments: + i18n: function (message, context) { + message = this.messages[message] || message.toString(); + if (context) { + $.each(context, function (key, value) { + message = message.replace('{' + key + '}', value); + }); + } + return message; + }, + + // Additional form data to be sent along with the file uploads can be set + // using this option, which accepts an array of objects with name and + // value properties, a function returning such an array, a FormData + // object (for XHR file uploads), or a simple object. + // The form of the first fileInput is given as parameter to the function: + formData: function (form) { + return form.serializeArray(); + }, + + // The add callback is invoked as soon as files are added to the fileupload + // widget (via file input selection, drag & drop, paste or add API call). + // If the singleFileUploads option is enabled, this callback will be + // called once for each file in the selection for XHR file uploads, else + // once for each file selection. + // + // The upload starts when the submit method is invoked on the data parameter. + // The data object contains a files property holding the added files + // and allows you to override plugin options as well as define ajax settings. + // + // Listeners for this callback can also be bound the following way: + // .bind('fileuploadadd', func); + // + // data.submit() returns a Promise object and allows to attach additional + // handlers using jQuery's Deferred callbacks: + // data.submit().done(func).fail(func).always(func); + add: function (e, data) { + if (data.autoUpload || (data.autoUpload !== false && + $(this).fileupload('option', 'autoUpload'))) { + data.process().done(function () { + data.submit(); + }); + } + }, + + // Other callbacks: + + // Callback for the submit event of each file upload: + // submit: function (e, data) {}, // .bind('fileuploadsubmit', func); + + // Callback for the start of each file upload request: + // send: function (e, data) {}, // .bind('fileuploadsend', func); + + // Callback for successful uploads: + // done: function (e, data) {}, // .bind('fileuploaddone', func); + + // Callback for failed (abort or error) uploads: + // fail: function (e, data) {}, // .bind('fileuploadfail', func); + + // Callback for completed (success, abort or error) requests: + // always: function (e, data) {}, // .bind('fileuploadalways', func); + + // Callback for upload progress events: + // progress: function (e, data) {}, // .bind('fileuploadprogress', func); + + // Callback for global upload progress events: + // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func); + + // Callback for uploads start, equivalent to the global ajaxStart event: + // start: function (e) {}, // .bind('fileuploadstart', func); + + // Callback for uploads stop, equivalent to the global ajaxStop event: + // stop: function (e) {}, // .bind('fileuploadstop', func); + + // Callback for change events of the fileInput(s): + // change: function (e, data) {}, // .bind('fileuploadchange', func); + + // Callback for paste events to the pasteZone(s): + // paste: function (e, data) {}, // .bind('fileuploadpaste', func); + + // Callback for drop events of the dropZone(s): + // drop: function (e, data) {}, // .bind('fileuploaddrop', func); + + // Callback for dragover events of the dropZone(s): + // dragover: function (e) {}, // .bind('fileuploaddragover', func); + + // Callback for the start of each chunk upload request: + // chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func); + + // Callback for successful chunk uploads: + // chunkdone: function (e, data) {}, // .bind('fileuploadchunkdone', func); + + // Callback for failed (abort or error) chunk uploads: + // chunkfail: function (e, data) {}, // .bind('fileuploadchunkfail', func); + + // Callback for completed (success, abort or error) chunk upload requests: + // chunkalways: function (e, data) {}, // .bind('fileuploadchunkalways', func); + + // The plugin options are used as settings object for the ajax calls. + // The following are jQuery ajax settings required for the file uploads: + processData: false, + contentType: false, + cache: false + }, + + // A list of options that require reinitializing event listeners and/or + // special initialization code: + _specialOptions: [ + 'fileInput', + 'dropZone', + 'pasteZone', + 'multipart', + 'forceIframeTransport' + ], + + _blobSlice: $.support.blobSlice && function () { + var slice = this.slice || this.webkitSlice || this.mozSlice; + return slice.apply(this, arguments); + }, + + _BitrateTimer: function () { + this.timestamp = ((Date.now) ? Date.now() : (new Date()).getTime()); + this.loaded = 0; + this.bitrate = 0; + this.getBitrate = function (now, loaded, interval) { + var timeDiff = now - this.timestamp; + if (!this.bitrate || !interval || timeDiff > interval) { + this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8; + this.loaded = loaded; + this.timestamp = now; + } + return this.bitrate; + }; + }, + + _isXHRUpload: function (options) { + return !options.forceIframeTransport && + ((!options.multipart && $.support.xhrFileUpload) || + $.support.xhrFormDataFileUpload); + }, + + _getFormData: function (options) { + var formData; + if (typeof options.formData === 'function') { + return options.formData(options.form); + } + if ($.isArray(options.formData)) { + return options.formData; + } + if ($.type(options.formData) === 'object') { + formData = []; + $.each(options.formData, function (name, value) { + formData.push({name: name, value: value}); + }); + return formData; + } + return []; + }, + + _getTotal: function (files) { + var total = 0; + $.each(files, function (index, file) { + total += file.size || 1; + }); + return total; + }, + + _initProgressObject: function (obj) { + var progress = { + loaded: 0, + total: 0, + bitrate: 0 + }; + if (obj._progress) { + $.extend(obj._progress, progress); + } else { + obj._progress = progress; + } + }, + + _initResponseObject: function (obj) { + var prop; + if (obj._response) { + for (prop in obj._response) { + if (obj._response.hasOwnProperty(prop)) { + delete obj._response[prop]; + } + } + } else { + obj._response = {}; + } + }, + + _onProgress: function (e, data) { + if (e.lengthComputable) { + var now = ((Date.now) ? Date.now() : (new Date()).getTime()), + loaded; + if (data._time && data.progressInterval && + (now - data._time < data.progressInterval) && + e.loaded !== e.total) { + return; + } + data._time = now; + loaded = Math.floor( + e.loaded / e.total * (data.chunkSize || data._progress.total) + ) + (data.uploadedBytes || 0); + // Add the difference from the previously loaded state + // to the global loaded counter: + this._progress.loaded += (loaded - data._progress.loaded); + this._progress.bitrate = this._bitrateTimer.getBitrate( + now, + this._progress.loaded, + data.bitrateInterval + ); + data._progress.loaded = data.loaded = loaded; + data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate( + now, + loaded, + data.bitrateInterval + ); + // Trigger a custom progress event with a total data property set + // to the file size(s) of the current upload and a loaded data + // property calculated accordingly: + this._trigger('progress', e, data); + // Trigger a global progress event for all current file uploads, + // including ajax calls queued for sequential file uploads: + this._trigger('progressall', e, this._progress); + } + }, + + _initProgressListener: function (options) { + var that = this, + xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr(); + // Accesss to the native XHR object is required to add event listeners + // for the upload progress event: + if (xhr.upload) { + $(xhr.upload).bind('progress', function (e) { + var oe = e.originalEvent; + // Make sure the progress event properties get copied over: + e.lengthComputable = oe.lengthComputable; + e.loaded = oe.loaded; + e.total = oe.total; + that._onProgress(e, options); + }); + options.xhr = function () { + return xhr; + }; + } + }, + + _isInstanceOf: function (type, obj) { + // Cross-frame instanceof check + return Object.prototype.toString.call(obj) === '[object ' + type + ']'; + }, + + _initXHRData: function (options) { + var that = this, + formData, + file = options.files[0], + // Ignore non-multipart setting if not supported: + multipart = options.multipart || !$.support.xhrFileUpload, + paramName = options.paramName[0]; + options.headers = options.headers || {}; + if (options.contentRange) { + options.headers['Content-Range'] = options.contentRange; + } + if (!multipart || options.blob || !this._isInstanceOf('File', file)) { + options.headers['Content-Disposition'] = 'attachment; filename="' + + encodeURI(file.name) + '"'; + } + if (!multipart) { + options.contentType = file.type; + options.data = options.blob || file; + } else if ($.support.xhrFormDataFileUpload) { + if (options.postMessage) { + // window.postMessage does not allow sending FormData + // objects, so we just add the File/Blob objects to + // the formData array and let the postMessage window + // create the FormData object out of this array: + formData = this._getFormData(options); + if (options.blob) { + formData.push({ + name: paramName, + value: options.blob + }); + } else { + $.each(options.files, function (index, file) { + formData.push({ + name: options.paramName[index] || paramName, + value: file + }); + }); + } + } else { + if (that._isInstanceOf('FormData', options.formData)) { + formData = options.formData; + } else { + formData = new FormData(); + $.each(this._getFormData(options), function (index, field) { + formData.append(field.name, field.value); + }); + } + if (options.blob) { + formData.append(paramName, options.blob, file.name); + } else { + $.each(options.files, function (index, file) { + // This check allows the tests to run with + // dummy objects: + if (that._isInstanceOf('File', file) || + that._isInstanceOf('Blob', file)) { + formData.append( + options.paramName[index] || paramName, + file, + file.name + ); + } + }); + } + } + options.data = formData; + } + // Blob reference is not needed anymore, free memory: + options.blob = null; + }, + + _initIframeSettings: function (options) { + var targetHost = $('').prop('href', options.url).prop('host'); + // Setting the dataType to iframe enables the iframe transport: + options.dataType = 'iframe ' + (options.dataType || ''); + // The iframe transport accepts a serialized array as form data: + options.formData = this._getFormData(options); + // Add redirect url to form data on cross-domain uploads: + if (options.redirect && targetHost && targetHost !== location.host) { + options.formData.push({ + name: options.redirectParamName || 'redirect', + value: options.redirect + }); + } + }, + + _initDataSettings: function (options) { + if (this._isXHRUpload(options)) { + if (!this._chunkedUpload(options, true)) { + if (!options.data) { + this._initXHRData(options); + } + this._initProgressListener(options); + } + if (options.postMessage) { + // Setting the dataType to postmessage enables the + // postMessage transport: + options.dataType = 'postmessage ' + (options.dataType || ''); + } + } else { + this._initIframeSettings(options); + } + }, + + _getParamName: function (options) { + var fileInput = $(options.fileInput), + paramName = options.paramName; + if (!paramName) { + paramName = []; + fileInput.each(function () { + var input = $(this), + name = input.prop('name') || 'files[]', + i = (input.prop('files') || [1]).length; + while (i) { + paramName.push(name); + i -= 1; + } + }); + if (!paramName.length) { + paramName = [fileInput.prop('name') || 'files[]']; + } + } else if (!$.isArray(paramName)) { + paramName = [paramName]; + } + return paramName; + }, + + _initFormSettings: function (options) { + // Retrieve missing options from the input field and the + // associated form, if available: + if (!options.form || !options.form.length) { + options.form = $(options.fileInput.prop('form')); + // If the given file input doesn't have an associated form, + // use the default widget file input's form: + if (!options.form.length) { + options.form = $(this.options.fileInput.prop('form')); + } + } + options.paramName = this._getParamName(options); + if (!options.url) { + options.url = options.form.prop('action') || location.href; + } + // The HTTP request method must be "POST" or "PUT": + options.type = (options.type || options.form.prop('method') || '') + .toUpperCase(); + if (options.type !== 'POST' && options.type !== 'PUT' && + options.type !== 'PATCH') { + options.type = 'POST'; + } + if (!options.formAcceptCharset) { + options.formAcceptCharset = options.form.attr('accept-charset'); + } + }, + + _getAJAXSettings: function (data) { + var options = $.extend({}, this.options, data); + this._initFormSettings(options); + this._initDataSettings(options); + return options; + }, + + // jQuery 1.6 doesn't provide .state(), + // while jQuery 1.8+ removed .isRejected() and .isResolved(): + _getDeferredState: function (deferred) { + if (deferred.state) { + return deferred.state(); + } + if (deferred.isResolved()) { + return 'resolved'; + } + if (deferred.isRejected()) { + return 'rejected'; + } + return 'pending'; + }, + + // Maps jqXHR callbacks to the equivalent + // methods of the given Promise object: + _enhancePromise: function (promise) { + promise.success = promise.done; + promise.error = promise.fail; + promise.complete = promise.always; + return promise; + }, + + // Creates and returns a Promise object enhanced with + // the jqXHR methods abort, success, error and complete: + _getXHRPromise: function (resolveOrReject, context, args) { + var dfd = $.Deferred(), + promise = dfd.promise(); + context = context || this.options.context || promise; + if (resolveOrReject === true) { + dfd.resolveWith(context, args); + } else if (resolveOrReject === false) { + dfd.rejectWith(context, args); + } + promise.abort = dfd.promise; + return this._enhancePromise(promise); + }, + + // Adds convenience methods to the data callback argument: + _addConvenienceMethods: function (e, data) { + var that = this, + getPromise = function (data) { + return $.Deferred().resolveWith(that, [data]).promise(); + }; + data.process = function (resolveFunc, rejectFunc) { + if (resolveFunc || rejectFunc) { + data._processQueue = this._processQueue = + (this._processQueue || getPromise(this)) + .pipe(resolveFunc, rejectFunc); + } + return this._processQueue || getPromise(this); + }; + data.submit = function () { + if (this.state() !== 'pending') { + data.jqXHR = this.jqXHR = + (that._trigger('submit', e, this) !== false) && + that._onSend(e, this); + } + return this.jqXHR || that._getXHRPromise(); + }; + data.abort = function () { + if (this.jqXHR) { + return this.jqXHR.abort(); + } + return that._getXHRPromise(); + }; + data.state = function () { + if (this.jqXHR) { + return that._getDeferredState(this.jqXHR); + } + if (this._processQueue) { + return that._getDeferredState(this._processQueue); + } + }; + data.progress = function () { + return this._progress; + }; + data.response = function () { + return this._response; + }; + }, + + // Parses the Range header from the server response + // and returns the uploaded bytes: + _getUploadedBytes: function (jqXHR) { + var range = jqXHR.getResponseHeader('Range'), + parts = range && range.split('-'), + upperBytesPos = parts && parts.length > 1 && + parseInt(parts[1], 10); + return upperBytesPos && upperBytesPos + 1; + }, + + // Uploads a file in multiple, sequential requests + // by splitting the file up in multiple blob chunks. + // If the second parameter is true, only tests if the file + // should be uploaded in chunks, but does not invoke any + // upload requests: + _chunkedUpload: function (options, testOnly) { + options.uploadedBytes = options.uploadedBytes || 0; + var that = this, + file = options.files[0], + fs = file.size, + ub = options.uploadedBytes, + mcs = options.maxChunkSize || fs, + slice = this._blobSlice, + dfd = $.Deferred(), + promise = dfd.promise(), + jqXHR, + upload; + if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) || + options.data) { + return false; + } + if (testOnly) { + return true; + } + if (ub >= fs) { + file.error = options.i18n('uploadedBytes'); + return this._getXHRPromise( + false, + options.context, + [null, 'error', file.error] + ); + } + // The chunk upload method: + upload = function () { + // Clone the options object for each chunk upload: + var o = $.extend({}, options), + currentLoaded = o._progress.loaded; + o.blob = slice.call( + file, + ub, + ub + mcs, + file.type + ); + // Store the current chunk size, as the blob itself + // will be dereferenced after data processing: + o.chunkSize = o.blob.size; + // Expose the chunk bytes position range: + o.contentRange = 'bytes ' + ub + '-' + + (ub + o.chunkSize - 1) + '/' + fs; + // Process the upload data (the blob and potential form data): + that._initXHRData(o); + // Add progress listeners for this chunk upload: + that._initProgressListener(o); + jqXHR = ((that._trigger('chunksend', null, o) !== false && $.ajax(o)) || + that._getXHRPromise(false, o.context)) + .done(function (result, textStatus, jqXHR) { + ub = that._getUploadedBytes(jqXHR) || + (ub + o.chunkSize); + // Create a progress event if no final progress event + // with loaded equaling total has been triggered + // for this chunk: + if (currentLoaded + o.chunkSize - o._progress.loaded) { + that._onProgress($.Event('progress', { + lengthComputable: true, + loaded: ub - o.uploadedBytes, + total: ub - o.uploadedBytes + }), o); + } + options.uploadedBytes = o.uploadedBytes = ub; + o.result = result; + o.textStatus = textStatus; + o.jqXHR = jqXHR; + that._trigger('chunkdone', null, o); + that._trigger('chunkalways', null, o); + if (ub < fs) { + // File upload not yet complete, + // continue with the next chunk: + upload(); + } else { + dfd.resolveWith( + o.context, + [result, textStatus, jqXHR] + ); + } + }) + .fail(function (jqXHR, textStatus, errorThrown) { + o.jqXHR = jqXHR; + o.textStatus = textStatus; + o.errorThrown = errorThrown; + that._trigger('chunkfail', null, o); + that._trigger('chunkalways', null, o); + dfd.rejectWith( + o.context, + [jqXHR, textStatus, errorThrown] + ); + }); + }; + this._enhancePromise(promise); + promise.abort = function () { + return jqXHR.abort(); + }; + upload(); + return promise; + }, + + _beforeSend: function (e, data) { + if (this._active === 0) { + // the start callback is triggered when an upload starts + // and no other uploads are currently running, + // equivalent to the global ajaxStart event: + this._trigger('start'); + // Set timer for global bitrate progress calculation: + this._bitrateTimer = new this._BitrateTimer(); + // Reset the global progress values: + this._progress.loaded = this._progress.total = 0; + this._progress.bitrate = 0; + } + // Make sure the container objects for the .response() and + // .progress() methods on the data object are available + // and reset to their initial state: + this._initResponseObject(data); + this._initProgressObject(data); + data._progress.loaded = data.loaded = data.uploadedBytes || 0; + data._progress.total = data.total = this._getTotal(data.files) || 1; + data._progress.bitrate = data.bitrate = 0; + this._active += 1; + // Initialize the global progress values: + this._progress.loaded += data.loaded; + this._progress.total += data.total; + }, + + _onDone: function (result, textStatus, jqXHR, options) { + var total = options._progress.total, + response = options._response; + if (options._progress.loaded < total) { + // Create a progress event if no final progress event + // with loaded equaling total has been triggered: + this._onProgress($.Event('progress', { + lengthComputable: true, + loaded: total, + total: total + }), options); + } + response.result = options.result = result; + response.textStatus = options.textStatus = textStatus; + response.jqXHR = options.jqXHR = jqXHR; + this._trigger('done', null, options); + }, + + _onFail: function (jqXHR, textStatus, errorThrown, options) { + var response = options._response; + if (options.recalculateProgress) { + // Remove the failed (error or abort) file upload from + // the global progress calculation: + this._progress.loaded -= options._progress.loaded; + this._progress.total -= options._progress.total; + } + response.jqXHR = options.jqXHR = jqXHR; + response.textStatus = options.textStatus = textStatus; + response.errorThrown = options.errorThrown = errorThrown; + this._trigger('fail', null, options); + }, + + _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) { + // jqXHRorResult, textStatus and jqXHRorError are added to the + // options object via done and fail callbacks + this._trigger('always', null, options); + }, + + _onSend: function (e, data) { + if (!data.submit) { + this._addConvenienceMethods(e, data); + } + var that = this, + jqXHR, + aborted, + slot, + pipe, + options = that._getAJAXSettings(data), + send = function () { + that._sending += 1; + // Set timer for bitrate progress calculation: + options._bitrateTimer = new that._BitrateTimer(); + jqXHR = jqXHR || ( + ((aborted || that._trigger('send', e, options) === false) && + that._getXHRPromise(false, options.context, aborted)) || + that._chunkedUpload(options) || $.ajax(options) + ).done(function (result, textStatus, jqXHR) { + that._onDone(result, textStatus, jqXHR, options); + }).fail(function (jqXHR, textStatus, errorThrown) { + that._onFail(jqXHR, textStatus, errorThrown, options); + }).always(function (jqXHRorResult, textStatus, jqXHRorError) { + that._onAlways( + jqXHRorResult, + textStatus, + jqXHRorError, + options + ); + that._sending -= 1; + that._active -= 1; + if (options.limitConcurrentUploads && + options.limitConcurrentUploads > that._sending) { + // Start the next queued upload, + // that has not been aborted: + var nextSlot = that._slots.shift(); + while (nextSlot) { + if (that._getDeferredState(nextSlot) === 'pending') { + nextSlot.resolve(); + break; + } + nextSlot = that._slots.shift(); + } + } + if (that._active === 0) { + // The stop callback is triggered when all uploads have + // been completed, equivalent to the global ajaxStop event: + that._trigger('stop'); + } + }); + return jqXHR; + }; + this._beforeSend(e, options); + if (this.options.sequentialUploads || + (this.options.limitConcurrentUploads && + this.options.limitConcurrentUploads <= this._sending)) { + if (this.options.limitConcurrentUploads > 1) { + slot = $.Deferred(); + this._slots.push(slot); + pipe = slot.pipe(send); + } else { + this._sequence = this._sequence.pipe(send, send); + pipe = this._sequence; + } + // Return the piped Promise object, enhanced with an abort method, + // which is delegated to the jqXHR object of the current upload, + // and jqXHR callbacks mapped to the equivalent Promise methods: + pipe.abort = function () { + aborted = [undefined, 'abort', 'abort']; + if (!jqXHR) { + if (slot) { + slot.rejectWith(options.context, aborted); + } + return send(); + } + return jqXHR.abort(); + }; + return this._enhancePromise(pipe); + } + return send(); + }, + + _onAdd: function (e, data) { + var that = this, + result = true, + options = $.extend({}, this.options, data), + limit = options.limitMultiFileUploads, + paramName = this._getParamName(options), + paramNameSet, + paramNameSlice, + fileSet, + i; + if (!(options.singleFileUploads || limit) || + !this._isXHRUpload(options)) { + fileSet = [data.files]; + paramNameSet = [paramName]; + } else if (!options.singleFileUploads && limit) { + fileSet = []; + paramNameSet = []; + for (i = 0; i < data.files.length; i += limit) { + fileSet.push(data.files.slice(i, i + limit)); + paramNameSlice = paramName.slice(i, i + limit); + if (!paramNameSlice.length) { + paramNameSlice = paramName; + } + paramNameSet.push(paramNameSlice); + } + } else { + paramNameSet = paramName; + } + data.originalFiles = data.files; + $.each(fileSet || data.files, function (index, element) { + var newData = $.extend({}, data); + newData.files = fileSet ? element : [element]; + newData.paramName = paramNameSet[index]; + that._initResponseObject(newData); + that._initProgressObject(newData); + that._addConvenienceMethods(e, newData); + result = that._trigger('add', e, newData); + return result; + }); + return result; + }, + + _replaceFileInput: function (input) { + var inputClone = input.clone(true); + $('
').append(inputClone)[0].reset(); + // Detaching allows to insert the fileInput on another form + // without loosing the file input value: + input.after(inputClone).detach(); + // Avoid memory leaks with the detached file input: + $.cleanData(input.unbind('remove')); + // Replace the original file input element in the fileInput + // elements set with the clone, which has been copied including + // event handlers: + this.options.fileInput = this.options.fileInput.map(function (i, el) { + if (el === input[0]) { + return inputClone[0]; + } + return el; + }); + // If the widget has been initialized on the file input itself, + // override this.element with the file input clone: + if (input[0] === this.element[0]) { + this.element = inputClone; + } + }, + + _handleFileTreeEntry: function (entry, path) { + var that = this, + dfd = $.Deferred(), + errorHandler = function (e) { + if (e && !e.entry) { + e.entry = entry; + } + // Since $.when returns immediately if one + // Deferred is rejected, we use resolve instead. + // This allows valid files and invalid items + // to be returned together in one set: + dfd.resolve([e]); + }, + dirReader; + path = path || ''; + if (entry.isFile) { + if (entry._file) { + // Workaround for Chrome bug #149735 + entry._file.relativePath = path; + dfd.resolve(entry._file); + } else { + entry.file(function (file) { + file.relativePath = path; + dfd.resolve(file); + }, errorHandler); + } + } else if (entry.isDirectory) { + dirReader = entry.createReader(); + dirReader.readEntries(function (entries) { + that._handleFileTreeEntries( + entries, + path + entry.name + '/' + ).done(function (files) { + dfd.resolve(files); + }).fail(errorHandler); + }, errorHandler); + } else { + // Return an empy list for file system items + // other than files or directories: + dfd.resolve([]); + } + return dfd.promise(); + }, + + _handleFileTreeEntries: function (entries, path) { + var that = this; + return $.when.apply( + $, + $.map(entries, function (entry) { + return that._handleFileTreeEntry(entry, path); + }) + ).pipe(function () { + return Array.prototype.concat.apply( + [], + arguments + ); + }); + }, + + _getDroppedFiles: function (dataTransfer) { + dataTransfer = dataTransfer || {}; + var items = dataTransfer.items; + if (items && items.length && (items[0].webkitGetAsEntry || + items[0].getAsEntry)) { + return this._handleFileTreeEntries( + $.map(items, function (item) { + var entry; + if (item.webkitGetAsEntry) { + entry = item.webkitGetAsEntry(); + if (entry) { + // Workaround for Chrome bug #149735: + entry._file = item.getAsFile(); + } + return entry; + } + return item.getAsEntry(); + }) + ); + } + return $.Deferred().resolve( + $.makeArray(dataTransfer.files) + ).promise(); + }, + + _getSingleFileInputFiles: function (fileInput) { + fileInput = $(fileInput); + var entries = fileInput.prop('webkitEntries') || + fileInput.prop('entries'), + files, + value; + if (entries && entries.length) { + return this._handleFileTreeEntries(entries); + } + files = $.makeArray(fileInput.prop('files')); + if (!files.length) { + value = fileInput.prop('value'); + if (!value) { + return $.Deferred().resolve([]).promise(); + } + // If the files property is not available, the browser does not + // support the File API and we add a pseudo File object with + // the input value as name with path information removed: + files = [{name: value.replace(/^.*\\/, '')}]; + } else if (files[0].name === undefined && files[0].fileName) { + // File normalization for Safari 4 and Firefox 3: + $.each(files, function (index, file) { + file.name = file.fileName; + file.size = file.fileSize; + }); + } + return $.Deferred().resolve(files).promise(); + }, + + _getFileInputFiles: function (fileInput) { + if (!(fileInput instanceof $) || fileInput.length === 1) { + return this._getSingleFileInputFiles(fileInput); + } + return $.when.apply( + $, + $.map(fileInput, this._getSingleFileInputFiles) + ).pipe(function () { + return Array.prototype.concat.apply( + [], + arguments + ); + }); + }, + + _onChange: function (e) { + var that = this, + data = { + fileInput: $(e.target), + form: $(e.target.form) + }; + this._getFileInputFiles(data.fileInput).always(function (files) { + data.files = files; + if (that.options.replaceFileInput) { + that._replaceFileInput(data.fileInput); + } + if (that._trigger('change', e, data) !== false) { + that._onAdd(e, data); + } + }); + }, + + _onPaste: function (e) { + var items = e.originalEvent && e.originalEvent.clipboardData && + e.originalEvent.clipboardData.items, + data = {files: []}; + if (items && items.length) { + $.each(items, function (index, item) { + var file = item.getAsFile && item.getAsFile(); + if (file) { + data.files.push(file); + } + }); + if (this._trigger('paste', e, data) === false || + this._onAdd(e, data) === false) { + return false; + } + } + }, + + _onDrop: function (e) { + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; + var that = this, + dataTransfer = e.dataTransfer, + data = {}; + if (dataTransfer && dataTransfer.files && dataTransfer.files.length) { + e.preventDefault(); + this._getDroppedFiles(dataTransfer).always(function (files) { + data.files = files; + if (that._trigger('drop', e, data) !== false) { + that._onAdd(e, data); + } + }); + } + }, + + _onDragOver: function (e) { + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; + var dataTransfer = e.dataTransfer; + if (dataTransfer) { + if (this._trigger('dragover', e) === false) { + return false; + } + if ($.inArray('Files', dataTransfer.types) !== -1) { + dataTransfer.dropEffect = 'copy'; + e.preventDefault(); + } + } + }, + + _initEventHandlers: function () { + if (this._isXHRUpload(this.options)) { + this._on(this.options.dropZone, { + dragover: this._onDragOver, + drop: this._onDrop + }); + this._on(this.options.pasteZone, { + paste: this._onPaste + }); + } + this._on(this.options.fileInput, { + change: this._onChange + }); + }, + + _destroyEventHandlers: function () { + this._off(this.options.dropZone, 'dragover drop'); + this._off(this.options.pasteZone, 'paste'); + this._off(this.options.fileInput, 'change'); + }, + + _setOption: function (key, value) { + var reinit = $.inArray(key, this._specialOptions) !== -1; + if (reinit) { + this._destroyEventHandlers(); + } + this._super(key, value); + if (reinit) { + this._initSpecialOptions(); + this._initEventHandlers(); + } + }, + + _initSpecialOptions: function () { + var options = this.options; + if (options.fileInput === undefined) { + options.fileInput = this.element.is('input[type="file"]') ? + this.element : this.element.find('input[type="file"]'); + } else if (!(options.fileInput instanceof $)) { + options.fileInput = $(options.fileInput); + } + if (!(options.dropZone instanceof $)) { + options.dropZone = $(options.dropZone); + } + if (!(options.pasteZone instanceof $)) { + options.pasteZone = $(options.pasteZone); + } + }, + + _getRegExp: function (str) { + var parts = str.split('/'), + modifiers = parts.pop(); + parts.shift(); + return new RegExp(parts.join('/'), modifiers); + }, + + _isRegExpOption: function (key, value) { + return key !== 'url' && $.type(value) === 'string' && + /^\/.*\/[igm]{0,3}$/.test(value); + }, + + _initDataAttributes: function () { + var that = this, + options = this.options; + // Initialize options set via HTML5 data-attributes: + $.each( + $(this.element[0].cloneNode(false)).data(), + function (key, value) { + if (that._isRegExpOption(key, value)) { + value = that._getRegExp(value); + } + options[key] = value; + } + ); + }, + + _create: function () { + this._initDataAttributes(); + this._initSpecialOptions(); + this._slots = []; + this._sequence = this._getXHRPromise(true); + this._sending = this._active = 0; + this._initProgressObject(this); + this._initEventHandlers(); + }, + + // This method is exposed to the widget API and allows to query + // the number of active uploads: + active: function () { + return this._active; + }, + + // This method is exposed to the widget API and allows to query + // the widget upload progress. + // It returns an object with loaded, total and bitrate properties + // for the running uploads: + progress: function () { + return this._progress; + }, + + // This method is exposed to the widget API and allows adding files + // using the fileupload API. The data parameter accepts an object which + // must have a files property and can contain additional options: + // .fileupload('add', {files: filesList}); + add: function (data) { + var that = this; + if (!data || this.options.disabled) { + return; + } + if (data.fileInput && !data.files) { + this._getFileInputFiles(data.fileInput).always(function (files) { + data.files = files; + that._onAdd(null, data); + }); + } else { + data.files = $.makeArray(data.files); + this._onAdd(null, data); + } + }, + + // This method is exposed to the widget API and allows sending files + // using the fileupload API. The data parameter accepts an object which + // must have a files or fileInput property and can contain additional options: + // .fileupload('send', {files: filesList}); + // The method returns a Promise object for the file upload call. + send: function (data) { + if (data && !this.options.disabled) { + if (data.fileInput && !data.files) { + var that = this, + dfd = $.Deferred(), + promise = dfd.promise(), + jqXHR, + aborted; + promise.abort = function () { + aborted = true; + if (jqXHR) { + return jqXHR.abort(); + } + dfd.reject(null, 'abort', 'abort'); + return promise; + }; + this._getFileInputFiles(data.fileInput).always( + function (files) { + if (aborted) { + return; + } + data.files = files; + jqXHR = that._onSend(null, data).then( + function (result, textStatus, jqXHR) { + dfd.resolve(result, textStatus, jqXHR); + }, + function (jqXHR, textStatus, errorThrown) { + dfd.reject(jqXHR, textStatus, errorThrown); + } + ); + } + ); + return this._enhancePromise(promise); + } + data.files = $.makeArray(data.files); + if (data.files.length) { + return this._onSend(null, data); + } + } + return this._getXHRPromise(false, data && data.context); + } + + }); + +})); diff --git a/samples/PhotoAlbumCake/webroot/js/jquery.iframe-transport.js b/samples/PhotoAlbumCake/webroot/js/jquery.iframe-transport.js new file mode 100644 index 00000000..073c5fbe --- /dev/null +++ b/samples/PhotoAlbumCake/webroot/js/jquery.iframe-transport.js @@ -0,0 +1,205 @@ +/* + * jQuery Iframe Transport Plugin 1.7 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/*jslint unparam: true, nomen: true */ +/*global define, window, document */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery'], factory); + } else { + // Browser globals: + factory(window.jQuery); + } +}(function ($) { + 'use strict'; + + // Helper variable to create unique names for the transport iframes: + var counter = 0; + + // The iframe transport accepts three additional options: + // options.fileInput: a jQuery collection of file input fields + // options.paramName: the parameter name for the file form data, + // overrides the name property of the file input field(s), + // can be a string or an array of strings. + // options.formData: an array of objects with name and value properties, + // equivalent to the return data of .serializeArray(), e.g.: + // [{name: 'a', value: 1}, {name: 'b', value: 2}] + $.ajaxTransport('iframe', function (options) { + if (options.async) { + var form, + iframe, + addParamChar; + return { + send: function (_, completeCallback) { + form = $('
'); + form.attr('accept-charset', options.formAcceptCharset); + addParamChar = /\?/.test(options.url) ? '&' : '?'; + // XDomainRequest only supports GET and POST: + if (options.type === 'DELETE') { + options.url = options.url + addParamChar + '_method=DELETE'; + options.type = 'POST'; + } else if (options.type === 'PUT') { + options.url = options.url + addParamChar + '_method=PUT'; + options.type = 'POST'; + } else if (options.type === 'PATCH') { + options.url = options.url + addParamChar + '_method=PATCH'; + options.type = 'POST'; + } + // javascript:false as initial iframe src + // prevents warning popups on HTTPS in IE6. + // IE versions below IE8 cannot set the name property of + // elements that have already been added to the DOM, + // so we set the name along with the iframe HTML markup: + counter += 1; + iframe = $( + '' + ).bind('load', function () { + var fileInputClones, + paramNames = $.isArray(options.paramName) ? + options.paramName : [options.paramName]; + iframe + .unbind('load') + .bind('load', function () { + var response; + // Wrap in a try/catch block to catch exceptions thrown + // when trying to access cross-domain iframe contents: + try { + response = iframe.contents(); + // Google Chrome and Firefox do not throw an + // exception when calling iframe.contents() on + // cross-domain requests, so we unify the response: + if (!response.length || !response[0].firstChild) { + throw new Error(); + } + } catch (e) { + response = undefined; + } + // The complete callback returns the + // iframe content document as response object: + completeCallback( + 200, + 'success', + {'iframe': response} + ); + // Fix for IE endless progress bar activity bug + // (happens on form submits to iframe targets): + $('') + .appendTo(form); + window.setTimeout(function () { + // Removing the form in a setTimeout call + // allows Chrome's developer tools to display + // the response result + form.remove(); + }, 0); + }); + form + .prop('target', iframe.prop('name')) + .prop('action', options.url) + .prop('method', options.type); + if (options.formData) { + $.each(options.formData, function (index, field) { + $('') + .prop('name', field.name) + .val(field.value) + .appendTo(form); + }); + } + if (options.fileInput && options.fileInput.length && + options.type === 'POST') { + fileInputClones = options.fileInput.clone(); + // Insert a clone for each file input field: + options.fileInput.after(function (index) { + return fileInputClones[index]; + }); + if (options.paramName) { + options.fileInput.each(function (index) { + $(this).prop( + 'name', + paramNames[index] || options.paramName + ); + }); + } + // Appending the file input fields to the hidden form + // removes them from their original location: + form + .append(options.fileInput) + .prop('enctype', 'multipart/form-data') + // enctype must be set as encoding for IE: + .prop('encoding', 'multipart/form-data'); + } + form.submit(); + // Insert the file input fields at their original location + // by replacing the clones with the originals: + if (fileInputClones && fileInputClones.length) { + options.fileInput.each(function (index, input) { + var clone = $(fileInputClones[index]); + $(input).prop('name', clone.prop('name')); + clone.replaceWith(input); + }); + } + }); + form.append(iframe).appendTo(document.body); + }, + abort: function () { + if (iframe) { + // javascript:false as iframe src aborts the request + // and prevents warning popups on HTTPS in IE6. + // concat is used to avoid the "Script URL" JSLint error: + iframe + .unbind('load') + .prop('src', 'javascript'.concat(':false;')); + } + if (form) { + form.remove(); + } + } + }; + } + }); + + // The iframe transport returns the iframe content document as response. + // The following adds converters from iframe to text, json, html, xml + // and script. + // Please note that the Content-Type for JSON responses has to be text/plain + // or text/html, if the browser doesn't include application/json in the + // Accept header, else IE will show a download dialog. + // The Content-Type for XML responses on the other hand has to be always + // application/xml or text/xml, so IE properly parses the XML response. + // See also + // https://github.com/blueimp/jQuery-File-Upload/wiki/Setup#content-type-negotiation + $.ajaxSetup({ + converters: { + 'iframe text': function (iframe) { + return iframe && $(iframe[0].body).text(); + }, + 'iframe json': function (iframe) { + return iframe && $.parseJSON($(iframe[0].body).text()); + }, + 'iframe html': function (iframe) { + return iframe && $(iframe[0].body).html(); + }, + 'iframe xml': function (iframe) { + var xmlDoc = iframe && iframe[0]; + return xmlDoc && $.isXMLDoc(xmlDoc) ? xmlDoc : + $.parseXML((xmlDoc.XMLDocument && xmlDoc.XMLDocument.xml) || + $(xmlDoc.body).html()); + }, + 'iframe script': function (iframe) { + return iframe && $.globalEval($(iframe[0].body).text()); + } + } + }); + +})); diff --git a/samples/PhotoAlbumCake/webroot/js/jquery.ui.widget.js b/samples/PhotoAlbumCake/webroot/js/jquery.ui.widget.js new file mode 100644 index 00000000..2d370893 --- /dev/null +++ b/samples/PhotoAlbumCake/webroot/js/jquery.ui.widget.js @@ -0,0 +1,530 @@ +/* + * jQuery UI Widget 1.10.3+amd + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/jQuery.widget/ + */ + +(function (factory) { + if (typeof define === "function" && define.amd) { + // Register as an anonymous AMD module: + define(["jquery"], factory); + } else { + // Browser globals: + factory(jQuery); + } +}(function( $, undefined ) { + +var uuid = 0, + slice = Array.prototype.slice, + _cleanData = $.cleanData; +$.cleanData = function( elems ) { + for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { + try { + $( elem ).triggerHandler( "remove" ); + // http://bugs.jquery.com/ticket/8235 + } catch( e ) {} + } + _cleanData( elems ); +}; + +$.widget = function( name, base, prototype ) { + var fullName, existingConstructor, constructor, basePrototype, + // proxiedPrototype allows the provided prototype to remain unmodified + // so that it can be used as a mixin for multiple widgets (#8876) + proxiedPrototype = {}, + namespace = name.split( "." )[ 0 ]; + + name = name.split( "." )[ 1 ]; + fullName = namespace + "-" + name; + + if ( !prototype ) { + prototype = base; + base = $.Widget; + } + + // create selector for plugin + $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { + return !!$.data( elem, fullName ); + }; + + $[ namespace ] = $[ namespace ] || {}; + existingConstructor = $[ namespace ][ name ]; + constructor = $[ namespace ][ name ] = function( options, element ) { + // allow instantiation without "new" keyword + if ( !this._createWidget ) { + return new constructor( options, element ); + } + + // allow instantiation without initializing for simple inheritance + // must use "new" keyword (the code above always passes args) + if ( arguments.length ) { + this._createWidget( options, element ); + } + }; + // extend with the existing constructor to carry over any static properties + $.extend( constructor, existingConstructor, { + version: prototype.version, + // copy the object used to create the prototype in case we need to + // redefine the widget later + _proto: $.extend( {}, prototype ), + // track widgets that inherit from this widget in case this widget is + // redefined after a widget inherits from it + _childConstructors: [] + }); + + basePrototype = new base(); + // we need to make the options hash a property directly on the new instance + // otherwise we'll modify the options hash on the prototype that we're + // inheriting from + basePrototype.options = $.widget.extend( {}, basePrototype.options ); + $.each( prototype, function( prop, value ) { + if ( !$.isFunction( value ) ) { + proxiedPrototype[ prop ] = value; + return; + } + proxiedPrototype[ prop ] = (function() { + var _super = function() { + return base.prototype[ prop ].apply( this, arguments ); + }, + _superApply = function( args ) { + return base.prototype[ prop ].apply( this, args ); + }; + return function() { + var __super = this._super, + __superApply = this._superApply, + returnValue; + + this._super = _super; + this._superApply = _superApply; + + returnValue = value.apply( this, arguments ); + + this._super = __super; + this._superApply = __superApply; + + return returnValue; + }; + })(); + }); + constructor.prototype = $.widget.extend( basePrototype, { + // TODO: remove support for widgetEventPrefix + // always use the name + a colon as the prefix, e.g., draggable:start + // don't prefix for widgets that aren't DOM-based + widgetEventPrefix: existingConstructor ? basePrototype.widgetEventPrefix : name + }, proxiedPrototype, { + constructor: constructor, + namespace: namespace, + widgetName: name, + widgetFullName: fullName + }); + + // If this widget is being redefined then we need to find all widgets that + // are inheriting from it and redefine all of them so that they inherit from + // the new version of this widget. We're essentially trying to replace one + // level in the prototype chain. + if ( existingConstructor ) { + $.each( existingConstructor._childConstructors, function( i, child ) { + var childPrototype = child.prototype; + + // redefine the child widget using the same prototype that was + // originally used, but inherit from the new version of the base + $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto ); + }); + // remove the list of existing child constructors from the old constructor + // so the old child constructors can be garbage collected + delete existingConstructor._childConstructors; + } else { + base._childConstructors.push( constructor ); + } + + $.widget.bridge( name, constructor ); +}; + +$.widget.extend = function( target ) { + var input = slice.call( arguments, 1 ), + inputIndex = 0, + inputLength = input.length, + key, + value; + for ( ; inputIndex < inputLength; inputIndex++ ) { + for ( key in input[ inputIndex ] ) { + value = input[ inputIndex ][ key ]; + if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { + // Clone objects + if ( $.isPlainObject( value ) ) { + target[ key ] = $.isPlainObject( target[ key ] ) ? + $.widget.extend( {}, target[ key ], value ) : + // Don't extend strings, arrays, etc. with objects + $.widget.extend( {}, value ); + // Copy everything else by reference + } else { + target[ key ] = value; + } + } + } + } + return target; +}; + +$.widget.bridge = function( name, object ) { + var fullName = object.prototype.widgetFullName || name; + $.fn[ name ] = function( options ) { + var isMethodCall = typeof options === "string", + args = slice.call( arguments, 1 ), + returnValue = this; + + // allow multiple hashes to be passed on init + options = !isMethodCall && args.length ? + $.widget.extend.apply( null, [ options ].concat(args) ) : + options; + + if ( isMethodCall ) { + this.each(function() { + var methodValue, + instance = $.data( this, fullName ); + if ( !instance ) { + return $.error( "cannot call methods on " + name + " prior to initialization; " + + "attempted to call method '" + options + "'" ); + } + if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) { + return $.error( "no such method '" + options + "' for " + name + " widget instance" ); + } + methodValue = instance[ options ].apply( instance, args ); + if ( methodValue !== instance && methodValue !== undefined ) { + returnValue = methodValue && methodValue.jquery ? + returnValue.pushStack( methodValue.get() ) : + methodValue; + return false; + } + }); + } else { + this.each(function() { + var instance = $.data( this, fullName ); + if ( instance ) { + instance.option( options || {} )._init(); + } else { + $.data( this, fullName, new object( options, this ) ); + } + }); + } + + return returnValue; + }; +}; + +$.Widget = function( /* options, element */ ) {}; +$.Widget._childConstructors = []; + +$.Widget.prototype = { + widgetName: "widget", + widgetEventPrefix: "", + defaultElement: "
", + options: { + disabled: false, + + // callbacks + create: null + }, + _createWidget: function( options, element ) { + element = $( element || this.defaultElement || this )[ 0 ]; + this.element = $( element ); + this.uuid = uuid++; + this.eventNamespace = "." + this.widgetName + this.uuid; + this.options = $.widget.extend( {}, + this.options, + this._getCreateOptions(), + options ); + + this.bindings = $(); + this.hoverable = $(); + this.focusable = $(); + + if ( element !== this ) { + $.data( element, this.widgetFullName, this ); + this._on( true, this.element, { + remove: function( event ) { + if ( event.target === element ) { + this.destroy(); + } + } + }); + this.document = $( element.style ? + // element within the document + element.ownerDocument : + // element is window or document + element.document || element ); + this.window = $( this.document[0].defaultView || this.document[0].parentWindow ); + } + + this._create(); + this._trigger( "create", null, this._getCreateEventData() ); + this._init(); + }, + _getCreateOptions: $.noop, + _getCreateEventData: $.noop, + _create: $.noop, + _init: $.noop, + + destroy: function() { + this._destroy(); + // we can probably remove the unbind calls in 2.0 + // all event bindings should go through this._on() + this.element + .unbind( this.eventNamespace ) + // 1.9 BC for #7810 + // TODO remove dual storage + .removeData( this.widgetName ) + .removeData( this.widgetFullName ) + // support: jquery <1.6.3 + // http://bugs.jquery.com/ticket/9413 + .removeData( $.camelCase( this.widgetFullName ) ); + this.widget() + .unbind( this.eventNamespace ) + .removeAttr( "aria-disabled" ) + .removeClass( + this.widgetFullName + "-disabled " + + "ui-state-disabled" ); + + // clean up events and states + this.bindings.unbind( this.eventNamespace ); + this.hoverable.removeClass( "ui-state-hover" ); + this.focusable.removeClass( "ui-state-focus" ); + }, + _destroy: $.noop, + + widget: function() { + return this.element; + }, + + option: function( key, value ) { + var options = key, + parts, + curOption, + i; + + if ( arguments.length === 0 ) { + // don't return a reference to the internal hash + return $.widget.extend( {}, this.options ); + } + + if ( typeof key === "string" ) { + // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } + options = {}; + parts = key.split( "." ); + key = parts.shift(); + if ( parts.length ) { + curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); + for ( i = 0; i < parts.length - 1; i++ ) { + curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; + curOption = curOption[ parts[ i ] ]; + } + key = parts.pop(); + if ( value === undefined ) { + return curOption[ key ] === undefined ? null : curOption[ key ]; + } + curOption[ key ] = value; + } else { + if ( value === undefined ) { + return this.options[ key ] === undefined ? null : this.options[ key ]; + } + options[ key ] = value; + } + } + + this._setOptions( options ); + + return this; + }, + _setOptions: function( options ) { + var key; + + for ( key in options ) { + this._setOption( key, options[ key ] ); + } + + return this; + }, + _setOption: function( key, value ) { + this.options[ key ] = value; + + if ( key === "disabled" ) { + this.widget() + .toggleClass( this.widgetFullName + "-disabled ui-state-disabled", !!value ) + .attr( "aria-disabled", value ); + this.hoverable.removeClass( "ui-state-hover" ); + this.focusable.removeClass( "ui-state-focus" ); + } + + return this; + }, + + enable: function() { + return this._setOption( "disabled", false ); + }, + disable: function() { + return this._setOption( "disabled", true ); + }, + + _on: function( suppressDisabledCheck, element, handlers ) { + var delegateElement, + instance = this; + + // no suppressDisabledCheck flag, shuffle arguments + if ( typeof suppressDisabledCheck !== "boolean" ) { + handlers = element; + element = suppressDisabledCheck; + suppressDisabledCheck = false; + } + + // no element argument, shuffle and use this.element + if ( !handlers ) { + handlers = element; + element = this.element; + delegateElement = this.widget(); + } else { + // accept selectors, DOM elements + element = delegateElement = $( element ); + this.bindings = this.bindings.add( element ); + } + + $.each( handlers, function( event, handler ) { + function handlerProxy() { + // allow widgets to customize the disabled handling + // - disabled as an array instead of boolean + // - disabled class as method for disabling individual parts + if ( !suppressDisabledCheck && + ( instance.options.disabled === true || + $( this ).hasClass( "ui-state-disabled" ) ) ) { + return; + } + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + + // copy the guid so direct unbinding works + if ( typeof handler !== "string" ) { + handlerProxy.guid = handler.guid = + handler.guid || handlerProxy.guid || $.guid++; + } + + var match = event.match( /^(\w+)\s*(.*)$/ ), + eventName = match[1] + instance.eventNamespace, + selector = match[2]; + if ( selector ) { + delegateElement.delegate( selector, eventName, handlerProxy ); + } else { + element.bind( eventName, handlerProxy ); + } + }); + }, + + _off: function( element, eventName ) { + eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + this.eventNamespace; + element.unbind( eventName ).undelegate( eventName ); + }, + + _delay: function( handler, delay ) { + function handlerProxy() { + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + var instance = this; + return setTimeout( handlerProxy, delay || 0 ); + }, + + _hoverable: function( element ) { + this.hoverable = this.hoverable.add( element ); + this._on( element, { + mouseenter: function( event ) { + $( event.currentTarget ).addClass( "ui-state-hover" ); + }, + mouseleave: function( event ) { + $( event.currentTarget ).removeClass( "ui-state-hover" ); + } + }); + }, + + _focusable: function( element ) { + this.focusable = this.focusable.add( element ); + this._on( element, { + focusin: function( event ) { + $( event.currentTarget ).addClass( "ui-state-focus" ); + }, + focusout: function( event ) { + $( event.currentTarget ).removeClass( "ui-state-focus" ); + } + }); + }, + + _trigger: function( type, event, data ) { + var prop, orig, + callback = this.options[ type ]; + + data = data || {}; + event = $.Event( event ); + event.type = ( type === this.widgetEventPrefix ? + type : + this.widgetEventPrefix + type ).toLowerCase(); + // the original event may come from any element + // so we need to reset the target on the new event + event.target = this.element[ 0 ]; + + // copy original event properties over to the new event + orig = event.originalEvent; + if ( orig ) { + for ( prop in orig ) { + if ( !( prop in event ) ) { + event[ prop ] = orig[ prop ]; + } + } + } + + this.element.trigger( event, data ); + return !( $.isFunction( callback ) && + callback.apply( this.element[0], [ event ].concat( data ) ) === false || + event.isDefaultPrevented() ); + } +}; + +$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { + $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { + if ( typeof options === "string" ) { + options = { effect: options }; + } + var hasOptions, + effectName = !options ? + method : + options === true || typeof options === "number" ? + defaultEffect : + options.effect || defaultEffect; + options = options || {}; + if ( typeof options === "number" ) { + options = { duration: options }; + } + hasOptions = !$.isEmptyObject( options ); + options.complete = callback; + if ( options.delay ) { + element.delay( options.delay ); + } + if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { + element[ method ]( options ); + } else if ( effectName !== method && element[ effectName ] ) { + element[ effectName ]( options.duration, options.easing, callback ); + } else { + element.queue(function( next ) { + $( this )[ method ](); + if ( callback ) { + callback.call( element[ 0 ] ); + } + next(); + }); + } + }; +}); + +})); diff --git a/samples/PhotoAlbumCake/webroot/js/load-image.min.js b/samples/PhotoAlbumCake/webroot/js/load-image.min.js new file mode 100644 index 00000000..b58f4e9f --- /dev/null +++ b/samples/PhotoAlbumCake/webroot/js/load-image.min.js @@ -0,0 +1 @@ +(function(e){"use strict";var t=function(e,i,a){var n,r,o=document.createElement("img");if(o.onerror=i,o.onload=function(){!r||a&&a.noRevoke||t.revokeObjectURL(r),i&&i(t.scale(o,a))},t.isInstanceOf("Blob",e)||t.isInstanceOf("File",e))n=r=t.createObjectURL(e),o._type=e.type;else{if("string"!=typeof e)return!1;n=e,a&&a.crossOrigin&&(o.crossOrigin=a.crossOrigin)}return n?(o.src=n,o):t.readFile(e,function(e){var t=e.target;t&&t.result?o.src=t.result:i&&i(e)})},i=window.createObjectURL&&window||window.URL&&URL.revokeObjectURL&&URL||window.webkitURL&&webkitURL;t.isInstanceOf=function(e,t){return Object.prototype.toString.call(t)==="[object "+e+"]"},t.transformCoordinates=function(e,t){var i=e.getContext("2d"),a=e.width,n=e.height;switch(t>4&&(e.width=n,e.height=a),t){case 2:i.translate(a,0),i.scale(-1,1);break;case 3:i.translate(a,n),i.rotate(Math.PI);break;case 4:i.translate(0,n),i.scale(1,-1);break;case 5:i.rotate(.5*Math.PI),i.scale(1,-1);break;case 6:i.rotate(.5*Math.PI),i.translate(0,-n);break;case 7:i.rotate(.5*Math.PI),i.translate(a,-n),i.scale(-1,1);break;case 8:i.rotate(-.5*Math.PI),i.translate(-a,0)}},t.renderImageToCanvas=function(e,t,i,a,n,r,o,s,d,l){return e.getContext("2d").drawImage(t,i,a,n,r,o,s,d,l),e},t.scale=function(e,i){i=i||{};var a,n,r,o,s,d,l,c=document.createElement("canvas"),u=e.getContext||(i.canvas||i.crop||i.orientation)&&c.getContext,g=e.width,f=e.height,h=g,m=f,p=0,S=0,x=0,b=0;return u&&i.orientation>4?(a=i.maxHeight,n=i.maxWidth,r=i.minHeight,o=i.minWidth):(a=i.maxWidth,n=i.maxHeight,r=i.minWidth,o=i.minHeight),u&&a&&n&&i.crop?(s=a,d=n,a/n>g/f?(m=n*g/a,S=(f-m)/2):(h=a*f/n,p=(g-h)/2)):(s=g,d=f,l=Math.max((r||s)/s,(o||d)/d),l>1&&(s=Math.ceil(s*l),d=Math.ceil(d*l)),l=Math.min((a||s)/s,(n||d)/d),1>l&&(s=Math.ceil(s*l),d=Math.ceil(d*l))),u?(c.width=s,c.height=d,t.transformCoordinates(c,i.orientation),t.renderImageToCanvas(c,e,p,S,h,m,x,b,s,d)):(e.width=s,e.height=d,e)},t.createObjectURL=function(e){return i?i.createObjectURL(e):!1},t.revokeObjectURL=function(e){return i?i.revokeObjectURL(e):!1},t.readFile=function(e,t,i){if(window.FileReader){var a=new FileReader;if(a.onload=a.onerror=t,i=i||"readAsDataURL",a[i])return a[i](e),a}return!1},"function"==typeof define&&define.amd?define(function(){return t}):e.loadImage=t})(this),function(e){"use strict";"function"==typeof define&&define.amd?define(["load-image"],e):e(window.loadImage)}(function(e){"use strict";if(window.navigator&&window.navigator.platform&&/iP(hone|od|ad)/.test(window.navigator.platform)){var t=e.renderImageToCanvas;e.detectSubsampling=function(e){var t,i;return e.width*e.height>1048576?(t=document.createElement("canvas"),t.width=t.height=1,i=t.getContext("2d"),i.drawImage(e,-e.width+1,0),0===i.getImageData(0,0,1,1).data[3]):!1},e.detectVerticalSquash=function(e,t){var i,a,n,r,o,s=document.createElement("canvas"),d=s.getContext("2d");for(s.width=1,s.height=t,d.drawImage(e,0,0),i=d.getImageData(0,0,1,t).data,a=0,n=t,r=t;r>a;)o=i[4*(r-1)+3],0===o?n=r:a=r,r=n+a>>1;return r/t||1},e.renderImageToCanvas=function(i,a,n,r,o,s,d,l,c,u){if("image/jpeg"===a._type){var g,f,h,m,p=i.getContext("2d"),S=document.createElement("canvas"),x=1024,b=S.getContext("2d");if(S.width=x,S.height=x,p.save(),g=e.detectSubsampling(a),g&&(o/=2,s/=2),f=e.detectVerticalSquash(a,s),g&&1!==f){for(c=Math.ceil(x*c/o),u=Math.ceil(x*u/s/f),l=0,m=0;s>m;){for(d=0,h=0;o>h;)b.clearRect(0,0,x,x),b.drawImage(a,n,r,o,s,-h,-m,o,s),p.drawImage(S,0,0,x,x,d,l,c,u),h+=x,d+=c;m+=x,l+=u}return p.restore(),i}}return t(i,a,n,r,o,s,d,l,c,u)}}}),function(e){"use strict";"function"==typeof define&&define.amd?define(["load-image"],e):e(window.loadImage)}(function(e){"use strict";var t=window.Blob&&(Blob.prototype.slice||Blob.prototype.webkitSlice||Blob.prototype.mozSlice);e.blobSlice=t&&function(){var e=this.slice||this.webkitSlice||this.mozSlice;return e.apply(this,arguments)},e.metaDataParsers={jpeg:{65505:[]}},e.parseMetaData=function(t,i,a){a=a||{};var n=this,r={},o=!(window.DataView&&t&&t.size>=12&&"image/jpeg"===t.type&&e.blobSlice);(o||!e.readFile(e.blobSlice.call(t,0,131072),function(t){var o,s,d,l,c=t.target.result,u=new DataView(c),g=2,f=u.byteLength-4,h=g;if(65496===u.getUint16(0)){for(;f>g&&(o=u.getUint16(g),o>=65504&&65519>=o||65534===o);)if(s=u.getUint16(g+2)+2,g+s>u.byteLength)console.log("Invalid meta data: Invalid segment size.");else{if(d=e.metaDataParsers.jpeg[o])for(l=0;d.length>l;l+=1)d[l].call(n,u,g,s,r,a);g+=s,h=g}!a.disableImageHead&&h>6&&(r.imageHead=c.slice?c.slice(0,h):new Uint8Array(c).subarray(0,h))}else console.log("Invalid JPEG file: Missing JPEG marker.");i(r)},"readAsArrayBuffer"))&&i(r)}}),function(e){"use strict";"function"==typeof define&&define.amd?define(["load-image","load-image-meta"],e):e(window.loadImage)}(function(e){"use strict";e.ExifMap=function(){return this},e.ExifMap.prototype.map={Orientation:274},e.ExifMap.prototype.get=function(e){return this[e]||this[this.map[e]]},e.getExifThumbnail=function(e,t,i){var a,n,r;if(!i||t+i>e.byteLength)return console.log("Invalid Exif data: Invalid thumbnail data."),void 0;for(a=[],n=0;i>n;n+=1)r=e.getUint8(t+n),a.push((16>r?"0":"")+r.toString(16));return"data:image/jpeg,%"+a.join("%")},e.exifTagTypes={1:{getValue:function(e,t){return e.getUint8(t)},size:1},2:{getValue:function(e,t){return String.fromCharCode(e.getUint8(t))},size:1,ascii:!0},3:{getValue:function(e,t,i){return e.getUint16(t,i)},size:2},4:{getValue:function(e,t,i){return e.getUint32(t,i)},size:4},5:{getValue:function(e,t,i){return e.getUint32(t,i)/e.getUint32(t+4,i)},size:8},9:{getValue:function(e,t,i){return e.getInt32(t,i)},size:4},10:{getValue:function(e,t,i){return e.getInt32(t,i)/e.getInt32(t+4,i)},size:8}},e.exifTagTypes[7]=e.exifTagTypes[1],e.getExifValue=function(t,i,a,n,r,o){var s,d,l,c,u,g,f=e.exifTagTypes[n];if(!f)return console.log("Invalid Exif data: Invalid tag type."),void 0;if(s=f.size*r,d=s>4?i+t.getUint32(a+8,o):a+8,d+s>t.byteLength)return console.log("Invalid Exif data: Invalid data offset."),void 0;if(1===r)return f.getValue(t,d,o);for(l=[],c=0;r>c;c+=1)l[c]=f.getValue(t,d+c*f.size,o);if(f.ascii){for(u="",c=0;l.length>c&&(g=l[c],"\0"!==g);c+=1)u+=g;return u}return l},e.parseExifTag=function(t,i,a,n,r){var o=t.getUint16(a,n);r.exif[o]=e.getExifValue(t,i,a,t.getUint16(a+2,n),t.getUint32(a+4,n),n)},e.parseExifTags=function(e,t,i,a,n){var r,o,s;if(i+6>e.byteLength)return console.log("Invalid Exif data: Invalid directory offset."),void 0;if(r=e.getUint16(i,a),o=i+2+12*r,o+4>e.byteLength)return console.log("Invalid Exif data: Invalid directory size."),void 0;for(s=0;r>s;s+=1)this.parseExifTag(e,t,i+2+12*s,a,n);return e.getUint32(o,a)},e.parseExifData=function(t,i,a,n,r){if(!r.disableExif){var o,s,d,l=i+10;if(1165519206===t.getUint32(i+4)){if(l+8>t.byteLength)return console.log("Invalid Exif data: Invalid segment size."),void 0;if(0!==t.getUint16(i+8))return console.log("Invalid Exif data: Missing byte alignment offset."),void 0;switch(t.getUint16(l)){case 18761:o=!0;break;case 19789:o=!1;break;default:return console.log("Invalid Exif data: Invalid byte alignment marker."),void 0}if(42!==t.getUint16(l+2,o))return console.log("Invalid Exif data: Missing TIFF marker."),void 0;s=t.getUint32(l+4,o),n.exif=new e.ExifMap,s=e.parseExifTags(t,l,l+s,o,n),s&&!r.disableExifThumbnail&&(d={exif:{}},s=e.parseExifTags(t,l,l+s,o,d),d.exif[513]&&(n.exif.Thumbnail=e.getExifThumbnail(t,l+d.exif[513],d.exif[514]))),n.exif[34665]&&!r.disableExifSub&&e.parseExifTags(t,l,l+n.exif[34665],o,n),n.exif[34853]&&!r.disableExifGps&&e.parseExifTags(t,l,l+n.exif[34853],o,n)}}},e.metaDataParsers.jpeg[65505].push(e.parseExifData)}),function(e){"use strict";"function"==typeof define&&define.amd?define(["load-image","load-image-exif"],e):e(window.loadImage)}(function(e){"use strict";var t,i,a;e.ExifMap.prototype.tags={256:"ImageWidth",257:"ImageHeight",34665:"ExifIFDPointer",34853:"GPSInfoIFDPointer",40965:"InteroperabilityIFDPointer",258:"BitsPerSample",259:"Compression",262:"PhotometricInterpretation",274:"Orientation",277:"SamplesPerPixel",284:"PlanarConfiguration",530:"YCbCrSubSampling",531:"YCbCrPositioning",282:"XResolution",283:"YResolution",296:"ResolutionUnit",273:"StripOffsets",278:"RowsPerStrip",279:"StripByteCounts",513:"JPEGInterchangeFormat",514:"JPEGInterchangeFormatLength",301:"TransferFunction",318:"WhitePoint",319:"PrimaryChromaticities",529:"YCbCrCoefficients",532:"ReferenceBlackWhite",306:"DateTime",270:"ImageDescription",271:"Make",272:"Model",305:"Software",315:"Artist",33432:"Copyright",36864:"ExifVersion",40960:"FlashpixVersion",40961:"ColorSpace",40962:"PixelXDimension",40963:"PixelYDimension",37121:"ComponentsConfiguration",37122:"CompressedBitsPerPixel",37500:"MakerNote",37510:"UserComment",40964:"RelatedSoundFile",36867:"DateTimeOriginal",36868:"DateTimeDigitized",37520:"SubsecTime",37521:"SubsecTimeOriginal",37522:"SubsecTimeDigitized",33434:"ExposureTime",33437:"FNumber",34850:"ExposureProgram",34852:"SpectralSensitivity",34855:"ISOSpeedRatings",34856:"OECF",37377:"ShutterSpeedValue",37378:"ApertureValue",37379:"BrightnessValue",37380:"ExposureBias",37381:"MaxApertureValue",37382:"SubjectDistance",37383:"MeteringMode",37384:"LightSource",37385:"Flash",37396:"SubjectArea",37386:"FocalLength",41483:"FlashEnergy",41484:"SpatialFrequencyResponse",41486:"FocalPlaneXResolution",41487:"FocalPlaneYResolution",41488:"FocalPlaneResolutionUnit",41492:"SubjectLocation",41493:"ExposureIndex",41495:"SensingMethod",41728:"FileSource",41729:"SceneType",41730:"CFAPattern",41985:"CustomRendered",41986:"ExposureMode",41987:"WhiteBalance",41988:"DigitalZoomRation",41989:"FocalLengthIn35mmFilm",41990:"SceneCaptureType",41991:"GainControl",41992:"Contrast",41993:"Saturation",41994:"Sharpness",41995:"DeviceSettingDescription",41996:"SubjectDistanceRange",42016:"ImageUniqueID",0:"GPSVersionID",1:"GPSLatitudeRef",2:"GPSLatitude",3:"GPSLongitudeRef",4:"GPSLongitude",5:"GPSAltitudeRef",6:"GPSAltitude",7:"GPSTimeStamp",8:"GPSSatellites",9:"GPSStatus",10:"GPSMeasureMode",11:"GPSDOP",12:"GPSSpeedRef",13:"GPSSpeed",14:"GPSTrackRef",15:"GPSTrack",16:"GPSImgDirectionRef",17:"GPSImgDirection",18:"GPSMapDatum",19:"GPSDestLatitudeRef",20:"GPSDestLatitude",21:"GPSDestLongitudeRef",22:"GPSDestLongitude",23:"GPSDestBearingRef",24:"GPSDestBearing",25:"GPSDestDistanceRef",26:"GPSDestDistance",27:"GPSProcessingMethod",28:"GPSAreaInformation",29:"GPSDateStamp",30:"GPSDifferential"},e.ExifMap.prototype.stringValues={ExposureProgram:{0:"Undefined",1:"Manual",2:"Normal program",3:"Aperture priority",4:"Shutter priority",5:"Creative program",6:"Action program",7:"Portrait mode",8:"Landscape mode"},MeteringMode:{0:"Unknown",1:"Average",2:"CenterWeightedAverage",3:"Spot",4:"MultiSpot",5:"Pattern",6:"Partial",255:"Other"},LightSource:{0:"Unknown",1:"Daylight",2:"Fluorescent",3:"Tungsten (incandescent light)",4:"Flash",9:"Fine weather",10:"Cloudy weather",11:"Shade",12:"Daylight fluorescent (D 5700 - 7100K)",13:"Day white fluorescent (N 4600 - 5400K)",14:"Cool white fluorescent (W 3900 - 4500K)",15:"White fluorescent (WW 3200 - 3700K)",17:"Standard light A",18:"Standard light B",19:"Standard light C",20:"D55",21:"D65",22:"D75",23:"D50",24:"ISO studio tungsten",255:"Other"},Flash:{0:"Flash did not fire",1:"Flash fired",5:"Strobe return light not detected",7:"Strobe return light detected",9:"Flash fired, compulsory flash mode",13:"Flash fired, compulsory flash mode, return light not detected",15:"Flash fired, compulsory flash mode, return light detected",16:"Flash did not fire, compulsory flash mode",24:"Flash did not fire, auto mode",25:"Flash fired, auto mode",29:"Flash fired, auto mode, return light not detected",31:"Flash fired, auto mode, return light detected",32:"No flash function",65:"Flash fired, red-eye reduction mode",69:"Flash fired, red-eye reduction mode, return light not detected",71:"Flash fired, red-eye reduction mode, return light detected",73:"Flash fired, compulsory flash mode, red-eye reduction mode",77:"Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected",79:"Flash fired, compulsory flash mode, red-eye reduction mode, return light detected",89:"Flash fired, auto mode, red-eye reduction mode",93:"Flash fired, auto mode, return light not detected, red-eye reduction mode",95:"Flash fired, auto mode, return light detected, red-eye reduction mode"},SensingMethod:{1:"Undefined",2:"One-chip color area sensor",3:"Two-chip color area sensor",4:"Three-chip color area sensor",5:"Color sequential area sensor",7:"Trilinear sensor",8:"Color sequential linear sensor"},SceneCaptureType:{0:"Standard",1:"Landscape",2:"Portrait",3:"Night scene"},SceneType:{1:"Directly photographed"},CustomRendered:{0:"Normal process",1:"Custom process"},WhiteBalance:{0:"Auto white balance",1:"Manual white balance"},GainControl:{0:"None",1:"Low gain up",2:"High gain up",3:"Low gain down",4:"High gain down"},Contrast:{0:"Normal",1:"Soft",2:"Hard"},Saturation:{0:"Normal",1:"Low saturation",2:"High saturation"},Sharpness:{0:"Normal",1:"Soft",2:"Hard"},SubjectDistanceRange:{0:"Unknown",1:"Macro",2:"Close view",3:"Distant view"},FileSource:{3:"DSC"},ComponentsConfiguration:{0:"",1:"Y",2:"Cb",3:"Cr",4:"R",5:"G",6:"B"},Orientation:{1:"top-left",2:"top-right",3:"bottom-right",4:"bottom-left",5:"left-top",6:"right-top",7:"right-bottom",8:"left-bottom"}},e.ExifMap.prototype.getText=function(e){var t=this.get(e);switch(e){case"LightSource":case"Flash":case"MeteringMode":case"ExposureProgram":case"SensingMethod":case"SceneCaptureType":case"SceneType":case"CustomRendered":case"WhiteBalance":case"GainControl":case"Contrast":case"Saturation":case"Sharpness":case"SubjectDistanceRange":case"FileSource":case"Orientation":return this.stringValues[e][t];case"ExifVersion":case"FlashpixVersion":return String.fromCharCode(t[0],t[1],t[2],t[3]);case"ComponentsConfiguration":return this.stringValues[e][t[0]]+this.stringValues[e][t[1]]+this.stringValues[e][t[2]]+this.stringValues[e][t[3]];case"GPSVersionID":return t[0]+"."+t[1]+"."+t[2]+"."+t[3]}return t+""},t=e.ExifMap.prototype.tags,i=e.ExifMap.prototype.map;for(a in t)t.hasOwnProperty(a)&&(i[t[a]]=a);e.ExifMap.prototype.getAll=function(){var e,i,a={};for(e in this)this.hasOwnProperty(e)&&(i=t[e],i&&(a[i]=this.getText(i)));return a}}); \ No newline at end of file From e39e7fdd506de53b42819c14d9fa4ef2921e7b1e Mon Sep 17 00:00:00 2001 From: Alon Hammerman Date: Thu, 5 Dec 2013 16:46:47 -0600 Subject: [PATCH 16/24] PhotoAlbumCake - Replace FormHelper with CloudinaryHelper --- .../Controller/PhotosController.php | 9 ++++++++- samples/PhotoAlbumCake/View/Photos/edit.ctp | 18 ++++++++++-------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/samples/PhotoAlbumCake/Controller/PhotosController.php b/samples/PhotoAlbumCake/Controller/PhotosController.php index d9876012..1f6fa996 100644 --- a/samples/PhotoAlbumCake/Controller/PhotosController.php +++ b/samples/PhotoAlbumCake/Controller/PhotosController.php @@ -14,7 +14,14 @@ class PhotosController extends AppController { * @var array */ public $components = array('Paginator'); - public $helpers = array('Html', 'Form', 'CloudinaryCake.Cloudinary'); + +/** + * Helpers + * + * @var array + */ + #public $helpers = array('Html', 'Form', 'CloudinaryCake.Cloudinary'); + public $helpers = array('Html', 'Form' => array('className' => 'CloudinaryCake.Cloudinary')); /** * index method diff --git a/samples/PhotoAlbumCake/View/Photos/edit.ctp b/samples/PhotoAlbumCake/View/Photos/edit.ctp index 3cc4a488..7bcba3f0 100644 --- a/samples/PhotoAlbumCake/View/Photos/edit.ctp +++ b/samples/PhotoAlbumCake/View/Photos/edit.ctp @@ -2,30 +2,32 @@ # Include jQuery echo $this->Html->script('//code.jquery.com/jquery-1.10.1.min.js'); # Include cloudinary_js dependencies (requires jQuery) - echo $this->Cloudinary->cloudinary_includes(); + echo $this->Form->cloudinary_includes(); # Setup cloudinary_js using the current cloudinary_php configuration echo cloudinary_js_config(); ?>
-Cloudinary->create('Photo', array('type' => 'file')); ?>
+Form->create('Photo', array('type' => 'file')); ?> Cloudinary->input('id'); + echo $this->Form->input('id'); + # Backend upload: - echo $this->Cloudinary->input('cloudinaryIdentifier'); + echo $this->Form->input('cloudinaryIdentifier'); # Direct upload: - #echo $this->Cloudinary->input('cloudinaryIdentifier', array("type" => "direct_upload")); - echo $this->Cloudinary->input('moderated'); + #echo $this->Form->input('cloudinaryIdentifier', array("type" => "direct_upload")); + + echo $this->Form->input('moderated'); ?>
-Cloudinary->end(__('Submit')); ?> +Form->end(__('Submit')); ?>

    -
  • Cloudinary->postLink(__('Delete'), array('action' => 'delete', $this->Cloudinary->value('Photo.id')), null, __('Are you sure you want to delete # %s?', $this->Cloudinary->value('Photo.id'))); ?>
  • +
  • Form->postLink(__('Delete'), array('action' => 'delete', $this->Form->value('Photo.id')), null, __('Are you sure you want to delete # %s?', $this->Form->value('Photo.id'))); ?>
  • Html->link(__('List Photos'), array('action' => 'index')); ?>
From d1b24e3061d26130e5e4cd8f59c4bcb9074fb07d Mon Sep 17 00:00:00 2001 From: Alon Hammerman Date: Thu, 5 Dec 2013 16:47:20 -0600 Subject: [PATCH 17/24] cake_plugin - better loading mechanism --- .../Config/IncludeCloudinary.php | 11 ------- .../CloudinaryCake/Config/bootstrap.php | 26 +++++++++++++++ .../Model/Behavior/CloudinaryBehavior.php | 1 - .../View/Helper/CloudinaryHelper.php | 1 - cake_plugin/CloudinaryCakeLoader.php | 32 +++++++++++++++++++ composer.json | 1 + samples/PhotoAlbumCake/Config/bootstrap.php | 15 +++------ 7 files changed, 64 insertions(+), 23 deletions(-) delete mode 100644 cake_plugin/CloudinaryCake/Config/IncludeCloudinary.php create mode 100644 cake_plugin/CloudinaryCake/Config/bootstrap.php create mode 100644 cake_plugin/CloudinaryCakeLoader.php diff --git a/cake_plugin/CloudinaryCake/Config/IncludeCloudinary.php b/cake_plugin/CloudinaryCake/Config/IncludeCloudinary.php deleted file mode 100644 index bd30b7f3..00000000 --- a/cake_plugin/CloudinaryCake/Config/IncludeCloudinary.php +++ /dev/null @@ -1,11 +0,0 @@ - true, 'routes' => false, + 'path' => __DIR__ . DS . 'CloudinaryCake' . DS)); + } + + private static function fixAutoload() { + // Remove and re-prepend CakePHP's autoloader as composer thinks it is the most important. + // See https://github.com/composer/composer/commit/c80cb76b9b5082ecc3e5b53b1050f76bb27b127b + spl_autoload_unregister(array('App', 'load')); + spl_autoload_register(array('App', 'load'), true, true); + } +} diff --git a/composer.json b/composer.json index bd5dd291..c8c9208c 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "issues": "https://github.com/cloudinary/cloudinary_php/issues" }, "autoload": { + "psr-0": { "": "cake_plugin" }, "classmap": ["src"] } } diff --git a/samples/PhotoAlbumCake/Config/bootstrap.php b/samples/PhotoAlbumCake/Config/bootstrap.php index 2d780096..ecf34ced 100644 --- a/samples/PhotoAlbumCake/Config/bootstrap.php +++ b/samples/PhotoAlbumCake/Config/bootstrap.php @@ -43,7 +43,6 @@ * )); * */ -App::build(array('Plugin' => APP . '..' . DS . '..' . DS . 'cake_plugin' . DS)); /** * Custom Inflector rules, can be set to correctly pluralize or singularize table, model, controller names or whatever other @@ -100,12 +99,8 @@ 'file' => 'error', )); -CakePlugin::load('CloudinaryCake', array('bootstrap' => false, 'routes' => false)); -Configure::load('CloudinaryCake.IncludeCloudinary'); -try { - Configure::load('private'); - \Cloudinary::config(Configure::read('cloudinary')); -} catch (Exception $e) { - $result = Configure::configured('default'); - $this->assertTrue($result); -} +CakePlugin::load('CloudinaryCake', array('bootstrap' => true, 'routes' => false, + 'path' => APP . '..' . DS . '..' . DS . 'cake_plugin' . DS . 'CloudinaryCake' . DS)); + +Configure::load('private'); +\Cloudinary::config(Configure::read('cloudinary')); From abc294bd517f9f233d3206aefdf530d077fbc1c7 Mon Sep 17 00:00:00 2001 From: Alon Hammerman Date: Wed, 4 Dec 2013 19:07:18 -0600 Subject: [PATCH 18/24] Cake - Create documentations --- README.md | 3 + cake_plugin/CloudinaryCake/README.md | 168 +++++++++++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 cake_plugin/CloudinaryCake/README.md diff --git a/README.md b/README.md index 87999112..f87160df 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,9 @@ For PHP, Cloudinary provides an extension for simplifying the integration even f ![](http://res.cloudinary.com/cloudinary/image/upload/see_more_bullet.png) **Take a look at our [Getting started guide for PHP](http://cloudinary.com/documentation/php_integration#getting_started_guide)**. +## CakePHP ## +Special support to the cake PHP is included. You can access the code, installation and usage information [in the `cake_plugin/CloudinaryCake/README.md` file](https://github.com/cloudinary/cloudinary_php/tree/master/cake_plugin/CloudinaryCake) + ## Setup ###################################################################### Download cloudinary_php from [here](https://github.com/cloudinary/cloudinary_php/tarball/master) diff --git a/cake_plugin/CloudinaryCake/README.md b/cake_plugin/CloudinaryCake/README.md new file mode 100644 index 00000000..a7d08ab7 --- /dev/null +++ b/cake_plugin/CloudinaryCake/README.md @@ -0,0 +1,168 @@ +Cloudinary CakePHP plugin +========================= + +Cloudinary CakePHP plugin provides seemless integration of Cloudinary services with CakePHP framework for simple and efficient management of applications images + +Explore the [PhotoAlbumCake sample](https://github.com/cloudinary/cloudinary_php/tree/master/samples/PhotoAlbumCake) for usage example. + +## Requirements +* PHP 5.3 or higher +* CakePHP 2.x + +## Installlation +### Manual +1. Create a CakePHP project +1. Download cloudinary\_php from [here](https://github.com/cloudinary/cloudinary_php/tarball/master) +1. Extract the cloudinary\_php archive into vendors library +1. Configure cloudinary + 1. Environment variable - `export CLOUDINARY\_URL = "cloudinary://API_KEY:API_SECRET@CLOUD_NAME"` ([Check your settings in Cloudinary console](https://cloudinary.com/console)) + 1. Create `app/Config/private.php` using `vendor/cloudinary_php/samples/PhotoAlbumCake/Config/private.php.sample` +1. Load the cloudinary plugin by adding the following lines to `app/Config/bootstrap.php`: + + // Load plugin + CakePlugin::load('CloudinaryCake', array('bootstrap' => true, 'routes' => false, + 'path' => ROOT . DS 'vendor' . DS 'cloudinary_php' . DS . 'cake_plugin' . DS . 'CloudinaryCake' . DS)); + + // required when using `private.php` for cloudinary configuration + Configure::load('private'); + \Cloudinary::config(Configure::read('cloudinary')); + +### Composer +1. Create a new directory for myapp + + mkdir myapp + cd myapp + +1. Install CakePHP using composer ([based on CakePHP Cookbook](http://book.cakephp.org/2.0/en/installation/advanced-installation.html#installing-cakephp-with-composer) + 1. Setup Composer and get CakePHP: + + echo '{}' > composer.json + composer config vendor-dir Vendor + composer config repositories.0 pear 'http://pear.cakephp.org' + composer require 'pear-cakephp/cakephp:>=2.4.0' + + 1. Bake a new project + + vendor/bin/cake bake project . + + 1. You may define `CAKE_CORE_INCLUDE_PATH` to a relative path as suggested in the cookbook by adding the following to `webroot/index.php`: + + define( + 'CAKE_CORE_INCLUDE_PATH', + ROOT . DS . APP_DIR . '/Vendor/pear-pear.cakephp.org/CakePHP' + ) + + 1. Add the following lines to `Config/bootstrap.php`: + + // Load composer autoload. + require APP . '/Vendor/autoload.php'; + + // Auto load CloudinaryCake plugin + \CloudinaryCakeLoader::load(); + + +1. Install Cloudinary + + composer require 'cloudinary/cloudinary_php:>=1.0.8' + +1. Configure Cloudinary using the `CLOUDINARY_URL` environment variable, or the `Config/private.php` configuration file + +## Usage + +### CloudinaryBehavior +CloudinaryBehavior adds Cloudinary support for CakePHP Models. It helps storing references to cloudinary images in a simple text field of your model. + +#### Setup +Assuming you have a `Photo` model with `cloudinaryIdentifier` text field for storing cloudinary images references - you can add the following code to your model + +`Models/photo.php`: + + [...] + class Photo extends AppModel { + public $actsAs = array('CloudinaryCake.Cloudinary' => array('fields' => array('cloudinaryIdentifier'))); + [...] + } + +#### Usage +This will allow you to access the `cloudinaryIdentifier` as a CloudinaryField. Here's a sample controller code - + +`Controller/PhotosController.php`: + + class PhotosController extends AppController { + [...] + // set the specified Photo's image to the default one + public function set_default_image($id) { + $options = array('conditions' => array('Photo.' . $this->Photo->primaryKey => $id)); + $photo = $this->Photo->find('first', $options); + + $photo['Photo']['cloudinaryIdentifier']->upload(DEFAULT_IMAGE_PATH); + $this->Photo->save($photo); + } + + [...] + // Creates a new image from post data. Sets $image_url to the cloudinary url of the image with the given transformation. + public function add() { + $this->Photo->create(); + $success = $this->Photo->save($this->request->data); + if ($success) { + $image_url = $this->Photo->data['Photo']['cloudinaryIdentifier']->url(array( + "width" => 100, "height" => 100, "crop" => "fill")); + } + $this->set('photo', $this->Photo->data); + } + [...] + } + +### CloudinaryHelper +CloudinaryHelper is an extension of the CakePHP InputHelper. It can be used for loading cloudinary\_js, presenting images, creating forms with image inputs and more. + +#### Setup +You can load CloudinaryHelper using two methods - + +`Controller/PhotosController.php`: + + [...] + class PhotosController extends AppController { + // Replace the FormHelper with CloudinaryHelper (recommended - accessible as $this->Form) + public $helpers = array('Html', 'Form' => array('className' => 'CloudinaryCake.Cloudinary')); + + // Add CloudinaryHelper in addition to the default FormHelper (accessible as $this->Cloudinary instead of $this->Form) + //public $helpers = array('Html', 'Form', 'CloudinaryCake.Cloudinary'); + [...] + } + +#### Usage +You then can use it in any view of the controller: + +`View/Layouts/default.ctp`: + + [...] + + [...] + # Include cloudinary_js dependencies (requires jQuery) + echo $this->Form->cloudinary_includes(); + # Setup cloudinary_js using the current cloudinary_php configuration + echo cloudinary_js_config(); + [...] + + [...] + +`View/Photos/add.ctp`: + + [...] + + Form->cl_image_tag($photo['Photo']['cloudinaryIdentifier'], + array("width" => 60, "height" => 60, "crop" => "thumb", "gravity" => "face")); ?> + + Form->create('Photo', array('type' => 'file')); ?> + + Form->input('id'); + # Backend upload: + echo $this->Form->input('cloudinaryIdentifier'); + # Direct upload: + #echo $this->Form->input('cloudinaryIdentifier', array("type" => "direct_upload")); + ?> + Form->end(__('Submit')); ?> + [...] + From 2e68039d2fbf0d5340abae9e2ca57d8d140b940c Mon Sep 17 00:00:00 2001 From: Alon Hammerman Date: Sat, 7 Dec 2013 16:47:39 -0600 Subject: [PATCH 19/24] PhotoAlbumCake - Add list_photos action/view --- .../Controller/PhotosController.php | 11 ++ .../View/Layouts/photoalbum.ctp | 59 ++++++++++ .../View/Photos/list_photos.ctp | 107 ++++++++++++++++++ .../PhotoAlbumCake/webroot/css/photoalbum.css | 38 +++++++ 4 files changed, 215 insertions(+) create mode 100644 samples/PhotoAlbumCake/View/Layouts/photoalbum.ctp create mode 100644 samples/PhotoAlbumCake/View/Photos/list_photos.ctp create mode 100644 samples/PhotoAlbumCake/webroot/css/photoalbum.css diff --git a/samples/PhotoAlbumCake/Controller/PhotosController.php b/samples/PhotoAlbumCake/Controller/PhotosController.php index 1f6fa996..163e8fda 100644 --- a/samples/PhotoAlbumCake/Controller/PhotosController.php +++ b/samples/PhotoAlbumCake/Controller/PhotosController.php @@ -23,6 +23,17 @@ class PhotosController extends AppController { #public $helpers = array('Html', 'Form', 'CloudinaryCake.Cloudinary'); public $helpers = array('Html', 'Form' => array('className' => 'CloudinaryCake.Cloudinary')); + +/** + * list method + * + * @return void + */ + public function list_photos() { + $this->Photo->recursive = 0; + $this->set('photos', $this->Paginator->paginate()); + } + /** * index method * diff --git a/samples/PhotoAlbumCake/View/Layouts/photoalbum.ctp b/samples/PhotoAlbumCake/View/Layouts/photoalbum.ctp new file mode 100644 index 00000000..5d5def6c --- /dev/null +++ b/samples/PhotoAlbumCake/View/Layouts/photoalbum.ctp @@ -0,0 +1,59 @@ + + + + + Html->charset(); ?> + PhotoAlubm - <?php echo $title_for_layout; ?> + Html->meta('favicon', cloudinary_url("http://cloudinary.com/favicon.png", + array("type" => "fetch")), array('type' => 'icon')); + echo $this->Html->script('//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js'); + + echo $this->Html->css('photoalbum'); + + echo $this->fetch('meta'); + echo $this->fetch('css'); + echo $this->fetch('script'); + ?> + + +
+ +
+ + Session->flash(); ?> + + fetch('content'); ?> +
+
+ element('sql_dump'); ?> + + diff --git a/samples/PhotoAlbumCake/View/Photos/list_photos.ctp b/samples/PhotoAlbumCake/View/Photos/list_photos.ctp new file mode 100644 index 00000000..e56aab47 --- /dev/null +++ b/samples/PhotoAlbumCake/View/Photos/list_photos.ctp @@ -0,0 +1,107 @@ +layout = 'photoalbum'; +$thumbs_params = array("format" => "jpg", "height" => 150, "width" => 150, + "class" => "thumbnail inline"); + +function array_to_table($array) { + $saved_error_reporting = error_reporting(0); + echo ""; + foreach ($array as $key => $value) { + if ($key != 'class') { + if ($key == 'url' || $key == 'secure_url') { + $display_value = '"' . $value . '"'; + } else { + $display_value = json_encode($value); + } + echo ""; + } + } + echo "
" . $key . ":" . $display_value . "
"; + error_reporting($saved_error_reporting); +} + +?> + +

Welcome!

+ +

+ This is the main demo page of the PhotoAlbum sample PHP application of Cloudinary.
+ Here you can see all images you have uploaded to this PHP application and find some information on how + to implement your own PHP application storing, manipulating and serving your photos using Cloudinary! +

+ +

+ All of the images you see here are transformed and served by Cloudinary. + For instance, the logo and the poster frame. + They are both generated in the cloud using the Cloudinary shortcut functions: fetch_image_tag and facebook_profile_image_tag. + These two pictures weren't even have to be uploaded to Cloudinary, they are retrieved by the service, transformed, cached and distributed through a CDN. +

+ +

Your Images

+
+

+ Following are the images uploaded by you. You can also upload more pictures. + + You can click on each picture to view its original size, and see more info about and additional transformations. + Upload Images... +

+ +

No images were uploaded yet.

+ + "; + echo cl_image_tag($photo["Photo"]["cloudinaryIdentifier"], array_merge($thumbs_params, array("crop" => "fill"))); + ?> + + + + +
+ Hide transformations... + + "fill", "radius" => 10), + array("crop" => "scale"), + array("crop" => "fit", "format" => "png"), + array("crop" => "thumb", "gravity" => "face"), + array("override" => true, "format" => "png", "angle" => 20, "transformation" => + array("crop" => "fill", "gravity" => "north", "width" => 150, "height" => 150, "effect" => "sepia") + ), + ); + foreach($thumbs as $params) { + $merged_params = array_merge((\Cloudinary::option_consume($params, "override")) ? array() : $thumbs_params, $params); + echo ""; + } + ?> + +
"; + echo ""; + echo "
"; + array_to_table($merged_params); + echo "
+ +
+ Take a look at our documentation of Image Transformations for a full list of supported transformations. +
+
+
+ +
diff --git a/samples/PhotoAlbumCake/webroot/css/photoalbum.css b/samples/PhotoAlbumCake/webroot/css/photoalbum.css new file mode 100644 index 00000000..d1d55ed5 --- /dev/null +++ b/samples/PhotoAlbumCake/webroot/css/photoalbum.css @@ -0,0 +1,38 @@ +body { font-family: Helvetica, Arial, sans-serif; color: #333; margin: 10px; width: 960px } +#posterframe { position: absolute; right: 10px; top: 10px; } +h1 { color: #0e2953; font-size: 18px; } +h2 { color: #666; font-size: 16px; } +p { font-size: 14px; line-height: 18px; } +#logo { height: 51px; width: 241px; } +a { color: #0b63b6 } + +.upload_link { display: block; color: #000; border: 1px solid #aaa; background-color: #e0e0e0; + font-size: 18px; padding: 5px 10px; width: 150px; margin: 10px 0 20px 0; + font-weight: bold; text-align: center; } + +.photo { margin: 10px; padding: 10px; border-bottom: 2px solid #ccc; } +.photo .thumbnail { margin-top: 10px; display: block; max-width: 200px; } +.toggle_info { margin-top: 10px; font-weight: bold; color: #e62401; display: block; } +.thumbnail_holder { height: 182px; margin-bottom: 5px; margin-right: 10px; } +.info td, .uploaded_info td { font-size: 12px } +.note { margin: 20px 0} + +.more_info, .show_more_info .less_info { display: none; } +.show_more_info .more_info, .less_info { display: inline-block; } +.inline { display: inline-block; } +td { vertical-align: top; padding-right: 5px; } + +#backend_upload, #direct_upload { padding: 20px 20px; margin: 20px 0; + border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; } + +#backend_upload h1, #direct_upload h1 { margin: 0 0 15px 0; } + +.back_link { font-weight: bold; display: block; font-size: 16px; margin: 10px 0; } +#direct_upload { position: relative; min-height: 90px} +.status { position: absolute; top: 20px; left: 500px; text-align: center; border: 1px solid #aaa; + padding: 10px; width: 200px } + +.uploaded_info { margin: 10px} +.uploaded_info .image { margin: 5px 0 } +.uploaded_info td { padding-right: 10px } + From 3199ea7ea98f35f121345f62b1364b971339d145 Mon Sep 17 00:00:00 2001 From: Alon Hammerman Date: Mon, 9 Dec 2013 23:35:10 -0600 Subject: [PATCH 20/24] php framework - Support folder in identifier --- src/Cloudinary.php | 11 ++++++----- src/Uploader.php | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Cloudinary.php b/src/Cloudinary.php index 0150ef70..825197c7 100644 --- a/src/Cloudinary.php +++ b/src/Cloudinary.php @@ -215,17 +215,18 @@ public static function cloudinary_url($source, &$options=array()) { // Warning: $options are being destructively updated! public static function check_cloudinary_field($source, &$options=array()) { $IDENTIFIER_RE = "~" . - "^(?:([^/]+)/)??(?:([^/]+)/)??(?:v(\\d+)/)?" . - "(?:([^#/]+?)(?:\\.([^.#/]+))?)(?:#([^/]+))?$" . "~"; + "^(?:([^/]+)/)??(?:([^/]+)/)??(?:(?:v(\\d+)/)(?:([^#]+)/)?)?" . + "([^#/]+?)(?:\\.([^.#/]+))?(?:#([^/]+))?$" . + "~"; $matches = array(); - if (!(is_object($source) && method_exists($source, 'identifier') && $source->identifier())) { + if (!(is_object($source) && method_exists($source, 'identifier'))) { return $source; } $identifier = $source->identifier(); - if (strstr(':', $identifier) !== false || !preg_match($IDENTIFIER_RE, $identifier, $matches)) { + if (!$identifier || strstr(':', $identifier) !== false || !preg_match($IDENTIFIER_RE, $identifier, $matches)) { return $source; } - $optionNames = array('resource_type', 'type', 'version', 'public_id', 'format'); + $optionNames = array('resource_type', 'type', 'version', 'folder', 'public_id', 'format'); foreach ($optionNames as $index => $optionName) { if (@$matches[$index+1]) { $options[$optionName] = $matches[$index+1]; diff --git a/src/Uploader.php b/src/Uploader.php index 5d470249..3037f83d 100644 --- a/src/Uploader.php +++ b/src/Uploader.php @@ -295,7 +295,7 @@ public function __toString() { namespace { function cl_upload_url($options = array()) { - if (!isset($options["resource_type"])) $options["resource_type"] = "auto"; + if (!@$options["resource_type"]) $options["resource_type"] = "auto"; return Cloudinary::cloudinary_api_url("upload", $options); } From dc9474d64b2ecf0b884f30920f1b1f8b209f5dd7 Mon Sep 17 00:00:00 2001 From: Alon Hammerman Date: Tue, 10 Dec 2013 10:54:48 -0600 Subject: [PATCH 21/24] cake_plugin - support cloudinary options for direct_upload --- cake_plugin/CloudinaryCake/View/Helper/CloudinaryHelper.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cake_plugin/CloudinaryCake/View/Helper/CloudinaryHelper.php b/cake_plugin/CloudinaryCake/View/Helper/CloudinaryHelper.php index 0d8fa0a4..8955930d 100644 --- a/cake_plugin/CloudinaryCake/View/Helper/CloudinaryHelper.php +++ b/cake_plugin/CloudinaryCake/View/Helper/CloudinaryHelper.php @@ -52,9 +52,10 @@ public function cloudinary_includes($options = array()) { } /// Called for input() when type => direct_upload - public function direct_upload() { + public function direct_upload($fieldName, $options = array()) { $modelKey = $this->model(); $fieldKey = $this->field(); - return \cl_image_upload_tag("data[" . $modelKey . "][" . $fieldKey . "]"); + $options = @$options["cloudinary"] ? $options["cloudinary"] : array(); + return \cl_image_upload_tag("data[" . $modelKey . "][" . $fieldKey . "]", $options); } } From f1de5414b2fbee4ee245ee65785ab479508da44a Mon Sep 17 00:00:00 2001 From: Alon Hammerman Date: Sat, 7 Dec 2013 17:43:41 -0600 Subject: [PATCH 22/24] PhotoAlbumCake - added upload --- .../Controller/PhotosController.php | 17 ++++ samples/PhotoAlbumCake/View/Photos/upload.ctp | 89 +++++++++++++++++++ .../webroot/cloudinary_cors.html | 41 +++++++++ 3 files changed, 147 insertions(+) create mode 100644 samples/PhotoAlbumCake/View/Photos/upload.ctp create mode 100644 samples/PhotoAlbumCake/webroot/cloudinary_cors.html diff --git a/samples/PhotoAlbumCake/Controller/PhotosController.php b/samples/PhotoAlbumCake/Controller/PhotosController.php index 163e8fda..3d01a8dd 100644 --- a/samples/PhotoAlbumCake/Controller/PhotosController.php +++ b/samples/PhotoAlbumCake/Controller/PhotosController.php @@ -24,6 +24,8 @@ class PhotosController extends AppController { public $helpers = array('Html', 'Form' => array('className' => 'CloudinaryCake.Cloudinary')); +/// Photo album actions /// + /** * list method * @@ -34,6 +36,21 @@ public function list_photos() { $this->set('photos', $this->Paginator->paginate()); } + public function upload() { + if ($this->request->is('post')) { + $this->Photo->create(); + if ($this->Photo->save($this->request->data)) { + $this->Session->setFlash(__('The photo has been saved.')); + return $this->redirect(array('action' => 'index')); + } else { + $this->Session->setFlash(__('The photo could not be saved. Please, try again.')); + } + } + $this->set('cors_location', Router::url('cloudinary_cors.html', true)); + } + +/// Default actions /// + /** * index method * diff --git a/samples/PhotoAlbumCake/View/Photos/upload.ctp b/samples/PhotoAlbumCake/View/Photos/upload.ctp new file mode 100644 index 00000000..664bf54e --- /dev/null +++ b/samples/PhotoAlbumCake/View/Photos/upload.ctp @@ -0,0 +1,89 @@ +layout = 'photoalbum'; + # Include jQuery + echo $this->Html->script('//code.jquery.com/jquery-1.10.1.min.js'); + # Include cloudinary_js dependencies (requires jQuery) + echo $this->Form->cloudinary_includes(); + # Setup cloudinary_js using the current cloudinary_php configuration + echo cloudinary_js_config(); +?> + + +
+

Upload through your server

+ Form->create('Photo', array('type' => 'file')); ?> + Form->input('cloudinaryIdentifier', array("label" => "", + "accept" => "image/gif, image/jpeg, image/png")); + ?> +
+ Form->end(__('Upload')); ?> +
+ + + +
+

Direct upload from the browser

+ Form->create('Photo', array('type' => 'file')); + # The callback URL is set to point to an HTML file on the local server which works-around restrictions + # in older browsers (e.g., IE) which don't full support CORS. + echo $this->Form->input('cloudinaryIdentifier', array("type" => "direct_upload", + "label" => "", "cloudinary" => array("callback" => $cors_location, "html" => array( + "multiple" => true, "accept" => "image/gif, image/jpeg, image/png")))); + # A simple $this->Form->input('cloudinaryIdentifier', array("type" => "direct_upload")) + # should be sufficient in most cases + + echo $this->Form->end(__('Upload')); + ?> + + +
+

Status

+ Idle +
+ +
+
+
+ +Back to list... + + diff --git a/samples/PhotoAlbumCake/webroot/cloudinary_cors.html b/samples/PhotoAlbumCake/webroot/cloudinary_cors.html new file mode 100644 index 00000000..18f02d3f --- /dev/null +++ b/samples/PhotoAlbumCake/webroot/cloudinary_cors.html @@ -0,0 +1,41 @@ + + + + + + + + + + From 1e14481f1c4be4d214e096d8289e9ffc7eee34a9 Mon Sep 17 00:00:00 2001 From: Alon Hammerman Date: Tue, 10 Dec 2013 12:12:26 -0600 Subject: [PATCH 23/24] PhotoAlbumCake - wire up together and fixes --- .../PhotoAlbumCake/Config/Schema/schema.php | 23 +++++++++++++++++++ samples/PhotoAlbumCake/Config/bootstrap.php | 7 ++---- samples/PhotoAlbumCake/Config/core.php | 5 ++-- samples/PhotoAlbumCake/Config/routes.php | 7 +++--- .../Controller/PhotosController.php | 5 ++-- .../View/Photos/list_photos.ctp | 7 ++++-- samples/PhotoAlbumCake/View/Photos/upload.ctp | 5 +++- 7 files changed, 43 insertions(+), 16 deletions(-) create mode 100644 samples/PhotoAlbumCake/Config/Schema/schema.php diff --git a/samples/PhotoAlbumCake/Config/Schema/schema.php b/samples/PhotoAlbumCake/Config/Schema/schema.php new file mode 100644 index 00000000..02ab16d6 --- /dev/null +++ b/samples/PhotoAlbumCake/Config/Schema/schema.php @@ -0,0 +1,23 @@ + array('type' => 'integer', 'null' => false, 'default' => null, 'length' => 10, 'key' => 'primary'), + 'cloudinaryIdentifier' => array('type' => 'string', 'null' => true, 'default' => null, 'collate' => 'utf8_general_ci', 'charset' => 'utf8'), + 'moderated' => array('type' => 'integer', 'null' => true, 'default' => '0', 'length' => 4), + 'created' => array('type' => 'datetime', 'null' => true, 'default' => null), + 'updated' => array('type' => 'datetime', 'null' => true, 'default' => null), + 'indexes' => array( + 'PRIMARY' => array('column' => 'id', 'unique' => 1) + ), + 'tableParameters' => array('charset' => 'utf8', 'collate' => 'utf8_general_ci', 'engine' => 'InnoDB') + ); + +} diff --git a/samples/PhotoAlbumCake/Config/bootstrap.php b/samples/PhotoAlbumCake/Config/bootstrap.php index ecf34ced..2421aee8 100644 --- a/samples/PhotoAlbumCake/Config/bootstrap.php +++ b/samples/PhotoAlbumCake/Config/bootstrap.php @@ -99,8 +99,5 @@ 'file' => 'error', )); -CakePlugin::load('CloudinaryCake', array('bootstrap' => true, 'routes' => false, - 'path' => APP . '..' . DS . '..' . DS . 'cake_plugin' . DS . 'CloudinaryCake' . DS)); - -Configure::load('private'); -\Cloudinary::config(Configure::read('cloudinary')); +require join(DS, array(APP . '..', '..', 'cake_plugin')) . DS . 'CloudinaryCakeLoader.php'; +CloudinaryCakeLoader::load(); diff --git a/samples/PhotoAlbumCake/Config/core.php b/samples/PhotoAlbumCake/Config/core.php index b527ed01..441f97e5 100644 --- a/samples/PhotoAlbumCake/Config/core.php +++ b/samples/PhotoAlbumCake/Config/core.php @@ -97,8 +97,9 @@ * included primarily as a development convenience - and * thus not recommended for production applications. */ - //Configure::write('App.baseUrl', env('SCRIPT_NAME')); - + if (getenv("NO_REWRITE")) { + Configure::write('App.baseUrl', env('SCRIPT_NAME')); + } /** * To configure CakePHP to use a particular domain URL * for any URL generation inside the application, set the following diff --git a/samples/PhotoAlbumCake/Config/routes.php b/samples/PhotoAlbumCake/Config/routes.php index d42ac984..8d7dbc09 100644 --- a/samples/PhotoAlbumCake/Config/routes.php +++ b/samples/PhotoAlbumCake/Config/routes.php @@ -14,11 +14,10 @@ */ /** - * Here, we are connecting '/' (base path) to controller called 'Pages', - * its action called 'display', and we pass a param to select the view file - * to use (in this case, /app/View/Pages/home.ctp)... + * Here, we are connecting '/' (base path) to to the list_photos action + * of the Photos controller */ - Router::connect('/', array('controller' => 'pages', 'action' => 'display', 'home')); + Router::connect('/', array('controller' => 'photos', 'action' => 'list_photos')); /** * ...and connect the rest of 'Pages' controller's URLs. */ diff --git a/samples/PhotoAlbumCake/Controller/PhotosController.php b/samples/PhotoAlbumCake/Controller/PhotosController.php index 3d01a8dd..3d8216bc 100644 --- a/samples/PhotoAlbumCake/Controller/PhotosController.php +++ b/samples/PhotoAlbumCake/Controller/PhotosController.php @@ -33,7 +33,8 @@ class PhotosController extends AppController { */ public function list_photos() { $this->Photo->recursive = 0; - $this->set('photos', $this->Paginator->paginate()); + $this->paginate = array("order" => array("created" => "desc")); + $this->set('photos', $this->Paginator->paginate()); } public function upload() { @@ -41,7 +42,7 @@ public function upload() { $this->Photo->create(); if ($this->Photo->save($this->request->data)) { $this->Session->setFlash(__('The photo has been saved.')); - return $this->redirect(array('action' => 'index')); + return $this->redirect(array('action' => 'list_photos')); } else { $this->Session->setFlash(__('The photo could not be saved. Please, try again.')); } diff --git a/samples/PhotoAlbumCake/View/Photos/list_photos.ctp b/samples/PhotoAlbumCake/View/Photos/list_photos.ctp index e56aab47..0f44dc12 100644 --- a/samples/PhotoAlbumCake/View/Photos/list_photos.ctp +++ b/samples/PhotoAlbumCake/View/Photos/list_photos.ctp @@ -50,7 +50,10 @@ function array_to_table($array) { Following are the images uploaded by you. You can also upload more pictures. You can click on each picture to view its original size, and see more info about and additional transformations. - Upload Images... + Html->link('Upload Images...', + array('controller' => 'photos', 'action' => 'upload'), + array('class' => 'upload_link')); + ?>

No images were uploaded yet.

@@ -58,7 +61,7 @@ function array_to_table($array) { } foreach ($photos as $photo) { ?> - -Back to list... +Html->link('Back to list...', + array('controller' => 'photos', 'action' => 'list_photos'), + array('class' => 'back_link')); +?>