diff --git a/application/config.php b/application/config.php index 38405e4a..fd8a9488 100644 --- a/application/config.php +++ b/application/config.php @@ -18,7 +18,7 @@ return [ // 应用命名空间 'app_namespace' => 'app', // 应用调试模式 - 'app_debug' => false, + 'app_debug' => true, // 应用Trace 'app_trace' => false, // 应用模式状态 diff --git a/application/index/controller/File.php b/application/index/controller/File.php index eb729005..fd744e59 100644 --- a/application/index/controller/File.php +++ b/application/index/controller/File.php @@ -44,7 +44,7 @@ class File extends Controller{ */ public function ListFile(){ $reqPath = stripslashes(json_decode(file_get_contents("php://input"),true)['path']); - return FileManage::ListFile($reqPath,$this->userObj->uid); + return json(FileManage::ListFile($reqPath,$this->userObj->uid)); } public function Delete(){ diff --git a/application/index/view/home/home.html b/application/index/view/home/home.html index 76332275..fa7b77ed 100644 --- a/application/index/view/home/home.html +++ b/application/index/view/home/home.html @@ -1,25 +1,26 @@ - - - - - - - 我的文件 - {$options.siteName} - - - - - - - - - - - - - - - - - -
- {include file="navbar_home" /} - - -
- -
-
- - - + + + + + +
+ + + diff --git a/application/index/view/home/home1.html b/application/index/view/home/home1.html new file mode 100644 index 00000000..76332275 --- /dev/null +++ b/application/index/view/home/home1.html @@ -0,0 +1,100 @@ + + + + + + + 我的文件 - {$options.siteName} + + + + + + + + + + + + + + + + + +
+ {include file="navbar_home" /} + + +
+ +
+
+ + + + + +{$options.js_code} + diff --git a/composer.lock b/composer.lock new file mode 100644 index 00000000..99877299 --- /dev/null +++ b/composer.lock @@ -0,0 +1,981 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "content-hash": "1d88e4f4d3f5fc270f16d563f3fb7c5e", + "packages": [ + { + "name": "aliyuncs/oss-sdk-php", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/aliyun/aliyun-oss-php-sdk.git", + "reference": "e69f57916678458642ac9d2fd341ae78a56996c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aliyun/aliyun-oss-php-sdk/zipball/e69f57916678458642ac9d2fd341ae78a56996c8", + "reference": "e69f57916678458642ac9d2fd341ae78a56996c8", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.0", + "satooshi/php-coveralls": "~1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "OSS\\": "src/OSS" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aliyuncs", + "homepage": "http://www.aliyun.com" + } + ], + "description": "Aliyun OSS SDK for PHP", + "homepage": "http://www.aliyun.com/product/oss/", + "time": "2018-01-08T06:59:35+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "6.3.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba", + "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba", + "shasum": "" + }, + "require": { + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.4", + "php": ">=5.5" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", + "psr/log": "^1.0" + }, + "suggest": { + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.3-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2018-04-22T15:46:56+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "v1.3.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "time": "2016-12-20T10:07:11+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.5.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "9f83dded91781a01c63574e387eaa769be769115" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/9f83dded91781a01c63574e387eaa769be769115", + "reference": "9f83dded91781a01c63574e387eaa769be769115", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "time": "2018-12-04T20:46:45+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "psr/log", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2018-11-20T15:27:04+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "2.0.5", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/5601c8a83fbba7ef674a7369456d12f1e0d0eafa", + "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "~3.7.0", + "satooshi/php-coveralls": ">=1.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "time": "2016-02-11T07:05:27+00:00" + }, + { + "name": "sabre/dav", + "version": "3.2.3", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/dav.git", + "reference": "a9780ce4f35560ecbd0af524ad32d9d2c8954b80" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/dav/zipball/a9780ce4f35560ecbd0af524ad32d9d2c8954b80", + "reference": "a9780ce4f35560ecbd0af524ad32d9d2c8954b80", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-dom": "*", + "ext-iconv": "*", + "ext-mbstring": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "ext-spl": "*", + "lib-libxml": ">=2.7.0", + "php": ">=5.5.0", + "psr/log": "^1.0", + "sabre/event": ">=2.0.0, <4.0.0", + "sabre/http": "^4.2.1", + "sabre/uri": "^1.0.1", + "sabre/vobject": "^4.1.0", + "sabre/xml": "^1.4.0" + }, + "require-dev": { + "evert/phpdoc-md": "~0.1.0", + "monolog/monolog": "^1.18", + "phpunit/phpunit": "> 4.8, <6.0.0", + "sabre/cs": "^1.0.0" + }, + "suggest": { + "ext-curl": "*", + "ext-pdo": "*" + }, + "bin": [ + "bin/sabredav", + "bin/naturalselection" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Sabre\\DAV\\": "lib/DAV/", + "Sabre\\DAVACL\\": "lib/DAVACL/", + "Sabre\\CalDAV\\": "lib/CalDAV/", + "Sabre\\CardDAV\\": "lib/CardDAV/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "WebDAV Framework for PHP", + "homepage": "http://sabre.io/", + "keywords": [ + "CalDAV", + "CardDAV", + "WebDAV", + "framework", + "iCalendar" + ], + "time": "2018-10-19T09:58:27+00:00" + }, + { + "name": "sabre/event", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/event.git", + "reference": "831d586f5a442dceacdcf5e9c4c36a4db99a3534" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/event/zipball/831d586f5a442dceacdcf5e9c4c36a4db99a3534", + "reference": "831d586f5a442dceacdcf5e9c4c36a4db99a3534", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "*", + "sabre/cs": "~0.0.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Sabre\\Event\\": "lib/" + }, + "files": [ + "lib/coroutine.php", + "lib/Loop/functions.php", + "lib/Promise/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "sabre/event is a library for lightweight event-based programming", + "homepage": "http://sabre.io/event/", + "keywords": [ + "EventEmitter", + "async", + "events", + "hooks", + "plugin", + "promise", + "signal" + ], + "time": "2015-11-05T20:14:39+00:00" + }, + { + "name": "sabre/http", + "version": "v4.2.4", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/http.git", + "reference": "acccec4ba863959b2d10c1fa0fb902736c5c8956" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/http/zipball/acccec4ba863959b2d10c1fa0fb902736c5c8956", + "reference": "acccec4ba863959b2d10c1fa0fb902736c5c8956", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-mbstring": "*", + "php": ">=5.4", + "sabre/event": ">=1.0.0,<4.0.0", + "sabre/uri": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.3", + "sabre/cs": "~0.0.1" + }, + "suggest": { + "ext-curl": " to make http requests with the Client class" + }, + "type": "library", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Sabre\\HTTP\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "The sabre/http library provides utilities for dealing with http requests and responses. ", + "homepage": "https://github.com/fruux/sabre-http", + "keywords": [ + "http" + ], + "time": "2018-02-23T11:10:29+00:00" + }, + { + "name": "sabre/uri", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/uri.git", + "reference": "ada354d83579565949d80b2e15593c2371225e61" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/uri/zipball/ada354d83579565949d80b2e15593c2371225e61", + "reference": "ada354d83579565949d80b2e15593c2371225e61", + "shasum": "" + }, + "require": { + "php": ">=5.4.7" + }, + "require-dev": { + "phpunit/phpunit": ">=4.0,<6.0", + "sabre/cs": "~1.0.0" + }, + "type": "library", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Sabre\\Uri\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "Functions for making sense out of URIs.", + "homepage": "http://sabre.io/uri/", + "keywords": [ + "rfc3986", + "uri", + "url" + ], + "time": "2017-02-20T19:59:28+00:00" + }, + { + "name": "sabre/vobject", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/vobject.git", + "reference": "bd500019764e434ff65872d426f523e7882a0739" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/vobject/zipball/bd500019764e434ff65872d426f523e7882a0739", + "reference": "bd500019764e434ff65872d426f523e7882a0739", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.5", + "sabre/xml": ">=1.5 <3.0" + }, + "require-dev": { + "phpunit/phpunit": "> 4.8.35, <6.0.0" + }, + "suggest": { + "hoa/bench": "If you would like to run the benchmark scripts" + }, + "bin": [ + "bin/vobject", + "bin/generate_vcards" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Sabre\\VObject\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + }, + { + "name": "Dominik Tobschall", + "email": "dominik@fruux.com", + "homepage": "http://tobschall.de/", + "role": "Developer" + }, + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net", + "homepage": "http://mnt.io/", + "role": "Developer" + } + ], + "description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects", + "homepage": "http://sabre.io/vobject/", + "keywords": [ + "availability", + "freebusy", + "iCalendar", + "ical", + "ics", + "jCal", + "jCard", + "recurrence", + "rfc2425", + "rfc2426", + "rfc2739", + "rfc4770", + "rfc5545", + "rfc5546", + "rfc6321", + "rfc6350", + "rfc6351", + "rfc6474", + "rfc6638", + "rfc6715", + "rfc6868", + "vCalendar", + "vCard", + "vcf", + "xCal", + "xCard" + ], + "time": "2019-02-19T13:05:37+00:00" + }, + { + "name": "sabre/xml", + "version": "1.5.1", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/xml.git", + "reference": "a367665f1df614c3b8fefc30a54de7cd295e444e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/xml/zipball/a367665f1df614c3b8fefc30a54de7cd295e444e", + "reference": "a367665f1df614c3b8fefc30a54de7cd295e444e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "lib-libxml": ">=2.6.20", + "php": ">=5.5.5", + "sabre/uri": ">=1.0,<3.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.8|~5.7", + "sabre/cs": "~1.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Sabre\\Xml\\": "lib/" + }, + "files": [ + "lib/Deserializer/functions.php", + "lib/Serializer/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + }, + { + "name": "Markus Staab", + "email": "markus.staab@redaxo.de", + "role": "Developer" + } + ], + "description": "sabre/xml is an XML library that you may not hate.", + "homepage": "https://sabre.io/xml/", + "keywords": [ + "XMLReader", + "XMLWriter", + "dom", + "xml" + ], + "time": "2019-01-09T13:51:57+00:00" + }, + { + "name": "topthink/framework", + "version": "v5.0.24", + "source": { + "type": "git", + "url": "https://github.com/top-think/framework.git", + "reference": "c255c22b2f5fa30f320ecf6c1d29f7740eb3e8be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/framework/zipball/c255c22b2f5fa30f320ecf6c1d29f7740eb3e8be", + "reference": "c255c22b2f5fa30f320ecf6c1d29f7740eb3e8be", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "topthink/think-installer": "~1.0" + }, + "require-dev": { + "johnkary/phpunit-speedtrap": "^1.0", + "mikey179/vfsstream": "~1.6", + "phpdocumentor/reflection-docblock": "^2.0", + "phploc/phploc": "2.*", + "phpunit/phpunit": "4.8.*", + "sebastian/phpcpd": "2.*" + }, + "type": "think-framework", + "autoload": { + "psr-4": { + "think\\": "library/think" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + } + ], + "description": "the new thinkphp framework", + "homepage": "http://thinkphp.cn/", + "keywords": [ + "framework", + "orm", + "thinkphp" + ], + "time": "2019-01-11T08:04:58+00:00" + }, + { + "name": "topthink/think-captcha", + "version": "v1.0.8", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-captcha.git", + "reference": "1d64363c814c92f6086c4fa5e3223fe7e23db09d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-captcha/zipball/1d64363c814c92f6086c4fa5e3223fe7e23db09d", + "reference": "1d64363c814c92f6086c4fa5e3223fe7e23db09d", + "shasum": "" + }, + "require": { + "topthink/framework": "~5.0.0", + "topthink/think-installer": ">=1.0.10" + }, + "type": "library", + "autoload": { + "psr-4": { + "think\\captcha\\": "src/" + }, + "files": [ + "src/helper.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "description": "captcha package for thinkphp5", + "time": "2019-01-28T04:48:36+00:00" + }, + { + "name": "topthink/think-installer", + "version": "v1.0.12", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-installer.git", + "reference": "1be326e68f63de4e95977ed50f46ae75f017556d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-installer/zipball/1be326e68f63de4e95977ed50f46ae75f017556d", + "reference": "1be326e68f63de4e95977ed50f46ae75f017556d", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0" + }, + "require-dev": { + "composer/composer": "1.0.*@dev" + }, + "type": "composer-plugin", + "extra": { + "class": "think\\composer\\Plugin" + }, + "autoload": { + "psr-4": { + "think\\composer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "time": "2017-05-27T06:58:09+00:00" + }, + { + "name": "upyun/sdk", + "version": "3.3.0", + "source": { + "type": "git", + "url": "https://github.com/upyun/php-sdk.git", + "reference": "1a2dd5ae31047956c733aef0f764f3a527d30628" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/upyun/php-sdk/zipball/1a2dd5ae31047956c733aef0f764f3a527d30628", + "reference": "1a2dd5ae31047956c733aef0f764f3a527d30628", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "guzzlehttp/guzzle": "~6.0", + "php": ">=5.5.0" + }, + "require-dev": { + "consolidation/robo": "^1.0", + "phpdocumentor/phpdocumentor": "^2.9", + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Upyun\\": "src/Upyun/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "totoleo", + "email": "totoleo@163.com" + }, + { + "name": "lfeng", + "email": "bonevv@gmail.com" + }, + { + "name": "lvtongda", + "email": "riyao.lyu@gmail.com" + }, + { + "name": "sabakugaara", + "email": "senellise@gmail.com" + } + ], + "description": "UPYUN sdk for php", + "homepage": "https://github.com/upyun/php-sdk/", + "keywords": [ + "sdk", + "upyun" + ], + "time": "2017-11-12T09:17:42+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "ext-curl": "*", + "ext-fileinfo": "*", + "ext-gd": "*" + }, + "platform-dev": [] +} diff --git a/static/js/uploader/i18n/zh_CN.js b/static/js/uploader/i18n/zh_CN.js new file mode 100644 index 00000000..f48e0c43 --- /dev/null +++ b/static/js/uploader/i18n/zh_CN.js @@ -0,0 +1,2 @@ +// Chinese (China) (zh_CN) +plupload.addI18n({"Stop Upload":"停止上传","Upload URL might be wrong or doesn't exist.":"上传的URL可能是错误的或不存在。","tb":"tb","Size":"大小","Close":"关闭","Init error.":"初始化错误。","Add files to the upload queue and click the start button.":"将文件添加到上传队列,然后点击”开始上传“按钮。","Filename":"文件名","Image format either wrong or not supported.":"图片格式错误或者不支持。","Status":"状态","HTTP Error.":"HTTP 错误。","Start Upload":"开始上传","mb":"mb","kb":"kb","Duplicate file error.":"重复文件错误。","File size error.":"文件大小错误。","N/A":"N/A","gb":"gb","Error: Invalid file extension:":"错误:无效的文件扩展名:","Select files":"选择文件","%s already present in the queue.":"%s 已经在当前队列里。","File: %s":"文件: %s","b":"b","Uploaded %d/%d files":"已上传 %d/%d 个文件","Upload element accepts only %d file(s) at a time. Extra files were stripped.":"每次只接受同时上传 %d 个文件,多余的文件将会被删除。","%d files queued":"%d 个文件加入到队列","File: %s, size: %d, max file size: %d":"文件: %s, 大小: %d, 最大文件大小: %d","Drag files here.":"把文件拖到这里。","Runtime ran out of available memory.":"运行时已消耗所有可用内存。","File count error.":"文件数量错误。","File extension error.":"文件扩展名错误。","Error: File too large:":"错误: 文件太大:","Add Files":"增加文件"}); \ No newline at end of file diff --git a/static/js/uploader/main.js b/static/js/uploader/main.js new file mode 100644 index 00000000..e0fa990b --- /dev/null +++ b/static/js/uploader/main.js @@ -0,0 +1,274 @@ +// /*global Qiniu */ +// /*global plupload */ +// /*global FileProgress */ +// /*global hljs */ +// function getCookieByString(cookieName){ +// var start = document.cookie.indexOf(cookieName+'='); +// if (start == -1) return false; +// start = start+cookieName.length+1; +// var end = document.cookie.indexOf(';', start); +// if (end == -1) end=document.cookie.length; +// return document.cookie.substring(start, end); +// } + +// if(uploadConfig.saveType == "oss" || uploadConfig.saveType == "upyun" || uploadConfig.saveType == "s3"){ +// ChunkSize = "0"; +// }else{ +// ChunkSize = "4mb"; +// } + +// uploader = Qiniu.uploader({ +// runtimes: 'html5,flash,html4', +// browse_button: 'pickfiles', +// container: 'container', +// drop_element: 'container', +// max_file_size: uploadConfig.maxSize, +// flash_swf_url: '/bower_components/plupload/js/Moxie.swf', +// dragdrop: true, +// chunk_size: ChunkSize, +// filters: { +// mime_types :uploadConfig.allowedType, +// }, +// multi_selection: !(moxie.core.utils.Env.OS.toLowerCase() === "ios"), +// uptoken_url: "/Upload/Token", +// // uptoken_func: function(){ +// // var ajax = new XMLHttpRequest(); +// // ajax.open('GET', $('#uptoken_url').val(), false); +// // ajax.setRequestHeader("If-Modified-Since", "0"); +// // ajax.send(); +// // if (ajax.status === 200) { +// // var res = JSON.parse(ajax.responseText); +// // console.log('custom uptoken_func:' + res.uptoken); +// // return res.uptoken; +// // } else { +// // console.log('custom uptoken_func err'); +// // return ''; +// // } +// // }, +// domain: $('#domain').val(), +// get_new_uptoken: true, +// // downtoken_url: '/downtoken', +// // unique_names: true, +// // save_key: true, +// // x_vars: { +// // 'id': '1234', +// // 'time': function(up, file) { +// // var time = (new Date()).getTime(); +// // // do something with 'time' +// // return time; +// // }, +// // }, +// auto_start: true, +// log_level: 5, +// init: { +// 'FilesAdded': function(up, files) { +// $('table').show(); +// $('#upload_box').show(); +// $('#success').hide(); +// $('#info_box').hide(); + +// $.cookie('path', decodeURI(getCookieByString("path_tmp"))); +// plupload.each(files, function(file) { +// var progress = new FileProgress(file, 'fsUploadProgress'); +// progress.setStatus("等待..."); +// progress.bindUploadCancel(up); +// }); + +// }, +// 'BeforeUpload': function(up, file) { +// var progress = new FileProgress(file, 'fsUploadProgress'); +// var chunk_size = plupload.parseSize(this.getOption('chunk_size')); +// if (up.runtime === 'html5' && chunk_size) { +// progress.setChunkProgess(chunk_size); +// } +// }, +// 'UploadProgress': function(up, file) { +// var progress = new FileProgress(file, 'fsUploadProgress'); +// var chunk_size = plupload.parseSize(this.getOption('chunk_size')); +// progress.setProgress(file.percent + "%", file.speed, chunk_size); +// }, +// 'UploadComplete': function(up, file) { +// $('#success').show(); +// toastr["success"]("队列全部文件处理完毕"); +// getMemory(); +// }, +// 'FileUploaded': function(up, file, info) { +// var progress = new FileProgress(file, 'fsUploadProgress'); +// progress.setComplete(up, info); +// }, +// 'Error': function(up, err, errTip) { +// $('#upload_box').show(); +// $('table').show(); +// $('#info_box').hide(); +// var progress = new FileProgress(err.file, 'fsUploadProgress'); +// progress.setError(); +// progress.setStatus(errTip); +// toastr["error"]("上传时遇到错误"); +// } +// // , +// // 'Key': function(up, file) { +// // var key = ""; +// // // do something with key +// // return key +// // } +// } +// }); + +// uploader.bind('FileUploaded', function(up,file) { +// console.log('a file is uploaded'); +// }); +// $('#container').on( +// 'dragenter', +// function(e) { +// e.preventDefault(); +// $('#container').addClass('draging'); +// e.stopPropagation(); +// } +// ).on('drop', function(e) { +// e.preventDefault(); +// $('#container').removeClass('draging'); +// e.stopPropagation(); +// }).on('dragleave', function(e) { +// e.preventDefault(); +// $('#container').removeClass('draging'); +// e.stopPropagation(); +// }).on('dragover', function(e) { +// e.preventDefault(); +// $('#container').addClass('draging'); +// e.stopPropagation(); +// }); + + + +// $('#show_code').on('click', function() { +// $('#myModal-code').modal(); +// $('pre code').each(function(i, e) { +// hljs.highlightBlock(e); +// }); +// }); + + +// $('body').on('click', 'table button.btn', function() { +// $(this).parents('tr').next().toggle(); +// }); + + +// var getRotate = function(url) { +// if (!url) { +// return 0; +// } +// var arr = url.split('/'); +// for (var i = 0, len = arr.length; i < len; i++) { +// if (arr[i] === 'rotate') { +// return parseInt(arr[i + 1], 10); +// } +// } +// return 0; +// }; + +// $('#myModal-img .modal-body-footer').find('a').on('click', function() { +// var img = $('#myModal-img').find('.modal-body img'); +// var key = img.data('key'); +// var oldUrl = img.attr('src'); +// var originHeight = parseInt(img.data('h'), 10); +// var fopArr = []; +// var rotate = getRotate(oldUrl); +// if (!$(this).hasClass('no-disable-click')) { +// $(this).addClass('disabled').siblings().removeClass('disabled'); +// if ($(this).data('imagemogr') !== 'no-rotate') { +// fopArr.push({ +// 'fop': 'imageMogr2', +// 'auto-orient': true, +// 'strip': true, +// 'rotate': rotate, +// 'format': 'png' +// }); +// } +// } else { +// $(this).siblings().removeClass('disabled'); +// var imageMogr = $(this).data('imagemogr'); +// if (imageMogr === 'left') { +// rotate = rotate - 90 < 0 ? rotate + 270 : rotate - 90; +// } else if (imageMogr === 'right') { +// rotate = rotate + 90 > 360 ? rotate - 270 : rotate + 90; +// } +// fopArr.push({ +// 'fop': 'imageMogr2', +// 'auto-orient': true, +// 'strip': true, +// 'rotate': rotate, +// 'format': 'png' +// }); +// } + +// $('#myModal-img .modal-body-footer').find('a.disabled').each(function() { + +// var watermark = $(this).data('watermark'); +// var imageView = $(this).data('imageview'); +// var imageMogr = $(this).data('imagemogr'); + +// if (watermark) { +// fopArr.push({ +// fop: 'watermark', +// mode: 1, +// image: 'http://www.b1.qiniudn.com/images/logo-2.png', +// dissolve: 100, +// gravity: watermark, +// dx: 100, +// dy: 100 +// }); +// } + +// if (imageView) { +// var height; +// switch (imageView) { +// case 'large': +// height = originHeight; +// break; +// case 'middle': +// height = originHeight * 0.5; +// break; +// case 'small': +// height = originHeight * 0.1; +// break; +// default: +// height = originHeight; +// break; +// } +// fopArr.push({ +// fop: 'imageView2', +// mode: 3, +// h: parseInt(height, 10), +// q: 100, +// format: 'png' +// }); +// } + +// if (imageMogr === 'no-rotate') { +// fopArr.push({ +// 'fop': 'imageMogr2', +// 'auto-orient': true, +// 'strip': true, +// 'rotate': 0, +// 'format': 'png' +// }); +// } +// }); + + + +// var newUrl = Qiniu.pipeline(fopArr, key); + +// var newImg = new Image(); +// img.attr('src', 'images/loading.gif'); +// newImg.onload = function() { +// img.attr('src', newUrl); +// img.parent('a').attr('href', newUrl); +// }; +// newImg.src = newUrl; +// return false; +// }); + +// function t(){ +// uploader.getNewUpToken(); +// } \ No newline at end of file diff --git a/static/js/uploader/moxie.js b/static/js/uploader/moxie.js new file mode 100644 index 00000000..d41397cd --- /dev/null +++ b/static/js/uploader/moxie.js @@ -0,0 +1,11509 @@ +;var MXI_DEBUG = true; +/** + * mOxie - multi-runtime File API & XMLHttpRequest L2 Polyfill + * v1.5.3 + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + * + * Date: 2017-02-02 + */ +;(function (global, factory) { + var extract = function() { + var ctx = {}; + factory.apply(ctx, arguments); + return ctx.moxie; + }; + + if (typeof define === "function" && define.amd) { + define("moxie", [], extract); + } else if (typeof module === "object" && module.exports) { + module.exports = extract(); + } else { + global.moxie = extract(); + } +}(this || window, function() { +/** + * Compiled inline version. (Library mode) + */ + +/*jshint smarttabs:true, undef:true, latedef:true, curly:true, bitwise:true, camelcase:true */ +/*globals $code */ + +(function(exports, undefined) { + "use strict"; + + var modules = {}; + + function require(ids, callback) { + var module, defs = []; + + for (var i = 0; i < ids.length; ++i) { + module = modules[ids[i]] || resolve(ids[i]); + if (!module) { + throw 'module definition dependecy not found: ' + ids[i]; + } + + defs.push(module); + } + + callback.apply(null, defs); + } + + function define(id, dependencies, definition) { + if (typeof id !== 'string') { + throw 'invalid module definition, module id must be defined and be a string'; + } + + if (dependencies === undefined) { + throw 'invalid module definition, dependencies must be specified'; + } + + if (definition === undefined) { + throw 'invalid module definition, definition function must be specified'; + } + + require(dependencies, function() { + modules[id] = definition.apply(null, arguments); + }); + } + + function defined(id) { + return !!modules[id]; + } + + function resolve(id) { + var target = exports; + var fragments = id.split(/[.\/]/); + + for (var fi = 0; fi < fragments.length; ++fi) { + if (!target[fragments[fi]]) { + return; + } + + target = target[fragments[fi]]; + } + + return target; + } + + function expose(ids) { + for (var i = 0; i < ids.length; i++) { + var target = exports; + var id = ids[i]; + var fragments = id.split(/[.\/]/); + + for (var fi = 0; fi < fragments.length - 1; ++fi) { + if (target[fragments[fi]] === undefined) { + target[fragments[fi]] = {}; + } + + target = target[fragments[fi]]; + } + + target[fragments[fragments.length - 1]] = modules[id]; + } + } + +// Included from: src/javascript/core/utils/Basic.js + +/** + * Basic.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/core/utils/Basic +@public +@static +*/ +define('moxie/core/utils/Basic', [], function() { + /** + Gets the true type of the built-in object (better version of typeof). + @author Angus Croll (http://javascriptweblog.wordpress.com/) + + @method typeOf + @for Utils + @static + @param {Object} o Object to check. + @return {String} Object [[Class]] + */ + function typeOf(o) { + var undef; + + if (o === undef) { + return 'undefined'; + } else if (o === null) { + return 'null'; + } else if (o.nodeType) { + return 'node'; + } + + // the snippet below is awesome, however it fails to detect null, undefined and arguments types in IE lte 8 + return ({}).toString.call(o).match(/\s([a-z|A-Z]+)/)[1].toLowerCase(); + } + + /** + Extends the specified object with another object(s). + + @method extend + @static + @param {Object} target Object to extend. + @param {Object} [obj]* Multiple objects to extend with. + @return {Object} Same as target, the extended object. + */ + function extend() { + return merge(false, false, arguments); + } + + + /** + Extends the specified object with another object(s), but only if the property exists in the target. + + @method extendIf + @static + @param {Object} target Object to extend. + @param {Object} [obj]* Multiple objects to extend with. + @return {Object} Same as target, the extended object. + */ + function extendIf() { + return merge(true, false, arguments); + } + + + function extendImmutable() { + return merge(false, true, arguments); + } + + + function extendImmutableIf() { + return merge(true, true, arguments); + } + + + function shallowCopy(obj) { + switch (typeOf(obj)) { + case 'array': + return Array.prototype.slice.call(obj); + + case 'object': + return extend({}, obj); + } + return obj; + } + + + function merge(strict, immutable, args) { + var undef; + var target = args[0]; + + each(args, function(arg, i) { + if (i > 0) { + each(arg, function(value, key) { + var isComplex = inArray(typeOf(value), ['array', 'object']) !== -1; + + if (value === undef || strict && target[key] === undef) { + return true; + } + + if (isComplex && immutable) { + value = shallowCopy(value); + } + + if (typeOf(target[key]) === typeOf(value) && isComplex) { + merge(strict, immutable, [target[key], value]); + } else { + target[key] = value; + } + }); + } + }); + + return target; + } + + + /** + A way to inherit one `class` from another in a consisstent way (more or less) + + @method inherit + @static + @since >1.4.1 + @param {Function} child + @param {Function} parent + @return {Function} Prepared constructor + */ + function inherit(child, parent) { + // copy over all parent properties + for (var key in parent) { + if ({}.hasOwnProperty.call(parent, key)) { + child[key] = parent[key]; + } + } + + // give child `class` a place to define its own methods + function ctor() { + this.constructor = child; + } + ctor.prototype = parent.prototype; + child.prototype = new ctor(); + + // keep a way to reference parent methods + child.__parent__ = parent.prototype; + return child; + } + + + /** + Executes the callback function for each item in array/object. If you return false in the + callback it will break the loop. + + @method each + @static + @param {Object} obj Object to iterate. + @param {function} callback Callback function to execute for each item. + */ + function each(obj, callback) { + var length, key, i, undef; + + if (obj) { + try { + length = obj.length; + } catch(ex) { + length = undef; + } + + if (length === undef || typeof(length) !== 'number') { + // Loop object items + for (key in obj) { + if (obj.hasOwnProperty(key)) { + if (callback(obj[key], key) === false) { + return; + } + } + } + } else { + // Loop array items + for (i = 0; i < length; i++) { + if (callback(obj[i], i) === false) { + return; + } + } + } + } + } + + /** + Checks if object is empty. + + @method isEmptyObj + @static + @param {Object} o Object to check. + @return {Boolean} + */ + function isEmptyObj(obj) { + var prop; + + if (!obj || typeOf(obj) !== 'object') { + return true; + } + + for (prop in obj) { + return false; + } + + return true; + } + + /** + Recieve an array of functions (usually async) to call in sequence, each function + receives a callback as first argument that it should call, when it completes. Finally, + after everything is complete, main callback is called. Passing truthy value to the + callback as a first argument will interrupt the sequence and invoke main callback + immediately. + + @method inSeries + @static + @param {Array} queue Array of functions to call in sequence + @param {Function} cb Main callback that is called in the end, or in case of error + */ + function inSeries(queue, cb) { + var i = 0, length = queue.length; + + if (typeOf(cb) !== 'function') { + cb = function() {}; + } + + if (!queue || !queue.length) { + cb(); + } + + function callNext(i) { + if (typeOf(queue[i]) === 'function') { + queue[i](function(error) { + /*jshint expr:true */ + ++i < length && !error ? callNext(i) : cb(error); + }); + } + } + callNext(i); + } + + + /** + Recieve an array of functions (usually async) to call in parallel, each function + receives a callback as first argument that it should call, when it completes. After + everything is complete, main callback is called. Passing truthy value to the + callback as a first argument will interrupt the process and invoke main callback + immediately. + + @method inParallel + @static + @param {Array} queue Array of functions to call in sequence + @param {Function} cb Main callback that is called in the end, or in case of erro + */ + function inParallel(queue, cb) { + var count = 0, num = queue.length, cbArgs = new Array(num); + + each(queue, function(fn, i) { + fn(function(error) { + if (error) { + return cb(error); + } + + var args = [].slice.call(arguments); + args.shift(); // strip error - undefined or not + + cbArgs[i] = args; + count++; + + if (count === num) { + cbArgs.unshift(null); + cb.apply(this, cbArgs); + } + }); + }); + } + + + /** + Find an element in array and return it's index if present, otherwise return -1. + + @method inArray + @static + @param {Mixed} needle Element to find + @param {Array} array + @return {Int} Index of the element, or -1 if not found + */ + function inArray(needle, array) { + if (array) { + if (Array.prototype.indexOf) { + return Array.prototype.indexOf.call(array, needle); + } + + for (var i = 0, length = array.length; i < length; i++) { + if (array[i] === needle) { + return i; + } + } + } + return -1; + } + + + /** + Returns elements of first array if they are not present in second. And false - otherwise. + + @private + @method arrayDiff + @param {Array} needles + @param {Array} array + @return {Array|Boolean} + */ + function arrayDiff(needles, array) { + var diff = []; + + if (typeOf(needles) !== 'array') { + needles = [needles]; + } + + if (typeOf(array) !== 'array') { + array = [array]; + } + + for (var i in needles) { + if (inArray(needles[i], array) === -1) { + diff.push(needles[i]); + } + } + return diff.length ? diff : false; + } + + + /** + Find intersection of two arrays. + + @private + @method arrayIntersect + @param {Array} array1 + @param {Array} array2 + @return {Array} Intersection of two arrays or null if there is none + */ + function arrayIntersect(array1, array2) { + var result = []; + each(array1, function(item) { + if (inArray(item, array2) !== -1) { + result.push(item); + } + }); + return result.length ? result : null; + } + + + /** + Forces anything into an array. + + @method toArray + @static + @param {Object} obj Object with length field. + @return {Array} Array object containing all items. + */ + function toArray(obj) { + var i, arr = []; + + for (i = 0; i < obj.length; i++) { + arr[i] = obj[i]; + } + + return arr; + } + + + /** + Generates an unique ID. The only way a user would be able to get the same ID is if the two persons + at the same exact millisecond manage to get the same 5 random numbers between 0-65535; it also uses + a counter so each ID is guaranteed to be unique for the given page. It is more probable for the earth + to be hit with an asteroid. + + @method guid + @static + @param {String} prefix to prepend (by default 'o' will be prepended). + @method guid + @return {String} Virtually unique id. + */ + var guid = (function() { + var counter = 0; + + return function(prefix) { + var guid = new Date().getTime().toString(32), i; + + for (i = 0; i < 5; i++) { + guid += Math.floor(Math.random() * 65535).toString(32); + } + + return (prefix || 'o_') + guid + (counter++).toString(32); + }; + }()); + + + /** + Trims white spaces around the string + + @method trim + @static + @param {String} str + @return {String} + */ + function trim(str) { + if (!str) { + return str; + } + return String.prototype.trim ? String.prototype.trim.call(str) : str.toString().replace(/^\s*/, '').replace(/\s*$/, ''); + } + + + /** + Parses the specified size string into a byte value. For example 10kb becomes 10240. + + @method parseSizeStr + @static + @param {String/Number} size String to parse or number to just pass through. + @return {Number} Size in bytes. + */ + function parseSizeStr(size) { + if (typeof(size) !== 'string') { + return size; + } + + var muls = { + t: 1099511627776, + g: 1073741824, + m: 1048576, + k: 1024 + }, + mul; + + size = /^([0-9\.]+)([tmgk]?)$/.exec(size.toLowerCase().replace(/[^0-9\.tmkg]/g, '')); + mul = size[2]; + size = +size[1]; + + if (muls.hasOwnProperty(mul)) { + size *= muls[mul]; + } + return Math.floor(size); + } + + + /** + * Pseudo sprintf implementation - simple way to replace tokens with specified values. + * + * @param {String} str String with tokens + * @return {String} String with replaced tokens + */ + function sprintf(str) { + var args = [].slice.call(arguments, 1); + + return str.replace(/%[a-z]/g, function() { + var value = args.shift(); + return typeOf(value) !== 'undefined' ? value : ''; + }); + } + + + + function delay(cb, timeout) { + var self = this; + setTimeout(function() { + cb.call(self); + }, timeout || 1); + } + + + return { + guid: guid, + typeOf: typeOf, + extend: extend, + extendIf: extendIf, + extendImmutable: extendImmutable, + extendImmutableIf: extendImmutableIf, + inherit: inherit, + each: each, + isEmptyObj: isEmptyObj, + inSeries: inSeries, + inParallel: inParallel, + inArray: inArray, + arrayDiff: arrayDiff, + arrayIntersect: arrayIntersect, + toArray: toArray, + trim: trim, + sprintf: sprintf, + parseSizeStr: parseSizeStr, + delay: delay + }; +}); + +// Included from: src/javascript/core/utils/Encode.js + +/** + * Encode.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +define('moxie/core/utils/Encode', [], function() { + + /** + @class moxie/core/utils/Encode + */ + + /** + Encode string with UTF-8 + + @method utf8_encode + @for Utils + @static + @param {String} str String to encode + @return {String} UTF-8 encoded string + */ + var utf8_encode = function(str) { + return unescape(encodeURIComponent(str)); + }; + + /** + Decode UTF-8 encoded string + + @method utf8_decode + @static + @param {String} str String to decode + @return {String} Decoded string + */ + var utf8_decode = function(str_data) { + return decodeURIComponent(escape(str_data)); + }; + + /** + Decode Base64 encoded string (uses browser's default method if available), + from: https://raw.github.com/kvz/phpjs/master/functions/url/base64_decode.js + + @method atob + @static + @param {String} data String to decode + @return {String} Decoded string + */ + var atob = function(data, utf8) { + if (typeof(window.atob) === 'function') { + return utf8 ? utf8_decode(window.atob(data)) : window.atob(data); + } + + // http://kevin.vanzonneveld.net + // + original by: Tyler Akins (http://rumkin.com) + // + improved by: Thunder.m + // + input by: Aman Gupta + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + bugfixed by: Onno Marsman + // + bugfixed by: Pellentesque Malesuada + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + input by: Brett Zamir (http://brett-zamir.me) + // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // * example 1: base64_decode('S2V2aW4gdmFuIFpvbm5ldmVsZA=='); + // * returns 1: 'Kevin van Zonneveld' + // mozilla has this native + // - but breaks in 2.0.0.12! + //if (typeof this.window.atob == 'function') { + // return atob(data); + //} + var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, + ac = 0, + dec = "", + tmp_arr = []; + + if (!data) { + return data; + } + + data += ''; + + do { // unpack four hexets into three octets using index points in b64 + h1 = b64.indexOf(data.charAt(i++)); + h2 = b64.indexOf(data.charAt(i++)); + h3 = b64.indexOf(data.charAt(i++)); + h4 = b64.indexOf(data.charAt(i++)); + + bits = h1 << 18 | h2 << 12 | h3 << 6 | h4; + + o1 = bits >> 16 & 0xff; + o2 = bits >> 8 & 0xff; + o3 = bits & 0xff; + + if (h3 == 64) { + tmp_arr[ac++] = String.fromCharCode(o1); + } else if (h4 == 64) { + tmp_arr[ac++] = String.fromCharCode(o1, o2); + } else { + tmp_arr[ac++] = String.fromCharCode(o1, o2, o3); + } + } while (i < data.length); + + dec = tmp_arr.join(''); + + return utf8 ? utf8_decode(dec) : dec; + }; + + /** + Base64 encode string (uses browser's default method if available), + from: https://raw.github.com/kvz/phpjs/master/functions/url/base64_encode.js + + @method btoa + @static + @param {String} data String to encode + @return {String} Base64 encoded string + */ + var btoa = function(data, utf8) { + if (utf8) { + data = utf8_encode(data); + } + + if (typeof(window.btoa) === 'function') { + return window.btoa(data); + } + + // http://kevin.vanzonneveld.net + // + original by: Tyler Akins (http://rumkin.com) + // + improved by: Bayron Guevara + // + improved by: Thunder.m + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + bugfixed by: Pellentesque Malesuada + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + improved by: Rafał Kukawski (http://kukawski.pl) + // * example 1: base64_encode('Kevin van Zonneveld'); + // * returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA==' + // mozilla has this native + // - but breaks in 2.0.0.12! + var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, + ac = 0, + enc = "", + tmp_arr = []; + + if (!data) { + return data; + } + + do { // pack three octets into four hexets + o1 = data.charCodeAt(i++); + o2 = data.charCodeAt(i++); + o3 = data.charCodeAt(i++); + + bits = o1 << 16 | o2 << 8 | o3; + + h1 = bits >> 18 & 0x3f; + h2 = bits >> 12 & 0x3f; + h3 = bits >> 6 & 0x3f; + h4 = bits & 0x3f; + + // use hexets to index into b64, and append result to encoded string + tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4); + } while (i < data.length); + + enc = tmp_arr.join(''); + + var r = data.length % 3; + + return (r ? enc.slice(0, r - 3) : enc) + '==='.slice(r || 3); + }; + + + return { + utf8_encode: utf8_encode, + utf8_decode: utf8_decode, + atob: atob, + btoa: btoa + }; +}); + +// Included from: src/javascript/core/utils/Env.js + +/** + * Env.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +define("moxie/core/utils/Env", [ + "moxie/core/utils/Basic" +], function(Basic) { + + /** + * UAParser.js v0.7.7 + * Lightweight JavaScript-based User-Agent string parser + * https://github.com/faisalman/ua-parser-js + * + * Copyright © 2012-2015 Faisal Salman + * Dual licensed under GPLv2 & MIT + */ + var UAParser = (function (undefined) { + + ////////////// + // Constants + ///////////// + + + var EMPTY = '', + UNKNOWN = '?', + FUNC_TYPE = 'function', + UNDEF_TYPE = 'undefined', + OBJ_TYPE = 'object', + MAJOR = 'major', + MODEL = 'model', + NAME = 'name', + TYPE = 'type', + VENDOR = 'vendor', + VERSION = 'version', + ARCHITECTURE= 'architecture', + CONSOLE = 'console', + MOBILE = 'mobile', + TABLET = 'tablet'; + + + /////////// + // Helper + ////////// + + + var util = { + has : function (str1, str2) { + return str2.toLowerCase().indexOf(str1.toLowerCase()) !== -1; + }, + lowerize : function (str) { + return str.toLowerCase(); + } + }; + + + /////////////// + // Map helper + ////////////// + + + var mapper = { + + rgx : function () { + + // loop through all regexes maps + for (var result, i = 0, j, k, p, q, matches, match, args = arguments; i < args.length; i += 2) { + + var regex = args[i], // even sequence (0,2,4,..) + props = args[i + 1]; // odd sequence (1,3,5,..) + + // construct object barebones + if (typeof(result) === UNDEF_TYPE) { + result = {}; + for (p in props) { + q = props[p]; + if (typeof(q) === OBJ_TYPE) { + result[q[0]] = undefined; + } else { + result[q] = undefined; + } + } + } + + // try matching uastring with regexes + for (j = k = 0; j < regex.length; j++) { + matches = regex[j].exec(this.getUA()); + if (!!matches) { + for (p = 0; p < props.length; p++) { + match = matches[++k]; + q = props[p]; + // check if given property is actually array + if (typeof(q) === OBJ_TYPE && q.length > 0) { + if (q.length == 2) { + if (typeof(q[1]) == FUNC_TYPE) { + // assign modified match + result[q[0]] = q[1].call(this, match); + } else { + // assign given value, ignore regex match + result[q[0]] = q[1]; + } + } else if (q.length == 3) { + // check whether function or regex + if (typeof(q[1]) === FUNC_TYPE && !(q[1].exec && q[1].test)) { + // call function (usually string mapper) + result[q[0]] = match ? q[1].call(this, match, q[2]) : undefined; + } else { + // sanitize match using given regex + result[q[0]] = match ? match.replace(q[1], q[2]) : undefined; + } + } else if (q.length == 4) { + result[q[0]] = match ? q[3].call(this, match.replace(q[1], q[2])) : undefined; + } + } else { + result[q] = match ? match : undefined; + } + } + break; + } + } + + if(!!matches) break; // break the loop immediately if match found + } + return result; + }, + + str : function (str, map) { + + for (var i in map) { + // check if array + if (typeof(map[i]) === OBJ_TYPE && map[i].length > 0) { + for (var j = 0; j < map[i].length; j++) { + if (util.has(map[i][j], str)) { + return (i === UNKNOWN) ? undefined : i; + } + } + } else if (util.has(map[i], str)) { + return (i === UNKNOWN) ? undefined : i; + } + } + return str; + } + }; + + + /////////////// + // String map + ////////////// + + + var maps = { + + browser : { + oldsafari : { + major : { + '1' : ['/8', '/1', '/3'], + '2' : '/4', + '?' : '/' + }, + version : { + '1.0' : '/8', + '1.2' : '/1', + '1.3' : '/3', + '2.0' : '/412', + '2.0.2' : '/416', + '2.0.3' : '/417', + '2.0.4' : '/419', + '?' : '/' + } + } + }, + + device : { + sprint : { + model : { + 'Evo Shift 4G' : '7373KT' + }, + vendor : { + 'HTC' : 'APA', + 'Sprint' : 'Sprint' + } + } + }, + + os : { + windows : { + version : { + 'ME' : '4.90', + 'NT 3.11' : 'NT3.51', + 'NT 4.0' : 'NT4.0', + '2000' : 'NT 5.0', + 'XP' : ['NT 5.1', 'NT 5.2'], + 'Vista' : 'NT 6.0', + '7' : 'NT 6.1', + '8' : 'NT 6.2', + '8.1' : 'NT 6.3', + 'RT' : 'ARM' + } + } + } + }; + + + ////////////// + // Regex map + ///////////// + + + var regexes = { + + browser : [[ + + // Presto based + /(opera\smini)\/([\w\.-]+)/i, // Opera Mini + /(opera\s[mobiletab]+).+version\/([\w\.-]+)/i, // Opera Mobi/Tablet + /(opera).+version\/([\w\.]+)/i, // Opera > 9.80 + /(opera)[\/\s]+([\w\.]+)/i // Opera < 9.80 + + ], [NAME, VERSION], [ + + /\s(opr)\/([\w\.]+)/i // Opera Webkit + ], [[NAME, 'Opera'], VERSION], [ + + // Mixed + /(kindle)\/([\w\.]+)/i, // Kindle + /(lunascape|maxthon|netfront|jasmine|blazer)[\/\s]?([\w\.]+)*/i, + // Lunascape/Maxthon/Netfront/Jasmine/Blazer + + // Trident based + /(avant\s|iemobile|slim|baidu)(?:browser)?[\/\s]?([\w\.]*)/i, + // Avant/IEMobile/SlimBrowser/Baidu + /(?:ms|\()(ie)\s([\w\.]+)/i, // Internet Explorer + + // Webkit/KHTML based + /(rekonq)\/([\w\.]+)*/i, // Rekonq + /(chromium|flock|rockmelt|midori|epiphany|silk|skyfire|ovibrowser|bolt|iron|vivaldi)\/([\w\.-]+)/i + // Chromium/Flock/RockMelt/Midori/Epiphany/Silk/Skyfire/Bolt/Iron + ], [NAME, VERSION], [ + + /(trident).+rv[:\s]([\w\.]+).+like\sgecko/i // IE11 + ], [[NAME, 'IE'], VERSION], [ + + /(edge)\/((\d+)?[\w\.]+)/i // Microsoft Edge + ], [NAME, VERSION], [ + + /(yabrowser)\/([\w\.]+)/i // Yandex + ], [[NAME, 'Yandex'], VERSION], [ + + /(comodo_dragon)\/([\w\.]+)/i // Comodo Dragon + ], [[NAME, /_/g, ' '], VERSION], [ + + /(chrome|omniweb|arora|[tizenoka]{5}\s?browser)\/v?([\w\.]+)/i, + // Chrome/OmniWeb/Arora/Tizen/Nokia + /(uc\s?browser|qqbrowser)[\/\s]?([\w\.]+)/i + // UCBrowser/QQBrowser + ], [NAME, VERSION], [ + + /(dolfin)\/([\w\.]+)/i // Dolphin + ], [[NAME, 'Dolphin'], VERSION], [ + + /((?:android.+)crmo|crios)\/([\w\.]+)/i // Chrome for Android/iOS + ], [[NAME, 'Chrome'], VERSION], [ + + /XiaoMi\/MiuiBrowser\/([\w\.]+)/i // MIUI Browser + ], [VERSION, [NAME, 'MIUI Browser']], [ + + /android.+version\/([\w\.]+)\s+(?:mobile\s?safari|safari)/i // Android Browser + ], [VERSION, [NAME, 'Android Browser']], [ + + /FBAV\/([\w\.]+);/i // Facebook App for iOS + ], [VERSION, [NAME, 'Facebook']], [ + + /version\/([\w\.]+).+?mobile\/\w+\s(safari)/i // Mobile Safari + ], [VERSION, [NAME, 'Mobile Safari']], [ + + /version\/([\w\.]+).+?(mobile\s?safari|safari)/i // Safari & Safari Mobile + ], [VERSION, NAME], [ + + /webkit.+?(mobile\s?safari|safari)(\/[\w\.]+)/i // Safari < 3.0 + ], [NAME, [VERSION, mapper.str, maps.browser.oldsafari.version]], [ + + /(konqueror)\/([\w\.]+)/i, // Konqueror + /(webkit|khtml)\/([\w\.]+)/i + ], [NAME, VERSION], [ + + // Gecko based + /(navigator|netscape)\/([\w\.-]+)/i // Netscape + ], [[NAME, 'Netscape'], VERSION], [ + /(swiftfox)/i, // Swiftfox + /(icedragon|iceweasel|camino|chimera|fennec|maemo\sbrowser|minimo|conkeror)[\/\s]?([\w\.\+]+)/i, + // IceDragon/Iceweasel/Camino/Chimera/Fennec/Maemo/Minimo/Conkeror + /(firefox|seamonkey|k-meleon|icecat|iceape|firebird|phoenix)\/([\w\.-]+)/i, + // Firefox/SeaMonkey/K-Meleon/IceCat/IceApe/Firebird/Phoenix + /(mozilla)\/([\w\.]+).+rv\:.+gecko\/\d+/i, // Mozilla + + // Other + /(polaris|lynx|dillo|icab|doris|amaya|w3m|netsurf)[\/\s]?([\w\.]+)/i, + // Polaris/Lynx/Dillo/iCab/Doris/Amaya/w3m/NetSurf + /(links)\s\(([\w\.]+)/i, // Links + /(gobrowser)\/?([\w\.]+)*/i, // GoBrowser + /(ice\s?browser)\/v?([\w\._]+)/i, // ICE Browser + /(mosaic)[\/\s]([\w\.]+)/i // Mosaic + ], [NAME, VERSION] + ], + + engine : [[ + + /windows.+\sedge\/([\w\.]+)/i // EdgeHTML + ], [VERSION, [NAME, 'EdgeHTML']], [ + + /(presto)\/([\w\.]+)/i, // Presto + /(webkit|trident|netfront|netsurf|amaya|lynx|w3m)\/([\w\.]+)/i, // WebKit/Trident/NetFront/NetSurf/Amaya/Lynx/w3m + /(khtml|tasman|links)[\/\s]\(?([\w\.]+)/i, // KHTML/Tasman/Links + /(icab)[\/\s]([23]\.[\d\.]+)/i // iCab + ], [NAME, VERSION], [ + + /rv\:([\w\.]+).*(gecko)/i // Gecko + ], [VERSION, NAME] + ], + + os : [[ + + // Windows based + /microsoft\s(windows)\s(vista|xp)/i // Windows (iTunes) + ], [NAME, VERSION], [ + /(windows)\snt\s6\.2;\s(arm)/i, // Windows RT + /(windows\sphone(?:\sos)*|windows\smobile|windows)[\s\/]?([ntce\d\.\s]+\w)/i + ], [NAME, [VERSION, mapper.str, maps.os.windows.version]], [ + /(win(?=3|9|n)|win\s9x\s)([nt\d\.]+)/i + ], [[NAME, 'Windows'], [VERSION, mapper.str, maps.os.windows.version]], [ + + // Mobile/Embedded OS + /\((bb)(10);/i // BlackBerry 10 + ], [[NAME, 'BlackBerry'], VERSION], [ + /(blackberry)\w*\/?([\w\.]+)*/i, // Blackberry + /(tizen)[\/\s]([\w\.]+)/i, // Tizen + /(android|webos|palm\os|qnx|bada|rim\stablet\sos|meego|contiki)[\/\s-]?([\w\.]+)*/i, + // Android/WebOS/Palm/QNX/Bada/RIM/MeeGo/Contiki + /linux;.+(sailfish);/i // Sailfish OS + ], [NAME, VERSION], [ + /(symbian\s?os|symbos|s60(?=;))[\/\s-]?([\w\.]+)*/i // Symbian + ], [[NAME, 'Symbian'], VERSION], [ + /\((series40);/i // Series 40 + ], [NAME], [ + /mozilla.+\(mobile;.+gecko.+firefox/i // Firefox OS + ], [[NAME, 'Firefox OS'], VERSION], [ + + // Console + /(nintendo|playstation)\s([wids3portablevu]+)/i, // Nintendo/Playstation + + // GNU/Linux based + /(mint)[\/\s\(]?(\w+)*/i, // Mint + /(mageia|vectorlinux)[;\s]/i, // Mageia/VectorLinux + /(joli|[kxln]?ubuntu|debian|[open]*suse|gentoo|arch|slackware|fedora|mandriva|centos|pclinuxos|redhat|zenwalk|linpus)[\/\s-]?([\w\.-]+)*/i, + // Joli/Ubuntu/Debian/SUSE/Gentoo/Arch/Slackware + // Fedora/Mandriva/CentOS/PCLinuxOS/RedHat/Zenwalk/Linpus + /(hurd|linux)\s?([\w\.]+)*/i, // Hurd/Linux + /(gnu)\s?([\w\.]+)*/i // GNU + ], [NAME, VERSION], [ + + /(cros)\s[\w]+\s([\w\.]+\w)/i // Chromium OS + ], [[NAME, 'Chromium OS'], VERSION],[ + + // Solaris + /(sunos)\s?([\w\.]+\d)*/i // Solaris + ], [[NAME, 'Solaris'], VERSION], [ + + // BSD based + /\s([frentopc-]{0,4}bsd|dragonfly)\s?([\w\.]+)*/i // FreeBSD/NetBSD/OpenBSD/PC-BSD/DragonFly + ], [NAME, VERSION],[ + + /(ip[honead]+)(?:.*os\s*([\w]+)*\slike\smac|;\sopera)/i // iOS + ], [[NAME, 'iOS'], [VERSION, /_/g, '.']], [ + + /(mac\sos\sx)\s?([\w\s\.]+\w)*/i, + /(macintosh|mac(?=_powerpc)\s)/i // Mac OS + ], [[NAME, 'Mac OS'], [VERSION, /_/g, '.']], [ + + // Other + /((?:open)?solaris)[\/\s-]?([\w\.]+)*/i, // Solaris + /(haiku)\s(\w+)/i, // Haiku + /(aix)\s((\d)(?=\.|\)|\s)[\w\.]*)*/i, // AIX + /(plan\s9|minix|beos|os\/2|amigaos|morphos|risc\sos|openvms)/i, + // Plan9/Minix/BeOS/OS2/AmigaOS/MorphOS/RISCOS/OpenVMS + /(unix)\s?([\w\.]+)*/i // UNIX + ], [NAME, VERSION] + ] + }; + + + ///////////////// + // Constructor + //////////////// + + + var UAParser = function (uastring) { + + var ua = uastring || ((window && window.navigator && window.navigator.userAgent) ? window.navigator.userAgent : EMPTY); + + this.getBrowser = function () { + return mapper.rgx.apply(this, regexes.browser); + }; + this.getEngine = function () { + return mapper.rgx.apply(this, regexes.engine); + }; + this.getOS = function () { + return mapper.rgx.apply(this, regexes.os); + }; + this.getResult = function() { + return { + ua : this.getUA(), + browser : this.getBrowser(), + engine : this.getEngine(), + os : this.getOS() + }; + }; + this.getUA = function () { + return ua; + }; + this.setUA = function (uastring) { + ua = uastring; + return this; + }; + this.setUA(ua); + }; + + return UAParser; + })(); + + + function version_compare(v1, v2, operator) { + // From: http://phpjs.org/functions + // + original by: Philippe Jausions (http://pear.php.net/user/jausions) + // + original by: Aidan Lister (http://aidanlister.com/) + // + reimplemented by: Kankrelune (http://www.webfaktory.info/) + // + improved by: Brett Zamir (http://brett-zamir.me) + // + improved by: Scott Baker + // + improved by: Theriault + // * example 1: version_compare('8.2.5rc', '8.2.5a'); + // * returns 1: 1 + // * example 2: version_compare('8.2.50', '8.2.52', '<'); + // * returns 2: true + // * example 3: version_compare('5.3.0-dev', '5.3.0'); + // * returns 3: -1 + // * example 4: version_compare('4.1.0.52','4.01.0.51'); + // * returns 4: 1 + + // Important: compare must be initialized at 0. + var i = 0, + x = 0, + compare = 0, + // vm maps textual PHP versions to negatives so they're less than 0. + // PHP currently defines these as CASE-SENSITIVE. It is important to + // leave these as negatives so that they can come before numerical versions + // and as if no letters were there to begin with. + // (1alpha is < 1 and < 1.1 but > 1dev1) + // If a non-numerical value can't be mapped to this table, it receives + // -7 as its value. + vm = { + 'dev': -6, + 'alpha': -5, + 'a': -5, + 'beta': -4, + 'b': -4, + 'RC': -3, + 'rc': -3, + '#': -2, + 'p': 1, + 'pl': 1 + }, + // This function will be called to prepare each version argument. + // It replaces every _, -, and + with a dot. + // It surrounds any nonsequence of numbers/dots with dots. + // It replaces sequences of dots with a single dot. + // version_compare('4..0', '4.0') == 0 + // Important: A string of 0 length needs to be converted into a value + // even less than an unexisting value in vm (-7), hence [-8]. + // It's also important to not strip spaces because of this. + // version_compare('', ' ') == 1 + prepVersion = function (v) { + v = ('' + v).replace(/[_\-+]/g, '.'); + v = v.replace(/([^.\d]+)/g, '.$1.').replace(/\.{2,}/g, '.'); + return (!v.length ? [-8] : v.split('.')); + }, + // This converts a version component to a number. + // Empty component becomes 0. + // Non-numerical component becomes a negative number. + // Numerical component becomes itself as an integer. + numVersion = function (v) { + return !v ? 0 : (isNaN(v) ? vm[v] || -7 : parseInt(v, 10)); + }; + + v1 = prepVersion(v1); + v2 = prepVersion(v2); + x = Math.max(v1.length, v2.length); + for (i = 0; i < x; i++) { + if (v1[i] == v2[i]) { + continue; + } + v1[i] = numVersion(v1[i]); + v2[i] = numVersion(v2[i]); + if (v1[i] < v2[i]) { + compare = -1; + break; + } else if (v1[i] > v2[i]) { + compare = 1; + break; + } + } + if (!operator) { + return compare; + } + + // Important: operator is CASE-SENSITIVE. + // "No operator" seems to be treated as "<." + // Any other values seem to make the function return null. + switch (operator) { + case '>': + case 'gt': + return (compare > 0); + case '>=': + case 'ge': + return (compare >= 0); + case '<=': + case 'le': + return (compare <= 0); + case '==': + case '=': + case 'eq': + return (compare === 0); + case '<>': + case '!=': + case 'ne': + return (compare !== 0); + case '': + case '<': + case 'lt': + return (compare < 0); + default: + return null; + } + } + + + var can = (function() { + var caps = { + define_property: (function() { + /* // currently too much extra code required, not exactly worth it + try { // as of IE8, getters/setters are supported only on DOM elements + var obj = {}; + if (Object.defineProperty) { + Object.defineProperty(obj, 'prop', { + enumerable: true, + configurable: true + }); + return true; + } + } catch(ex) {} + + if (Object.prototype.__defineGetter__ && Object.prototype.__defineSetter__) { + return true; + }*/ + return false; + }()), + + create_canvas: (function() { + // On the S60 and BB Storm, getContext exists, but always returns undefined + // so we actually have to call getContext() to verify + // github.com/Modernizr/Modernizr/issues/issue/97/ + var el = document.createElement('canvas'); + return !!(el.getContext && el.getContext('2d')); + }()), + + return_response_type: function(responseType) { + try { + if (Basic.inArray(responseType, ['', 'text', 'document']) !== -1) { + return true; + } else if (window.XMLHttpRequest) { + var xhr = new XMLHttpRequest(); + xhr.open('get', '/'); // otherwise Gecko throws an exception + if ('responseType' in xhr) { + xhr.responseType = responseType; + // as of 23.0.1271.64, Chrome switched from throwing exception to merely logging it to the console (why? o why?) + if (xhr.responseType !== responseType) { + return false; + } + return true; + } + } + } catch (ex) {} + return false; + }, + + // ideas for this heavily come from Modernizr (http://modernizr.com/) + use_data_uri: (function() { + var du = new Image(); + + du.onload = function() { + caps.use_data_uri = (du.width === 1 && du.height === 1); + }; + + setTimeout(function() { + du.src = "data:image/gif;base64,R0lGODlhAQABAIAAAP8AAAAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=="; + }, 1); + return false; + }()), + + use_data_uri_over32kb: function() { // IE8 + return caps.use_data_uri && (Env.browser !== 'IE' || Env.version >= 9); + }, + + use_data_uri_of: function(bytes) { + return (caps.use_data_uri && bytes < 33000 || caps.use_data_uri_over32kb()); + }, + + use_fileinput: function() { + if (navigator.userAgent.match(/(Android (1.0|1.1|1.5|1.6|2.0|2.1))|(Windows Phone (OS 7|8.0))|(XBLWP)|(ZuneWP)|(w(eb)?OSBrowser)|(webOS)|(Kindle\/(1.0|2.0|2.5|3.0))/)) { + return false; + } + + var el = document.createElement('input'); + el.setAttribute('type', 'file'); + return !el.disabled; + } + }; + + return function(cap) { + var args = [].slice.call(arguments); + args.shift(); // shift of cap + return Basic.typeOf(caps[cap]) === 'function' ? caps[cap].apply(this, args) : !!caps[cap]; + }; + }()); + + + var uaResult = new UAParser().getResult(); + + + var Env = { + can: can, + + uaParser: UAParser, + + browser: uaResult.browser.name, + version: uaResult.browser.version, + os: uaResult.os.name, // everybody intuitively types it in a lowercase for some reason + osVersion: uaResult.os.version, + + verComp: version_compare, + + swf_url: "../flash/Moxie.swf", + xap_url: "../silverlight/Moxie.xap", + global_event_dispatcher: "moxie.core.EventTarget.instance.dispatchEvent" + }; + + // for backward compatibility + // @deprecated Use `Env.os` instead + Env.OS = Env.os; + + if (MXI_DEBUG) { + Env.debug = { + runtime: true, + events: false + }; + + Env.log = function() { + + function logObj(data) { + // TODO: this should recursively print out the object in a pretty way + console.appendChild(document.createTextNode(data + "\n")); + } + + var data = arguments[0]; + + if (Basic.typeOf(data) === 'string') { + data = Basic.sprintf.apply(this, arguments); + } + + if (window && window.console && window.console.log) { + window.console.log(data); + } else if (document) { + var console = document.getElementById('moxie-console'); + if (!console) { + console = document.createElement('pre'); + console.id = 'moxie-console'; + //console.style.display = 'none'; + document.body.appendChild(console); + } + + if (Basic.inArray(Basic.typeOf(data), ['object', 'array']) !== -1) { + logObj(data); + } else { + console.appendChild(document.createTextNode(data + "\n")); + } + } + }; + } + + return Env; +}); + +// Included from: src/javascript/core/Exceptions.js + +/** + * Exceptions.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +define('moxie/core/Exceptions', [ + 'moxie/core/utils/Basic' +], function(Basic) { + + function _findKey(obj, value) { + var key; + for (key in obj) { + if (obj[key] === value) { + return key; + } + } + return null; + } + + /** + @class moxie/core/Exception + */ + return { + RuntimeError: (function() { + var namecodes = { + NOT_INIT_ERR: 1, + EXCEPTION_ERR: 3, + NOT_SUPPORTED_ERR: 9, + JS_ERR: 4 + }; + + function RuntimeError(code, message) { + this.code = code; + this.name = _findKey(namecodes, code); + this.message = this.name + (message || ": RuntimeError " + this.code); + } + + Basic.extend(RuntimeError, namecodes); + RuntimeError.prototype = Error.prototype; + return RuntimeError; + }()), + + OperationNotAllowedException: (function() { + + function OperationNotAllowedException(code) { + this.code = code; + this.name = 'OperationNotAllowedException'; + } + + Basic.extend(OperationNotAllowedException, { + NOT_ALLOWED_ERR: 1 + }); + + OperationNotAllowedException.prototype = Error.prototype; + + return OperationNotAllowedException; + }()), + + ImageError: (function() { + var namecodes = { + WRONG_FORMAT: 1, + MAX_RESOLUTION_ERR: 2, + INVALID_META_ERR: 3 + }; + + function ImageError(code) { + this.code = code; + this.name = _findKey(namecodes, code); + this.message = this.name + ": ImageError " + this.code; + } + + Basic.extend(ImageError, namecodes); + ImageError.prototype = Error.prototype; + + return ImageError; + }()), + + FileException: (function() { + var namecodes = { + NOT_FOUND_ERR: 1, + SECURITY_ERR: 2, + ABORT_ERR: 3, + NOT_READABLE_ERR: 4, + ENCODING_ERR: 5, + NO_MODIFICATION_ALLOWED_ERR: 6, + INVALID_STATE_ERR: 7, + SYNTAX_ERR: 8 + }; + + function FileException(code) { + this.code = code; + this.name = _findKey(namecodes, code); + this.message = this.name + ": FileException " + this.code; + } + + Basic.extend(FileException, namecodes); + FileException.prototype = Error.prototype; + return FileException; + }()), + + DOMException: (function() { + var namecodes = { + INDEX_SIZE_ERR: 1, + DOMSTRING_SIZE_ERR: 2, + HIERARCHY_REQUEST_ERR: 3, + WRONG_DOCUMENT_ERR: 4, + INVALID_CHARACTER_ERR: 5, + NO_DATA_ALLOWED_ERR: 6, + NO_MODIFICATION_ALLOWED_ERR: 7, + NOT_FOUND_ERR: 8, + NOT_SUPPORTED_ERR: 9, + INUSE_ATTRIBUTE_ERR: 10, + INVALID_STATE_ERR: 11, + SYNTAX_ERR: 12, + INVALID_MODIFICATION_ERR: 13, + NAMESPACE_ERR: 14, + INVALID_ACCESS_ERR: 15, + VALIDATION_ERR: 16, + TYPE_MISMATCH_ERR: 17, + SECURITY_ERR: 18, + NETWORK_ERR: 19, + ABORT_ERR: 20, + URL_MISMATCH_ERR: 21, + QUOTA_EXCEEDED_ERR: 22, + TIMEOUT_ERR: 23, + INVALID_NODE_TYPE_ERR: 24, + DATA_CLONE_ERR: 25 + }; + + function DOMException(code) { + this.code = code; + this.name = _findKey(namecodes, code); + this.message = this.name + ": DOMException " + this.code; + } + + Basic.extend(DOMException, namecodes); + DOMException.prototype = Error.prototype; + return DOMException; + }()), + + EventException: (function() { + function EventException(code) { + this.code = code; + this.name = 'EventException'; + } + + Basic.extend(EventException, { + UNSPECIFIED_EVENT_TYPE_ERR: 0 + }); + + EventException.prototype = Error.prototype; + + return EventException; + }()) + }; +}); + +// Included from: src/javascript/core/utils/Dom.js + +/** + * Dom.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +define('moxie/core/utils/Dom', ['moxie/core/utils/Env'], function(Env) { + + /** + Get DOM Element by it's id. + + @method get + @for Utils + @param {String} id Identifier of the DOM Element + @return {DOMElement} + */ + var get = function(id) { + if (typeof id !== 'string') { + return id; + } + return document.getElementById(id); + }; + + /** + Checks if specified DOM element has specified class. + + @method hasClass + @static + @param {Object} obj DOM element like object to add handler to. + @param {String} name Class name + */ + var hasClass = function(obj, name) { + if (!obj.className) { + return false; + } + + var regExp = new RegExp("(^|\\s+)"+name+"(\\s+|$)"); + return regExp.test(obj.className); + }; + + /** + Adds specified className to specified DOM element. + + @method addClass + @static + @param {Object} obj DOM element like object to add handler to. + @param {String} name Class name + */ + var addClass = function(obj, name) { + if (!hasClass(obj, name)) { + obj.className = !obj.className ? name : obj.className.replace(/\s+$/, '') + ' ' + name; + } + }; + + /** + Removes specified className from specified DOM element. + + @method removeClass + @static + @param {Object} obj DOM element like object to add handler to. + @param {String} name Class name + */ + var removeClass = function(obj, name) { + if (obj.className) { + var regExp = new RegExp("(^|\\s+)"+name+"(\\s+|$)"); + obj.className = obj.className.replace(regExp, function($0, $1, $2) { + return $1 === ' ' && $2 === ' ' ? ' ' : ''; + }); + } + }; + + /** + Returns a given computed style of a DOM element. + + @method getStyle + @static + @param {Object} obj DOM element like object. + @param {String} name Style you want to get from the DOM element + */ + var getStyle = function(obj, name) { + if (obj.currentStyle) { + return obj.currentStyle[name]; + } else if (window.getComputedStyle) { + return window.getComputedStyle(obj, null)[name]; + } + }; + + + /** + Returns the absolute x, y position of an Element. The position will be returned in a object with x, y fields. + + @method getPos + @static + @param {Element} node HTML element or element id to get x, y position from. + @param {Element} root Optional root element to stop calculations at. + @return {object} Absolute position of the specified element object with x, y fields. + */ + var getPos = function(node, root) { + var x = 0, y = 0, parent, doc = document, nodeRect, rootRect; + + node = node; + root = root || doc.body; + + // Returns the x, y cordinate for an element on IE 6 and IE 7 + function getIEPos(node) { + var bodyElm, rect, x = 0, y = 0; + + if (node) { + rect = node.getBoundingClientRect(); + bodyElm = doc.compatMode === "CSS1Compat" ? doc.documentElement : doc.body; + x = rect.left + bodyElm.scrollLeft; + y = rect.top + bodyElm.scrollTop; + } + + return { + x : x, + y : y + }; + } + + // Use getBoundingClientRect on IE 6 and IE 7 but not on IE 8 in standards mode + if (node && node.getBoundingClientRect && Env.browser === 'IE' && (!doc.documentMode || doc.documentMode < 8)) { + nodeRect = getIEPos(node); + rootRect = getIEPos(root); + + return { + x : nodeRect.x - rootRect.x, + y : nodeRect.y - rootRect.y + }; + } + + parent = node; + while (parent && parent != root && parent.nodeType) { + x += parent.offsetLeft || 0; + y += parent.offsetTop || 0; + parent = parent.offsetParent; + } + + parent = node.parentNode; + while (parent && parent != root && parent.nodeType) { + x -= parent.scrollLeft || 0; + y -= parent.scrollTop || 0; + parent = parent.parentNode; + } + + return { + x : x, + y : y + }; + }; + + /** + Returns the size of the specified node in pixels. + + @method getSize + @static + @param {Node} node Node to get the size of. + @return {Object} Object with a w and h property. + */ + var getSize = function(node) { + return { + w : node.offsetWidth || node.clientWidth, + h : node.offsetHeight || node.clientHeight + }; + }; + + return { + get: get, + hasClass: hasClass, + addClass: addClass, + removeClass: removeClass, + getStyle: getStyle, + getPos: getPos, + getSize: getSize + }; +}); + +// Included from: src/javascript/core/EventTarget.js + +/** + * EventTarget.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +define('moxie/core/EventTarget', [ + 'moxie/core/utils/Env', + 'moxie/core/Exceptions', + 'moxie/core/utils/Basic' +], function(Env, x, Basic) { + + // hash of event listeners by object uid + var eventpool = {}; + + /** + Parent object for all event dispatching components and objects + + @class moxie/core/EventTarget + @constructor EventTarget + */ + function EventTarget() { + /** + Unique id of the event dispatcher, usually overriden by children + + @property uid + @type String + */ + this.uid = Basic.guid(); + } + + + Basic.extend(EventTarget.prototype, { + + /** + Can be called from within a child in order to acquire uniqie id in automated manner + + @method init + */ + init: function() { + if (!this.uid) { + this.uid = Basic.guid('uid_'); + } + }, + + /** + Register a handler to a specific event dispatched by the object + + @method addEventListener + @param {String} type Type or basically a name of the event to subscribe to + @param {Function} fn Callback function that will be called when event happens + @param {Number} [priority=0] Priority of the event handler - handlers with higher priorities will be called first + @param {Object} [scope=this] A scope to invoke event handler in + */ + addEventListener: function(type, fn, priority, scope) { + var self = this, list; + + // without uid no event handlers can be added, so make sure we got one + if (!this.hasOwnProperty('uid')) { + this.uid = Basic.guid('uid_'); + } + + type = Basic.trim(type); + + if (/\s/.test(type)) { + // multiple event types were passed for one handler + Basic.each(type.split(/\s+/), function(type) { + self.addEventListener(type, fn, priority, scope); + }); + return; + } + + type = type.toLowerCase(); + priority = parseInt(priority, 10) || 0; + + list = eventpool[this.uid] && eventpool[this.uid][type] || []; + list.push({fn : fn, priority : priority, scope : scope || this}); + + if (!eventpool[this.uid]) { + eventpool[this.uid] = {}; + } + eventpool[this.uid][type] = list; + }, + + /** + Check if any handlers were registered to the specified event + + @method hasEventListener + @param {String} [type] Type or basically a name of the event to check + @return {Mixed} Returns a handler if it was found and false, if - not + */ + hasEventListener: function(type) { + var list; + if (type) { + type = type.toLowerCase(); + list = eventpool[this.uid] && eventpool[this.uid][type]; + } else { + list = eventpool[this.uid]; + } + return list ? list : false; + }, + + /** + Unregister the handler from the event, or if former was not specified - unregister all handlers + + @method removeEventListener + @param {String} type Type or basically a name of the event + @param {Function} [fn] Handler to unregister + */ + removeEventListener: function(type, fn) { + var self = this, list, i; + + type = type.toLowerCase(); + + if (/\s/.test(type)) { + // multiple event types were passed for one handler + Basic.each(type.split(/\s+/), function(type) { + self.removeEventListener(type, fn); + }); + return; + } + + list = eventpool[this.uid] && eventpool[this.uid][type]; + + if (list) { + if (fn) { + for (i = list.length - 1; i >= 0; i--) { + if (list[i].fn === fn) { + list.splice(i, 1); + break; + } + } + } else { + list = []; + } + + // delete event list if it has become empty + if (!list.length) { + delete eventpool[this.uid][type]; + + // and object specific entry in a hash if it has no more listeners attached + if (Basic.isEmptyObj(eventpool[this.uid])) { + delete eventpool[this.uid]; + } + } + } + }, + + /** + Remove all event handlers from the object + + @method removeAllEventListeners + */ + removeAllEventListeners: function() { + if (eventpool[this.uid]) { + delete eventpool[this.uid]; + } + }, + + /** + Dispatch the event + + @method dispatchEvent + @param {String/Object} Type of event or event object to dispatch + @param {Mixed} [...] Variable number of arguments to be passed to a handlers + @return {Boolean} true by default and false if any handler returned false + */ + dispatchEvent: function(type) { + var uid, list, args, tmpEvt, evt = {}, result = true, undef; + + if (Basic.typeOf(type) !== 'string') { + // we can't use original object directly (because of Silverlight) + tmpEvt = type; + + if (Basic.typeOf(tmpEvt.type) === 'string') { + type = tmpEvt.type; + + if (tmpEvt.total !== undef && tmpEvt.loaded !== undef) { // progress event + evt.total = tmpEvt.total; + evt.loaded = tmpEvt.loaded; + } + evt.async = tmpEvt.async || false; + } else { + throw new x.EventException(x.EventException.UNSPECIFIED_EVENT_TYPE_ERR); + } + } + + // check if event is meant to be dispatched on an object having specific uid + if (type.indexOf('::') !== -1) { + (function(arr) { + uid = arr[0]; + type = arr[1]; + }(type.split('::'))); + } else { + uid = this.uid; + } + + type = type.toLowerCase(); + + list = eventpool[uid] && eventpool[uid][type]; + + if (list) { + // sort event list by prority + list.sort(function(a, b) { return b.priority - a.priority; }); + + args = [].slice.call(arguments); + + // first argument will be pseudo-event object + args.shift(); + evt.type = type; + args.unshift(evt); + + if (MXI_DEBUG && Env.debug.events) { + Env.log("Event '%s' fired on %u", evt.type, uid); + } + + // Dispatch event to all listeners + var queue = []; + Basic.each(list, function(handler) { + // explicitly set the target, otherwise events fired from shims do not get it + args[0].target = handler.scope; + // if event is marked as async, detach the handler + if (evt.async) { + queue.push(function(cb) { + setTimeout(function() { + cb(handler.fn.apply(handler.scope, args) === false); + }, 1); + }); + } else { + queue.push(function(cb) { + cb(handler.fn.apply(handler.scope, args) === false); // if handler returns false stop propagation + }); + } + }); + if (queue.length) { + Basic.inSeries(queue, function(err) { + result = !err; + }); + } + } + return result; + }, + + /** + Register a handler to the event type that will run only once + + @method bindOnce + @since >1.4.1 + @param {String} type Type or basically a name of the event to subscribe to + @param {Function} fn Callback function that will be called when event happens + @param {Number} [priority=0] Priority of the event handler - handlers with higher priorities will be called first + @param {Object} [scope=this] A scope to invoke event handler in + */ + bindOnce: function(type, fn, priority, scope) { + var self = this; + self.bind.call(this, type, function cb() { + self.unbind(type, cb); + return fn.apply(this, arguments); + }, priority, scope); + }, + + /** + Alias for addEventListener + + @method bind + @protected + */ + bind: function() { + this.addEventListener.apply(this, arguments); + }, + + /** + Alias for removeEventListener + + @method unbind + @protected + */ + unbind: function() { + this.removeEventListener.apply(this, arguments); + }, + + /** + Alias for removeAllEventListeners + + @method unbindAll + @protected + */ + unbindAll: function() { + this.removeAllEventListeners.apply(this, arguments); + }, + + /** + Alias for dispatchEvent + + @method trigger + @protected + */ + trigger: function() { + return this.dispatchEvent.apply(this, arguments); + }, + + + /** + Handle properties of on[event] type. + + @method handleEventProps + @private + */ + handleEventProps: function(dispatches) { + var self = this; + + this.bind(dispatches.join(' '), function(e) { + var prop = 'on' + e.type.toLowerCase(); + if (Basic.typeOf(this[prop]) === 'function') { + this[prop].apply(this, arguments); + } + }); + + // object must have defined event properties, even if it doesn't make use of them + Basic.each(dispatches, function(prop) { + prop = 'on' + prop.toLowerCase(prop); + if (Basic.typeOf(self[prop]) === 'undefined') { + self[prop] = null; + } + }); + } + + }); + + + EventTarget.instance = new EventTarget(); + + return EventTarget; +}); + +// Included from: src/javascript/runtime/Runtime.js + +/** + * Runtime.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +define('moxie/runtime/Runtime', [ + "moxie/core/utils/Env", + "moxie/core/utils/Basic", + "moxie/core/utils/Dom", + "moxie/core/EventTarget" +], function(Env, Basic, Dom, EventTarget) { + var runtimeConstructors = {}, runtimes = {}; + + /** + Common set of methods and properties for every runtime instance + + @class moxie/runtime/Runtime + + @param {Object} options + @param {String} type Sanitized name of the runtime + @param {Object} [caps] Set of capabilities that differentiate specified runtime + @param {Object} [modeCaps] Set of capabilities that do require specific operational mode + @param {String} [preferredMode='browser'] Preferred operational mode to choose if no required capabilities were requested + */ + function Runtime(options, type, caps, modeCaps, preferredMode) { + /** + Dispatched when runtime is initialized and ready. + Results in RuntimeInit on a connected component. + + @event Init + */ + + /** + Dispatched when runtime fails to initialize. + Results in RuntimeError on a connected component. + + @event Error + */ + + var self = this + , _shim + , _uid = Basic.guid(type + '_') + , defaultMode = preferredMode || 'browser' + ; + + options = options || {}; + + // register runtime in private hash + runtimes[_uid] = this; + + /** + Default set of capabilities, which can be redifined later by specific runtime + + @private + @property caps + @type Object + */ + caps = Basic.extend({ + // Runtime can: + // provide access to raw binary data of the file + access_binary: false, + // provide access to raw binary data of the image (image extension is optional) + access_image_binary: false, + // display binary data as thumbs for example + display_media: false, + // make cross-domain requests + do_cors: false, + // accept files dragged and dropped from the desktop + drag_and_drop: false, + // filter files in selection dialog by their extensions + filter_by_extension: true, + // resize image (and manipulate it raw data of any file in general) + resize_image: false, + // periodically report how many bytes of total in the file were uploaded (loaded) + report_upload_progress: false, + // provide access to the headers of http response + return_response_headers: false, + // support response of specific type, which should be passed as an argument + // e.g. runtime.can('return_response_type', 'blob') + return_response_type: false, + // return http status code of the response + return_status_code: true, + // send custom http header with the request + send_custom_headers: false, + // pick up the files from a dialog + select_file: false, + // select whole folder in file browse dialog + select_folder: false, + // select multiple files at once in file browse dialog + select_multiple: true, + // send raw binary data, that is generated after image resizing or manipulation of other kind + send_binary_string: false, + // send cookies with http request and therefore retain session + send_browser_cookies: true, + // send data formatted as multipart/form-data + send_multipart: true, + // slice the file or blob to smaller parts + slice_blob: false, + // upload file without preloading it to memory, stream it out directly from disk + stream_upload: false, + // programmatically trigger file browse dialog + summon_file_dialog: false, + // upload file of specific size, size should be passed as argument + // e.g. runtime.can('upload_filesize', '500mb') + upload_filesize: true, + // initiate http request with specific http method, method should be passed as argument + // e.g. runtime.can('use_http_method', 'put') + use_http_method: true + }, caps); + + + // default to the mode that is compatible with preferred caps + if (options.preferred_caps) { + defaultMode = Runtime.getMode(modeCaps, options.preferred_caps, defaultMode); + } + + if (MXI_DEBUG && Env.debug.runtime) { + Env.log("\tdefault mode: %s", defaultMode); + } + + // small extension factory here (is meant to be extended with actual extensions constructors) + _shim = (function() { + var objpool = {}; + return { + exec: function(uid, comp, fn, args) { + if (_shim[comp]) { + if (!objpool[uid]) { + objpool[uid] = { + context: this, + instance: new _shim[comp]() + }; + } + if (objpool[uid].instance[fn]) { + return objpool[uid].instance[fn].apply(this, args); + } + } + }, + + removeInstance: function(uid) { + delete objpool[uid]; + }, + + removeAllInstances: function() { + var self = this; + Basic.each(objpool, function(obj, uid) { + if (Basic.typeOf(obj.instance.destroy) === 'function') { + obj.instance.destroy.call(obj.context); + } + self.removeInstance(uid); + }); + } + }; + }()); + + + // public methods + Basic.extend(this, { + /** + Specifies whether runtime instance was initialized or not + + @property initialized + @type {Boolean} + @default false + */ + initialized: false, // shims require this flag to stop initialization retries + + /** + Unique ID of the runtime + + @property uid + @type {String} + */ + uid: _uid, + + /** + Runtime type (e.g. flash, html5, etc) + + @property type + @type {String} + */ + type: type, + + /** + Runtime (not native one) may operate in browser or client mode. + + @property mode + @private + @type {String|Boolean} current mode or false, if none possible + */ + mode: Runtime.getMode(modeCaps, (options.required_caps), defaultMode), + + /** + id of the DOM container for the runtime (if available) + + @property shimid + @type {String} + */ + shimid: _uid + '_container', + + /** + Number of connected clients. If equal to zero, runtime can be destroyed + + @property clients + @type {Number} + */ + clients: 0, + + /** + Runtime initialization options + + @property options + @type {Object} + */ + options: options, + + /** + Checks if the runtime has specific capability + + @method can + @param {String} cap Name of capability to check + @param {Mixed} [value] If passed, capability should somehow correlate to the value + @param {Object} [refCaps] Set of capabilities to check the specified cap against (defaults to internal set) + @return {Boolean} true if runtime has such capability and false, if - not + */ + can: function(cap, value) { + var refCaps = arguments[2] || caps; + + // if cap var is a comma-separated list of caps, convert it to object (key/value) + if (Basic.typeOf(cap) === 'string' && Basic.typeOf(value) === 'undefined') { + cap = Runtime.parseCaps(cap); + } + + if (Basic.typeOf(cap) === 'object') { + for (var key in cap) { + if (!this.can(key, cap[key], refCaps)) { + return false; + } + } + return true; + } + + // check the individual cap + if (Basic.typeOf(refCaps[cap]) === 'function') { + return refCaps[cap].call(this, value); + } else { + return (value === refCaps[cap]); + } + }, + + /** + Returns container for the runtime as DOM element + + @method getShimContainer + @return {DOMElement} + */ + getShimContainer: function() { + var container, shimContainer = Dom.get(this.shimid); + + // if no container for shim, create one + if (!shimContainer) { + container = Dom.get(this.options.container) || document.body; + + // create shim container and insert it at an absolute position into the outer container + shimContainer = document.createElement('div'); + shimContainer.id = this.shimid; + shimContainer.className = 'moxie-shim moxie-shim-' + this.type; + + Basic.extend(shimContainer.style, { + position: 'absolute', + top: '0px', + left: '0px', + width: '1px', + height: '1px', + overflow: 'hidden' + }); + + container.appendChild(shimContainer); + container = null; + } + + return shimContainer; + }, + + /** + Returns runtime as DOM element (if appropriate) + + @method getShim + @return {DOMElement} + */ + getShim: function() { + return _shim; + }, + + /** + Invokes a method within the runtime itself (might differ across the runtimes) + + @method shimExec + @param {Mixed} [] + @protected + @return {Mixed} Depends on the action and component + */ + shimExec: function(component, action) { + var args = [].slice.call(arguments, 2); + return self.getShim().exec.call(this, this.uid, component, action, args); + }, + + /** + Operaional interface that is used by components to invoke specific actions on the runtime + (is invoked in the scope of component) + + @method exec + @param {Mixed} []* + @protected + @return {Mixed} Depends on the action and component + */ + exec: function(component, action) { // this is called in the context of component, not runtime + var args = [].slice.call(arguments, 2); + + if (self[component] && self[component][action]) { + return self[component][action].apply(this, args); + } + return self.shimExec.apply(this, arguments); + }, + + /** + Destroys the runtime (removes all events and deletes DOM structures) + + @method destroy + */ + destroy: function() { + if (!self) { + return; // obviously already destroyed + } + + var shimContainer = Dom.get(this.shimid); + if (shimContainer) { + shimContainer.parentNode.removeChild(shimContainer); + } + + if (_shim) { + _shim.removeAllInstances(); + } + + this.unbindAll(); + delete runtimes[this.uid]; + this.uid = null; // mark this runtime as destroyed + _uid = self = _shim = shimContainer = null; + } + }); + + // once we got the mode, test against all caps + if (this.mode && options.required_caps && !this.can(options.required_caps)) { + this.mode = false; + } + } + + + /** + Default order to try different runtime types + + @property order + @type String + @static + */ + Runtime.order = 'html5,flash,silverlight,html4'; + + + /** + Retrieves runtime from private hash by it's uid + + @method getRuntime + @private + @static + @param {String} uid Unique identifier of the runtime + @return {Runtime|Boolean} Returns runtime, if it exists and false, if - not + */ + Runtime.getRuntime = function(uid) { + return runtimes[uid] ? runtimes[uid] : false; + }; + + + /** + Register constructor for the Runtime of new (or perhaps modified) type + + @method addConstructor + @static + @param {String} type Runtime type (e.g. flash, html5, etc) + @param {Function} construct Constructor for the Runtime type + */ + Runtime.addConstructor = function(type, constructor) { + constructor.prototype = EventTarget.instance; + runtimeConstructors[type] = constructor; + }; + + + /** + Get the constructor for the specified type. + + method getConstructor + @static + @param {String} type Runtime type (e.g. flash, html5, etc) + @return {Function} Constructor for the Runtime type + */ + Runtime.getConstructor = function(type) { + return runtimeConstructors[type] || null; + }; + + + /** + Get info about the runtime (uid, type, capabilities) + + @method getInfo + @static + @param {String} uid Unique identifier of the runtime + @return {Mixed} Info object or null if runtime doesn't exist + */ + Runtime.getInfo = function(uid) { + var runtime = Runtime.getRuntime(uid); + + if (runtime) { + return { + uid: runtime.uid, + type: runtime.type, + mode: runtime.mode, + can: function() { + return runtime.can.apply(runtime, arguments); + } + }; + } + return null; + }; + + + /** + Convert caps represented by a comma-separated string to the object representation. + + @method parseCaps + @static + @param {String} capStr Comma-separated list of capabilities + @return {Object} + */ + Runtime.parseCaps = function(capStr) { + var capObj = {}; + + if (Basic.typeOf(capStr) !== 'string') { + return capStr || {}; + } + + Basic.each(capStr.split(','), function(key) { + capObj[key] = true; // we assume it to be - true + }); + + return capObj; + }; + + /** + Test the specified runtime for specific capabilities. + + @method can + @static + @param {String} type Runtime type (e.g. flash, html5, etc) + @param {String|Object} caps Set of capabilities to check + @return {Boolean} Result of the test + */ + Runtime.can = function(type, caps) { + var runtime + , constructor = Runtime.getConstructor(type) + , mode + ; + if (constructor) { + runtime = new constructor({ + required_caps: caps + }); + mode = runtime.mode; + runtime.destroy(); + return !!mode; + } + return false; + }; + + + /** + Figure out a runtime that supports specified capabilities. + + @method thatCan + @static + @param {String|Object} caps Set of capabilities to check + @param {String} [runtimeOrder] Comma-separated list of runtimes to check against + @return {String} Usable runtime identifier or null + */ + Runtime.thatCan = function(caps, runtimeOrder) { + var types = (runtimeOrder || Runtime.order).split(/\s*,\s*/); + for (var i in types) { + if (Runtime.can(types[i], caps)) { + return types[i]; + } + } + return null; + }; + + + /** + Figure out an operational mode for the specified set of capabilities. + + @method getMode + @static + @param {Object} modeCaps Set of capabilities that depend on particular runtime mode + @param {Object} [requiredCaps] Supplied set of capabilities to find operational mode for + @param {String|Boolean} [defaultMode='browser'] Default mode to use + @return {String|Boolean} Compatible operational mode + */ + Runtime.getMode = function(modeCaps, requiredCaps, defaultMode) { + var mode = null; + + if (Basic.typeOf(defaultMode) === 'undefined') { // only if not specified + defaultMode = 'browser'; + } + + if (requiredCaps && !Basic.isEmptyObj(modeCaps)) { + // loop over required caps and check if they do require the same mode + Basic.each(requiredCaps, function(value, cap) { + if (modeCaps.hasOwnProperty(cap)) { + var capMode = modeCaps[cap](value); + + // make sure we always have an array + if (typeof(capMode) === 'string') { + capMode = [capMode]; + } + + if (!mode) { + mode = capMode; + } else if (!(mode = Basic.arrayIntersect(mode, capMode))) { + // if cap requires conflicting mode - runtime cannot fulfill required caps + + if (MXI_DEBUG && Env.debug.runtime) { + Env.log("\t\t%c: %v (conflicting mode requested: %s)", cap, value, capMode); + } + + return (mode = false); + } + } + + if (MXI_DEBUG && Env.debug.runtime) { + Env.log("\t\t%c: %v (compatible modes: %s)", cap, value, mode); + } + }); + + if (mode) { + return Basic.inArray(defaultMode, mode) !== -1 ? defaultMode : mode[0]; + } else if (mode === false) { + return false; + } + } + return defaultMode; + }; + + + /** + Capability check that always returns true + + @private + @static + @return {True} + */ + Runtime.capTrue = function() { + return true; + }; + + /** + Capability check that always returns false + + @private + @static + @return {False} + */ + Runtime.capFalse = function() { + return false; + }; + + /** + Evaluate the expression to boolean value and create a function that always returns it. + + @private + @static + @param {Mixed} expr Expression to evaluate + @return {Function} Function returning the result of evaluation + */ + Runtime.capTest = function(expr) { + return function() { + return !!expr; + }; + }; + + return Runtime; +}); + +// Included from: src/javascript/runtime/RuntimeClient.js + +/** + * RuntimeClient.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +define('moxie/runtime/RuntimeClient', [ + 'moxie/core/utils/Env', + 'moxie/core/Exceptions', + 'moxie/core/utils/Basic', + 'moxie/runtime/Runtime' +], function(Env, x, Basic, Runtime) { + /** + Set of methods and properties, required by a component to acquire ability to connect to a runtime + + @class moxie/runtime/RuntimeClient + */ + return function RuntimeClient() { + var runtime; + + Basic.extend(this, { + /** + Connects to the runtime specified by the options. Will either connect to existing runtime or create a new one. + Increments number of clients connected to the specified runtime. + + @private + @method connectRuntime + @param {Mixed} options Can be a runtme uid or a set of key-value pairs defining requirements and pre-requisites + */ + connectRuntime: function(options) { + var comp = this, ruid; + + function initialize(items) { + var type, constructor; + + // if we ran out of runtimes + if (!items.length) { + comp.trigger('RuntimeError', new x.RuntimeError(x.RuntimeError.NOT_INIT_ERR)); + runtime = null; + return; + } + + type = items.shift().toLowerCase(); + constructor = Runtime.getConstructor(type); + if (!constructor) { + if (MXI_DEBUG && Env.debug.runtime) { + Env.log("Constructor for '%s' runtime is not available.", type); + } + initialize(items); + return; + } + + if (MXI_DEBUG && Env.debug.runtime) { + Env.log("Trying runtime: %s", type); + Env.log(options); + } + + // try initializing the runtime + runtime = new constructor(options); + + runtime.bind('Init', function() { + // mark runtime as initialized + runtime.initialized = true; + + if (MXI_DEBUG && Env.debug.runtime) { + Env.log("Runtime '%s' initialized", runtime.type); + } + + // jailbreak ... + setTimeout(function() { + runtime.clients++; + comp.ruid = runtime.uid; + // this will be triggered on component + comp.trigger('RuntimeInit', runtime); + }, 1); + }); + + runtime.bind('Error', function() { + if (MXI_DEBUG && Env.debug.runtime) { + Env.log("Runtime '%s' failed to initialize", runtime.type); + } + + runtime.destroy(); // runtime cannot destroy itself from inside at a right moment, thus we do it here + initialize(items); + }); + + runtime.bind('Exception', function(e, err) { + var message = err.name + "(#" + err.code + ")" + (err.message ? ", from: " + err.message : ''); + + if (MXI_DEBUG && Env.debug.runtime) { + Env.log("Runtime '%s' has thrown an exception: %s", this.type, message); + } + comp.trigger('RuntimeError', new x.RuntimeError(x.RuntimeError.EXCEPTION_ERR, message)); + }); + + if (MXI_DEBUG && Env.debug.runtime) { + Env.log("\tselected mode: %s", runtime.mode); + } + + // check if runtime managed to pick-up operational mode + if (!runtime.mode) { + runtime.trigger('Error'); + return; + } + + runtime.init(); + } + + // check if a particular runtime was requested + if (Basic.typeOf(options) === 'string') { + ruid = options; + } else if (Basic.typeOf(options.ruid) === 'string') { + ruid = options.ruid; + } + + if (ruid) { + runtime = Runtime.getRuntime(ruid); + if (runtime) { + comp.ruid = ruid; + runtime.clients++; + return runtime; + } else { + // there should be a runtime and there's none - weird case + throw new x.RuntimeError(x.RuntimeError.NOT_INIT_ERR); + } + } + + // initialize a fresh one, that fits runtime list and required features best + initialize((options.runtime_order || Runtime.order).split(/\s*,\s*/)); + }, + + + /** + Disconnects from the runtime. Decrements number of clients connected to the specified runtime. + + @private + @method disconnectRuntime + */ + disconnectRuntime: function() { + if (runtime && --runtime.clients <= 0) { + runtime.destroy(); + } + + // once the component is disconnected, it shouldn't have access to the runtime + runtime = null; + }, + + + /** + Returns the runtime to which the client is currently connected. + + @method getRuntime + @return {Runtime} Runtime or null if client is not connected + */ + getRuntime: function() { + if (runtime && runtime.uid) { + return runtime; + } + return runtime = null; // make sure we do not leave zombies rambling around + }, + + + /** + Handy shortcut to safely invoke runtime extension methods. + + @private + @method exec + @return {Mixed} Whatever runtime extension method returns + */ + exec: function() { + return runtime ? runtime.exec.apply(this, arguments) : null; + }, + + + /** + Test runtime client for specific capability + + @method can + @param {String} cap + @return {Bool} + */ + can: function(cap) { + return runtime ? runtime.can(cap) : false; + } + + }); + }; + + +}); + +// Included from: src/javascript/file/Blob.js + +/** + * Blob.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +define('moxie/file/Blob', [ + 'moxie/core/utils/Basic', + 'moxie/core/utils/Encode', + 'moxie/runtime/RuntimeClient' +], function(Basic, Encode, RuntimeClient) { + + var blobpool = {}; + + /** + @class moxie/file/Blob + @constructor + @param {String} ruid Unique id of the runtime, to which this blob belongs to + @param {Object} blob Object "Native" blob object, as it is represented in the runtime + */ + function Blob(ruid, blob) { + + function _sliceDetached(start, end, type) { + var blob, data = blobpool[this.uid]; + + if (Basic.typeOf(data) !== 'string' || !data.length) { + return null; // or throw exception + } + + blob = new Blob(null, { + type: type, + size: end - start + }); + blob.detach(data.substr(start, blob.size)); + + return blob; + } + + RuntimeClient.call(this); + + if (ruid) { + this.connectRuntime(ruid); + } + + if (!blob) { + blob = {}; + } else if (Basic.typeOf(blob) === 'string') { // dataUrl or binary string + blob = { data: blob }; + } + + Basic.extend(this, { + + /** + Unique id of the component + + @property uid + @type {String} + */ + uid: blob.uid || Basic.guid('uid_'), + + /** + Unique id of the connected runtime, if falsy, then runtime will have to be initialized + before this Blob can be used, modified or sent + + @property ruid + @type {String} + */ + ruid: ruid, + + /** + Size of blob + + @property size + @type {Number} + @default 0 + */ + size: blob.size || 0, + + /** + Mime type of blob + + @property type + @type {String} + @default '' + */ + type: blob.type || '', + + /** + @method slice + @param {Number} [start=0] + */ + slice: function(start, end, type) { + if (this.isDetached()) { + return _sliceDetached.apply(this, arguments); + } + return this.getRuntime().exec.call(this, 'Blob', 'slice', this.getSource(), start, end, type); + }, + + /** + Returns "native" blob object (as it is represented in connected runtime) or null if not found + + @method getSource + @return {Blob} Returns "native" blob object or null if not found + */ + getSource: function() { + if (!blobpool[this.uid]) { + return null; + } + return blobpool[this.uid]; + }, + + /** + Detaches blob from any runtime that it depends on and initialize with standalone value + + @method detach + @protected + @param {DOMString} [data=''] Standalone value + */ + detach: function(data) { + if (this.ruid) { + this.getRuntime().exec.call(this, 'Blob', 'destroy'); + this.disconnectRuntime(); + this.ruid = null; + } + + data = data || ''; + + // if dataUrl, convert to binary string + if (data.substr(0, 5) == 'data:') { + var base64Offset = data.indexOf(';base64,'); + this.type = data.substring(5, base64Offset); + data = Encode.atob(data.substring(base64Offset + 8)); + } + + this.size = data.length; + + blobpool[this.uid] = data; + }, + + /** + Checks if blob is standalone (detached of any runtime) + + @method isDetached + @protected + @return {Boolean} + */ + isDetached: function() { + return !this.ruid && Basic.typeOf(blobpool[this.uid]) === 'string'; + }, + + /** + Destroy Blob and free any resources it was using + + @method destroy + */ + destroy: function() { + this.detach(); + delete blobpool[this.uid]; + } + }); + + + if (blob.data) { + this.detach(blob.data); // auto-detach if payload has been passed + } else { + blobpool[this.uid] = blob; + } + } + + return Blob; +}); + +// Included from: src/javascript/core/I18n.js + +/** + * I18n.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +define("moxie/core/I18n", [ + "moxie/core/utils/Basic" +], function(Basic) { + var i18n = {}; + + /** + @class moxie/core/I18n + */ + return { + /** + * Extends the language pack object with new items. + * + * @param {Object} pack Language pack items to add. + * @return {Object} Extended language pack object. + */ + addI18n: function(pack) { + return Basic.extend(i18n, pack); + }, + + /** + * Translates the specified string by checking for the english string in the language pack lookup. + * + * @param {String} str String to look for. + * @return {String} Translated string or the input string if it wasn't found. + */ + translate: function(str) { + return i18n[str] || str; + }, + + /** + * Shortcut for translate function + * + * @param {String} str String to look for. + * @return {String} Translated string or the input string if it wasn't found. + */ + _: function(str) { + return this.translate(str); + }, + + /** + * Pseudo sprintf implementation - simple way to replace tokens with specified values. + * + * @param {String} str String with tokens + * @return {String} String with replaced tokens + */ + sprintf: function(str) { + var args = [].slice.call(arguments, 1); + + return str.replace(/%[a-z]/g, function() { + var value = args.shift(); + return Basic.typeOf(value) !== 'undefined' ? value : ''; + }); + } + }; +}); + +// Included from: src/javascript/core/utils/Mime.js + +/** + * Mime.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +define("moxie/core/utils/Mime", [ + "moxie/core/utils/Basic", + "moxie/core/I18n" +], function(Basic, I18n) { + + var mimeData = "" + + "application/msword,doc dot," + + "application/pdf,pdf," + + "application/pgp-signature,pgp," + + "application/postscript,ps ai eps," + + "application/rtf,rtf," + + "application/vnd.ms-excel,xls xlb," + + "application/vnd.ms-powerpoint,ppt pps pot," + + "application/zip,zip," + + "application/x-shockwave-flash,swf swfl," + + "application/vnd.openxmlformats-officedocument.wordprocessingml.document,docx," + + "application/vnd.openxmlformats-officedocument.wordprocessingml.template,dotx," + + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,xlsx," + + "application/vnd.openxmlformats-officedocument.presentationml.presentation,pptx," + + "application/vnd.openxmlformats-officedocument.presentationml.template,potx," + + "application/vnd.openxmlformats-officedocument.presentationml.slideshow,ppsx," + + "application/x-javascript,js," + + "application/json,json," + + "audio/mpeg,mp3 mpga mpega mp2," + + "audio/x-wav,wav," + + "audio/x-m4a,m4a," + + "audio/ogg,oga ogg," + + "audio/aiff,aiff aif," + + "audio/flac,flac," + + "audio/aac,aac," + + "audio/ac3,ac3," + + "audio/x-ms-wma,wma," + + "image/bmp,bmp," + + "image/gif,gif," + + "image/jpeg,jpg jpeg jpe," + + "image/photoshop,psd," + + "image/png,png," + + "image/svg+xml,svg svgz," + + "image/tiff,tiff tif," + + "text/plain,asc txt text diff log," + + "text/html,htm html xhtml," + + "text/css,css," + + "text/csv,csv," + + "text/rtf,rtf," + + "video/mpeg,mpeg mpg mpe m2v," + + "video/quicktime,qt mov," + + "video/mp4,mp4," + + "video/x-m4v,m4v," + + "video/x-flv,flv," + + "video/x-ms-wmv,wmv," + + "video/avi,avi," + + "video/webm,webm," + + "video/3gpp,3gpp 3gp," + + "video/3gpp2,3g2," + + "video/vnd.rn-realvideo,rv," + + "video/ogg,ogv," + + "video/x-matroska,mkv," + + "application/vnd.oasis.opendocument.formula-template,otf," + + "application/octet-stream,exe"; + + + var Mime = { + + mimes: {}, + + extensions: {}, + + // Parses the default mime types string into a mimes and extensions lookup maps + addMimeType: function (mimeData) { + var items = mimeData.split(/,/), i, ii, ext; + + for (i = 0; i < items.length; i += 2) { + ext = items[i + 1].split(/ /); + + // extension to mime lookup + for (ii = 0; ii < ext.length; ii++) { + this.mimes[ext[ii]] = items[i]; + } + // mime to extension lookup + this.extensions[items[i]] = ext; + } + }, + + + extList2mimes: function (filters, addMissingExtensions) { + var self = this, ext, i, ii, type, mimes = []; + + // convert extensions to mime types list + for (i = 0; i < filters.length; i++) { + ext = filters[i].extensions.toLowerCase().split(/\s*,\s*/); + + for (ii = 0; ii < ext.length; ii++) { + + // if there's an asterisk in the list, then accept attribute is not required + if (ext[ii] === '*') { + return []; + } + + type = self.mimes[ext[ii]]; + + // future browsers should filter by extension, finally + if (addMissingExtensions && /^\w+$/.test(ext[ii])) { + mimes.push('.' + ext[ii]); + } else if (type && Basic.inArray(type, mimes) === -1) { + mimes.push(type); + } else if (!type) { + // if we have no type in our map, then accept all + return []; + } + } + } + return mimes; + }, + + + mimes2exts: function(mimes) { + var self = this, exts = []; + + Basic.each(mimes, function(mime) { + mime = mime.toLowerCase(); + + if (mime === '*') { + exts = []; + return false; + } + + // check if this thing looks like mime type + var m = mime.match(/^(\w+)\/(\*|\w+)$/); + if (m) { + if (m[2] === '*') { + // wildcard mime type detected + Basic.each(self.extensions, function(arr, mime) { + if ((new RegExp('^' + m[1] + '/')).test(mime)) { + [].push.apply(exts, self.extensions[mime]); + } + }); + } else if (self.extensions[mime]) { + [].push.apply(exts, self.extensions[mime]); + } + } + }); + return exts; + }, + + + mimes2extList: function(mimes) { + var accept = [], exts = []; + + if (Basic.typeOf(mimes) === 'string') { + mimes = Basic.trim(mimes).split(/\s*,\s*/); + } + + exts = this.mimes2exts(mimes); + + accept.push({ + title: I18n.translate('Files'), + extensions: exts.length ? exts.join(',') : '*' + }); + + // save original mimes string + accept.mimes = mimes; + + return accept; + }, + + + getFileExtension: function(fileName) { + var matches = fileName && fileName.match(/\.([^.]+)$/); + if (matches) { + return matches[1].toLowerCase(); + } + return ''; + }, + + getFileMime: function(fileName) { + return this.mimes[this.getFileExtension(fileName)] || ''; + } + }; + + Mime.addMimeType(mimeData); + + return Mime; +}); + +// Included from: src/javascript/file/FileInput.js + +/** + * FileInput.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +define('moxie/file/FileInput', [ + 'moxie/core/utils/Basic', + 'moxie/core/utils/Env', + 'moxie/core/utils/Mime', + 'moxie/core/utils/Dom', + 'moxie/core/Exceptions', + 'moxie/core/EventTarget', + 'moxie/core/I18n', + 'moxie/runtime/Runtime', + 'moxie/runtime/RuntimeClient' +], function(Basic, Env, Mime, Dom, x, EventTarget, I18n, Runtime, RuntimeClient) { + /** + Provides a convenient way to create cross-browser file-picker. Generates file selection dialog on click, + converts selected files to _File_ objects, to be used in conjunction with _Image_, preloaded in memory + with _FileReader_ or uploaded to a server through _XMLHttpRequest_. + + @class moxie/file/FileInput + @constructor + @extends EventTarget + @uses RuntimeClient + @param {Object|String|DOMElement} options If options is string or node, argument is considered as _browse\_button_. + @param {String|DOMElement} options.browse_button DOM Element to turn into file picker. + @param {Array} [options.accept] Array of mime types to accept. By default accepts all. + @param {Boolean} [options.multiple=false] Enable selection of multiple files. + @param {Boolean} [options.directory=false] Turn file input into the folder input (cannot be both at the same time). + @param {String|DOMElement} [options.container] DOM Element to use as a container for file-picker. Defaults to parentNode + for _browse\_button_. + @param {Object|String} [options.required_caps] Set of required capabilities, that chosen runtime must support. + + @example +
+ Browse... +
+ + + */ + var dispatches = [ + /** + Dispatched when runtime is connected and file-picker is ready to be used. + + @event ready + @param {Object} event + */ + 'ready', + + /** + Dispatched right after [ready](#event_ready) event, and whenever [refresh()](#method_refresh) is invoked. + Check [corresponding documentation entry](#method_refresh) for more info. + + @event refresh + @param {Object} event + */ + + /** + Dispatched when selection of files in the dialog is complete. + + @event change + @param {Object} event + */ + 'change', + + 'cancel', // TODO: might be useful + + /** + Dispatched when mouse cursor enters file-picker area. Can be used to style element + accordingly. + + @event mouseenter + @param {Object} event + */ + 'mouseenter', + + /** + Dispatched when mouse cursor leaves file-picker area. Can be used to style element + accordingly. + + @event mouseleave + @param {Object} event + */ + 'mouseleave', + + /** + Dispatched when functional mouse button is pressed on top of file-picker area. + + @event mousedown + @param {Object} event + */ + 'mousedown', + + /** + Dispatched when functional mouse button is released on top of file-picker area. + + @event mouseup + @param {Object} event + */ + 'mouseup' + ]; + + function FileInput(options) { + if (MXI_DEBUG) { + Env.log("Instantiating FileInput..."); + } + + var container, browseButton, defaults; + + // if flat argument passed it should be browse_button id + if (Basic.inArray(Basic.typeOf(options), ['string', 'node']) !== -1) { + options = { browse_button : options }; + } + + // this will help us to find proper default container + browseButton = Dom.get(options.browse_button); + if (!browseButton) { + // browse button is required + throw new x.DOMException(x.DOMException.NOT_FOUND_ERR); + } + + // figure out the options + defaults = { + accept: [{ + title: I18n.translate('All Files'), + extensions: '*' + }], + multiple: false, + required_caps: false, + container: browseButton.parentNode || document.body + }; + + options = Basic.extend({}, defaults, options); + + // convert to object representation + if (typeof(options.required_caps) === 'string') { + options.required_caps = Runtime.parseCaps(options.required_caps); + } + + // normalize accept option (could be list of mime types or array of title/extensions pairs) + if (typeof(options.accept) === 'string') { + options.accept = Mime.mimes2extList(options.accept); + } + + container = Dom.get(options.container); + // make sure we have container + if (!container) { + container = document.body; + } + + // make container relative, if it's not + if (Dom.getStyle(container, 'position') === 'static') { + container.style.position = 'relative'; + } + + container = browseButton = null; // IE + + RuntimeClient.call(this); + + Basic.extend(this, { + /** + Unique id of the component + + @property uid + @protected + @readOnly + @type {String} + @default UID + */ + uid: Basic.guid('uid_'), + + /** + Unique id of the connected runtime, if any. + + @property ruid + @protected + @type {String} + */ + ruid: null, + + /** + Unique id of the runtime container. Useful to get hold of it for various manipulations. + + @property shimid + @protected + @type {String} + */ + shimid: null, + + /** + Array of selected mOxie.File objects + + @property files + @type {Array} + @default null + */ + files: null, + + /** + Initializes the file-picker, connects it to runtime and dispatches event ready when done. + + @method init + */ + init: function() { + var self = this; + + self.bind('RuntimeInit', function(e, runtime) { + self.ruid = runtime.uid; + self.shimid = runtime.shimid; + + self.bind("Ready", function() { + self.trigger("Refresh"); + }, 999); + + // re-position and resize shim container + self.bind('Refresh', function() { + var pos, size, browseButton, shimContainer, zIndex; + + browseButton = Dom.get(options.browse_button); + shimContainer = Dom.get(runtime.shimid); // do not use runtime.getShimContainer(), since it will create container if it doesn't exist + + if (browseButton) { + pos = Dom.getPos(browseButton, Dom.get(options.container)); + size = Dom.getSize(browseButton); + zIndex = parseInt(Dom.getStyle(browseButton, 'z-index'), 10) || 0; + + if (shimContainer) { + Basic.extend(shimContainer.style, { + top: pos.y + 'px', + left: pos.x + 'px', + width: size.w + 'px', + height: size.h + 'px', + zIndex: zIndex + 1 + }); + } + } + shimContainer = browseButton = null; + }); + + runtime.exec.call(self, 'FileInput', 'init', options); + }); + + // runtime needs: options.required_features, options.runtime_order and options.container + self.connectRuntime(Basic.extend({}, options, { + required_caps: { + select_file: true + } + })); + }, + + + /** + * Get current option value by its name + * + * @method getOption + * @param name + * @return {Mixed} + */ + getOption: function(name) { + return options[name]; + }, + + + /** + * Sets a new value for the option specified by name + * + * @method setOption + * @param name + * @param value + */ + setOption: function(name, value) { + if (!options.hasOwnProperty(name)) { + return; + } + + var oldValue = options[name]; + + switch (name) { + case 'accept': + if (typeof(value) === 'string') { + value = Mime.mimes2extList(value); + } + break; + + case 'container': + case 'required_caps': + throw new x.FileException(x.FileException.NO_MODIFICATION_ALLOWED_ERR); + } + + options[name] = value; + this.exec('FileInput', 'setOption', name, value); + + this.trigger('OptionChanged', name, value, oldValue); + }, + + /** + Disables file-picker element, so that it doesn't react to mouse clicks. + + @method disable + @param {Boolean} [state=true] Disable component if - true, enable if - false + */ + disable: function(state) { + var runtime = this.getRuntime(); + if (runtime) { + this.exec('FileInput', 'disable', Basic.typeOf(state) === 'undefined' ? true : state); + } + }, + + + /** + Reposition and resize dialog trigger to match the position and size of browse_button element. + + @method refresh + */ + refresh: function() { + this.trigger("Refresh"); + }, + + + /** + Destroy component. + + @method destroy + */ + destroy: function() { + var runtime = this.getRuntime(); + if (runtime) { + runtime.exec.call(this, 'FileInput', 'destroy'); + this.disconnectRuntime(); + } + + if (Basic.typeOf(this.files) === 'array') { + // no sense in leaving associated files behind + Basic.each(this.files, function(file) { + file.destroy(); + }); + } + this.files = null; + + this.unbindAll(); + } + }); + + this.handleEventProps(dispatches); + } + + FileInput.prototype = EventTarget.instance; + + return FileInput; +}); + +// Included from: src/javascript/file/File.js + +/** + * File.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +define('moxie/file/File', [ + 'moxie/core/utils/Basic', + 'moxie/core/utils/Mime', + 'moxie/file/Blob' +], function(Basic, Mime, Blob) { + /** + @class moxie/file/File + @extends Blob + @constructor + @param {String} ruid Unique id of the runtime, to which this blob belongs to + @param {Object} file Object "Native" file object, as it is represented in the runtime + */ + function File(ruid, file) { + if (!file) { // avoid extra errors in case we overlooked something + file = {}; + } + + Blob.apply(this, arguments); + + if (!this.type) { + this.type = Mime.getFileMime(file.name); + } + + // sanitize file name or generate new one + var name; + if (file.name) { + name = file.name.replace(/\\/g, '/'); + name = name.substr(name.lastIndexOf('/') + 1); + } else if (this.type) { + var prefix = this.type.split('/')[0]; + name = Basic.guid((prefix !== '' ? prefix : 'file') + '_'); + + if (Mime.extensions[this.type]) { + name += '.' + Mime.extensions[this.type][0]; // append proper extension if possible + } + } + + + Basic.extend(this, { + /** + File name + + @property name + @type {String} + @default UID + */ + name: name || Basic.guid('file_'), + + /** + Relative path to the file inside a directory + + @property relativePath + @type {String} + @default '' + */ + relativePath: '', + + /** + Date of last modification + + @property lastModifiedDate + @type {String} + @default now + */ + lastModifiedDate: file.lastModifiedDate || (new Date()).toLocaleString() // Thu Aug 23 2012 19:40:00 GMT+0400 (GET) + }); + } + + File.prototype = Blob.prototype; + + return File; +}); + +// Included from: src/javascript/file/FileDrop.js + +/** + * FileDrop.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +define('moxie/file/FileDrop', [ + 'moxie/core/I18n', + 'moxie/core/utils/Dom', + 'moxie/core/Exceptions', + 'moxie/core/utils/Basic', + 'moxie/core/utils/Env', + 'moxie/file/File', + 'moxie/runtime/RuntimeClient', + 'moxie/core/EventTarget', + 'moxie/core/utils/Mime' +], function(I18n, Dom, x, Basic, Env, File, RuntimeClient, EventTarget, Mime) { + /** + Turn arbitrary DOM element to a drop zone accepting files. Converts selected files to _File_ objects, to be used + in conjunction with _Image_, preloaded in memory with _FileReader_ or uploaded to a server through + _XMLHttpRequest_. + + @example +
+ Drop files here +
+
+
+ + + + @class moxie/file/FileDrop + @constructor + @extends EventTarget + @uses RuntimeClient + @param {Object|String} options If options has typeof string, argument is considered as options.drop_zone + @param {String|DOMElement} options.drop_zone DOM Element to turn into a drop zone + @param {Array} [options.accept] Array of mime types to accept. By default accepts all + @param {Object|String} [options.required_caps] Set of required capabilities, that chosen runtime must support + */ + var dispatches = [ + /** + Dispatched when runtime is connected and drop zone is ready to accept files. + + @event ready + @param {Object} event + */ + 'ready', + + /** + Dispatched when dragging cursor enters the drop zone. + + @event dragenter + @param {Object} event + */ + 'dragenter', + + /** + Dispatched when dragging cursor leaves the drop zone. + + @event dragleave + @param {Object} event + */ + 'dragleave', + + /** + Dispatched when file is dropped onto the drop zone. + + @event drop + @param {Object} event + */ + 'drop', + + /** + Dispatched if error occurs. + + @event error + @param {Object} event + */ + 'error' + ]; + + function FileDrop(options) { + if (MXI_DEBUG) { + Env.log("Instantiating FileDrop..."); + } + + var self = this, defaults; + + // if flat argument passed it should be drop_zone id + if (typeof(options) === 'string') { + options = { drop_zone : options }; + } + + // figure out the options + defaults = { + accept: [{ + title: I18n.translate('All Files'), + extensions: '*' + }], + required_caps: { + drag_and_drop: true + } + }; + + options = typeof(options) === 'object' ? Basic.extend({}, defaults, options) : defaults; + + // this will help us to find proper default container + options.container = Dom.get(options.drop_zone) || document.body; + + // make container relative, if it is not + if (Dom.getStyle(options.container, 'position') === 'static') { + options.container.style.position = 'relative'; + } + + // normalize accept option (could be list of mime types or array of title/extensions pairs) + if (typeof(options.accept) === 'string') { + options.accept = Mime.mimes2extList(options.accept); + } + + RuntimeClient.call(self); + + Basic.extend(self, { + uid: Basic.guid('uid_'), + + ruid: null, + + files: null, + + init: function() { + self.bind('RuntimeInit', function(e, runtime) { + self.ruid = runtime.uid; + runtime.exec.call(self, 'FileDrop', 'init', options); + self.dispatchEvent('ready'); + }); + + // runtime needs: options.required_features, options.runtime_order and options.container + self.connectRuntime(options); // throws RuntimeError + }, + + destroy: function() { + var runtime = this.getRuntime(); + if (runtime) { + runtime.exec.call(this, 'FileDrop', 'destroy'); + this.disconnectRuntime(); + } + this.files = null; + + this.unbindAll(); + } + }); + + this.handleEventProps(dispatches); + } + + FileDrop.prototype = EventTarget.instance; + + return FileDrop; +}); + +// Included from: src/javascript/file/FileReader.js + +/** + * FileReader.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +define('moxie/file/FileReader', [ + 'moxie/core/utils/Basic', + 'moxie/core/utils/Encode', + 'moxie/core/Exceptions', + 'moxie/core/EventTarget', + 'moxie/file/Blob', + 'moxie/runtime/RuntimeClient' +], function(Basic, Encode, x, EventTarget, Blob, RuntimeClient) { + /** + Utility for preloading o.Blob/o.File objects in memory. By design closely follows [W3C FileReader](http://www.w3.org/TR/FileAPI/#dfn-filereader) + interface. Where possible uses native FileReader, where - not falls back to shims. + + @class moxie/file/FileReader + @constructor FileReader + @extends EventTarget + @uses RuntimeClient + */ + var dispatches = [ + + /** + Dispatched when the read starts. + + @event loadstart + @param {Object} event + */ + 'loadstart', + + /** + Dispatched while reading (and decoding) blob, and reporting partial Blob data (progess.loaded/progress.total). + + @event progress + @param {Object} event + */ + 'progress', + + /** + Dispatched when the read has successfully completed. + + @event load + @param {Object} event + */ + 'load', + + /** + Dispatched when the read has been aborted. For instance, by invoking the abort() method. + + @event abort + @param {Object} event + */ + 'abort', + + /** + Dispatched when the read has failed. + + @event error + @param {Object} event + */ + 'error', + + /** + Dispatched when the request has completed (either in success or failure). + + @event loadend + @param {Object} event + */ + 'loadend' + ]; + + function FileReader() { + + RuntimeClient.call(this); + + Basic.extend(this, { + /** + UID of the component instance. + + @property uid + @type {String} + */ + uid: Basic.guid('uid_'), + + /** + Contains current state of FileReader object. Can take values of FileReader.EMPTY, FileReader.LOADING + and FileReader.DONE. + + @property readyState + @type {Number} + @default FileReader.EMPTY + */ + readyState: FileReader.EMPTY, + + /** + Result of the successful read operation. + + @property result + @type {String} + */ + result: null, + + /** + Stores the error of failed asynchronous read operation. + + @property error + @type {DOMError} + */ + error: null, + + /** + Initiates reading of File/Blob object contents to binary string. + + @method readAsBinaryString + @param {Blob|File} blob Object to preload + */ + readAsBinaryString: function(blob) { + _read.call(this, 'readAsBinaryString', blob); + }, + + /** + Initiates reading of File/Blob object contents to dataURL string. + + @method readAsDataURL + @param {Blob|File} blob Object to preload + */ + readAsDataURL: function(blob) { + _read.call(this, 'readAsDataURL', blob); + }, + + /** + Initiates reading of File/Blob object contents to string. + + @method readAsText + @param {Blob|File} blob Object to preload + */ + readAsText: function(blob) { + _read.call(this, 'readAsText', blob); + }, + + /** + Aborts preloading process. + + @method abort + */ + abort: function() { + this.result = null; + + if (Basic.inArray(this.readyState, [FileReader.EMPTY, FileReader.DONE]) !== -1) { + return; + } else if (this.readyState === FileReader.LOADING) { + this.readyState = FileReader.DONE; + } + + this.exec('FileReader', 'abort'); + + this.trigger('abort'); + this.trigger('loadend'); + }, + + /** + Destroy component and release resources. + + @method destroy + */ + destroy: function() { + this.abort(); + this.exec('FileReader', 'destroy'); + this.disconnectRuntime(); + this.unbindAll(); + } + }); + + // uid must already be assigned + this.handleEventProps(dispatches); + + this.bind('Error', function(e, err) { + this.readyState = FileReader.DONE; + this.error = err; + }, 999); + + this.bind('Load', function(e) { + this.readyState = FileReader.DONE; + }, 999); + + + function _read(op, blob) { + var self = this; + + this.trigger('loadstart'); + + if (this.readyState === FileReader.LOADING) { + this.trigger('error', new x.DOMException(x.DOMException.INVALID_STATE_ERR)); + this.trigger('loadend'); + return; + } + + // if source is not o.Blob/o.File + if (!(blob instanceof Blob)) { + this.trigger('error', new x.DOMException(x.DOMException.NOT_FOUND_ERR)); + this.trigger('loadend'); + return; + } + + this.result = null; + this.readyState = FileReader.LOADING; + + if (blob.isDetached()) { + var src = blob.getSource(); + switch (op) { + case 'readAsText': + case 'readAsBinaryString': + this.result = src; + break; + case 'readAsDataURL': + this.result = 'data:' + blob.type + ';base64,' + Encode.btoa(src); + break; + } + this.readyState = FileReader.DONE; + this.trigger('load'); + this.trigger('loadend'); + } else { + this.connectRuntime(blob.ruid); + this.exec('FileReader', 'read', op, blob); + } + } + } + + /** + Initial FileReader state + + @property EMPTY + @type {Number} + @final + @static + @default 0 + */ + FileReader.EMPTY = 0; + + /** + FileReader switches to this state when it is preloading the source + + @property LOADING + @type {Number} + @final + @static + @default 1 + */ + FileReader.LOADING = 1; + + /** + Preloading is complete, this is a final state + + @property DONE + @type {Number} + @final + @static + @default 2 + */ + FileReader.DONE = 2; + + FileReader.prototype = EventTarget.instance; + + return FileReader; +}); + +// Included from: src/javascript/core/utils/Url.js + +/** + * Url.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +define('moxie/core/utils/Url', [ + 'moxie/core/utils/Basic' +], function(Basic) { + /** + Parse url into separate components and fill in absent parts with parts from current url, + based on https://raw.github.com/kvz/phpjs/master/functions/url/parse_url.js + + @method parseUrl + @for Utils + @static + @param {String} url Url to parse (defaults to empty string if undefined) + @return {Object} Hash containing extracted uri components + */ + var parseUrl = function(url, currentUrl) { + var key = ['source', 'scheme', 'authority', 'userInfo', 'user', 'pass', 'host', 'port', 'relative', 'path', 'directory', 'file', 'query', 'fragment'] + , i = key.length + , ports = { + http: 80, + https: 443 + } + , uri = {} + , regex = /^(?:([^:\/?#]+):)?(?:\/\/()(?:(?:()(?:([^:@\/]*):?([^:@\/]*))?@)?(\[[\da-fA-F:]+\]|[^:\/?#]*)(?::(\d*))?))?()(?:(()(?:(?:[^?#\/]*\/)*)()(?:[^?#]*))(?:\\?([^#]*))?(?:#(.*))?)/ + , m = regex.exec(url || '') + , isRelative + , isSchemeLess = /^\/\/\w/.test(url) + ; + + switch (Basic.typeOf(currentUrl)) { + case 'undefined': + currentUrl = parseUrl(document.location.href, false); + break; + + case 'string': + currentUrl = parseUrl(currentUrl, false); + break; + } + + while (i--) { + if (m[i]) { + uri[key[i]] = m[i]; + } + } + + isRelative = !isSchemeLess && !uri.scheme; + + if (isSchemeLess || isRelative) { + uri.scheme = currentUrl.scheme; + } + + // when url is relative, we set the origin and the path ourselves + if (isRelative) { + uri.host = currentUrl.host; + uri.port = currentUrl.port; + + var path = ''; + // for urls without trailing slash we need to figure out the path + if (/^[^\/]/.test(uri.path)) { + path = currentUrl.path; + // if path ends with a filename, strip it + if (/\/[^\/]*\.[^\/]*$/.test(path)) { + path = path.replace(/\/[^\/]+$/, '/'); + } else { + // avoid double slash at the end (see #127) + path = path.replace(/\/?$/, '/'); + } + } + uri.path = path + (uri.path || ''); // site may reside at domain.com or domain.com/subdir + } + + if (!uri.port) { + uri.port = ports[uri.scheme] || 80; + } + + uri.port = parseInt(uri.port, 10); + + if (!uri.path) { + uri.path = "/"; + } + + delete uri.source; + + return uri; + }; + + /** + Resolve url - among other things will turn relative url to absolute + + @method resolveUrl + @static + @param {String|Object} url Either absolute or relative, or a result of parseUrl call + @return {String} Resolved, absolute url + */ + var resolveUrl = function(url) { + var ports = { // we ignore default ports + http: 80, + https: 443 + } + , urlp = typeof(url) === 'object' ? url : parseUrl(url); + ; + + return urlp.scheme + '://' + urlp.host + (urlp.port !== ports[urlp.scheme] ? ':' + urlp.port : '') + urlp.path + (urlp.query ? urlp.query : ''); + }; + + /** + Check if specified url has the same origin as the current document + + @method hasSameOrigin + @param {String|Object} url + @return {Boolean} + */ + var hasSameOrigin = function(url) { + function origin(url) { + return [url.scheme, url.host, url.port].join('/'); + } + + if (typeof url === 'string') { + url = parseUrl(url); + } + + return origin(parseUrl()) === origin(url); + }; + + return { + parseUrl: parseUrl, + resolveUrl: resolveUrl, + hasSameOrigin: hasSameOrigin + }; +}); + +// Included from: src/javascript/runtime/RuntimeTarget.js + +/** + * RuntimeTarget.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +define('moxie/runtime/RuntimeTarget', [ + 'moxie/core/utils/Basic', + 'moxie/runtime/RuntimeClient', + "moxie/core/EventTarget" +], function(Basic, RuntimeClient, EventTarget) { + /** + Instance of this class can be used as a target for the events dispatched by shims, + when allowing them onto components is for either reason inappropriate + + @class moxie/runtime/RuntimeTarget + @constructor + @protected + @extends EventTarget + */ + function RuntimeTarget() { + this.uid = Basic.guid('uid_'); + + RuntimeClient.call(this); + + this.destroy = function() { + this.disconnectRuntime(); + this.unbindAll(); + }; + } + + RuntimeTarget.prototype = EventTarget.instance; + + return RuntimeTarget; +}); + +// Included from: src/javascript/file/FileReaderSync.js + +/** + * FileReaderSync.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +define('moxie/file/FileReaderSync', [ + 'moxie/core/utils/Basic', + 'moxie/runtime/RuntimeClient', + 'moxie/core/utils/Encode' +], function(Basic, RuntimeClient, Encode) { + /** + Synchronous FileReader implementation. Something like this is available in WebWorkers environment, here + it can be used to read only preloaded blobs/files and only below certain size (not yet sure what that'd be, + but probably < 1mb). Not meant to be used directly by user. + + @class moxie/file/FileReaderSync + @private + @constructor + */ + return function() { + RuntimeClient.call(this); + + Basic.extend(this, { + uid: Basic.guid('uid_'), + + readAsBinaryString: function(blob) { + return _read.call(this, 'readAsBinaryString', blob); + }, + + readAsDataURL: function(blob) { + return _read.call(this, 'readAsDataURL', blob); + }, + + /*readAsArrayBuffer: function(blob) { + return _read.call(this, 'readAsArrayBuffer', blob); + },*/ + + readAsText: function(blob) { + return _read.call(this, 'readAsText', blob); + } + }); + + function _read(op, blob) { + if (blob.isDetached()) { + var src = blob.getSource(); + switch (op) { + case 'readAsBinaryString': + return src; + case 'readAsDataURL': + return 'data:' + blob.type + ';base64,' + Encode.btoa(src); + case 'readAsText': + var txt = ''; + for (var i = 0, length = src.length; i < length; i++) { + txt += String.fromCharCode(src[i]); + } + return txt; + } + } else { + var result = this.connectRuntime(blob.ruid).exec.call(this, 'FileReaderSync', 'read', op, blob); + this.disconnectRuntime(); + return result; + } + } + }; +}); + +// Included from: src/javascript/xhr/FormData.js + +/** + * FormData.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +define("moxie/xhr/FormData", [ + "moxie/core/Exceptions", + "moxie/core/utils/Basic", + "moxie/file/Blob" +], function(x, Basic, Blob) { + /** + FormData + + @class moxie/xhr/FormData + @constructor + */ + function FormData() { + var _blob, _fields = []; + + Basic.extend(this, { + /** + Append another key-value pair to the FormData object + + @method append + @param {String} name Name for the new field + @param {String|Blob|Array|Object} value Value for the field + */ + append: function(name, value) { + var self = this, valueType = Basic.typeOf(value); + + // according to specs value might be either Blob or String + if (value instanceof Blob) { + _blob = { + name: name, + value: value // unfortunately we can only send single Blob in one FormData + }; + } else if ('array' === valueType) { + name += '[]'; + + Basic.each(value, function(value) { + self.append(name, value); + }); + } else if ('object' === valueType) { + Basic.each(value, function(value, key) { + self.append(name + '[' + key + ']', value); + }); + } else if ('null' === valueType || 'undefined' === valueType || 'number' === valueType && isNaN(value)) { + self.append(name, "false"); + } else { + _fields.push({ + name: name, + value: value.toString() + }); + } + }, + + /** + Checks if FormData contains Blob. + + @method hasBlob + @return {Boolean} + */ + hasBlob: function() { + return !!this.getBlob(); + }, + + /** + Retrieves blob. + + @method getBlob + @return {Object} Either Blob if found or null + */ + getBlob: function() { + return _blob && _blob.value || null; + }, + + /** + Retrieves blob field name. + + @method getBlobName + @return {String} Either Blob field name or null + */ + getBlobName: function() { + return _blob && _blob.name || null; + }, + + /** + Loop over the fields in FormData and invoke the callback for each of them. + + @method each + @param {Function} cb Callback to call for each field + */ + each: function(cb) { + Basic.each(_fields, function(field) { + cb(field.value, field.name); + }); + + if (_blob) { + cb(_blob.value, _blob.name); + } + }, + + destroy: function() { + _blob = null; + _fields = []; + } + }); + } + + return FormData; +}); + +// Included from: src/javascript/xhr/XMLHttpRequest.js + +/** + * XMLHttpRequest.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +define("moxie/xhr/XMLHttpRequest", [ + "moxie/core/utils/Basic", + "moxie/core/Exceptions", + "moxie/core/EventTarget", + "moxie/core/utils/Encode", + "moxie/core/utils/Url", + "moxie/runtime/Runtime", + "moxie/runtime/RuntimeTarget", + "moxie/file/Blob", + "moxie/file/FileReaderSync", + "moxie/xhr/FormData", + "moxie/core/utils/Env", + "moxie/core/utils/Mime" +], function(Basic, x, EventTarget, Encode, Url, Runtime, RuntimeTarget, Blob, FileReaderSync, FormData, Env, Mime) { + + var httpCode = { + 100: 'Continue', + 101: 'Switching Protocols', + 102: 'Processing', + + 200: 'OK', + 201: 'Created', + 202: 'Accepted', + 203: 'Non-Authoritative Information', + 204: 'No Content', + 205: 'Reset Content', + 206: 'Partial Content', + 207: 'Multi-Status', + 226: 'IM Used', + + 300: 'Multiple Choices', + 301: 'Moved Permanently', + 302: 'Found', + 303: 'See Other', + 304: 'Not Modified', + 305: 'Use Proxy', + 306: 'Reserved', + 307: 'Temporary Redirect', + + 400: 'Bad Request', + 401: 'Unauthorized', + 402: 'Payment Required', + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 406: 'Not Acceptable', + 407: 'Proxy Authentication Required', + 408: 'Request Timeout', + 409: 'Conflict', + 410: 'Gone', + 411: 'Length Required', + 412: 'Precondition Failed', + 413: 'Request Entity Too Large', + 414: 'Request-URI Too Long', + 415: 'Unsupported Media Type', + 416: 'Requested Range Not Satisfiable', + 417: 'Expectation Failed', + 422: 'Unprocessable Entity', + 423: 'Locked', + 424: 'Failed Dependency', + 426: 'Upgrade Required', + + 500: 'Internal Server Error', + 501: 'Not Implemented', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + 504: 'Gateway Timeout', + 505: 'HTTP Version Not Supported', + 506: 'Variant Also Negotiates', + 507: 'Insufficient Storage', + 510: 'Not Extended' + }; + + function XMLHttpRequestUpload() { + this.uid = Basic.guid('uid_'); + } + + XMLHttpRequestUpload.prototype = EventTarget.instance; + + /** + Implementation of XMLHttpRequest + + @class moxie/xhr/XMLHttpRequest + @constructor + @uses RuntimeClient + @extends EventTarget + */ + var dispatches = [ + 'loadstart', + + 'progress', + + 'abort', + + 'error', + + 'load', + + 'timeout', + + 'loadend' + + // readystatechange (for historical reasons) + ]; + + var NATIVE = 1, RUNTIME = 2; + + function XMLHttpRequest() { + var self = this, + // this (together with _p() @see below) is here to gracefully upgrade to setter/getter syntax where possible + props = { + /** + The amount of milliseconds a request can take before being terminated. Initially zero. Zero means there is no timeout. + + @property timeout + @type Number + @default 0 + */ + timeout: 0, + + /** + Current state, can take following values: + UNSENT (numeric value 0) + The object has been constructed. + + OPENED (numeric value 1) + The open() method has been successfully invoked. During this state request headers can be set using setRequestHeader() and the request can be made using the send() method. + + HEADERS_RECEIVED (numeric value 2) + All redirects (if any) have been followed and all HTTP headers of the final response have been received. Several response members of the object are now available. + + LOADING (numeric value 3) + The response entity body is being received. + + DONE (numeric value 4) + + @property readyState + @type Number + @default 0 (UNSENT) + */ + readyState: XMLHttpRequest.UNSENT, + + /** + True when user credentials are to be included in a cross-origin request. False when they are to be excluded + in a cross-origin request and when cookies are to be ignored in its response. Initially false. + + @property withCredentials + @type Boolean + @default false + */ + withCredentials: false, + + /** + Returns the HTTP status code. + + @property status + @type Number + @default 0 + */ + status: 0, + + /** + Returns the HTTP status text. + + @property statusText + @type String + */ + statusText: "", + + /** + Returns the response type. Can be set to change the response type. Values are: + the empty string (default), "arraybuffer", "blob", "document", "json", and "text". + + @property responseType + @type String + */ + responseType: "", + + /** + Returns the document response entity body. + + Throws an "InvalidStateError" exception if responseType is not the empty string or "document". + + @property responseXML + @type Document + */ + responseXML: null, + + /** + Returns the text response entity body. + + Throws an "InvalidStateError" exception if responseType is not the empty string or "text". + + @property responseText + @type String + */ + responseText: null, + + /** + Returns the response entity body (http://www.w3.org/TR/XMLHttpRequest/#response-entity-body). + Can become: ArrayBuffer, Blob, Document, JSON, Text + + @property response + @type Mixed + */ + response: null + }, + + _async = true, + _url, + _method, + _headers = {}, + _user, + _password, + _encoding = null, + _mimeType = null, + + // flags + _sync_flag = false, + _send_flag = false, + _upload_events_flag = false, + _upload_complete_flag = false, + _error_flag = false, + _same_origin_flag = false, + + // times + _start_time, + _timeoutset_time, + + _finalMime = null, + _finalCharset = null, + + _options = {}, + _xhr, + _responseHeaders = '', + _responseHeadersBag + ; + + + Basic.extend(this, props, { + /** + Unique id of the component + + @property uid + @type String + */ + uid: Basic.guid('uid_'), + + /** + Target for Upload events + + @property upload + @type XMLHttpRequestUpload + */ + upload: new XMLHttpRequestUpload(), + + + /** + Sets the request method, request URL, synchronous flag, request username, and request password. + + Throws a "SyntaxError" exception if one of the following is true: + + method is not a valid HTTP method. + url cannot be resolved. + url contains the "user:password" format in the userinfo production. + Throws a "SecurityError" exception if method is a case-insensitive match for CONNECT, TRACE or TRACK. + + Throws an "InvalidAccessError" exception if one of the following is true: + + Either user or password is passed as argument and the origin of url does not match the XMLHttpRequest origin. + There is an associated XMLHttpRequest document and either the timeout attribute is not zero, + the withCredentials attribute is true, or the responseType attribute is not the empty string. + + + @method open + @param {String} method HTTP method to use on request + @param {String} url URL to request + @param {Boolean} [async=true] If false request will be done in synchronous manner. Asynchronous by default. + @param {String} [user] Username to use in HTTP authentication process on server-side + @param {String} [password] Password to use in HTTP authentication process on server-side + */ + open: function(method, url, async, user, password) { + var urlp; + + // first two arguments are required + if (!method || !url) { + throw new x.DOMException(x.DOMException.SYNTAX_ERR); + } + + // 2 - check if any code point in method is higher than U+00FF or after deflating method it does not match the method + if (/[\u0100-\uffff]/.test(method) || Encode.utf8_encode(method) !== method) { + throw new x.DOMException(x.DOMException.SYNTAX_ERR); + } + + // 3 + if (!!~Basic.inArray(method.toUpperCase(), ['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT', 'TRACE', 'TRACK'])) { + _method = method.toUpperCase(); + } + + + // 4 - allowing these methods poses a security risk + if (!!~Basic.inArray(_method, ['CONNECT', 'TRACE', 'TRACK'])) { + throw new x.DOMException(x.DOMException.SECURITY_ERR); + } + + // 5 + url = Encode.utf8_encode(url); + + // 6 - Resolve url relative to the XMLHttpRequest base URL. If the algorithm returns an error, throw a "SyntaxError". + urlp = Url.parseUrl(url); + + _same_origin_flag = Url.hasSameOrigin(urlp); + + // 7 - manually build up absolute url + _url = Url.resolveUrl(url); + + // 9-10, 12-13 + if ((user || password) && !_same_origin_flag) { + throw new x.DOMException(x.DOMException.INVALID_ACCESS_ERR); + } + + _user = user || urlp.user; + _password = password || urlp.pass; + + // 11 + _async = async || true; + + if (_async === false && (_p('timeout') || _p('withCredentials') || _p('responseType') !== "")) { + throw new x.DOMException(x.DOMException.INVALID_ACCESS_ERR); + } + + // 14 - terminate abort() + + // 15 - terminate send() + + // 18 + _sync_flag = !_async; + _send_flag = false; + _headers = {}; + _reset.call(this); + + // 19 + _p('readyState', XMLHttpRequest.OPENED); + + // 20 + this.dispatchEvent('readystatechange'); + }, + + /** + Appends an header to the list of author request headers, or if header is already + in the list of author request headers, combines its value with value. + + Throws an "InvalidStateError" exception if the state is not OPENED or if the send() flag is set. + Throws a "SyntaxError" exception if header is not a valid HTTP header field name or if value + is not a valid HTTP header field value. + + @method setRequestHeader + @param {String} header + @param {String|Number} value + */ + setRequestHeader: function(header, value) { + var uaHeaders = [ // these headers are controlled by the user agent + "accept-charset", + "accept-encoding", + "access-control-request-headers", + "access-control-request-method", + "connection", + "content-length", + "cookie", + "cookie2", + "content-transfer-encoding", + "date", + "expect", + "host", + "keep-alive", + "origin", + "referer", + "te", + "trailer", + "transfer-encoding", + "upgrade", + "user-agent", + "via" + ]; + + // 1-2 + if (_p('readyState') !== XMLHttpRequest.OPENED || _send_flag) { + throw new x.DOMException(x.DOMException.INVALID_STATE_ERR); + } + + // 3 + if (/[\u0100-\uffff]/.test(header) || Encode.utf8_encode(header) !== header) { + throw new x.DOMException(x.DOMException.SYNTAX_ERR); + } + + // 4 + /* this step is seemingly bypassed in browsers, probably to allow various unicode characters in header values + if (/[\u0100-\uffff]/.test(value) || Encode.utf8_encode(value) !== value) { + throw new x.DOMException(x.DOMException.SYNTAX_ERR); + }*/ + + header = Basic.trim(header).toLowerCase(); + + // setting of proxy-* and sec-* headers is prohibited by spec + if (!!~Basic.inArray(header, uaHeaders) || /^(proxy\-|sec\-)/.test(header)) { + return false; + } + + // camelize + // browsers lowercase header names (at least for custom ones) + // header = header.replace(/\b\w/g, function($1) { return $1.toUpperCase(); }); + + if (!_headers[header]) { + _headers[header] = value; + } else { + // http://tools.ietf.org/html/rfc2616#section-4.2 (last paragraph) + _headers[header] += ', ' + value; + } + return true; + }, + + /** + * Test if the specified header is already set on this request. + * Returns a header value or boolean false if it's not yet set. + * + * @method hasRequestHeader + * @param {String} header Name of the header to test + * @return {Boolean|String} + */ + hasRequestHeader: function(header) { + return header && _headers[header.toLowerCase()] || false; + }, + + /** + Returns all headers from the response, with the exception of those whose field name is Set-Cookie or Set-Cookie2. + + @method getAllResponseHeaders + @return {String} reponse headers or empty string + */ + getAllResponseHeaders: function() { + return _responseHeaders || ''; + }, + + /** + Returns the header field value from the response of which the field name matches header, + unless the field name is Set-Cookie or Set-Cookie2. + + @method getResponseHeader + @param {String} header + @return {String} value(s) for the specified header or null + */ + getResponseHeader: function(header) { + header = header.toLowerCase(); + + if (_error_flag || !!~Basic.inArray(header, ['set-cookie', 'set-cookie2'])) { + return null; + } + + if (_responseHeaders && _responseHeaders !== '') { + // if we didn't parse response headers until now, do it and keep for later + if (!_responseHeadersBag) { + _responseHeadersBag = {}; + Basic.each(_responseHeaders.split(/\r\n/), function(line) { + var pair = line.split(/:\s+/); + if (pair.length === 2) { // last line might be empty, omit + pair[0] = Basic.trim(pair[0]); // just in case + _responseHeadersBag[pair[0].toLowerCase()] = { // simply to retain header name in original form + header: pair[0], + value: Basic.trim(pair[1]) + }; + } + }); + } + if (_responseHeadersBag.hasOwnProperty(header)) { + return _responseHeadersBag[header].header + ': ' + _responseHeadersBag[header].value; + } + } + return null; + }, + + /** + Sets the Content-Type header for the response to mime. + Throws an "InvalidStateError" exception if the state is LOADING or DONE. + Throws a "SyntaxError" exception if mime is not a valid media type. + + @method overrideMimeType + @param String mime Mime type to set + */ + overrideMimeType: function(mime) { + var matches, charset; + + // 1 + if (!!~Basic.inArray(_p('readyState'), [XMLHttpRequest.LOADING, XMLHttpRequest.DONE])) { + throw new x.DOMException(x.DOMException.INVALID_STATE_ERR); + } + + // 2 + mime = Basic.trim(mime.toLowerCase()); + + if (/;/.test(mime) && (matches = mime.match(/^([^;]+)(?:;\scharset\=)?(.*)$/))) { + mime = matches[1]; + if (matches[2]) { + charset = matches[2]; + } + } + + if (!Mime.mimes[mime]) { + throw new x.DOMException(x.DOMException.SYNTAX_ERR); + } + + // 3-4 + _finalMime = mime; + _finalCharset = charset; + }, + + /** + Initiates the request. The optional argument provides the request entity body. + The argument is ignored if request method is GET or HEAD. + + Throws an "InvalidStateError" exception if the state is not OPENED or if the send() flag is set. + + @method send + @param {Blob|Document|String|FormData} [data] Request entity body + @param {Object} [options] Set of requirements and pre-requisities for runtime initialization + */ + send: function(data, options) { + if (Basic.typeOf(options) === 'string') { + _options = { ruid: options }; + } else if (!options) { + _options = {}; + } else { + _options = options; + } + + // 1-2 + if (this.readyState !== XMLHttpRequest.OPENED || _send_flag) { + throw new x.DOMException(x.DOMException.INVALID_STATE_ERR); + } + + // 3 + // sending Blob + if (data instanceof Blob) { + _options.ruid = data.ruid; + _mimeType = data.type || 'application/octet-stream'; + } + + // FormData + else if (data instanceof FormData) { + if (data.hasBlob()) { + var blob = data.getBlob(); + _options.ruid = blob.ruid; + _mimeType = blob.type || 'application/octet-stream'; + } + } + + // DOMString + else if (typeof data === 'string') { + _encoding = 'UTF-8'; + _mimeType = 'text/plain;charset=UTF-8'; + + // data should be converted to Unicode and encoded as UTF-8 + data = Encode.utf8_encode(data); + } + + // if withCredentials not set, but requested, set it automatically + if (!this.withCredentials) { + this.withCredentials = (_options.required_caps && _options.required_caps.send_browser_cookies) && !_same_origin_flag; + } + + // 4 - storage mutex + // 5 + _upload_events_flag = (!_sync_flag && this.upload.hasEventListener()); // DSAP + // 6 + _error_flag = false; + // 7 + _upload_complete_flag = !data; + // 8 - Asynchronous steps + if (!_sync_flag) { + // 8.1 + _send_flag = true; + // 8.2 + // this.dispatchEvent('loadstart'); // will be dispatched either by native or runtime xhr + // 8.3 + //if (!_upload_complete_flag) { + // this.upload.dispatchEvent('loadstart'); // will be dispatched either by native or runtime xhr + //} + } + // 8.5 - Return the send() method call, but continue running the steps in this algorithm. + _doXHR.call(this, data); + }, + + /** + Cancels any network activity. + + @method abort + */ + abort: function() { + _error_flag = true; + _sync_flag = false; + + if (!~Basic.inArray(_p('readyState'), [XMLHttpRequest.UNSENT, XMLHttpRequest.OPENED, XMLHttpRequest.DONE])) { + _p('readyState', XMLHttpRequest.DONE); + _send_flag = false; + + if (_xhr) { + _xhr.getRuntime().exec.call(_xhr, 'XMLHttpRequest', 'abort', _upload_complete_flag); + } else { + throw new x.DOMException(x.DOMException.INVALID_STATE_ERR); + } + + _upload_complete_flag = true; + } else { + _p('readyState', XMLHttpRequest.UNSENT); + } + }, + + destroy: function() { + if (_xhr) { + if (Basic.typeOf(_xhr.destroy) === 'function') { + _xhr.destroy(); + } + _xhr = null; + } + + this.unbindAll(); + + if (this.upload) { + this.upload.unbindAll(); + this.upload = null; + } + } + }); + + this.handleEventProps(dispatches.concat(['readystatechange'])); // for historical reasons + this.upload.handleEventProps(dispatches); + + /* this is nice, but maybe too lengthy + + // if supported by JS version, set getters/setters for specific properties + o.defineProperty(this, 'readyState', { + configurable: false, + + get: function() { + return _p('readyState'); + } + }); + + o.defineProperty(this, 'timeout', { + configurable: false, + + get: function() { + return _p('timeout'); + }, + + set: function(value) { + + if (_sync_flag) { + throw new x.DOMException(x.DOMException.INVALID_ACCESS_ERR); + } + + // timeout still should be measured relative to the start time of request + _timeoutset_time = (new Date).getTime(); + + _p('timeout', value); + } + }); + + // the withCredentials attribute has no effect when fetching same-origin resources + o.defineProperty(this, 'withCredentials', { + configurable: false, + + get: function() { + return _p('withCredentials'); + }, + + set: function(value) { + // 1-2 + if (!~o.inArray(_p('readyState'), [XMLHttpRequest.UNSENT, XMLHttpRequest.OPENED]) || _send_flag) { + throw new x.DOMException(x.DOMException.INVALID_STATE_ERR); + } + + // 3-4 + if (_anonymous_flag || _sync_flag) { + throw new x.DOMException(x.DOMException.INVALID_ACCESS_ERR); + } + + // 5 + _p('withCredentials', value); + } + }); + + o.defineProperty(this, 'status', { + configurable: false, + + get: function() { + return _p('status'); + } + }); + + o.defineProperty(this, 'statusText', { + configurable: false, + + get: function() { + return _p('statusText'); + } + }); + + o.defineProperty(this, 'responseType', { + configurable: false, + + get: function() { + return _p('responseType'); + }, + + set: function(value) { + // 1 + if (!!~o.inArray(_p('readyState'), [XMLHttpRequest.LOADING, XMLHttpRequest.DONE])) { + throw new x.DOMException(x.DOMException.INVALID_STATE_ERR); + } + + // 2 + if (_sync_flag) { + throw new x.DOMException(x.DOMException.INVALID_ACCESS_ERR); + } + + // 3 + _p('responseType', value.toLowerCase()); + } + }); + + o.defineProperty(this, 'responseText', { + configurable: false, + + get: function() { + // 1 + if (!~o.inArray(_p('responseType'), ['', 'text'])) { + throw new x.DOMException(x.DOMException.INVALID_STATE_ERR); + } + + // 2-3 + if (_p('readyState') !== XMLHttpRequest.DONE && _p('readyState') !== XMLHttpRequest.LOADING || _error_flag) { + throw new x.DOMException(x.DOMException.INVALID_STATE_ERR); + } + + return _p('responseText'); + } + }); + + o.defineProperty(this, 'responseXML', { + configurable: false, + + get: function() { + // 1 + if (!~o.inArray(_p('responseType'), ['', 'document'])) { + throw new x.DOMException(x.DOMException.INVALID_STATE_ERR); + } + + // 2-3 + if (_p('readyState') !== XMLHttpRequest.DONE || _error_flag) { + throw new x.DOMException(x.DOMException.INVALID_STATE_ERR); + } + + return _p('responseXML'); + } + }); + + o.defineProperty(this, 'response', { + configurable: false, + + get: function() { + if (!!~o.inArray(_p('responseType'), ['', 'text'])) { + if (_p('readyState') !== XMLHttpRequest.DONE && _p('readyState') !== XMLHttpRequest.LOADING || _error_flag) { + return ''; + } + } + + if (_p('readyState') !== XMLHttpRequest.DONE || _error_flag) { + return null; + } + + return _p('response'); + } + }); + + */ + + function _p(prop, value) { + if (!props.hasOwnProperty(prop)) { + return; + } + if (arguments.length === 1) { // get + return Env.can('define_property') ? props[prop] : self[prop]; + } else { // set + if (Env.can('define_property')) { + props[prop] = value; + } else { + self[prop] = value; + } + } + } + + /* + function _toASCII(str, AllowUnassigned, UseSTD3ASCIIRules) { + // TODO: http://tools.ietf.org/html/rfc3490#section-4.1 + return str.toLowerCase(); + } + */ + + + function _doXHR(data) { + var self = this; + + _start_time = new Date().getTime(); + + _xhr = new RuntimeTarget(); + + function loadEnd() { + if (_xhr) { // it could have been destroyed by now + _xhr.destroy(); + _xhr = null; + } + self.dispatchEvent('loadend'); + self = null; + } + + function exec(runtime) { + _xhr.bind('LoadStart', function(e) { + _p('readyState', XMLHttpRequest.LOADING); + self.dispatchEvent('readystatechange'); + + self.dispatchEvent(e); + + if (_upload_events_flag) { + self.upload.dispatchEvent(e); + } + }); + + _xhr.bind('Progress', function(e) { + if (_p('readyState') !== XMLHttpRequest.LOADING) { + _p('readyState', XMLHttpRequest.LOADING); // LoadStart unreliable (in Flash for example) + self.dispatchEvent('readystatechange'); + } + self.dispatchEvent(e); + }); + + _xhr.bind('UploadProgress', function(e) { + if (_upload_events_flag) { + self.upload.dispatchEvent({ + type: 'progress', + lengthComputable: false, + total: e.total, + loaded: e.loaded + }); + } + }); + + _xhr.bind('Load', function(e) { + _p('readyState', XMLHttpRequest.DONE); + _p('status', Number(runtime.exec.call(_xhr, 'XMLHttpRequest', 'getStatus') || 0)); + _p('statusText', httpCode[_p('status')] || ""); + + _p('response', runtime.exec.call(_xhr, 'XMLHttpRequest', 'getResponse', _p('responseType'))); + + if (!!~Basic.inArray(_p('responseType'), ['text', ''])) { + _p('responseText', _p('response')); + } else if (_p('responseType') === 'document') { + _p('responseXML', _p('response')); + } + + _responseHeaders = runtime.exec.call(_xhr, 'XMLHttpRequest', 'getAllResponseHeaders'); + + self.dispatchEvent('readystatechange'); + + if (_p('status') > 0) { // status 0 usually means that server is unreachable + if (_upload_events_flag) { + self.upload.dispatchEvent(e); + } + self.dispatchEvent(e); + } else { + _error_flag = true; + self.dispatchEvent('error'); + } + loadEnd(); + }); + + _xhr.bind('Abort', function(e) { + self.dispatchEvent(e); + loadEnd(); + }); + + _xhr.bind('Error', function(e) { + _error_flag = true; + _p('readyState', XMLHttpRequest.DONE); + self.dispatchEvent('readystatechange'); + _upload_complete_flag = true; + self.dispatchEvent(e); + loadEnd(); + }); + + runtime.exec.call(_xhr, 'XMLHttpRequest', 'send', { + url: _url, + method: _method, + async: _async, + user: _user, + password: _password, + headers: _headers, + mimeType: _mimeType, + encoding: _encoding, + responseType: self.responseType, + withCredentials: self.withCredentials, + options: _options + }, data); + } + + // clarify our requirements + if (typeof(_options.required_caps) === 'string') { + _options.required_caps = Runtime.parseCaps(_options.required_caps); + } + + _options.required_caps = Basic.extend({}, _options.required_caps, { + return_response_type: self.responseType + }); + + if (data instanceof FormData) { + _options.required_caps.send_multipart = true; + } + + if (!Basic.isEmptyObj(_headers)) { + _options.required_caps.send_custom_headers = true; + } + + if (!_same_origin_flag) { + _options.required_caps.do_cors = true; + } + + + if (_options.ruid) { // we do not need to wait if we can connect directly + exec(_xhr.connectRuntime(_options)); + } else { + _xhr.bind('RuntimeInit', function(e, runtime) { + exec(runtime); + }); + _xhr.bind('RuntimeError', function(e, err) { + self.dispatchEvent('RuntimeError', err); + }); + _xhr.connectRuntime(_options); + } + } + + + function _reset() { + _p('responseText', ""); + _p('responseXML', null); + _p('response', null); + _p('status', 0); + _p('statusText', ""); + _start_time = _timeoutset_time = null; + } + } + + XMLHttpRequest.UNSENT = 0; + XMLHttpRequest.OPENED = 1; + XMLHttpRequest.HEADERS_RECEIVED = 2; + XMLHttpRequest.LOADING = 3; + XMLHttpRequest.DONE = 4; + + XMLHttpRequest.prototype = EventTarget.instance; + + return XMLHttpRequest; +}); + +// Included from: src/javascript/runtime/Transporter.js + +/** + * Transporter.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +define("moxie/runtime/Transporter", [ + "moxie/core/utils/Basic", + "moxie/core/utils/Encode", + "moxie/runtime/RuntimeClient", + "moxie/core/EventTarget" +], function(Basic, Encode, RuntimeClient, EventTarget) { + + /** + @class moxie/runtime/Transporter + @constructor + */ + function Transporter() { + var mod, _runtime, _data, _size, _pos, _chunk_size; + + RuntimeClient.call(this); + + Basic.extend(this, { + uid: Basic.guid('uid_'), + + state: Transporter.IDLE, + + result: null, + + transport: function(data, type, options) { + var self = this; + + options = Basic.extend({ + chunk_size: 204798 + }, options); + + // should divide by three, base64 requires this + if ((mod = options.chunk_size % 3)) { + options.chunk_size += 3 - mod; + } + + _chunk_size = options.chunk_size; + + _reset.call(this); + _data = data; + _size = data.length; + + if (Basic.typeOf(options) === 'string' || options.ruid) { + _run.call(self, type, this.connectRuntime(options)); + } else { + // we require this to run only once + var cb = function(e, runtime) { + self.unbind("RuntimeInit", cb); + _run.call(self, type, runtime); + }; + this.bind("RuntimeInit", cb); + this.connectRuntime(options); + } + }, + + abort: function() { + var self = this; + + self.state = Transporter.IDLE; + if (_runtime) { + _runtime.exec.call(self, 'Transporter', 'clear'); + self.trigger("TransportingAborted"); + } + + _reset.call(self); + }, + + + destroy: function() { + this.unbindAll(); + _runtime = null; + this.disconnectRuntime(); + _reset.call(this); + } + }); + + function _reset() { + _size = _pos = 0; + _data = this.result = null; + } + + function _run(type, runtime) { + var self = this; + + _runtime = runtime; + + //self.unbind("RuntimeInit"); + + self.bind("TransportingProgress", function(e) { + _pos = e.loaded; + + if (_pos < _size && Basic.inArray(self.state, [Transporter.IDLE, Transporter.DONE]) === -1) { + _transport.call(self); + } + }, 999); + + self.bind("TransportingComplete", function() { + _pos = _size; + self.state = Transporter.DONE; + _data = null; // clean a bit + self.result = _runtime.exec.call(self, 'Transporter', 'getAsBlob', type || ''); + }, 999); + + self.state = Transporter.BUSY; + self.trigger("TransportingStarted"); + _transport.call(self); + } + + function _transport() { + var self = this, + chunk, + bytesLeft = _size - _pos; + + if (_chunk_size > bytesLeft) { + _chunk_size = bytesLeft; + } + + chunk = Encode.btoa(_data.substr(_pos, _chunk_size)); + _runtime.exec.call(self, 'Transporter', 'receive', chunk, _size); + } + } + + Transporter.IDLE = 0; + Transporter.BUSY = 1; + Transporter.DONE = 2; + + Transporter.prototype = EventTarget.instance; + + return Transporter; +}); + +// Included from: src/javascript/image/Image.js + +/** + * Image.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +define("moxie/image/Image", [ + "moxie/core/utils/Basic", + "moxie/core/utils/Dom", + "moxie/core/Exceptions", + "moxie/file/FileReaderSync", + "moxie/xhr/XMLHttpRequest", + "moxie/runtime/Runtime", + "moxie/runtime/RuntimeClient", + "moxie/runtime/Transporter", + "moxie/core/utils/Env", + "moxie/core/EventTarget", + "moxie/file/Blob", + "moxie/file/File", + "moxie/core/utils/Encode" +], function(Basic, Dom, x, FileReaderSync, XMLHttpRequest, Runtime, RuntimeClient, Transporter, Env, EventTarget, Blob, File, Encode) { + /** + Image preloading and manipulation utility. Additionally it provides access to image meta info (Exif, GPS) and raw binary data. + + @class moxie/image/Image + @constructor + @extends EventTarget + */ + var dispatches = [ + 'progress', + + /** + Dispatched when loading is complete. + + @event load + @param {Object} event + */ + 'load', + + 'error', + + /** + Dispatched when resize operation is complete. + + @event resize + @param {Object} event + */ + 'resize', + + /** + Dispatched when visual representation of the image is successfully embedded + into the corresponsing container. + + @event embedded + @param {Object} event + */ + 'embedded' + ]; + + function Image() { + + RuntimeClient.call(this); + + Basic.extend(this, { + /** + Unique id of the component + + @property uid + @type {String} + */ + uid: Basic.guid('uid_'), + + /** + Unique id of the connected runtime, if any. + + @property ruid + @type {String} + */ + ruid: null, + + /** + Name of the file, that was used to create an image, if available. If not equals to empty string. + + @property name + @type {String} + @default "" + */ + name: "", + + /** + Size of the image in bytes. Actual value is set only after image is preloaded. + + @property size + @type {Number} + @default 0 + */ + size: 0, + + /** + Width of the image. Actual value is set only after image is preloaded. + + @property width + @type {Number} + @default 0 + */ + width: 0, + + /** + Height of the image. Actual value is set only after image is preloaded. + + @property height + @type {Number} + @default 0 + */ + height: 0, + + /** + Mime type of the image. Currently only image/jpeg and image/png are supported. Actual value is set only after image is preloaded. + + @property type + @type {String} + @default "" + */ + type: "", + + /** + Holds meta info (Exif, GPS). Is populated only for image/jpeg. Actual value is set only after image is preloaded. + + @property meta + @type {Object} + @default {} + */ + meta: {}, + + /** + Alias for load method, that takes another mOxie.Image object as a source (see load). + + @method clone + @param {Image} src Source for the image + @param {Boolean} [exact=false] Whether to activate in-depth clone mode + */ + clone: function() { + this.load.apply(this, arguments); + }, + + /** + Loads image from various sources. Currently the source for new image can be: mOxie.Image, mOxie.Blob/mOxie.File, + native Blob/File, dataUrl or URL. Depending on the type of the source, arguments - differ. When source is URL, + Image will be downloaded from remote destination and loaded in memory. + + @example + var img = new mOxie.Image(); + img.onload = function() { + var blob = img.getAsBlob(); + + var formData = new mOxie.FormData(); + formData.append('file', blob); + + var xhr = new mOxie.XMLHttpRequest(); + xhr.onload = function() { + // upload complete + }; + xhr.open('post', 'upload.php'); + xhr.send(formData); + }; + img.load("http://www.moxiecode.com/images/mox-logo.jpg"); // notice file extension (.jpg) + + + @method load + @param {Image|Blob|File|String} src Source for the image + @param {Boolean|Object} [mixed] + */ + load: function() { + _load.apply(this, arguments); + }, + + + /** + Resizes the image to fit the specified width/height. If crop is specified, image will also be + cropped to the exact dimensions. + + @method resize + @since 3.0 + @param {Object} options + @param {Number} options.width Resulting width + @param {Number} [options.height=width] Resulting height (optional, if not supplied will default to width) + @param {String} [options.type='image/jpeg'] MIME type of the resulting image + @param {Number} [options.quality=90] In the case of JPEG, controls the quality of resulting image + @param {Boolean} [options.crop='cc'] If not falsy, image will be cropped, by default from center + @param {Boolean} [options.fit=true] In case of crop whether to upscale the image to fit the exact dimensions + @param {Boolean} [options.preserveHeaders=true] Whether to preserve meta headers (on JPEGs after resize) + @param {String} [options.resample='default'] Resampling algorithm to use during resize + @param {Boolean} [options.multipass=true] Whether to scale the image in steps (results in better quality) + */ + resize: function(options) { + var self = this; + var orientation; + var scale; + + var srcRect = { + x: 0, + y: 0, + width: self.width, + height: self.height + }; + + var opts = Basic.extendIf({ + width: self.width, + height: self.height, + type: self.type || 'image/jpeg', + quality: 90, + crop: false, + fit: true, + preserveHeaders: true, + resample: 'default', + multipass: true + }, options); + + try { + if (!self.size) { // only preloaded image objects can be used as source + throw new x.DOMException(x.DOMException.INVALID_STATE_ERR); + } + + // no way to reliably intercept the crash due to high resolution, so we simply avoid it + if (self.width > Image.MAX_RESIZE_WIDTH || self.height > Image.MAX_RESIZE_HEIGHT) { + throw new x.ImageError(x.ImageError.MAX_RESOLUTION_ERR); + } + + // take into account orientation tag + orientation = (self.meta && self.meta.tiff && self.meta.tiff.Orientation) || 1; + + if (Basic.inArray(orientation, [5,6,7,8]) !== -1) { // values that require 90 degree rotation + var tmp = opts.width; + opts.width = opts.height; + opts.height = tmp; + } + + if (opts.crop) { + scale = Math.max(opts.width/self.width, opts.height/self.height); + + if (options.fit) { + // first scale it up or down to fit the original image + srcRect.width = Math.min(Math.ceil(opts.width/scale), self.width); + srcRect.height = Math.min(Math.ceil(opts.height/scale), self.height); + + // recalculate the scale for adapted dimensions + scale = opts.width/srcRect.width; + } else { + srcRect.width = Math.min(opts.width, self.width); + srcRect.height = Math.min(opts.height, self.height); + + // now we do not need to scale it any further + scale = 1; + } + + if (typeof(opts.crop) === 'boolean') { + opts.crop = 'cc'; + } + + switch (opts.crop.toLowerCase().replace(/_/, '-')) { + case 'rb': + case 'right-bottom': + srcRect.x = self.width - srcRect.width; + srcRect.y = self.height - srcRect.height; + break; + + case 'cb': + case 'center-bottom': + srcRect.x = Math.floor((self.width - srcRect.width) / 2); + srcRect.y = self.height - srcRect.height; + break; + + case 'lb': + case 'left-bottom': + srcRect.x = 0; + srcRect.y = self.height - srcRect.height; + break; + + case 'lt': + case 'left-top': + srcRect.x = 0; + srcRect.y = 0; + break; + + case 'ct': + case 'center-top': + srcRect.x = Math.floor((self.width - srcRect.width) / 2); + srcRect.y = 0; + break; + + case 'rt': + case 'right-top': + srcRect.x = self.width - srcRect.width; + srcRect.y = 0; + break; + + case 'rc': + case 'right-center': + case 'right-middle': + srcRect.x = self.width - srcRect.width; + srcRect.y = Math.floor((self.height - srcRect.height) / 2); + break; + + + case 'lc': + case 'left-center': + case 'left-middle': + srcRect.x = 0; + srcRect.y = Math.floor((self.height - srcRect.height) / 2); + break; + + case 'cc': + case 'center-center': + case 'center-middle': + default: + srcRect.x = Math.floor((self.width - srcRect.width) / 2); + srcRect.y = Math.floor((self.height - srcRect.height) / 2); + } + + // original image might be smaller than requested crop, so - avoid negative values + srcRect.x = Math.max(srcRect.x, 0); + srcRect.y = Math.max(srcRect.y, 0); + } else { + scale = Math.min(opts.width/self.width, opts.height/self.height); + } + + this.exec('Image', 'resize', srcRect, scale, opts); + } catch(ex) { + // for now simply trigger error event + self.trigger('error', ex.code); + } + }, + + /** + Downsizes the image to fit the specified width/height. If crop is supplied, image will be cropped to exact dimensions. + + @method downsize + @deprecated use resize() + */ + downsize: function(options) { + var defaults = { + width: this.width, + height: this.height, + type: this.type || 'image/jpeg', + quality: 90, + crop: false, + preserveHeaders: true, + resample: 'default' + }, opts; + + if (typeof(options) === 'object') { + opts = Basic.extend(defaults, options); + } else { + // for backward compatibility + opts = Basic.extend(defaults, { + width: arguments[0], + height: arguments[1], + crop: arguments[2], + preserveHeaders: arguments[3] + }); + } + + this.resize(opts); + }, + + /** + Alias for downsize(width, height, true). (see downsize) + + @method crop + @param {Number} width Resulting width + @param {Number} [height=width] Resulting height (optional, if not supplied will default to width) + @param {Boolean} [preserveHeaders=true] Whether to preserve meta headers (on JPEGs after resize) + */ + crop: function(width, height, preserveHeaders) { + this.downsize(width, height, true, preserveHeaders); + }, + + getAsCanvas: function() { + if (!Env.can('create_canvas')) { + throw new x.RuntimeError(x.RuntimeError.NOT_SUPPORTED_ERR); + } + return this.exec('Image', 'getAsCanvas'); + }, + + /** + Retrieves image in it's current state as mOxie.Blob object. Cannot be run on empty or image in progress (throws + DOMException.INVALID_STATE_ERR). + + @method getAsBlob + @param {String} [type="image/jpeg"] Mime type of resulting blob. Can either be image/jpeg or image/png + @param {Number} [quality=90] Applicable only together with mime type image/jpeg + @return {Blob} Image as Blob + */ + getAsBlob: function(type, quality) { + if (!this.size) { + throw new x.DOMException(x.DOMException.INVALID_STATE_ERR); + } + return this.exec('Image', 'getAsBlob', type || 'image/jpeg', quality || 90); + }, + + /** + Retrieves image in it's current state as dataURL string. Cannot be run on empty or image in progress (throws + DOMException.INVALID_STATE_ERR). + + @method getAsDataURL + @param {String} [type="image/jpeg"] Mime type of resulting blob. Can either be image/jpeg or image/png + @param {Number} [quality=90] Applicable only together with mime type image/jpeg + @return {String} Image as dataURL string + */ + getAsDataURL: function(type, quality) { + if (!this.size) { + throw new x.DOMException(x.DOMException.INVALID_STATE_ERR); + } + return this.exec('Image', 'getAsDataURL', type || 'image/jpeg', quality || 90); + }, + + /** + Retrieves image in it's current state as binary string. Cannot be run on empty or image in progress (throws + DOMException.INVALID_STATE_ERR). + + @method getAsBinaryString + @param {String} [type="image/jpeg"] Mime type of resulting blob. Can either be image/jpeg or image/png + @param {Number} [quality=90] Applicable only together with mime type image/jpeg + @return {String} Image as binary string + */ + getAsBinaryString: function(type, quality) { + var dataUrl = this.getAsDataURL(type, quality); + return Encode.atob(dataUrl.substring(dataUrl.indexOf('base64,') + 7)); + }, + + /** + Embeds a visual representation of the image into the specified node. Depending on the runtime, + it might be a canvas, an img node or a thrid party shim object (Flash or SilverLight - very rare, + can be used in legacy browsers that do not have canvas or proper dataURI support). + + @method embed + @param {DOMElement} el DOM element to insert the image object into + @param {Object} [options] + @param {Number} [options.width] The width of an embed (defaults to the image width) + @param {Number} [options.height] The height of an embed (defaults to the image height) + @param {String} [options.type="image/jpeg"] Mime type + @param {Number} [options.quality=90] Quality of an embed, if mime type is image/jpeg + @param {Boolean} [options.crop=false] Whether to crop an embed to the specified dimensions + */ + embed: function(el, options) { + var self = this + , runtime // this has to be outside of all the closures to contain proper runtime + ; + + var opts = Basic.extend({ + width: this.width, + height: this.height, + type: this.type || 'image/jpeg', + quality: 90 + }, options); + + + function render(type, quality) { + var img = this; + + // if possible, embed a canvas element directly + if (Env.can('create_canvas')) { + var canvas = img.getAsCanvas(); + if (canvas) { + el.appendChild(canvas); + canvas = null; + img.destroy(); + self.trigger('embedded'); + return; + } + } + + var dataUrl = img.getAsDataURL(type, quality); + if (!dataUrl) { + throw new x.ImageError(x.ImageError.WRONG_FORMAT); + } + + if (Env.can('use_data_uri_of', dataUrl.length)) { + el.innerHTML = ''; + img.destroy(); + self.trigger('embedded'); + } else { + var tr = new Transporter(); + + tr.bind("TransportingComplete", function() { + runtime = self.connectRuntime(this.result.ruid); + + self.bind("Embedded", function() { + // position and size properly + Basic.extend(runtime.getShimContainer().style, { + //position: 'relative', + top: '0px', + left: '0px', + width: img.width + 'px', + height: img.height + 'px' + }); + + // some shims (Flash/SilverLight) reinitialize, if parent element is hidden, reordered or it's + // position type changes (in Gecko), but since we basically need this only in IEs 6/7 and + // sometimes 8 and they do not have this problem, we can comment this for now + /*tr.bind("RuntimeInit", function(e, runtime) { + tr.destroy(); + runtime.destroy(); + onResize.call(self); // re-feed our image data + });*/ + + runtime = null; // release + }, 999); + + runtime.exec.call(self, "ImageView", "display", this.result.uid, width, height); + img.destroy(); + }); + + tr.transport(Encode.atob(dataUrl.substring(dataUrl.indexOf('base64,') + 7)), type, { + required_caps: { + display_media: true + }, + runtime_order: 'flash,silverlight', + container: el + }); + } + } + + try { + if (!(el = Dom.get(el))) { + throw new x.DOMException(x.DOMException.INVALID_NODE_TYPE_ERR); + } + + if (!this.size) { // only preloaded image objects can be used as source + throw new x.DOMException(x.DOMException.INVALID_STATE_ERR); + } + + // high-resolution images cannot be consistently handled across the runtimes + if (this.width > Image.MAX_RESIZE_WIDTH || this.height > Image.MAX_RESIZE_HEIGHT) { + //throw new x.ImageError(x.ImageError.MAX_RESOLUTION_ERR); + } + + var imgCopy = new Image(); + + imgCopy.bind("Resize", function() { + render.call(this, opts.type, opts.quality); + }); + + imgCopy.bind("Load", function() { + this.downsize(opts); + }); + + // if embedded thumb data is available and dimensions are big enough, use it + if (this.meta.thumb && this.meta.thumb.width >= opts.width && this.meta.thumb.height >= opts.height) { + imgCopy.load(this.meta.thumb.data); + } else { + imgCopy.clone(this, false); + } + + return imgCopy; + } catch(ex) { + // for now simply trigger error event + this.trigger('error', ex.code); + } + }, + + /** + Properly destroys the image and frees resources in use. If any. Recommended way to dispose mOxie.Image object. + + @method destroy + */ + destroy: function() { + if (this.ruid) { + this.getRuntime().exec.call(this, 'Image', 'destroy'); + this.disconnectRuntime(); + } + if (this.meta && this.meta.thumb) { + // thumb is blob, make sure we destroy it first + this.meta.thumb.data.destroy(); + } + this.unbindAll(); + } + }); + + + // this is here, because in order to bind properly, we need uid, which is created above + this.handleEventProps(dispatches); + + this.bind('Load Resize', function() { + return _updateInfo.call(this); // if operation fails (e.g. image is neither PNG nor JPEG) cancel all pending events + }, 999); + + + function _updateInfo(info) { + try { + if (!info) { + info = this.exec('Image', 'getInfo'); + } + + this.size = info.size; + this.width = info.width; + this.height = info.height; + this.type = info.type; + this.meta = info.meta; + + // update file name, only if empty + if (this.name === '') { + this.name = info.name; + } + + return true; + } catch(ex) { + this.trigger('error', ex.code); + return false; + } + } + + + function _load(src) { + var srcType = Basic.typeOf(src); + + try { + // if source is Image + if (src instanceof Image) { + if (!src.size) { // only preloaded image objects can be used as source + throw new x.DOMException(x.DOMException.INVALID_STATE_ERR); + } + _loadFromImage.apply(this, arguments); + } + // if source is o.Blob/o.File + else if (src instanceof Blob) { + if (!~Basic.inArray(src.type, ['image/jpeg', 'image/png'])) { + throw new x.ImageError(x.ImageError.WRONG_FORMAT); + } + _loadFromBlob.apply(this, arguments); + } + // if native blob/file + else if (Basic.inArray(srcType, ['blob', 'file']) !== -1) { + _load.call(this, new File(null, src), arguments[1]); + } + // if String + else if (srcType === 'string') { + // if dataUrl String + if (src.substr(0, 5) === 'data:') { + _load.call(this, new Blob(null, { data: src }), arguments[1]); + } + // else assume Url, either relative or absolute + else { + _loadFromUrl.apply(this, arguments); + } + } + // if source seems to be an img node + else if (srcType === 'node' && src.nodeName.toLowerCase() === 'img') { + _load.call(this, src.src, arguments[1]); + } + else { + throw new x.DOMException(x.DOMException.TYPE_MISMATCH_ERR); + } + } catch(ex) { + // for now simply trigger error event + this.trigger('error', ex.code); + } + } + + + function _loadFromImage(img, exact) { + var runtime = this.connectRuntime(img.ruid); + this.ruid = runtime.uid; + runtime.exec.call(this, 'Image', 'loadFromImage', img, (Basic.typeOf(exact) === 'undefined' ? true : exact)); + } + + + function _loadFromBlob(blob, options) { + var self = this; + + self.name = blob.name || ''; + + function exec(runtime) { + self.ruid = runtime.uid; + runtime.exec.call(self, 'Image', 'loadFromBlob', blob); + } + + if (blob.isDetached()) { + this.bind('RuntimeInit', function(e, runtime) { + exec(runtime); + }); + + // convert to object representation + if (options && typeof(options.required_caps) === 'string') { + options.required_caps = Runtime.parseCaps(options.required_caps); + } + + this.connectRuntime(Basic.extend({ + required_caps: { + access_image_binary: true, + resize_image: true + } + }, options)); + } else { + exec(this.connectRuntime(blob.ruid)); + } + } + + + function _loadFromUrl(url, options) { + var self = this, xhr; + + xhr = new XMLHttpRequest(); + + xhr.open('get', url); + xhr.responseType = 'blob'; + + xhr.onprogress = function(e) { + self.trigger(e); + }; + + xhr.onload = function() { + _loadFromBlob.call(self, xhr.response, true); + }; + + xhr.onerror = function(e) { + self.trigger(e); + }; + + xhr.onloadend = function() { + xhr.destroy(); + }; + + xhr.bind('RuntimeError', function(e, err) { + self.trigger('RuntimeError', err); + }); + + xhr.send(null, options); + } + } + + // virtual world will crash on you if image has a resolution higher than this: + Image.MAX_RESIZE_WIDTH = 8192; + Image.MAX_RESIZE_HEIGHT = 8192; + + Image.prototype = EventTarget.instance; + + return Image; +}); + +// Included from: src/javascript/runtime/html5/Runtime.js + +/** + * Runtime.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/*global File:true */ + +/** +Defines constructor for HTML5 runtime. + +@class moxie/runtime/html5/Runtime +@private +*/ +define("moxie/runtime/html5/Runtime", [ + "moxie/core/utils/Basic", + "moxie/core/Exceptions", + "moxie/runtime/Runtime", + "moxie/core/utils/Env" +], function(Basic, x, Runtime, Env) { + + var type = "html5", extensions = {}; + + function Html5Runtime(options) { + var I = this + , Test = Runtime.capTest + , True = Runtime.capTrue + ; + + var caps = Basic.extend({ + access_binary: Test(window.FileReader || window.File && window.File.getAsDataURL), + access_image_binary: function() { + return I.can('access_binary') && !!extensions.Image; + }, + display_media: Test( + (Env.can('create_canvas') || Env.can('use_data_uri_over32kb')) && + defined('moxie/image/Image') + ), + do_cors: Test(window.XMLHttpRequest && 'withCredentials' in new XMLHttpRequest()), + drag_and_drop: Test(function() { + // this comes directly from Modernizr: http://www.modernizr.com/ + var div = document.createElement('div'); + // IE has support for drag and drop since version 5, but doesn't support dropping files from desktop + return (('draggable' in div) || ('ondragstart' in div && 'ondrop' in div)) && + (Env.browser !== 'IE' || Env.verComp(Env.version, 9, '>')); + }()), + filter_by_extension: Test(function() { // if you know how to feature-detect this, please suggest + return !( + (Env.browser === 'Chrome' && Env.verComp(Env.version, 28, '<')) || + (Env.browser === 'IE' && Env.verComp(Env.version, 10, '<')) || + (Env.browser === 'Safari' && Env.verComp(Env.version, 7, '<')) || + (Env.browser === 'Firefox' && Env.verComp(Env.version, 37, '<')) + ); + }()), + return_response_headers: True, + return_response_type: function(responseType) { + if (responseType === 'json' && !!window.JSON) { // we can fake this one even if it's not supported + return true; + } + return Env.can('return_response_type', responseType); + }, + return_status_code: True, + report_upload_progress: Test(window.XMLHttpRequest && new XMLHttpRequest().upload), + resize_image: function() { + return I.can('access_binary') && Env.can('create_canvas'); + }, + select_file: function() { + return Env.can('use_fileinput') && window.File; + }, + select_folder: function() { + return I.can('select_file') && ( + Env.browser === 'Chrome' && Env.verComp(Env.version, 21, '>=') || + Env.browser === 'Firefox' && Env.verComp(Env.version, 42, '>=') // https://developer.mozilla.org/en-US/Firefox/Releases/42 + ); + }, + select_multiple: function() { + // it is buggy on Safari Windows and iOS + return I.can('select_file') && + !(Env.browser === 'Safari' && Env.os === 'Windows') && + !(Env.os === 'iOS' && Env.verComp(Env.osVersion, "7.0.0", '>') && Env.verComp(Env.osVersion, "8.0.0", '<')); + }, + send_binary_string: Test(window.XMLHttpRequest && (new XMLHttpRequest().sendAsBinary || (window.Uint8Array && window.ArrayBuffer))), + send_custom_headers: Test(window.XMLHttpRequest), + send_multipart: function() { + return !!(window.XMLHttpRequest && new XMLHttpRequest().upload && window.FormData) || I.can('send_binary_string'); + }, + slice_blob: Test(window.File && (File.prototype.mozSlice || File.prototype.webkitSlice || File.prototype.slice)), + stream_upload: function(){ + return I.can('slice_blob') && I.can('send_multipart'); + }, + summon_file_dialog: function() { // yeah... some dirty sniffing here... + return I.can('select_file') && ( + (Env.browser === 'Firefox' && Env.verComp(Env.version, 4, '>=')) || + (Env.browser === 'Opera' && Env.verComp(Env.version, 12, '>=')) || + (Env.browser === 'IE' && Env.verComp(Env.version, 10, '>=')) || + !!~Basic.inArray(Env.browser, ['Chrome', 'Safari', 'Edge']) + ); + }, + upload_filesize: True, + use_http_method: True + }, + arguments[2] + ); + + Runtime.call(this, options, (arguments[1] || type), caps); + + + Basic.extend(this, { + + init : function() { + this.trigger("Init"); + }, + + destroy: (function(destroy) { // extend default destroy method + return function() { + destroy.call(I); + destroy = I = null; + }; + }(this.destroy)) + }); + + Basic.extend(this.getShim(), extensions); + } + + Runtime.addConstructor(type, Html5Runtime); + + return extensions; +}); + +// Included from: src/javascript/runtime/html5/file/Blob.js + +/** + * Blob.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/html5/file/Blob +@private +*/ +define("moxie/runtime/html5/file/Blob", [ + "moxie/runtime/html5/Runtime", + "moxie/file/Blob" +], function(extensions, Blob) { + + function HTML5Blob() { + function w3cBlobSlice(blob, start, end) { + var blobSlice; + + if (window.File.prototype.slice) { + try { + blob.slice(); // depricated version will throw WRONG_ARGUMENTS_ERR exception + return blob.slice(start, end); + } catch (e) { + // depricated slice method + return blob.slice(start, end - start); + } + // slice method got prefixed: https://bugzilla.mozilla.org/show_bug.cgi?id=649672 + } else if ((blobSlice = window.File.prototype.webkitSlice || window.File.prototype.mozSlice)) { + return blobSlice.call(blob, start, end); + } else { + return null; // or throw some exception + } + } + + this.slice = function() { + return new Blob(this.getRuntime().uid, w3cBlobSlice.apply(this, arguments)); + }; + } + + return (extensions.Blob = HTML5Blob); +}); + +// Included from: src/javascript/core/utils/Events.js + +/** + * Events.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +define('moxie/core/utils/Events', [ + 'moxie/core/utils/Basic' +], function(Basic) { + var eventhash = {}, uid = 'moxie_' + Basic.guid(); + + // IE W3C like event funcs + function preventDefault() { + this.returnValue = false; + } + + function stopPropagation() { + this.cancelBubble = true; + } + + /** + Adds an event handler to the specified object and store reference to the handler + in objects internal Plupload registry (@see removeEvent). + + @method addEvent + @for Utils + @static + @param {Object} obj DOM element like object to add handler to. + @param {String} name Name to add event listener to. + @param {Function} callback Function to call when event occurs. + @param {String} [key] that might be used to add specifity to the event record. + */ + var addEvent = function(obj, name, callback, key) { + var func, events; + + name = name.toLowerCase(); + + // Add event listener + if (obj.addEventListener) { + func = callback; + + obj.addEventListener(name, func, false); + } else if (obj.attachEvent) { + func = function() { + var evt = window.event; + + if (!evt.target) { + evt.target = evt.srcElement; + } + + evt.preventDefault = preventDefault; + evt.stopPropagation = stopPropagation; + + callback(evt); + }; + + obj.attachEvent('on' + name, func); + } + + // Log event handler to objects internal mOxie registry + if (!obj[uid]) { + obj[uid] = Basic.guid(); + } + + if (!eventhash.hasOwnProperty(obj[uid])) { + eventhash[obj[uid]] = {}; + } + + events = eventhash[obj[uid]]; + + if (!events.hasOwnProperty(name)) { + events[name] = []; + } + + events[name].push({ + func: func, + orig: callback, // store original callback for IE + key: key + }); + }; + + + /** + Remove event handler from the specified object. If third argument (callback) + is not specified remove all events with the specified name. + + @method removeEvent + @static + @param {Object} obj DOM element to remove event listener(s) from. + @param {String} name Name of event listener to remove. + @param {Function|String} [callback] might be a callback or unique key to match. + */ + var removeEvent = function(obj, name, callback) { + var type, undef; + + name = name.toLowerCase(); + + if (obj[uid] && eventhash[obj[uid]] && eventhash[obj[uid]][name]) { + type = eventhash[obj[uid]][name]; + } else { + return; + } + + for (var i = type.length - 1; i >= 0; i--) { + // undefined or not, key should match + if (type[i].orig === callback || type[i].key === callback) { + if (obj.removeEventListener) { + obj.removeEventListener(name, type[i].func, false); + } else if (obj.detachEvent) { + obj.detachEvent('on'+name, type[i].func); + } + + type[i].orig = null; + type[i].func = null; + type.splice(i, 1); + + // If callback was passed we are done here, otherwise proceed + if (callback !== undef) { + break; + } + } + } + + // If event array got empty, remove it + if (!type.length) { + delete eventhash[obj[uid]][name]; + } + + // If mOxie registry has become empty, remove it + if (Basic.isEmptyObj(eventhash[obj[uid]])) { + delete eventhash[obj[uid]]; + + // IE doesn't let you remove DOM object property with - delete + try { + delete obj[uid]; + } catch(e) { + obj[uid] = undef; + } + } + }; + + + /** + Remove all kind of events from the specified object + + @method removeAllEvents + @static + @param {Object} obj DOM element to remove event listeners from. + @param {String} [key] unique key to match, when removing events. + */ + var removeAllEvents = function(obj, key) { + if (!obj || !obj[uid]) { + return; + } + + Basic.each(eventhash[obj[uid]], function(events, name) { + removeEvent(obj, name, key); + }); + }; + + return { + addEvent: addEvent, + removeEvent: removeEvent, + removeAllEvents: removeAllEvents + }; +}); + +// Included from: src/javascript/runtime/html5/file/FileInput.js + +/** + * FileInput.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/html5/file/FileInput +@private +*/ +define("moxie/runtime/html5/file/FileInput", [ + "moxie/runtime/html5/Runtime", + "moxie/file/File", + "moxie/core/utils/Basic", + "moxie/core/utils/Dom", + "moxie/core/utils/Events", + "moxie/core/utils/Mime", + "moxie/core/utils/Env" +], function(extensions, File, Basic, Dom, Events, Mime, Env) { + + function FileInput() { + var _options, _browseBtnZIndex; // save original z-index + + Basic.extend(this, { + init: function(options) { + var comp = this, I = comp.getRuntime(), input, shimContainer, mimes, browseButton, zIndex, top; + + _options = options; + + // figure out accept string + mimes = _options.accept.mimes || Mime.extList2mimes(_options.accept, I.can('filter_by_extension')); + + shimContainer = I.getShimContainer(); + + shimContainer.innerHTML = ''; + + input = Dom.get(I.uid); + + // prepare file input to be placed underneath the browse_button element + Basic.extend(input.style, { + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%' + }); + + + browseButton = Dom.get(_options.browse_button); + _browseBtnZIndex = Dom.getStyle(browseButton, 'z-index') || 'auto'; + + // Route click event to the input[type=file] element for browsers that support such behavior + if (I.can('summon_file_dialog')) { + if (Dom.getStyle(browseButton, 'position') === 'static') { + browseButton.style.position = 'relative'; + } + + Events.addEvent(browseButton, 'click', function(e) { + var input = Dom.get(I.uid); + if (input && !input.disabled) { // for some reason FF (up to 8.0.1 so far) lets to click disabled input[type=file] + input.click(); + } + e.preventDefault(); + }, comp.uid); + + comp.bind('Refresh', function() { + zIndex = parseInt(_browseBtnZIndex, 10) || 1; + + Dom.get(_options.browse_button).style.zIndex = zIndex; + this.getRuntime().getShimContainer().style.zIndex = zIndex - 1; + }); + } + + /* Since we have to place input[type=file] on top of the browse_button for some browsers, + browse_button loses interactivity, so we restore it here */ + top = I.can('summon_file_dialog') ? browseButton : shimContainer; + + Events.addEvent(top, 'mouseover', function() { + comp.trigger('mouseenter'); + }, comp.uid); + + Events.addEvent(top, 'mouseout', function() { + comp.trigger('mouseleave'); + }, comp.uid); + + Events.addEvent(top, 'mousedown', function() { + comp.trigger('mousedown'); + }, comp.uid); + + Events.addEvent(Dom.get(_options.container), 'mouseup', function() { + comp.trigger('mouseup'); + }, comp.uid); + + + input.onchange = function onChange(e) { // there should be only one handler for this + comp.files = []; + + Basic.each(this.files, function(file) { + var relativePath = ''; + + if (_options.directory) { + // folders are represented by dots, filter them out (Chrome 11+) + if (file.name == ".") { + // if it looks like a folder... + return true; + } + } + + if (file.webkitRelativePath) { + relativePath = '/' + file.webkitRelativePath.replace(/^\//, ''); + } + + file = new File(I.uid, file); + file.relativePath = relativePath; + + comp.files.push(file); + }); + + // clearing the value enables the user to select the same file again if they want to + if (Env.browser !== 'IE' && Env.browser !== 'IEMobile') { + this.value = ''; + } else { + // in IE input[type="file"] is read-only so the only way to reset it is to re-insert it + var clone = this.cloneNode(true); + this.parentNode.replaceChild(clone, this); + clone.onchange = onChange; + } + + if (comp.files.length) { + comp.trigger('change'); + } + }; + + // ready event is perfectly asynchronous + comp.trigger({ + type: 'ready', + async: true + }); + + shimContainer = null; + }, + + + setOption: function(name, value) { + var I = this.getRuntime(); + var input = Dom.get(I.uid); + + switch (name) { + case 'accept': + if (value) { + var mimes = value.mimes || Mime.extList2mimes(value, I.can('filter_by_extension')); + input.setAttribute('accept', mimes.join(',')); + } else { + input.removeAttribute('accept'); + } + break; + + case 'directory': + if (value && I.can('select_folder')) { + input.setAttribute('directory', ''); + input.setAttribute('webkitdirectory', ''); + } else { + input.removeAttribute('directory'); + input.removeAttribute('webkitdirectory'); + } + break; + + case 'multiple': + if (value && I.can('select_multiple')) { + input.setAttribute('multiple', ''); + } else { + input.removeAttribute('multiple'); + } + + } + }, + + + disable: function(state) { + var I = this.getRuntime(), input; + + if ((input = Dom.get(I.uid))) { + input.disabled = !!state; + } + }, + + destroy: function() { + var I = this.getRuntime() + , shim = I.getShim() + , shimContainer = I.getShimContainer() + , container = _options && Dom.get(_options.container) + , browseButton = _options && Dom.get(_options.browse_button) + ; + + if (container) { + Events.removeAllEvents(container, this.uid); + } + + if (browseButton) { + Events.removeAllEvents(browseButton, this.uid); + browseButton.style.zIndex = _browseBtnZIndex; // reset to original value + } + + if (shimContainer) { + Events.removeAllEvents(shimContainer, this.uid); + shimContainer.innerHTML = ''; + } + + shim.removeInstance(this.uid); + + _options = shimContainer = container = browseButton = shim = null; + } + }); + } + + return (extensions.FileInput = FileInput); +}); + +// Included from: src/javascript/runtime/html5/file/FileDrop.js + +/** + * FileDrop.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/html5/file/FileDrop +@private +*/ +define("moxie/runtime/html5/file/FileDrop", [ + "moxie/runtime/html5/Runtime", + 'moxie/file/File', + "moxie/core/utils/Basic", + "moxie/core/utils/Dom", + "moxie/core/utils/Events", + "moxie/core/utils/Mime" +], function(extensions, File, Basic, Dom, Events, Mime) { + + function FileDrop() { + var _files = [], _allowedExts = [], _options, _ruid; + + Basic.extend(this, { + init: function(options) { + var comp = this, dropZone; + + _options = options; + _ruid = comp.ruid; // every dropped-in file should have a reference to the runtime + _allowedExts = _extractExts(_options.accept); + dropZone = _options.container; + + Events.addEvent(dropZone, 'dragover', function(e) { + if (!_hasFiles(e)) { + return; + } + e.preventDefault(); + e.dataTransfer.dropEffect = 'copy'; + }, comp.uid); + + Events.addEvent(dropZone, 'drop', function(e) { + if (!_hasFiles(e)) { + return; + } + e.preventDefault(); + + _files = []; + + // Chrome 21+ accepts folders via Drag'n'Drop + if (e.dataTransfer.items && e.dataTransfer.items[0].webkitGetAsEntry) { + _readItems(e.dataTransfer.items, function() { + comp.files = _files; + comp.trigger("drop"); + }); + } else { + Basic.each(e.dataTransfer.files, function(file) { + _addFile(file); + }); + comp.files = _files; + comp.trigger("drop"); + } + }, comp.uid); + + Events.addEvent(dropZone, 'dragenter', function(e) { + comp.trigger("dragenter"); + }, comp.uid); + + Events.addEvent(dropZone, 'dragleave', function(e) { + comp.trigger("dragleave"); + }, comp.uid); + }, + + destroy: function() { + Events.removeAllEvents(_options && Dom.get(_options.container), this.uid); + _ruid = _files = _allowedExts = _options = null; + } + }); + + + function _hasFiles(e) { + if (!e.dataTransfer || !e.dataTransfer.types) { // e.dataTransfer.files is not available in Gecko during dragover + return false; + } + + var types = Basic.toArray(e.dataTransfer.types || []); + + return Basic.inArray("Files", types) !== -1 || + Basic.inArray("public.file-url", types) !== -1 || // Safari < 5 + Basic.inArray("application/x-moz-file", types) !== -1 // Gecko < 1.9.2 (< Firefox 3.6) + ; + } + + + function _addFile(file, relativePath) { + if (_isAcceptable(file)) { + var fileObj = new File(_ruid, file); + fileObj.relativePath = relativePath || ''; + _files.push(fileObj); + } + } + + + function _extractExts(accept) { + var exts = []; + for (var i = 0; i < accept.length; i++) { + [].push.apply(exts, accept[i].extensions.split(/\s*,\s*/)); + } + return Basic.inArray('*', exts) === -1 ? exts : []; + } + + + function _isAcceptable(file) { + if (!_allowedExts.length) { + return true; + } + var ext = Mime.getFileExtension(file.name); + return !ext || Basic.inArray(ext, _allowedExts) !== -1; + } + + + function _readItems(items, cb) { + var entries = []; + Basic.each(items, function(item) { + var entry = item.webkitGetAsEntry(); + // Address #998 (https://code.google.com/p/chromium/issues/detail?id=332579) + if (entry) { + // file() fails on OSX when the filename contains a special character (e.g. umlaut): see #61 + if (entry.isFile) { + _addFile(item.getAsFile(), entry.fullPath); + } else { + entries.push(entry); + } + } + }); + + if (entries.length) { + _readEntries(entries, cb); + } else { + cb(); + } + } + + + function _readEntries(entries, cb) { + var queue = []; + Basic.each(entries, function(entry) { + queue.push(function(cbcb) { + _readEntry(entry, cbcb); + }); + }); + Basic.inSeries(queue, function() { + cb(); + }); + } + + + function _readEntry(entry, cb) { + if (entry.isFile) { + entry.file(function(file) { + _addFile(file, entry.fullPath); + cb(); + }, function() { + // fire an error event maybe + cb(); + }); + } else if (entry.isDirectory) { + _readDirEntry(entry, cb); + } else { + cb(); // not file, not directory? what then?.. + } + } + + + function _readDirEntry(dirEntry, cb) { + var entries = [], dirReader = dirEntry.createReader(); + + // keep quering recursively till no more entries + function getEntries(cbcb) { + dirReader.readEntries(function(moreEntries) { + if (moreEntries.length) { + [].push.apply(entries, moreEntries); + getEntries(cbcb); + } else { + cbcb(); + } + }, cbcb); + } + + // ...and you thought FileReader was crazy... + getEntries(function() { + _readEntries(entries, cb); + }); + } + } + + return (extensions.FileDrop = FileDrop); +}); + +// Included from: src/javascript/runtime/html5/file/FileReader.js + +/** + * FileReader.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/html5/file/FileReader +@private +*/ +define("moxie/runtime/html5/file/FileReader", [ + "moxie/runtime/html5/Runtime", + "moxie/core/utils/Encode", + "moxie/core/utils/Basic" +], function(extensions, Encode, Basic) { + + function FileReader() { + var _fr, _convertToBinary = false; + + Basic.extend(this, { + + read: function(op, blob) { + var comp = this; + + comp.result = ''; + + _fr = new window.FileReader(); + + _fr.addEventListener('progress', function(e) { + comp.trigger(e); + }); + + _fr.addEventListener('load', function(e) { + comp.result = _convertToBinary ? _toBinary(_fr.result) : _fr.result; + comp.trigger(e); + }); + + _fr.addEventListener('error', function(e) { + comp.trigger(e, _fr.error); + }); + + _fr.addEventListener('loadend', function(e) { + _fr = null; + comp.trigger(e); + }); + + if (Basic.typeOf(_fr[op]) === 'function') { + _convertToBinary = false; + _fr[op](blob.getSource()); + } else if (op === 'readAsBinaryString') { // readAsBinaryString is depricated in general and never existed in IE10+ + _convertToBinary = true; + _fr.readAsDataURL(blob.getSource()); + } + }, + + abort: function() { + if (_fr) { + _fr.abort(); + } + }, + + destroy: function() { + _fr = null; + } + }); + + function _toBinary(str) { + return Encode.atob(str.substring(str.indexOf('base64,') + 7)); + } + } + + return (extensions.FileReader = FileReader); +}); + +// Included from: src/javascript/runtime/html5/xhr/XMLHttpRequest.js + +/** + * XMLHttpRequest.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/*global ActiveXObject:true */ + +/** +@class moxie/runtime/html5/xhr/XMLHttpRequest +@private +*/ +define("moxie/runtime/html5/xhr/XMLHttpRequest", [ + "moxie/runtime/html5/Runtime", + "moxie/core/utils/Basic", + "moxie/core/utils/Mime", + "moxie/core/utils/Url", + "moxie/file/File", + "moxie/file/Blob", + "moxie/xhr/FormData", + "moxie/core/Exceptions", + "moxie/core/utils/Env" +], function(extensions, Basic, Mime, Url, File, Blob, FormData, x, Env) { + + function XMLHttpRequest() { + var self = this + , _xhr + , _filename + ; + + Basic.extend(this, { + send: function(meta, data) { + var target = this + , isGecko2_5_6 = (Env.browser === 'Mozilla' && Env.verComp(Env.version, 4, '>=') && Env.verComp(Env.version, 7, '<')) + , isAndroidBrowser = Env.browser === 'Android Browser' + , mustSendAsBinary = false + ; + + // extract file name + _filename = meta.url.replace(/^.+?\/([\w\-\.]+)$/, '$1').toLowerCase(); + + _xhr = _getNativeXHR(); + _xhr.open(meta.method, meta.url, meta.async, meta.user, meta.password); + + + // prepare data to be sent + if (data instanceof Blob) { + if (data.isDetached()) { + mustSendAsBinary = true; + } + data = data.getSource(); + } else if (data instanceof FormData) { + + if (data.hasBlob()) { + if (data.getBlob().isDetached()) { + data = _prepareMultipart.call(target, data); // _xhr must be instantiated and be in OPENED state + mustSendAsBinary = true; + } else if ((isGecko2_5_6 || isAndroidBrowser) && Basic.typeOf(data.getBlob().getSource()) === 'blob' && window.FileReader) { + // Gecko 2/5/6 can't send blob in FormData: https://bugzilla.mozilla.org/show_bug.cgi?id=649150 + // Android browsers (default one and Dolphin) seem to have the same issue, see: #613 + _preloadAndSend.call(target, meta, data); + return; // _preloadAndSend will reinvoke send() with transmutated FormData =%D + } + } + + // transfer fields to real FormData + if (data instanceof FormData) { // if still a FormData, e.g. not mangled by _prepareMultipart() + var fd = new window.FormData(); + data.each(function(value, name) { + if (value instanceof Blob) { + fd.append(name, value.getSource()); + } else { + fd.append(name, value); + } + }); + data = fd; + } + } + + + // if XHR L2 + if (_xhr.upload) { + if (meta.withCredentials) { + _xhr.withCredentials = true; + } + + _xhr.addEventListener('load', function(e) { + target.trigger(e); + }); + + _xhr.addEventListener('error', function(e) { + target.trigger(e); + }); + + // additionally listen to progress events + _xhr.addEventListener('progress', function(e) { + target.trigger(e); + }); + + _xhr.upload.addEventListener('progress', function(e) { + target.trigger({ + type: 'UploadProgress', + loaded: e.loaded, + total: e.total + }); + }); + // ... otherwise simulate XHR L2 + } else { + _xhr.onreadystatechange = function onReadyStateChange() { + + // fake Level 2 events + switch (_xhr.readyState) { + + case 1: // XMLHttpRequest.OPENED + // readystatechanged is fired twice for OPENED state (in IE and Mozilla) - neu + break; + + // looks like HEADERS_RECEIVED (state 2) is not reported in Opera (or it's old versions) - neu + case 2: // XMLHttpRequest.HEADERS_RECEIVED + break; + + case 3: // XMLHttpRequest.LOADING + // try to fire progress event for not XHR L2 + var total, loaded; + + try { + if (Url.hasSameOrigin(meta.url)) { // Content-Length not accessible for cross-domain on some browsers + total = _xhr.getResponseHeader('Content-Length') || 0; // old Safari throws an exception here + } + + if (_xhr.responseText) { // responseText was introduced in IE7 + loaded = _xhr.responseText.length; + } + } catch(ex) { + total = loaded = 0; + } + + target.trigger({ + type: 'progress', + lengthComputable: !!total, + total: parseInt(total, 10), + loaded: loaded + }); + break; + + case 4: // XMLHttpRequest.DONE + // release readystatechange handler (mostly for IE) + _xhr.onreadystatechange = function() {}; + + // usually status 0 is returned when server is unreachable, but FF also fails to status 0 for 408 timeout + if (_xhr.status === 0) { + target.trigger('error'); + } else { + target.trigger('load'); + } + break; + } + }; + } + + + // set request headers + if (!Basic.isEmptyObj(meta.headers)) { + Basic.each(meta.headers, function(value, header) { + _xhr.setRequestHeader(header, value); + }); + } + + // request response type + if ("" !== meta.responseType && 'responseType' in _xhr) { + if ('json' === meta.responseType && !Env.can('return_response_type', 'json')) { // we can fake this one + _xhr.responseType = 'text'; + } else { + _xhr.responseType = meta.responseType; + } + } + + // send ... + if (!mustSendAsBinary) { + _xhr.send(data); + } else { + if (_xhr.sendAsBinary) { // Gecko + _xhr.sendAsBinary(data); + } else { // other browsers having support for typed arrays + (function() { + // mimic Gecko's sendAsBinary + var ui8a = new Uint8Array(data.length); + for (var i = 0; i < data.length; i++) { + ui8a[i] = (data.charCodeAt(i) & 0xff); + } + _xhr.send(ui8a.buffer); + }()); + } + } + + target.trigger('loadstart'); + }, + + getStatus: function() { + // according to W3C spec it should return 0 for readyState < 3, but instead it throws an exception + try { + if (_xhr) { + return _xhr.status; + } + } catch(ex) {} + return 0; + }, + + getResponse: function(responseType) { + var I = this.getRuntime(); + + try { + switch (responseType) { + case 'blob': + var file = new File(I.uid, _xhr.response); + + // try to extract file name from content-disposition if possible (might be - not, if CORS for example) + var disposition = _xhr.getResponseHeader('Content-Disposition'); + if (disposition) { + // extract filename from response header if available + var match = disposition.match(/filename=([\'\"'])([^\1]+)\1/); + if (match) { + _filename = match[2]; + } + } + file.name = _filename; + + // pre-webkit Opera doesn't set type property on the blob response + if (!file.type) { + file.type = Mime.getFileMime(_filename); + } + return file; + + case 'json': + if (!Env.can('return_response_type', 'json')) { + return _xhr.status === 200 && !!window.JSON ? JSON.parse(_xhr.responseText) : null; + } + return _xhr.response; + + case 'document': + return _getDocument(_xhr); + + default: + return _xhr.responseText !== '' ? _xhr.responseText : null; // against the specs, but for consistency across the runtimes + } + } catch(ex) { + return null; + } + }, + + getAllResponseHeaders: function() { + try { + return _xhr.getAllResponseHeaders(); + } catch(ex) {} + return ''; + }, + + abort: function() { + if (_xhr) { + _xhr.abort(); + } + }, + + destroy: function() { + self = _filename = null; + } + }); + + + // here we go... ugly fix for ugly bug + function _preloadAndSend(meta, data) { + var target = this, blob, fr; + + // get original blob + blob = data.getBlob().getSource(); + + // preload blob in memory to be sent as binary string + fr = new window.FileReader(); + fr.onload = function() { + // overwrite original blob + data.append(data.getBlobName(), new Blob(null, { + type: blob.type, + data: fr.result + })); + // invoke send operation again + self.send.call(target, meta, data); + }; + fr.readAsBinaryString(blob); + } + + + function _getNativeXHR() { + if (window.XMLHttpRequest && !(Env.browser === 'IE' && Env.verComp(Env.version, 8, '<'))) { // IE7 has native XHR but it's buggy + return new window.XMLHttpRequest(); + } else { + return (function() { + var progIDs = ['Msxml2.XMLHTTP.6.0', 'Microsoft.XMLHTTP']; // if 6.0 available, use it, otherwise failback to default 3.0 + for (var i = 0; i < progIDs.length; i++) { + try { + return new ActiveXObject(progIDs[i]); + } catch (ex) {} + } + })(); + } + } + + // @credits Sergey Ilinsky (http://www.ilinsky.com/) + function _getDocument(xhr) { + var rXML = xhr.responseXML; + var rText = xhr.responseText; + + // Try parsing responseText (@see: http://www.ilinsky.com/articles/XMLHttpRequest/#bugs-ie-responseXML-content-type) + if (Env.browser === 'IE' && rText && rXML && !rXML.documentElement && /[^\/]+\/[^\+]+\+xml/.test(xhr.getResponseHeader("Content-Type"))) { + rXML = new window.ActiveXObject("Microsoft.XMLDOM"); + rXML.async = false; + rXML.validateOnParse = false; + rXML.loadXML(rText); + } + + // Check if there is no error in document + if (rXML) { + if ((Env.browser === 'IE' && rXML.parseError !== 0) || !rXML.documentElement || rXML.documentElement.tagName === "parsererror") { + return null; + } + } + return rXML; + } + + + function _prepareMultipart(fd) { + var boundary = '----moxieboundary' + new Date().getTime() + , dashdash = '--' + , crlf = '\r\n' + , multipart = '' + , I = this.getRuntime() + ; + + if (!I.can('send_binary_string')) { + throw new x.RuntimeError(x.RuntimeError.NOT_SUPPORTED_ERR); + } + + _xhr.setRequestHeader('Content-Type', 'multipart/form-data; boundary=' + boundary); + + // append multipart parameters + fd.each(function(value, name) { + // Firefox 3.6 failed to convert multibyte characters to UTF-8 in sendAsBinary(), + // so we try it here ourselves with: unescape(encodeURIComponent(value)) + if (value instanceof Blob) { + // Build RFC2388 blob + multipart += dashdash + boundary + crlf + + 'Content-Disposition: form-data; name="' + name + '"; filename="' + unescape(encodeURIComponent(value.name || 'blob')) + '"' + crlf + + 'Content-Type: ' + (value.type || 'application/octet-stream') + crlf + crlf + + value.getSource() + crlf; + } else { + multipart += dashdash + boundary + crlf + + 'Content-Disposition: form-data; name="' + name + '"' + crlf + crlf + + unescape(encodeURIComponent(value)) + crlf; + } + }); + + multipart += dashdash + boundary + dashdash + crlf; + + return multipart; + } + } + + return (extensions.XMLHttpRequest = XMLHttpRequest); +}); + +// Included from: src/javascript/runtime/html5/utils/BinaryReader.js + +/** + * BinaryReader.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/html5/utils/BinaryReader +@private +*/ +define("moxie/runtime/html5/utils/BinaryReader", [ + "moxie/core/utils/Basic" +], function(Basic) { + + + function BinaryReader(data) { + if (data instanceof ArrayBuffer) { + ArrayBufferReader.apply(this, arguments); + } else { + UTF16StringReader.apply(this, arguments); + } + } + + Basic.extend(BinaryReader.prototype, { + + littleEndian: false, + + + read: function(idx, size) { + var sum, mv, i; + + if (idx + size > this.length()) { + throw new Error("You are trying to read outside the source boundaries."); + } + + mv = this.littleEndian + ? 0 + : -8 * (size - 1) + ; + + for (i = 0, sum = 0; i < size; i++) { + sum |= (this.readByteAt(idx + i) << Math.abs(mv + i*8)); + } + return sum; + }, + + + write: function(idx, num, size) { + var mv, i, str = ''; + + if (idx > this.length()) { + throw new Error("You are trying to write outside the source boundaries."); + } + + mv = this.littleEndian + ? 0 + : -8 * (size - 1) + ; + + for (i = 0; i < size; i++) { + this.writeByteAt(idx + i, (num >> Math.abs(mv + i*8)) & 255); + } + }, + + + BYTE: function(idx) { + return this.read(idx, 1); + }, + + + SHORT: function(idx) { + return this.read(idx, 2); + }, + + + LONG: function(idx) { + return this.read(idx, 4); + }, + + + SLONG: function(idx) { // 2's complement notation + var num = this.read(idx, 4); + return (num > 2147483647 ? num - 4294967296 : num); + }, + + + CHAR: function(idx) { + return String.fromCharCode(this.read(idx, 1)); + }, + + + STRING: function(idx, count) { + return this.asArray('CHAR', idx, count).join(''); + }, + + + asArray: function(type, idx, count) { + var values = []; + + for (var i = 0; i < count; i++) { + values[i] = this[type](idx + i); + } + return values; + } + }); + + + function ArrayBufferReader(data) { + var _dv = new DataView(data); + + Basic.extend(this, { + + readByteAt: function(idx) { + return _dv.getUint8(idx); + }, + + + writeByteAt: function(idx, value) { + _dv.setUint8(idx, value); + }, + + + SEGMENT: function(idx, size, value) { + switch (arguments.length) { + case 2: + return data.slice(idx, idx + size); + + case 1: + return data.slice(idx); + + case 3: + if (value === null) { + value = new ArrayBuffer(); + } + + if (value instanceof ArrayBuffer) { + var arr = new Uint8Array(this.length() - size + value.byteLength); + if (idx > 0) { + arr.set(new Uint8Array(data.slice(0, idx)), 0); + } + arr.set(new Uint8Array(value), idx); + arr.set(new Uint8Array(data.slice(idx + size)), idx + value.byteLength); + + this.clear(); + data = arr.buffer; + _dv = new DataView(data); + break; + } + + default: return data; + } + }, + + + length: function() { + return data ? data.byteLength : 0; + }, + + + clear: function() { + _dv = data = null; + } + }); + } + + + function UTF16StringReader(data) { + Basic.extend(this, { + + readByteAt: function(idx) { + return data.charCodeAt(idx); + }, + + + writeByteAt: function(idx, value) { + putstr(String.fromCharCode(value), idx, 1); + }, + + + SEGMENT: function(idx, length, segment) { + switch (arguments.length) { + case 1: + return data.substr(idx); + case 2: + return data.substr(idx, length); + case 3: + putstr(segment !== null ? segment : '', idx, length); + break; + default: return data; + } + }, + + + length: function() { + return data ? data.length : 0; + }, + + clear: function() { + data = null; + } + }); + + + function putstr(segment, idx, length) { + length = arguments.length === 3 ? length : data.length - idx - 1; + data = data.substr(0, idx) + segment + data.substr(length + idx); + } + } + + + return BinaryReader; +}); + +// Included from: src/javascript/runtime/html5/image/JPEGHeaders.js + +/** + * JPEGHeaders.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/html5/image/JPEGHeaders +@private +*/ +define("moxie/runtime/html5/image/JPEGHeaders", [ + "moxie/runtime/html5/utils/BinaryReader", + "moxie/core/Exceptions" +], function(BinaryReader, x) { + + return function JPEGHeaders(data) { + var headers = [], _br, idx, marker, length = 0; + + _br = new BinaryReader(data); + + // Check if data is jpeg + if (_br.SHORT(0) !== 0xFFD8) { + _br.clear(); + throw new x.ImageError(x.ImageError.WRONG_FORMAT); + } + + idx = 2; + + while (idx <= _br.length()) { + marker = _br.SHORT(idx); + + // omit RST (restart) markers + if (marker >= 0xFFD0 && marker <= 0xFFD7) { + idx += 2; + continue; + } + + // no headers allowed after SOS marker + if (marker === 0xFFDA || marker === 0xFFD9) { + break; + } + + length = _br.SHORT(idx + 2) + 2; + + // APPn marker detected + if (marker >= 0xFFE1 && marker <= 0xFFEF) { + headers.push({ + hex: marker, + name: 'APP' + (marker & 0x000F), + start: idx, + length: length, + segment: _br.SEGMENT(idx, length) + }); + } + + idx += length; + } + + _br.clear(); + + return { + headers: headers, + + restore: function(data) { + var max, i, br; + + br = new BinaryReader(data); + + idx = br.SHORT(2) == 0xFFE0 ? 4 + br.SHORT(4) : 2; + + for (i = 0, max = headers.length; i < max; i++) { + br.SEGMENT(idx, 0, headers[i].segment); + idx += headers[i].length; + } + + data = br.SEGMENT(); + br.clear(); + return data; + }, + + strip: function(data) { + var br, headers, jpegHeaders, i; + + jpegHeaders = new JPEGHeaders(data); + headers = jpegHeaders.headers; + jpegHeaders.purge(); + + br = new BinaryReader(data); + + i = headers.length; + while (i--) { + br.SEGMENT(headers[i].start, headers[i].length, ''); + } + + data = br.SEGMENT(); + br.clear(); + return data; + }, + + get: function(name) { + var array = []; + + for (var i = 0, max = headers.length; i < max; i++) { + if (headers[i].name === name.toUpperCase()) { + array.push(headers[i].segment); + } + } + return array; + }, + + set: function(name, segment) { + var array = [], i, ii, max; + + if (typeof(segment) === 'string') { + array.push(segment); + } else { + array = segment; + } + + for (i = ii = 0, max = headers.length; i < max; i++) { + if (headers[i].name === name.toUpperCase()) { + headers[i].segment = array[ii]; + headers[i].length = array[ii].length; + ii++; + } + if (ii >= array.length) { + break; + } + } + }, + + purge: function() { + this.headers = headers = []; + } + }; + }; +}); + +// Included from: src/javascript/runtime/html5/image/ExifParser.js + +/** + * ExifParser.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/html5/image/ExifParser +@private +*/ +define("moxie/runtime/html5/image/ExifParser", [ + "moxie/core/utils/Basic", + "moxie/runtime/html5/utils/BinaryReader", + "moxie/core/Exceptions" +], function(Basic, BinaryReader, x) { + + function ExifParser(data) { + var __super__, tags, tagDescs, offsets, idx, Tiff; + + BinaryReader.call(this, data); + + tags = { + tiff: { + /* + The image orientation viewed in terms of rows and columns. + + 1 = The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side. + 2 = The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side. + 3 = The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side. + 4 = The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side. + 5 = The 0th row is the visual left-hand side of the image, and the 0th column is the visual top. + 6 = The 0th row is the visual right-hand side of the image, and the 0th column is the visual top. + 7 = The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom. + 8 = The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom. + */ + 0x0112: 'Orientation', + 0x010E: 'ImageDescription', + 0x010F: 'Make', + 0x0110: 'Model', + 0x0131: 'Software', + 0x8769: 'ExifIFDPointer', + 0x8825: 'GPSInfoIFDPointer' + }, + exif: { + 0x9000: 'ExifVersion', + 0xA001: 'ColorSpace', + 0xA002: 'PixelXDimension', + 0xA003: 'PixelYDimension', + 0x9003: 'DateTimeOriginal', + 0x829A: 'ExposureTime', + 0x829D: 'FNumber', + 0x8827: 'ISOSpeedRatings', + 0x9201: 'ShutterSpeedValue', + 0x9202: 'ApertureValue' , + 0x9207: 'MeteringMode', + 0x9208: 'LightSource', + 0x9209: 'Flash', + 0x920A: 'FocalLength', + 0xA402: 'ExposureMode', + 0xA403: 'WhiteBalance', + 0xA406: 'SceneCaptureType', + 0xA404: 'DigitalZoomRatio', + 0xA408: 'Contrast', + 0xA409: 'Saturation', + 0xA40A: 'Sharpness' + }, + gps: { + 0x0000: 'GPSVersionID', + 0x0001: 'GPSLatitudeRef', + 0x0002: 'GPSLatitude', + 0x0003: 'GPSLongitudeRef', + 0x0004: 'GPSLongitude' + }, + + thumb: { + 0x0201: 'JPEGInterchangeFormat', + 0x0202: 'JPEGInterchangeFormatLength' + } + }; + + tagDescs = { + 'ColorSpace': { + 1: 'sRGB', + 0: 'Uncalibrated' + }, + + 'MeteringMode': { + 0: 'Unknown', + 1: 'Average', + 2: 'CenterWeightedAverage', + 3: 'Spot', + 4: 'MultiSpot', + 5: 'Pattern', + 6: 'Partial', + 255: 'Other' + }, + + 'LightSource': { + 1: 'Daylight', + 2: 'Fliorescent', + 3: 'Tungsten', + 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': { + 0x0000: 'Flash did not fire', + 0x0001: 'Flash fired', + 0x0005: 'Strobe return light not detected', + 0x0007: 'Strobe return light detected', + 0x0009: 'Flash fired, compulsory flash mode', + 0x000D: 'Flash fired, compulsory flash mode, return light not detected', + 0x000F: 'Flash fired, compulsory flash mode, return light detected', + 0x0010: 'Flash did not fire, compulsory flash mode', + 0x0018: 'Flash did not fire, auto mode', + 0x0019: 'Flash fired, auto mode', + 0x001D: 'Flash fired, auto mode, return light not detected', + 0x001F: 'Flash fired, auto mode, return light detected', + 0x0020: 'No flash function', + 0x0041: 'Flash fired, red-eye reduction mode', + 0x0045: 'Flash fired, red-eye reduction mode, return light not detected', + 0x0047: 'Flash fired, red-eye reduction mode, return light detected', + 0x0049: 'Flash fired, compulsory flash mode, red-eye reduction mode', + 0x004D: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected', + 0x004F: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light detected', + 0x0059: 'Flash fired, auto mode, red-eye reduction mode', + 0x005D: 'Flash fired, auto mode, return light not detected, red-eye reduction mode', + 0x005F: 'Flash fired, auto mode, return light detected, red-eye reduction mode' + }, + + 'ExposureMode': { + 0: 'Auto exposure', + 1: 'Manual exposure', + 2: 'Auto bracket' + }, + + 'WhiteBalance': { + 0: 'Auto white balance', + 1: 'Manual white balance' + }, + + 'SceneCaptureType': { + 0: 'Standard', + 1: 'Landscape', + 2: 'Portrait', + 3: 'Night scene' + }, + + 'Contrast': { + 0: 'Normal', + 1: 'Soft', + 2: 'Hard' + }, + + 'Saturation': { + 0: 'Normal', + 1: 'Low saturation', + 2: 'High saturation' + }, + + 'Sharpness': { + 0: 'Normal', + 1: 'Soft', + 2: 'Hard' + }, + + // GPS related + 'GPSLatitudeRef': { + N: 'North latitude', + S: 'South latitude' + }, + + 'GPSLongitudeRef': { + E: 'East longitude', + W: 'West longitude' + } + }; + + offsets = { + tiffHeader: 10 + }; + + idx = offsets.tiffHeader; + + __super__ = { + clear: this.clear + }; + + // Public functions + Basic.extend(this, { + + read: function() { + try { + return ExifParser.prototype.read.apply(this, arguments); + } catch (ex) { + throw new x.ImageError(x.ImageError.INVALID_META_ERR); + } + }, + + + write: function() { + try { + return ExifParser.prototype.write.apply(this, arguments); + } catch (ex) { + throw new x.ImageError(x.ImageError.INVALID_META_ERR); + } + }, + + + UNDEFINED: function() { + return this.BYTE.apply(this, arguments); + }, + + + RATIONAL: function(idx) { + return this.LONG(idx) / this.LONG(idx + 4) + }, + + + SRATIONAL: function(idx) { + return this.SLONG(idx) / this.SLONG(idx + 4) + }, + + ASCII: function(idx) { + return this.CHAR(idx); + }, + + TIFF: function() { + return Tiff || null; + }, + + + EXIF: function() { + var Exif = null; + + if (offsets.exifIFD) { + try { + Exif = extractTags.call(this, offsets.exifIFD, tags.exif); + } catch(ex) { + return null; + } + + // Fix formatting of some tags + if (Exif.ExifVersion && Basic.typeOf(Exif.ExifVersion) === 'array') { + for (var i = 0, exifVersion = ''; i < Exif.ExifVersion.length; i++) { + exifVersion += String.fromCharCode(Exif.ExifVersion[i]); + } + Exif.ExifVersion = exifVersion; + } + } + + return Exif; + }, + + + GPS: function() { + var GPS = null; + + if (offsets.gpsIFD) { + try { + GPS = extractTags.call(this, offsets.gpsIFD, tags.gps); + } catch (ex) { + return null; + } + + // iOS devices (and probably some others) do not put in GPSVersionID tag (why?..) + if (GPS.GPSVersionID && Basic.typeOf(GPS.GPSVersionID) === 'array') { + GPS.GPSVersionID = GPS.GPSVersionID.join('.'); + } + } + + return GPS; + }, + + + thumb: function() { + if (offsets.IFD1) { + try { + var IFD1Tags = extractTags.call(this, offsets.IFD1, tags.thumb); + + if ('JPEGInterchangeFormat' in IFD1Tags) { + return this.SEGMENT(offsets.tiffHeader + IFD1Tags.JPEGInterchangeFormat, IFD1Tags.JPEGInterchangeFormatLength); + } + } catch (ex) {} + } + return null; + }, + + + setExif: function(tag, value) { + // Right now only setting of width/height is possible + if (tag !== 'PixelXDimension' && tag !== 'PixelYDimension') { return false; } + + return setTag.call(this, 'exif', tag, value); + }, + + + clear: function() { + __super__.clear(); + data = tags = tagDescs = Tiff = offsets = __super__ = null; + } + }); + + + // Check if that's APP1 and that it has EXIF + if (this.SHORT(0) !== 0xFFE1 || this.STRING(4, 5).toUpperCase() !== "EXIF\0") { + throw new x.ImageError(x.ImageError.INVALID_META_ERR); + } + + // Set read order of multi-byte data + this.littleEndian = (this.SHORT(idx) == 0x4949); + + // Check if always present bytes are indeed present + if (this.SHORT(idx+=2) !== 0x002A) { + throw new x.ImageError(x.ImageError.INVALID_META_ERR); + } + + offsets.IFD0 = offsets.tiffHeader + this.LONG(idx += 2); + Tiff = extractTags.call(this, offsets.IFD0, tags.tiff); + + if ('ExifIFDPointer' in Tiff) { + offsets.exifIFD = offsets.tiffHeader + Tiff.ExifIFDPointer; + delete Tiff.ExifIFDPointer; + } + + if ('GPSInfoIFDPointer' in Tiff) { + offsets.gpsIFD = offsets.tiffHeader + Tiff.GPSInfoIFDPointer; + delete Tiff.GPSInfoIFDPointer; + } + + if (Basic.isEmptyObj(Tiff)) { + Tiff = null; + } + + // check if we have a thumb as well + var IFD1Offset = this.LONG(offsets.IFD0 + this.SHORT(offsets.IFD0) * 12 + 2); + if (IFD1Offset) { + offsets.IFD1 = offsets.tiffHeader + IFD1Offset; + } + + + function extractTags(IFD_offset, tags2extract) { + var data = this; + var length, i, tag, type, count, size, offset, value, values = [], hash = {}; + + var types = { + 1 : 'BYTE', + 7 : 'UNDEFINED', + 2 : 'ASCII', + 3 : 'SHORT', + 4 : 'LONG', + 5 : 'RATIONAL', + 9 : 'SLONG', + 10: 'SRATIONAL' + }; + + var sizes = { + 'BYTE' : 1, + 'UNDEFINED' : 1, + 'ASCII' : 1, + 'SHORT' : 2, + 'LONG' : 4, + 'RATIONAL' : 8, + 'SLONG' : 4, + 'SRATIONAL' : 8 + }; + + length = data.SHORT(IFD_offset); + + // The size of APP1 including all these elements shall not exceed the 64 Kbytes specified in the JPEG standard. + + for (i = 0; i < length; i++) { + values = []; + + // Set binary reader pointer to beginning of the next tag + offset = IFD_offset + 2 + i*12; + + tag = tags2extract[data.SHORT(offset)]; + + if (tag === undefined) { + continue; // Not the tag we requested + } + + type = types[data.SHORT(offset+=2)]; + count = data.LONG(offset+=2); + size = sizes[type]; + + if (!size) { + throw new x.ImageError(x.ImageError.INVALID_META_ERR); + } + + offset += 4; + + // tag can only fit 4 bytes of data, if data is larger we should look outside + if (size * count > 4) { + // instead of data tag contains an offset of the data + offset = data.LONG(offset) + offsets.tiffHeader; + } + + // in case we left the boundaries of data throw an early exception + if (offset + size * count >= this.length()) { + throw new x.ImageError(x.ImageError.INVALID_META_ERR); + } + + // special care for the string + if (type === 'ASCII') { + hash[tag] = Basic.trim(data.STRING(offset, count).replace(/\0$/, '')); // strip trailing NULL + continue; + } else { + values = data.asArray(type, offset, count); + value = (count == 1 ? values[0] : values); + + if (tagDescs.hasOwnProperty(tag) && typeof value != 'object') { + hash[tag] = tagDescs[tag][value]; + } else { + hash[tag] = value; + } + } + } + + return hash; + } + + // At the moment only setting of simple (LONG) values, that do not require offset recalculation, is supported + function setTag(ifd, tag, value) { + var offset, length, tagOffset, valueOffset = 0; + + // If tag name passed translate into hex key + if (typeof(tag) === 'string') { + var tmpTags = tags[ifd.toLowerCase()]; + for (var hex in tmpTags) { + if (tmpTags[hex] === tag) { + tag = hex; + break; + } + } + } + offset = offsets[ifd.toLowerCase() + 'IFD']; + length = this.SHORT(offset); + + for (var i = 0; i < length; i++) { + tagOffset = offset + 12 * i + 2; + + if (this.SHORT(tagOffset) == tag) { + valueOffset = tagOffset + 8; + break; + } + } + + if (!valueOffset) { + return false; + } + + try { + this.write(valueOffset, value, 4); + } catch(ex) { + return false; + } + + return true; + } + } + + ExifParser.prototype = BinaryReader.prototype; + + return ExifParser; +}); + +// Included from: src/javascript/runtime/html5/image/JPEG.js + +/** + * JPEG.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/html5/image/JPEG +@private +*/ +define("moxie/runtime/html5/image/JPEG", [ + "moxie/core/utils/Basic", + "moxie/core/Exceptions", + "moxie/runtime/html5/image/JPEGHeaders", + "moxie/runtime/html5/utils/BinaryReader", + "moxie/runtime/html5/image/ExifParser" +], function(Basic, x, JPEGHeaders, BinaryReader, ExifParser) { + + function JPEG(data) { + var _br, _hm, _ep, _info; + + _br = new BinaryReader(data); + + // check if it is jpeg + if (_br.SHORT(0) !== 0xFFD8) { + throw new x.ImageError(x.ImageError.WRONG_FORMAT); + } + + // backup headers + _hm = new JPEGHeaders(data); + + // extract exif info + try { + _ep = new ExifParser(_hm.get('app1')[0]); + } catch(ex) {} + + // get dimensions + _info = _getDimensions.call(this); + + Basic.extend(this, { + type: 'image/jpeg', + + size: _br.length(), + + width: _info && _info.width || 0, + + height: _info && _info.height || 0, + + setExif: function(tag, value) { + if (!_ep) { + return false; // or throw an exception + } + + if (Basic.typeOf(tag) === 'object') { + Basic.each(tag, function(value, tag) { + _ep.setExif(tag, value); + }); + } else { + _ep.setExif(tag, value); + } + + // update internal headers + _hm.set('app1', _ep.SEGMENT()); + }, + + writeHeaders: function() { + if (!arguments.length) { + // if no arguments passed, update headers internally + return _hm.restore(data); + } + return _hm.restore(arguments[0]); + }, + + stripHeaders: function(data) { + return _hm.strip(data); + }, + + purge: function() { + _purge.call(this); + } + }); + + if (_ep) { + this.meta = { + tiff: _ep.TIFF(), + exif: _ep.EXIF(), + gps: _ep.GPS(), + thumb: _getThumb() + }; + } + + + function _getDimensions(br) { + var idx = 0 + , marker + , length + ; + + if (!br) { + br = _br; + } + + // examine all through the end, since some images might have very large APP segments + while (idx <= br.length()) { + marker = br.SHORT(idx += 2); + + if (marker >= 0xFFC0 && marker <= 0xFFC3) { // SOFn + idx += 5; // marker (2 bytes) + length (2 bytes) + Sample precision (1 byte) + return { + height: br.SHORT(idx), + width: br.SHORT(idx += 2) + }; + } + length = br.SHORT(idx += 2); + idx += length - 2; + } + return null; + } + + + function _getThumb() { + var data = _ep.thumb() + , br + , info + ; + + if (data) { + br = new BinaryReader(data); + info = _getDimensions(br); + br.clear(); + + if (info) { + info.data = data; + return info; + } + } + return null; + } + + + function _purge() { + if (!_ep || !_hm || !_br) { + return; // ignore any repeating purge requests + } + _ep.clear(); + _hm.purge(); + _br.clear(); + _info = _hm = _ep = _br = null; + } + } + + return JPEG; +}); + +// Included from: src/javascript/runtime/html5/image/PNG.js + +/** + * PNG.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/html5/image/PNG +@private +*/ +define("moxie/runtime/html5/image/PNG", [ + "moxie/core/Exceptions", + "moxie/core/utils/Basic", + "moxie/runtime/html5/utils/BinaryReader" +], function(x, Basic, BinaryReader) { + + function PNG(data) { + var _br, _hm, _ep, _info; + + _br = new BinaryReader(data); + + // check if it's png + (function() { + var idx = 0, i = 0 + , signature = [0x8950, 0x4E47, 0x0D0A, 0x1A0A] + ; + + for (i = 0; i < signature.length; i++, idx += 2) { + if (signature[i] != _br.SHORT(idx)) { + throw new x.ImageError(x.ImageError.WRONG_FORMAT); + } + } + }()); + + function _getDimensions() { + var chunk, idx; + + chunk = _getChunkAt.call(this, 8); + + if (chunk.type == 'IHDR') { + idx = chunk.start; + return { + width: _br.LONG(idx), + height: _br.LONG(idx += 4) + }; + } + return null; + } + + function _purge() { + if (!_br) { + return; // ignore any repeating purge requests + } + _br.clear(); + data = _info = _hm = _ep = _br = null; + } + + _info = _getDimensions.call(this); + + Basic.extend(this, { + type: 'image/png', + + size: _br.length(), + + width: _info.width, + + height: _info.height, + + purge: function() { + _purge.call(this); + } + }); + + // for PNG we can safely trigger purge automatically, as we do not keep any data for later + _purge.call(this); + + function _getChunkAt(idx) { + var length, type, start, CRC; + + length = _br.LONG(idx); + type = _br.STRING(idx += 4, 4); + start = idx += 4; + CRC = _br.LONG(idx + length); + + return { + length: length, + type: type, + start: start, + CRC: CRC + }; + } + } + + return PNG; +}); + +// Included from: src/javascript/runtime/html5/image/ImageInfo.js + +/** + * ImageInfo.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/html5/image/ImageInfo +@private +*/ +define("moxie/runtime/html5/image/ImageInfo", [ + "moxie/core/utils/Basic", + "moxie/core/Exceptions", + "moxie/runtime/html5/image/JPEG", + "moxie/runtime/html5/image/PNG" +], function(Basic, x, JPEG, PNG) { + /** + Optional image investigation tool for HTML5 runtime. Provides the following features: + - ability to distinguish image type (JPEG or PNG) by signature + - ability to extract image width/height directly from it's internals, without preloading in memory (fast) + - ability to extract APP headers from JPEGs (Exif, GPS, etc) + - ability to replace width/height tags in extracted JPEG headers + - ability to restore APP headers, that were for example stripped during image manipulation + + @class ImageInfo + @constructor + @param {String} data Image source as binary string + */ + return function(data) { + var _cs = [JPEG, PNG], _img; + + // figure out the format, throw: ImageError.WRONG_FORMAT if not supported + _img = (function() { + for (var i = 0; i < _cs.length; i++) { + try { + return new _cs[i](data); + } catch (ex) { + // console.info(ex); + } + } + throw new x.ImageError(x.ImageError.WRONG_FORMAT); + }()); + + Basic.extend(this, { + /** + Image Mime Type extracted from it's depths + + @property type + @type {String} + @default '' + */ + type: '', + + /** + Image size in bytes + + @property size + @type {Number} + @default 0 + */ + size: 0, + + /** + Image width extracted from image source + + @property width + @type {Number} + @default 0 + */ + width: 0, + + /** + Image height extracted from image source + + @property height + @type {Number} + @default 0 + */ + height: 0, + + /** + Sets Exif tag. Currently applicable only for width and height tags. Obviously works only with JPEGs. + + @method setExif + @param {String} tag Tag to set + @param {Mixed} value Value to assign to the tag + */ + setExif: function() {}, + + /** + Restores headers to the source. + + @method writeHeaders + @param {String} data Image source as binary string + @return {String} Updated binary string + */ + writeHeaders: function(data) { + return data; + }, + + /** + Strip all headers from the source. + + @method stripHeaders + @param {String} data Image source as binary string + @return {String} Updated binary string + */ + stripHeaders: function(data) { + return data; + }, + + /** + Dispose resources. + + @method purge + */ + purge: function() { + data = null; + } + }); + + Basic.extend(this, _img); + + this.purge = function() { + _img.purge(); + _img = null; + }; + }; +}); + +// Included from: src/javascript/runtime/html5/image/ResizerCanvas.js + +/** + * ResizerCanvas.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** + * Resizes image/canvas using canvas + */ +define("moxie/runtime/html5/image/ResizerCanvas", [], function() { + + function scale(image, ratio) { + var sW = image.width; + var dW = Math.floor(sW * ratio); + var scaleCapped = false; + + if (ratio < 0.5 || ratio > 2) { + ratio = ratio < 0.5 ? 0.5 : 2; + scaleCapped = true; + } + + var tCanvas = _scale(image, ratio); + + if (scaleCapped) { + return scale(tCanvas, dW / tCanvas.width); + } else { + return tCanvas; + } + } + + + function _scale(image, ratio) { + var sW = image.width; + var sH = image.height; + var dW = Math.floor(sW * ratio); + var dH = Math.floor(sH * ratio); + + var canvas = document.createElement('canvas'); + canvas.width = dW; + canvas.height = dH; + canvas.getContext("2d").drawImage(image, 0, 0, sW, sH, 0, 0, dW, dH); + + image = null; // just in case + return canvas; + } + + return { + scale: scale + }; + +}); + +// Included from: src/javascript/runtime/html5/image/Image.js + +/** + * Image.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/html5/image/Image +@private +*/ +define("moxie/runtime/html5/image/Image", [ + "moxie/runtime/html5/Runtime", + "moxie/core/utils/Basic", + "moxie/core/Exceptions", + "moxie/core/utils/Encode", + "moxie/file/Blob", + "moxie/file/File", + "moxie/runtime/html5/image/ImageInfo", + "moxie/runtime/html5/image/ResizerCanvas", + "moxie/core/utils/Mime", + "moxie/core/utils/Env" +], function(extensions, Basic, x, Encode, Blob, File, ImageInfo, ResizerCanvas, Mime, Env) { + + function HTML5Image() { + var me = this + , _img, _imgInfo, _canvas, _binStr, _blob + , _modified = false // is set true whenever image is modified + , _preserveHeaders = true + ; + + Basic.extend(this, { + loadFromBlob: function(blob) { + var I = this.getRuntime() + , asBinary = arguments.length > 1 ? arguments[1] : true + ; + + if (!I.can('access_binary')) { + throw new x.RuntimeError(x.RuntimeError.NOT_SUPPORTED_ERR); + } + + _blob = blob; + + if (blob.isDetached()) { + _binStr = blob.getSource(); + _preload.call(this, _binStr); + return; + } else { + _readAsDataUrl.call(this, blob.getSource(), function(dataUrl) { + if (asBinary) { + _binStr = _toBinary(dataUrl); + } + _preload.call(this, dataUrl); + }); + } + }, + + loadFromImage: function(img, exact) { + this.meta = img.meta; + + _blob = new File(null, { + name: img.name, + size: img.size, + type: img.type + }); + + _preload.call(this, exact ? (_binStr = img.getAsBinaryString()) : img.getAsDataURL()); + }, + + getInfo: function() { + var I = this.getRuntime(), info; + + if (!_imgInfo && _binStr && I.can('access_image_binary')) { + _imgInfo = new ImageInfo(_binStr); + } + + // this stuff below is definitely having fun with itself + info = { + width: _getImg().width || 0, + height: _getImg().height || 0, + type: _blob.type || Mime.getFileMime(_blob.name), + size: _binStr && _binStr.length || _blob.size || 0, + name: _blob.name || '', + meta: null + }; + + if (_preserveHeaders) { + info.meta = _imgInfo && _imgInfo.meta || this.meta || {}; + + // if data was taken from ImageInfo it will be a binary string, so we convert it to blob + if (info.meta && info.meta.thumb && !(info.meta.thumb.data instanceof Blob)) { + info.meta.thumb.data = new Blob(null, { + type: 'image/jpeg', + data: info.meta.thumb.data + }); + } + } + + return info; + }, + + + resize: function(rect, ratio, options) { + var canvas = document.createElement('canvas'); + canvas.width = rect.width; + canvas.height = rect.height; + + canvas.getContext("2d").drawImage(_getImg(), rect.x, rect.y, rect.width, rect.height, 0, 0, canvas.width, canvas.height); + + _canvas = ResizerCanvas.scale(canvas, ratio); + + _preserveHeaders = options.preserveHeaders; + + // rotate if required, according to orientation tag + if (!_preserveHeaders) { + var orientation = (this.meta && this.meta.tiff && this.meta.tiff.Orientation) || 1; + _canvas = _rotateToOrientaion(_canvas, orientation); + } + + this.width = _canvas.width; + this.height = _canvas.height; + + _modified = true; + + this.trigger('Resize'); + }, + + getAsCanvas: function() { + if (!_canvas) { + _canvas = _getCanvas(); + } + _canvas.id = this.uid + '_canvas'; + return _canvas; + }, + + getAsBlob: function(type, quality) { + if (type !== this.type) { + _modified = true; // reconsider the state + return new File(null, { + name: _blob.name || '', + type: type, + data: me.getAsDataURL(type, quality) + }); + } + return new File(null, { + name: _blob.name || '', + type: type, + data: me.getAsBinaryString(type, quality) + }); + }, + + getAsDataURL: function(type) { + var quality = arguments[1] || 90; + + // if image has not been modified, return the source right away + if (!_modified) { + return _img.src; + } + + // make sure we have a canvas to work with + _getCanvas(); + + if ('image/jpeg' !== type) { + return _canvas.toDataURL('image/png'); + } else { + try { + // older Geckos used to result in an exception on quality argument + return _canvas.toDataURL('image/jpeg', quality/100); + } catch (ex) { + return _canvas.toDataURL('image/jpeg'); + } + } + }, + + getAsBinaryString: function(type, quality) { + // if image has not been modified, return the source right away + if (!_modified) { + // if image was not loaded from binary string + if (!_binStr) { + _binStr = _toBinary(me.getAsDataURL(type, quality)); + } + return _binStr; + } + + if ('image/jpeg' !== type) { + _binStr = _toBinary(me.getAsDataURL(type, quality)); + } else { + var dataUrl; + + // if jpeg + if (!quality) { + quality = 90; + } + + // make sure we have a canvas to work with + _getCanvas(); + + try { + // older Geckos used to result in an exception on quality argument + dataUrl = _canvas.toDataURL('image/jpeg', quality/100); + } catch (ex) { + dataUrl = _canvas.toDataURL('image/jpeg'); + } + + _binStr = _toBinary(dataUrl); + + if (_imgInfo) { + _binStr = _imgInfo.stripHeaders(_binStr); + + if (_preserveHeaders) { + // update dimensions info in exif + if (_imgInfo.meta && _imgInfo.meta.exif) { + _imgInfo.setExif({ + PixelXDimension: this.width, + PixelYDimension: this.height + }); + } + + // re-inject the headers + _binStr = _imgInfo.writeHeaders(_binStr); + } + + // will be re-created from fresh on next getInfo call + _imgInfo.purge(); + _imgInfo = null; + } + } + + _modified = false; + + return _binStr; + }, + + destroy: function() { + me = null; + _purge.call(this); + this.getRuntime().getShim().removeInstance(this.uid); + } + }); + + + function _getImg() { + if (!_canvas && !_img) { + throw new x.ImageError(x.DOMException.INVALID_STATE_ERR); + } + return _canvas || _img; + } + + + function _getCanvas() { + var canvas = _getImg(); + if (canvas.nodeName.toLowerCase() == 'canvas') { + return canvas; + } + _canvas = document.createElement('canvas'); + _canvas.width = canvas.width; + _canvas.height = canvas.height; + _canvas.getContext("2d").drawImage(canvas, 0, 0); + return _canvas; + } + + + function _toBinary(str) { + return Encode.atob(str.substring(str.indexOf('base64,') + 7)); + } + + + function _toDataUrl(str, type) { + return 'data:' + (type || '') + ';base64,' + Encode.btoa(str); + } + + + function _preload(str) { + var comp = this; + + _img = new Image(); + _img.onerror = function() { + _purge.call(this); + comp.trigger('error', x.ImageError.WRONG_FORMAT); + }; + _img.onload = function() { + comp.trigger('load'); + }; + + _img.src = str.substr(0, 5) == 'data:' ? str : _toDataUrl(str, _blob.type); + } + + + function _readAsDataUrl(file, callback) { + var comp = this, fr; + + // use FileReader if it's available + if (window.FileReader) { + fr = new FileReader(); + fr.onload = function() { + callback.call(comp, this.result); + }; + fr.onerror = function() { + comp.trigger('error', x.ImageError.WRONG_FORMAT); + }; + fr.readAsDataURL(file); + } else { + return callback.call(this, file.getAsDataURL()); + } + } + + /** + * Transform canvas coordination according to specified frame size and orientation + * Orientation value is from EXIF tag + * @author Shinichi Tomita + */ + function _rotateToOrientaion(img, orientation) { + var RADIANS = Math.PI/180; + var canvas = document.createElement('canvas'); + var ctx = canvas.getContext('2d'); + var width = img.width; + var height = img.height; + + if (Basic.inArray(orientation, [5,6,7,8]) > -1) { + canvas.width = height; + canvas.height = width; + } else { + canvas.width = width; + canvas.height = height; + } + + /** + 1 = The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side. + 2 = The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side. + 3 = The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side. + 4 = The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side. + 5 = The 0th row is the visual left-hand side of the image, and the 0th column is the visual top. + 6 = The 0th row is the visual right-hand side of the image, and the 0th column is the visual top. + 7 = The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom. + 8 = The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom. + */ + switch (orientation) { + case 2: + // horizontal flip + ctx.translate(width, 0); + ctx.scale(-1, 1); + break; + case 3: + // 180 rotate left + ctx.translate(width, height); + ctx.rotate(180 * RADIANS); + break; + case 4: + // vertical flip + ctx.translate(0, height); + ctx.scale(1, -1); + break; + case 5: + // vertical flip + 90 rotate right + ctx.rotate(90 * RADIANS); + ctx.scale(1, -1); + break; + case 6: + // 90 rotate right + ctx.rotate(90 * RADIANS); + ctx.translate(0, -height); + break; + case 7: + // horizontal flip + 90 rotate right + ctx.rotate(90 * RADIANS); + ctx.translate(width, -height); + ctx.scale(-1, 1); + break; + case 8: + // 90 rotate left + ctx.rotate(-90 * RADIANS); + ctx.translate(-width, 0); + break; + } + + ctx.drawImage(img, 0, 0, width, height); + return canvas; + } + + + function _purge() { + if (_imgInfo) { + _imgInfo.purge(); + _imgInfo = null; + } + + _binStr = _img = _canvas = _blob = null; + _modified = false; + } + } + + return (extensions.Image = HTML5Image); +}); + +// Included from: src/javascript/runtime/flash/Runtime.js + +/** + * Runtime.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/*global ActiveXObject:true */ + +/** +Defines constructor for Flash runtime. + +@class moxie/runtime/flash/Runtime +@private +*/ +define("moxie/runtime/flash/Runtime", [ + "moxie/core/utils/Basic", + "moxie/core/utils/Env", + "moxie/core/utils/Dom", + "moxie/core/Exceptions", + "moxie/runtime/Runtime" +], function(Basic, Env, Dom, x, Runtime) { + + var type = 'flash', extensions = {}; + + /** + Get the version of the Flash Player + + @method getShimVersion + @private + @return {Number} Flash Player version + */ + function getShimVersion() { + var version; + + try { + version = navigator.plugins['Shockwave Flash']; + version = version.description; + } catch (e1) { + try { + version = new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version'); + } catch (e2) { + version = '0.0'; + } + } + version = version.match(/\d+/g); + return parseFloat(version[0] + '.' + version[1]); + } + + + /** + Cross-browser SWF removal + - Especially needed to safely and completely remove a SWF in Internet Explorer + + Originated from SWFObject v2.2 + */ + function removeSWF(id) { + var obj = Dom.get(id); + if (obj && obj.nodeName == "OBJECT") { + if (Env.browser === 'IE') { + obj.style.display = "none"; + (function onInit(){ + // http://msdn.microsoft.com/en-us/library/ie/ms534360(v=vs.85).aspx + if (obj.readyState == 4) { + removeObjectInIE(id); + } + else { + setTimeout(onInit, 10); + } + })(); + } + else { + obj.parentNode.removeChild(obj); + } + } + } + + + function removeObjectInIE(id) { + var obj = Dom.get(id); + if (obj) { + for (var i in obj) { + if (typeof obj[i] == "function") { + obj[i] = null; + } + } + obj.parentNode.removeChild(obj); + } + } + + /** + Constructor for the Flash Runtime + + @class FlashRuntime + @extends Runtime + */ + function FlashRuntime(options) { + var I = this, initTimer; + + options = Basic.extend({ swf_url: Env.swf_url }, options); + + Runtime.call(this, options, type, { + access_binary: function(value) { + return value && I.mode === 'browser'; + }, + access_image_binary: function(value) { + return value && I.mode === 'browser'; + }, + display_media: Runtime.capTest(defined('moxie/image/Image')), + do_cors: Runtime.capTrue, + drag_and_drop: false, + report_upload_progress: function() { + return I.mode === 'client'; + }, + resize_image: Runtime.capTrue, + return_response_headers: false, + return_response_type: function(responseType) { + if (responseType === 'json' && !!window.JSON) { + return true; + } + return !Basic.arrayDiff(responseType, ['', 'text', 'document']) || I.mode === 'browser'; + }, + return_status_code: function(code) { + return I.mode === 'browser' || !Basic.arrayDiff(code, [200, 404]); + }, + select_file: Runtime.capTrue, + select_multiple: Runtime.capTrue, + send_binary_string: function(value) { + return value && I.mode === 'browser'; + }, + send_browser_cookies: function(value) { + return value && I.mode === 'browser'; + }, + send_custom_headers: function(value) { + return value && I.mode === 'browser'; + }, + send_multipart: Runtime.capTrue, + slice_blob: function(value) { + return value && I.mode === 'browser'; + }, + stream_upload: function(value) { + return value && I.mode === 'browser'; + }, + summon_file_dialog: false, + upload_filesize: function(size) { + return Basic.parseSizeStr(size) <= 2097152 || I.mode === 'client'; + }, + use_http_method: function(methods) { + return !Basic.arrayDiff(methods, ['GET', 'POST']); + } + }, { + // capabilities that require specific mode + access_binary: function(value) { + return value ? 'browser' : 'client'; + }, + access_image_binary: function(value) { + return value ? 'browser' : 'client'; + }, + report_upload_progress: function(value) { + return value ? 'browser' : 'client'; + }, + return_response_type: function(responseType) { + return Basic.arrayDiff(responseType, ['', 'text', 'json', 'document']) ? 'browser' : ['client', 'browser']; + }, + return_status_code: function(code) { + return Basic.arrayDiff(code, [200, 404]) ? 'browser' : ['client', 'browser']; + }, + send_binary_string: function(value) { + return value ? 'browser' : 'client'; + }, + send_browser_cookies: function(value) { + return value ? 'browser' : 'client'; + }, + send_custom_headers: function(value) { + return value ? 'browser' : 'client'; + }, + slice_blob: function(value) { + return value ? 'browser' : 'client'; + }, + stream_upload: function(value) { + return value ? 'client' : 'browser'; + }, + upload_filesize: function(size) { + return Basic.parseSizeStr(size) >= 2097152 ? 'client' : 'browser'; + } + }, 'client'); + + + // minimal requirement for Flash Player version + if (getShimVersion() < 11.3) { + if (MXI_DEBUG && Env.debug.runtime) { + Env.log("\tFlash didn't meet minimal version requirement (11.3)."); + } + + this.mode = false; // with falsy mode, runtime won't operable, no matter what the mode was before + } + + + Basic.extend(this, { + + getShim: function() { + return Dom.get(this.uid); + }, + + shimExec: function(component, action) { + var args = [].slice.call(arguments, 2); + return I.getShim().exec(this.uid, component, action, args); + }, + + init: function() { + var html, el, container; + + container = this.getShimContainer(); + + // if not the minimal height, shims are not initialized in older browsers (e.g FF3.6, IE6,7,8, Safari 4.0,5.0, etc) + Basic.extend(container.style, { + position: 'absolute', + top: '-8px', + left: '-8px', + width: '9px', + height: '9px', + overflow: 'hidden' + }); + + // insert flash object + html = '' + + '' + + '' + + '' + + ''; + + if (Env.browser === 'IE') { + el = document.createElement('div'); + container.appendChild(el); + el.outerHTML = html; + el = container = null; // just in case + } else { + container.innerHTML = html; + } + + // Init is dispatched by the shim + initTimer = setTimeout(function() { + if (I && !I.initialized) { // runtime might be already destroyed by this moment + I.trigger("Error", new x.RuntimeError(x.RuntimeError.NOT_INIT_ERR)); + + if (MXI_DEBUG && Env.debug.runtime) { + Env.log("\tFlash failed to initialize within a specified period of time (typically 5s)."); + } + } + }, 5000); + }, + + destroy: (function(destroy) { // extend default destroy method + return function() { + removeSWF(I.uid); // SWF removal requires special care in IE + + destroy.call(I); + clearTimeout(initTimer); // initialization check might be still onwait + options = initTimer = destroy = I = null; + }; + }(this.destroy)) + + }, extensions); + } + + Runtime.addConstructor(type, FlashRuntime); + + return extensions; +}); + +// Included from: src/javascript/runtime/flash/file/Blob.js + +/** + * Blob.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/flash/file/Blob +@private +*/ +define("moxie/runtime/flash/file/Blob", [ + "moxie/runtime/flash/Runtime", + "moxie/file/Blob" +], function(extensions, Blob) { + + var FlashBlob = { + slice: function(blob, start, end, type) { + var self = this.getRuntime(); + + if (start < 0) { + start = Math.max(blob.size + start, 0); + } else if (start > 0) { + start = Math.min(start, blob.size); + } + + if (end < 0) { + end = Math.max(blob.size + end, 0); + } else if (end > 0) { + end = Math.min(end, blob.size); + } + + blob = self.shimExec.call(this, 'Blob', 'slice', start, end, type || ''); + + if (blob) { + blob = new Blob(self.uid, blob); + } + return blob; + } + }; + + return (extensions.Blob = FlashBlob); +}); + +// Included from: src/javascript/runtime/flash/file/FileInput.js + +/** + * FileInput.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/flash/file/FileInput +@private +*/ +define("moxie/runtime/flash/file/FileInput", [ + "moxie/runtime/flash/Runtime", + "moxie/file/File", + "moxie/core/utils/Basic" +], function(extensions, File, Basic) { + + var FileInput = { + init: function(options) { + var comp = this, I = this.getRuntime(); + + this.bind("Change", function() { + var files = I.shimExec.call(comp, 'FileInput', 'getFiles'); + comp.files = []; + Basic.each(files, function(file) { + comp.files.push(new File(I.uid, file)); + }); + }, 999); + + this.getRuntime().shimExec.call(this, 'FileInput', 'init', { + accept: options.accept, + multiple: options.multiple + }); + + this.trigger('ready'); + } + }; + + return (extensions.FileInput = FileInput); +}); + +// Included from: src/javascript/runtime/flash/file/FileReader.js + +/** + * FileReader.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/flash/file/FileReader +@private +*/ +define("moxie/runtime/flash/file/FileReader", [ + "moxie/runtime/flash/Runtime", + "moxie/core/utils/Encode" +], function(extensions, Encode) { + + function _formatData(data, op) { + switch (op) { + case 'readAsText': + return Encode.atob(data, 'utf8'); + case 'readAsBinaryString': + return Encode.atob(data); + case 'readAsDataURL': + return data; + } + return null; + } + + var FileReader = { + read: function(op, blob) { + var comp = this; + + comp.result = ''; + + // special prefix for DataURL read mode + if (op === 'readAsDataURL') { + comp.result = 'data:' + (blob.type || '') + ';base64,'; + } + + comp.bind('Progress', function(e, data) { + if (data) { + comp.result += _formatData(data, op); + } + }, 999); + + return comp.getRuntime().shimExec.call(this, 'FileReader', 'readAsBase64', blob.uid); + } + }; + + return (extensions.FileReader = FileReader); +}); + +// Included from: src/javascript/runtime/flash/file/FileReaderSync.js + +/** + * FileReaderSync.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/flash/file/FileReaderSync +@private +*/ +define("moxie/runtime/flash/file/FileReaderSync", [ + "moxie/runtime/flash/Runtime", + "moxie/core/utils/Encode" +], function(extensions, Encode) { + + function _formatData(data, op) { + switch (op) { + case 'readAsText': + return Encode.atob(data, 'utf8'); + case 'readAsBinaryString': + return Encode.atob(data); + case 'readAsDataURL': + return data; + } + return null; + } + + var FileReaderSync = { + read: function(op, blob) { + var result, self = this.getRuntime(); + + result = self.shimExec.call(this, 'FileReaderSync', 'readAsBase64', blob.uid); + if (!result) { + return null; // or throw ex + } + + // special prefix for DataURL read mode + if (op === 'readAsDataURL') { + result = 'data:' + (blob.type || '') + ';base64,' + result; + } + + return _formatData(result, op, blob.type); + } + }; + + return (extensions.FileReaderSync = FileReaderSync); +}); + +// Included from: src/javascript/runtime/flash/runtime/Transporter.js + +/** + * Transporter.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/flash/runtime/Transporter +@private +*/ +define("moxie/runtime/flash/runtime/Transporter", [ + "moxie/runtime/flash/Runtime", + "moxie/file/Blob" +], function(extensions, Blob) { + + var Transporter = { + getAsBlob: function(type) { + var self = this.getRuntime() + , blob = self.shimExec.call(this, 'Transporter', 'getAsBlob', type) + ; + if (blob) { + return new Blob(self.uid, blob); + } + return null; + } + }; + + return (extensions.Transporter = Transporter); +}); + +// Included from: src/javascript/runtime/flash/xhr/XMLHttpRequest.js + +/** + * XMLHttpRequest.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/flash/xhr/XMLHttpRequest +@private +*/ +define("moxie/runtime/flash/xhr/XMLHttpRequest", [ + "moxie/runtime/flash/Runtime", + "moxie/core/utils/Basic", + "moxie/file/Blob", + "moxie/file/File", + "moxie/file/FileReaderSync", + "moxie/runtime/flash/file/FileReaderSync", + "moxie/xhr/FormData", + "moxie/runtime/Transporter", + "moxie/runtime/flash/runtime/Transporter" +], function(extensions, Basic, Blob, File, FileReaderSync, FileReaderSyncFlash, FormData, Transporter, TransporterFlash) { + + var XMLHttpRequest = { + + send: function(meta, data) { + var target = this, self = target.getRuntime(); + + function send() { + meta.transport = self.mode; + self.shimExec.call(target, 'XMLHttpRequest', 'send', meta, data); + } + + + function appendBlob(name, blob) { + self.shimExec.call(target, 'XMLHttpRequest', 'appendBlob', name, blob.uid); + data = null; + send(); + } + + + function attachBlob(blob, cb) { + var tr = new Transporter(); + + tr.bind("TransportingComplete", function() { + cb(this.result); + }); + + tr.transport(blob.getSource(), blob.type, { + ruid: self.uid + }); + } + + // copy over the headers if any + if (!Basic.isEmptyObj(meta.headers)) { + Basic.each(meta.headers, function(value, header) { + self.shimExec.call(target, 'XMLHttpRequest', 'setRequestHeader', header, value.toString()); // Silverlight doesn't accept integers into the arguments of type object + }); + } + + // transfer over multipart params and blob itself + if (data instanceof FormData) { + var blobField; + data.each(function(value, name) { + if (value instanceof Blob) { + blobField = name; + } else { + self.shimExec.call(target, 'XMLHttpRequest', 'append', name, value); + } + }); + + if (!data.hasBlob()) { + data = null; + send(); + } else { + var blob = data.getBlob(); + if (blob.isDetached()) { + attachBlob(blob, function(attachedBlob) { + blob.destroy(); + appendBlob(blobField, attachedBlob); + }); + } else { + appendBlob(blobField, blob); + } + } + } else if (data instanceof Blob) { + if (data.isDetached()) { + attachBlob(data, function(attachedBlob) { + data.destroy(); + data = attachedBlob.uid; + send(); + }); + } else { + data = data.uid; + send(); + } + } else { + send(); + } + }, + + getResponse: function(responseType) { + var frs, blob, self = this.getRuntime(); + + blob = self.shimExec.call(this, 'XMLHttpRequest', 'getResponseAsBlob'); + + if (blob) { + blob = new File(self.uid, blob); + + if ('blob' === responseType) { + return blob; + } + + try { + frs = new FileReaderSync(); + + if (!!~Basic.inArray(responseType, ["", "text"])) { + return frs.readAsText(blob); + } else if ('json' === responseType && !!window.JSON) { + return JSON.parse(frs.readAsText(blob)); + } + } finally { + blob.destroy(); + } + } + return null; + }, + + abort: function(upload_complete_flag) { + var self = this.getRuntime(); + + self.shimExec.call(this, 'XMLHttpRequest', 'abort'); + + this.dispatchEvent('readystatechange'); + // this.dispatchEvent('progress'); + this.dispatchEvent('abort'); + + //if (!upload_complete_flag) { + // this.dispatchEvent('uploadprogress'); + //} + } + }; + + return (extensions.XMLHttpRequest = XMLHttpRequest); +}); + +// Included from: src/javascript/runtime/flash/image/Image.js + +/** + * Image.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/flash/image/Image +@private +*/ +define("moxie/runtime/flash/image/Image", [ + "moxie/runtime/flash/Runtime", + "moxie/core/utils/Basic", + "moxie/runtime/Transporter", + "moxie/file/Blob", + "moxie/file/FileReaderSync" +], function(extensions, Basic, Transporter, Blob, FileReaderSync) { + + var Image = { + loadFromBlob: function(blob) { + var comp = this, self = comp.getRuntime(); + + function exec(srcBlob) { + self.shimExec.call(comp, 'Image', 'loadFromBlob', srcBlob.uid); + comp = self = null; + } + + if (blob.isDetached()) { // binary string + var tr = new Transporter(); + tr.bind("TransportingComplete", function() { + exec(tr.result.getSource()); + }); + tr.transport(blob.getSource(), blob.type, { ruid: self.uid }); + } else { + exec(blob.getSource()); + } + }, + + loadFromImage: function(img) { + var self = this.getRuntime(); + return self.shimExec.call(this, 'Image', 'loadFromImage', img.uid); + }, + + getInfo: function() { + var self = this.getRuntime() + , info = self.shimExec.call(this, 'Image', 'getInfo') + ; + + if (info.meta && info.meta.thumb && info.meta.thumb.data && !(self.meta.thumb.data instanceof Blob)) { + info.meta.thumb.data = new Blob(self.uid, info.meta.thumb.data); + } + return info; + }, + + getAsBlob: function(type, quality) { + var self = this.getRuntime() + , blob = self.shimExec.call(this, 'Image', 'getAsBlob', type, quality) + ; + if (blob) { + return new Blob(self.uid, blob); + } + return null; + }, + + getAsDataURL: function() { + var self = this.getRuntime() + , blob = self.Image.getAsBlob.apply(this, arguments) + , frs + ; + if (!blob) { + return null; + } + frs = new FileReaderSync(); + return frs.readAsDataURL(blob); + } + }; + + return (extensions.Image = Image); +}); + +// Included from: src/javascript/runtime/silverlight/Runtime.js + +/** + * RunTime.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/*global ActiveXObject:true */ + +/** +Defines constructor for Silverlight runtime. + +@class moxie/runtime/silverlight/Runtime +@private +*/ +define("moxie/runtime/silverlight/Runtime", [ + "moxie/core/utils/Basic", + "moxie/core/utils/Env", + "moxie/core/utils/Dom", + "moxie/core/Exceptions", + "moxie/runtime/Runtime" +], function(Basic, Env, Dom, x, Runtime) { + + var type = "silverlight", extensions = {}; + + function isInstalled(version) { + var isVersionSupported = false, control = null, actualVer, + actualVerArray, reqVerArray, requiredVersionPart, actualVersionPart, index = 0; + + try { + try { + control = new ActiveXObject('AgControl.AgControl'); + + if (control.IsVersionSupported(version)) { + isVersionSupported = true; + } + + control = null; + } catch (e) { + var plugin = navigator.plugins["Silverlight Plug-In"]; + + if (plugin) { + actualVer = plugin.description; + + if (actualVer === "1.0.30226.2") { + actualVer = "2.0.30226.2"; + } + + actualVerArray = actualVer.split("."); + + while (actualVerArray.length > 3) { + actualVerArray.pop(); + } + + while ( actualVerArray.length < 4) { + actualVerArray.push(0); + } + + reqVerArray = version.split("."); + + while (reqVerArray.length > 4) { + reqVerArray.pop(); + } + + do { + requiredVersionPart = parseInt(reqVerArray[index], 10); + actualVersionPart = parseInt(actualVerArray[index], 10); + index++; + } while (index < reqVerArray.length && requiredVersionPart === actualVersionPart); + + if (requiredVersionPart <= actualVersionPart && !isNaN(requiredVersionPart)) { + isVersionSupported = true; + } + } + } + } catch (e2) { + isVersionSupported = false; + } + + return isVersionSupported; + } + + /** + Constructor for the Silverlight Runtime + + @class SilverlightRuntime + @extends Runtime + */ + function SilverlightRuntime(options) { + var I = this, initTimer; + + options = Basic.extend({ xap_url: Env.xap_url }, options); + + Runtime.call(this, options, type, { + access_binary: Runtime.capTrue, + access_image_binary: Runtime.capTrue, + display_media: Runtime.capTest(defined('moxie/image/Image')), + do_cors: Runtime.capTrue, + drag_and_drop: false, + report_upload_progress: Runtime.capTrue, + resize_image: Runtime.capTrue, + return_response_headers: function(value) { + return value && I.mode === 'client'; + }, + return_response_type: function(responseType) { + if (responseType !== 'json') { + return true; + } else { + return !!window.JSON; + } + }, + return_status_code: function(code) { + return I.mode === 'client' || !Basic.arrayDiff(code, [200, 404]); + }, + select_file: Runtime.capTrue, + select_multiple: Runtime.capTrue, + send_binary_string: Runtime.capTrue, + send_browser_cookies: function(value) { + return value && I.mode === 'browser'; + }, + send_custom_headers: function(value) { + return value && I.mode === 'client'; + }, + send_multipart: Runtime.capTrue, + slice_blob: Runtime.capTrue, + stream_upload: true, + summon_file_dialog: false, + upload_filesize: Runtime.capTrue, + use_http_method: function(methods) { + return I.mode === 'client' || !Basic.arrayDiff(methods, ['GET', 'POST']); + } + }, { + // capabilities that require specific mode + return_response_headers: function(value) { + return value ? 'client' : 'browser'; + }, + return_status_code: function(code) { + return Basic.arrayDiff(code, [200, 404]) ? 'client' : ['client', 'browser']; + }, + send_browser_cookies: function(value) { + return value ? 'browser' : 'client'; + }, + send_custom_headers: function(value) { + return value ? 'client' : 'browser'; + }, + use_http_method: function(methods) { + return Basic.arrayDiff(methods, ['GET', 'POST']) ? 'client' : ['client', 'browser']; + } + }); + + + // minimal requirement + if (!isInstalled('2.0.31005.0') || Env.browser === 'Opera') { + if (MXI_DEBUG && Env.debug.runtime) { + Env.log("\tSilverlight is not installed or minimal version (2.0.31005.0) requirement not met (not likely)."); + } + + this.mode = false; + } + + + Basic.extend(this, { + getShim: function() { + return Dom.get(this.uid).content.Moxie; + }, + + shimExec: function(component, action) { + var args = [].slice.call(arguments, 2); + return I.getShim().exec(this.uid, component, action, args); + }, + + init : function() { + var container; + + container = this.getShimContainer(); + + container.innerHTML = '' + + '' + + '' + + '' + + '' + + '' + + ''; + + // Init is dispatched by the shim + initTimer = setTimeout(function() { + if (I && !I.initialized) { // runtime might be already destroyed by this moment + I.trigger("Error", new x.RuntimeError(x.RuntimeError.NOT_INIT_ERR)); + + if (MXI_DEBUG && Env.debug.runtime) { + Env.log("\Silverlight failed to initialize within a specified period of time (5-10s)."); + } + } + }, Env.OS !== 'Windows'? 10000 : 5000); // give it more time to initialize in non Windows OS (like Mac) + }, + + destroy: (function(destroy) { // extend default destroy method + return function() { + destroy.call(I); + clearTimeout(initTimer); // initialization check might be still onwait + options = initTimer = destroy = I = null; + }; + }(this.destroy)) + + }, extensions); + } + + Runtime.addConstructor(type, SilverlightRuntime); + + return extensions; +}); + +// Included from: src/javascript/runtime/silverlight/file/Blob.js + +/** + * Blob.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/silverlight/file/Blob +@private +*/ +define("moxie/runtime/silverlight/file/Blob", [ + "moxie/runtime/silverlight/Runtime", + "moxie/core/utils/Basic", + "moxie/runtime/flash/file/Blob" +], function(extensions, Basic, Blob) { + return (extensions.Blob = Basic.extend({}, Blob)); +}); + +// Included from: src/javascript/runtime/silverlight/file/FileInput.js + +/** + * FileInput.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/silverlight/file/FileInput +@private +*/ +define("moxie/runtime/silverlight/file/FileInput", [ + "moxie/runtime/silverlight/Runtime", + "moxie/file/File", + "moxie/core/utils/Basic" +], function(extensions, File, Basic) { + + function toFilters(accept) { + var filter = ''; + for (var i = 0; i < accept.length; i++) { + filter += (filter !== '' ? '|' : '') + accept[i].title + " | *." + accept[i].extensions.replace(/,/g, ';*.'); + } + return filter; + } + + + var FileInput = { + init: function(options) { + var comp = this, I = this.getRuntime(); + + this.bind("Change", function() { + var files = I.shimExec.call(comp, 'FileInput', 'getFiles'); + comp.files = []; + Basic.each(files, function(file) { + comp.files.push(new File(I.uid, file)); + }); + }, 999); + + I.shimExec.call(this, 'FileInput', 'init', toFilters(options.accept), options.multiple); + this.trigger('ready'); + }, + + setOption: function(name, value) { + if (name == 'accept') { + value = toFilters(value); + } + this.getRuntime().shimExec.call(this, 'FileInput', 'setOption', name, value); + } + }; + + return (extensions.FileInput = FileInput); +}); + +// Included from: src/javascript/runtime/silverlight/file/FileDrop.js + +/** + * FileDrop.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/silverlight/file/FileDrop +@private +*/ +define("moxie/runtime/silverlight/file/FileDrop", [ + "moxie/runtime/silverlight/Runtime", + "moxie/core/utils/Dom", + "moxie/core/utils/Events" +], function(extensions, Dom, Events) { + + // not exactly useful, since works only in safari (...crickets...) + var FileDrop = { + init: function() { + var comp = this, self = comp.getRuntime(), dropZone; + + dropZone = self.getShimContainer(); + + Events.addEvent(dropZone, 'dragover', function(e) { + e.preventDefault(); + e.stopPropagation(); + e.dataTransfer.dropEffect = 'copy'; + }, comp.uid); + + Events.addEvent(dropZone, 'dragenter', function(e) { + e.preventDefault(); + var flag = Dom.get(self.uid).dragEnter(e); + // If handled, then stop propagation of event in DOM + if (flag) { + e.stopPropagation(); + } + }, comp.uid); + + Events.addEvent(dropZone, 'drop', function(e) { + e.preventDefault(); + var flag = Dom.get(self.uid).dragDrop(e); + // If handled, then stop propagation of event in DOM + if (flag) { + e.stopPropagation(); + } + }, comp.uid); + + return self.shimExec.call(this, 'FileDrop', 'init'); + } + }; + + return (extensions.FileDrop = FileDrop); +}); + +// Included from: src/javascript/runtime/silverlight/file/FileReader.js + +/** + * FileReader.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/silverlight/file/FileReader +@private +*/ +define("moxie/runtime/silverlight/file/FileReader", [ + "moxie/runtime/silverlight/Runtime", + "moxie/core/utils/Basic", + "moxie/runtime/flash/file/FileReader" +], function(extensions, Basic, FileReader) { + return (extensions.FileReader = Basic.extend({}, FileReader)); +}); + +// Included from: src/javascript/runtime/silverlight/file/FileReaderSync.js + +/** + * FileReaderSync.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/silverlight/file/FileReaderSync +@private +*/ +define("moxie/runtime/silverlight/file/FileReaderSync", [ + "moxie/runtime/silverlight/Runtime", + "moxie/core/utils/Basic", + "moxie/runtime/flash/file/FileReaderSync" +], function(extensions, Basic, FileReaderSync) { + return (extensions.FileReaderSync = Basic.extend({}, FileReaderSync)); +}); + +// Included from: src/javascript/runtime/silverlight/runtime/Transporter.js + +/** + * Transporter.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/silverlight/runtime/Transporter +@private +*/ +define("moxie/runtime/silverlight/runtime/Transporter", [ + "moxie/runtime/silverlight/Runtime", + "moxie/core/utils/Basic", + "moxie/runtime/flash/runtime/Transporter" +], function(extensions, Basic, Transporter) { + return (extensions.Transporter = Basic.extend({}, Transporter)); +}); + +// Included from: src/javascript/runtime/silverlight/xhr/XMLHttpRequest.js + +/** + * XMLHttpRequest.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/silverlight/xhr/XMLHttpRequest +@private +*/ +define("moxie/runtime/silverlight/xhr/XMLHttpRequest", [ + "moxie/runtime/silverlight/Runtime", + "moxie/core/utils/Basic", + "moxie/runtime/flash/xhr/XMLHttpRequest", + "moxie/runtime/silverlight/file/FileReaderSync", + "moxie/runtime/silverlight/runtime/Transporter" +], function(extensions, Basic, XMLHttpRequest, FileReaderSyncSilverlight, TransporterSilverlight) { + return (extensions.XMLHttpRequest = Basic.extend({}, XMLHttpRequest)); +}); + +// Included from: src/javascript/runtime/silverlight/image/Image.js + +/** + * Image.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/silverlight/image/Image +@private +*/ +define("moxie/runtime/silverlight/image/Image", [ + "moxie/runtime/silverlight/Runtime", + "moxie/core/utils/Basic", + "moxie/file/Blob", + "moxie/runtime/flash/image/Image" +], function(extensions, Basic, Blob, Image) { + return (extensions.Image = Basic.extend({}, Image, { + + getInfo: function() { + var self = this.getRuntime() + , grps = ['tiff', 'exif', 'gps', 'thumb'] + , info = { meta: {} } + , rawInfo = self.shimExec.call(this, 'Image', 'getInfo') + ; + + if (rawInfo.meta) { + Basic.each(grps, function(grp) { + var meta = rawInfo.meta[grp] + , tag + , i + , length + , value + ; + if (meta && meta.keys) { + info.meta[grp] = {}; + for (i = 0, length = meta.keys.length; i < length; i++) { + tag = meta.keys[i]; + value = meta[tag]; + if (value) { + // convert numbers + if (/^(\d|[1-9]\d+)$/.test(value)) { // integer (make sure doesn't start with zero) + value = parseInt(value, 10); + } else if (/^\d*\.\d+$/.test(value)) { // double + value = parseFloat(value); + } + info.meta[grp][tag] = value; + } + } + } + }); + + // save thumb data as blob + if (info.meta && info.meta.thumb && info.meta.thumb.data && !(self.meta.thumb.data instanceof Blob)) { + info.meta.thumb.data = new Blob(self.uid, info.meta.thumb.data); + } + } + + info.width = parseInt(rawInfo.width, 10); + info.height = parseInt(rawInfo.height, 10); + info.size = parseInt(rawInfo.size, 10); + info.type = rawInfo.type; + info.name = rawInfo.name; + + return info; + }, + + resize: function(rect, ratio, opts) { + this.getRuntime().shimExec.call(this, 'Image', 'resize', rect.x, rect.y, rect.width, rect.height, ratio, opts.preserveHeaders, opts.resample); + } + })); +}); + +// Included from: src/javascript/runtime/html4/Runtime.js + +/** + * Runtime.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/*global File:true */ + +/** +Defines constructor for HTML4 runtime. + +@class moxie/runtime/html4/Runtime +@private +*/ +define("moxie/runtime/html4/Runtime", [ + "moxie/core/utils/Basic", + "moxie/core/Exceptions", + "moxie/runtime/Runtime", + "moxie/core/utils/Env" +], function(Basic, x, Runtime, Env) { + + var type = 'html4', extensions = {}; + + function Html4Runtime(options) { + var I = this + , Test = Runtime.capTest + , True = Runtime.capTrue + ; + + Runtime.call(this, options, type, { + access_binary: Test(window.FileReader || window.File && File.getAsDataURL), + access_image_binary: false, + display_media: Test( + (Env.can('create_canvas') || Env.can('use_data_uri_over32kb')) && + defined('moxie/image/Image') + ), + do_cors: false, + drag_and_drop: false, + filter_by_extension: Test(function() { // if you know how to feature-detect this, please suggest + return !( + (Env.browser === 'Chrome' && Env.verComp(Env.version, 28, '<')) || + (Env.browser === 'IE' && Env.verComp(Env.version, 10, '<')) || + (Env.browser === 'Safari' && Env.verComp(Env.version, 7, '<')) || + (Env.browser === 'Firefox' && Env.verComp(Env.version, 37, '<')) + ); + }()), + resize_image: function() { + return extensions.Image && I.can('access_binary') && Env.can('create_canvas'); + }, + report_upload_progress: false, + return_response_headers: false, + return_response_type: function(responseType) { + if (responseType === 'json' && !!window.JSON) { + return true; + } + return !!~Basic.inArray(responseType, ['text', 'document', '']); + }, + return_status_code: function(code) { + return !Basic.arrayDiff(code, [200, 404]); + }, + select_file: function() { + return Env.can('use_fileinput'); + }, + select_multiple: false, + send_binary_string: false, + send_custom_headers: false, + send_multipart: true, + slice_blob: false, + stream_upload: function() { + return I.can('select_file'); + }, + summon_file_dialog: function() { // yeah... some dirty sniffing here... + return I.can('select_file') && ( + (Env.browser === 'Firefox' && Env.verComp(Env.version, 4, '>=')) || + (Env.browser === 'Opera' && Env.verComp(Env.version, 12, '>=')) || + (Env.browser === 'IE' && Env.verComp(Env.version, 10, '>=')) || + !!~Basic.inArray(Env.browser, ['Chrome', 'Safari']) + ); + }, + upload_filesize: True, + use_http_method: function(methods) { + return !Basic.arrayDiff(methods, ['GET', 'POST']); + } + }); + + + Basic.extend(this, { + init : function() { + this.trigger("Init"); + }, + + destroy: (function(destroy) { // extend default destroy method + return function() { + destroy.call(I); + destroy = I = null; + }; + }(this.destroy)) + }); + + Basic.extend(this.getShim(), extensions); + } + + Runtime.addConstructor(type, Html4Runtime); + + return extensions; +}); + +// Included from: src/javascript/runtime/html4/file/FileInput.js + +/** + * FileInput.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/html4/file/FileInput +@private +*/ +define("moxie/runtime/html4/file/FileInput", [ + "moxie/runtime/html4/Runtime", + "moxie/file/File", + "moxie/core/utils/Basic", + "moxie/core/utils/Dom", + "moxie/core/utils/Events", + "moxie/core/utils/Mime", + "moxie/core/utils/Env" +], function(extensions, File, Basic, Dom, Events, Mime, Env) { + + function FileInput() { + var _uid, _mimes = [], _options, _browseBtnZIndex; // save original z-index; + + function addInput() { + var comp = this, I = comp.getRuntime(), shimContainer, browseButton, currForm, form, input, uid; + + uid = Basic.guid('uid_'); + + shimContainer = I.getShimContainer(); // we get new ref every time to avoid memory leaks in IE + + if (_uid) { // move previous form out of the view + currForm = Dom.get(_uid + '_form'); + if (currForm) { + Basic.extend(currForm.style, { top: '100%' }); + } + } + + // build form in DOM, since innerHTML version not able to submit file for some reason + form = document.createElement('form'); + form.setAttribute('id', uid + '_form'); + form.setAttribute('method', 'post'); + form.setAttribute('enctype', 'multipart/form-data'); + form.setAttribute('encoding', 'multipart/form-data'); + + Basic.extend(form.style, { + overflow: 'hidden', + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%' + }); + + input = document.createElement('input'); + input.setAttribute('id', uid); + input.setAttribute('type', 'file'); + input.setAttribute('accept', _mimes.join(',')); + + Basic.extend(input.style, { + fontSize: '999px', + opacity: 0 + }); + + form.appendChild(input); + shimContainer.appendChild(form); + + // prepare file input to be placed underneath the browse_button element + Basic.extend(input.style, { + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%' + }); + + if (Env.browser === 'IE' && Env.verComp(Env.version, 10, '<')) { + Basic.extend(input.style, { + filter : "progid:DXImageTransform.Microsoft.Alpha(opacity=0)" + }); + } + + input.onchange = function() { // there should be only one handler for this + var file; + + if (!this.value) { + return; + } + + if (this.files) { // check if browser is fresh enough + file = this.files[0]; + + // ignore empty files (IE10 for example hangs if you try to send them via XHR) + if (file.size === 0) { + form.parentNode.removeChild(form); + return; + } + } else { + file = { + name: this.value + }; + } + + file = new File(I.uid, file); + + // clear event handler + this.onchange = function() {}; + addInput.call(comp); + + comp.files = [file]; + + // substitute all ids with file uids (consider file.uid read-only - we cannot do it the other way around) + input.setAttribute('id', file.uid); + form.setAttribute('id', file.uid + '_form'); + + comp.trigger('change'); + + input = form = null; + }; + + + // route click event to the input + if (I.can('summon_file_dialog')) { + browseButton = Dom.get(_options.browse_button); + Events.removeEvent(browseButton, 'click', comp.uid); + Events.addEvent(browseButton, 'click', function(e) { + if (input && !input.disabled) { // for some reason FF (up to 8.0.1 so far) lets to click disabled input[type=file] + input.click(); + } + e.preventDefault(); + }, comp.uid); + } + + _uid = uid; + + shimContainer = currForm = browseButton = null; + } + + Basic.extend(this, { + init: function(options) { + var comp = this, I = comp.getRuntime(), shimContainer; + + // figure out accept string + _options = options; + _mimes = options.accept.mimes || Mime.extList2mimes(options.accept, I.can('filter_by_extension')); + + shimContainer = I.getShimContainer(); + + (function() { + var browseButton, zIndex, top; + + browseButton = Dom.get(options.browse_button); + _browseBtnZIndex = Dom.getStyle(browseButton, 'z-index') || 'auto'; + + // Route click event to the input[type=file] element for browsers that support such behavior + if (I.can('summon_file_dialog')) { + if (Dom.getStyle(browseButton, 'position') === 'static') { + browseButton.style.position = 'relative'; + } + + comp.bind('Refresh', function() { + zIndex = parseInt(_browseBtnZIndex, 10) || 1; + + Dom.get(_options.browse_button).style.zIndex = zIndex; + this.getRuntime().getShimContainer().style.zIndex = zIndex - 1; + }); + } + + /* Since we have to place input[type=file] on top of the browse_button for some browsers, + browse_button loses interactivity, so we restore it here */ + top = I.can('summon_file_dialog') ? browseButton : shimContainer; + + Events.addEvent(top, 'mouseover', function() { + comp.trigger('mouseenter'); + }, comp.uid); + + Events.addEvent(top, 'mouseout', function() { + comp.trigger('mouseleave'); + }, comp.uid); + + Events.addEvent(top, 'mousedown', function() { + comp.trigger('mousedown'); + }, comp.uid); + + Events.addEvent(Dom.get(options.container), 'mouseup', function() { + comp.trigger('mouseup'); + }, comp.uid); + + browseButton = null; + }()); + + addInput.call(this); + + shimContainer = null; + + // trigger ready event asynchronously + comp.trigger({ + type: 'ready', + async: true + }); + }, + + setOption: function(name, value) { + var I = this.getRuntime(); + var input; + + if (name == 'accept') { + _mimes = value.mimes || Mime.extList2mimes(value, I.can('filter_by_extension')); + } + + // update current input + input = Dom.get(_uid) + if (input) { + input.setAttribute('accept', _mimes.join(',')); + } + }, + + + disable: function(state) { + var input; + + if ((input = Dom.get(_uid))) { + input.disabled = !!state; + } + }, + + destroy: function() { + var I = this.getRuntime() + , shim = I.getShim() + , shimContainer = I.getShimContainer() + , container = _options && Dom.get(_options.container) + , browseButton = _options && Dom.get(_options.browse_button) + ; + + if (container) { + Events.removeAllEvents(container, this.uid); + } + + if (browseButton) { + Events.removeAllEvents(browseButton, this.uid); + browseButton.style.zIndex = _browseBtnZIndex; // reset to original value + } + + if (shimContainer) { + Events.removeAllEvents(shimContainer, this.uid); + shimContainer.innerHTML = ''; + } + + shim.removeInstance(this.uid); + + _uid = _mimes = _options = shimContainer = container = browseButton = shim = null; + } + }); + } + + return (extensions.FileInput = FileInput); +}); + +// Included from: src/javascript/runtime/html4/file/FileReader.js + +/** + * FileReader.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/html4/file/FileReader +@private +*/ +define("moxie/runtime/html4/file/FileReader", [ + "moxie/runtime/html4/Runtime", + "moxie/runtime/html5/file/FileReader" +], function(extensions, FileReader) { + return (extensions.FileReader = FileReader); +}); + +// Included from: src/javascript/runtime/html4/xhr/XMLHttpRequest.js + +/** + * XMLHttpRequest.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/html4/xhr/XMLHttpRequest +@private +*/ +define("moxie/runtime/html4/xhr/XMLHttpRequest", [ + "moxie/runtime/html4/Runtime", + "moxie/core/utils/Basic", + "moxie/core/utils/Dom", + "moxie/core/utils/Url", + "moxie/core/Exceptions", + "moxie/core/utils/Events", + "moxie/file/Blob", + "moxie/xhr/FormData" +], function(extensions, Basic, Dom, Url, x, Events, Blob, FormData) { + + function XMLHttpRequest() { + var _status, _response, _iframe; + + function cleanup(cb) { + var target = this, uid, form, inputs, i, hasFile = false; + + if (!_iframe) { + return; + } + + uid = _iframe.id.replace(/_iframe$/, ''); + + form = Dom.get(uid + '_form'); + if (form) { + inputs = form.getElementsByTagName('input'); + i = inputs.length; + + while (i--) { + switch (inputs[i].getAttribute('type')) { + case 'hidden': + inputs[i].parentNode.removeChild(inputs[i]); + break; + case 'file': + hasFile = true; // flag the case for later + break; + } + } + inputs = []; + + if (!hasFile) { // we need to keep the form for sake of possible retries + form.parentNode.removeChild(form); + } + form = null; + } + + // without timeout, request is marked as canceled (in console) + setTimeout(function() { + Events.removeEvent(_iframe, 'load', target.uid); + if (_iframe.parentNode) { // #382 + _iframe.parentNode.removeChild(_iframe); + } + + // check if shim container has any other children, if - not, remove it as well + var shimContainer = target.getRuntime().getShimContainer(); + if (!shimContainer.children.length) { + shimContainer.parentNode.removeChild(shimContainer); + } + + shimContainer = _iframe = null; + cb(); + }, 1); + } + + Basic.extend(this, { + send: function(meta, data) { + var target = this, I = target.getRuntime(), uid, form, input, blob; + + _status = _response = null; + + function createIframe() { + var container = I.getShimContainer() || document.body + , temp = document.createElement('div') + ; + + // IE 6 won't be able to set the name using setAttribute or iframe.name + temp.innerHTML = ''; + _iframe = temp.firstChild; + container.appendChild(_iframe); + + /* _iframe.onreadystatechange = function() { + console.info(_iframe.readyState); + };*/ + + Events.addEvent(_iframe, 'load', function() { // _iframe.onload doesn't work in IE lte 8 + var el; + + try { + el = _iframe.contentWindow.document || _iframe.contentDocument || window.frames[_iframe.id].document; + + // try to detect some standard error pages + if (/^4(0[0-9]|1[0-7]|2[2346])\s/.test(el.title)) { // test if title starts with 4xx HTTP error + _status = el.title.replace(/^(\d+).*$/, '$1'); + } else { + _status = 200; + // get result + _response = Basic.trim(el.body.innerHTML); + + // we need to fire these at least once + target.trigger({ + type: 'progress', + loaded: _response.length, + total: _response.length + }); + + if (blob) { // if we were uploading a file + target.trigger({ + type: 'uploadprogress', + loaded: blob.size || 1025, + total: blob.size || 1025 + }); + } + } + } catch (ex) { + if (Url.hasSameOrigin(meta.url)) { + // if response is sent with error code, iframe in IE gets redirected to res://ieframe.dll/http_x.htm + // which obviously results to cross domain error (wtf?) + _status = 404; + } else { + cleanup.call(target, function() { + target.trigger('error'); + }); + return; + } + } + + cleanup.call(target, function() { + target.trigger('load'); + }); + }, target.uid); + } // end createIframe + + // prepare data to be sent and convert if required + if (data instanceof FormData && data.hasBlob()) { + blob = data.getBlob(); + uid = blob.uid; + input = Dom.get(uid); + form = Dom.get(uid + '_form'); + if (!form) { + throw new x.DOMException(x.DOMException.NOT_FOUND_ERR); + } + } else { + uid = Basic.guid('uid_'); + + form = document.createElement('form'); + form.setAttribute('id', uid + '_form'); + form.setAttribute('method', meta.method); + form.setAttribute('enctype', 'multipart/form-data'); + form.setAttribute('encoding', 'multipart/form-data'); + + I.getShimContainer().appendChild(form); + } + + // set upload target + form.setAttribute('target', uid + '_iframe'); + + if (data instanceof FormData) { + data.each(function(value, name) { + if (value instanceof Blob) { + if (input) { + input.setAttribute('name', name); + } + } else { + var hidden = document.createElement('input'); + + Basic.extend(hidden, { + type : 'hidden', + name : name, + value : value + }); + + // make sure that input[type="file"], if it's there, comes last + if (input) { + form.insertBefore(hidden, input); + } else { + form.appendChild(hidden); + } + } + }); + } + + // set destination url + form.setAttribute("action", meta.url); + + createIframe(); + form.submit(); + target.trigger('loadstart'); + }, + + getStatus: function() { + return _status; + }, + + getResponse: function(responseType) { + if ('json' === responseType) { + // strip off
..
tags that might be enclosing the response + if (Basic.typeOf(_response) === 'string' && !!window.JSON) { + try { + return JSON.parse(_response.replace(/^\s*]*>/, '').replace(/<\/pre>\s*$/, '')); + } catch (ex) { + return null; + } + } + } else if ('document' === responseType) { + + } + return _response; + }, + + abort: function() { + var target = this; + + if (_iframe && _iframe.contentWindow) { + if (_iframe.contentWindow.stop) { // FireFox/Safari/Chrome + _iframe.contentWindow.stop(); + } else if (_iframe.contentWindow.document.execCommand) { // IE + _iframe.contentWindow.document.execCommand('Stop'); + } else { + _iframe.src = "about:blank"; + } + } + + cleanup.call(this, function() { + // target.dispatchEvent('readystatechange'); + target.dispatchEvent('abort'); + }); + } + }); + } + + return (extensions.XMLHttpRequest = XMLHttpRequest); +}); + +// Included from: src/javascript/runtime/html4/image/Image.js + +/** + * Image.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +/** +@class moxie/runtime/html4/image/Image +@private +*/ +define("moxie/runtime/html4/image/Image", [ + "moxie/runtime/html4/Runtime", + "moxie/runtime/html5/image/Image" +], function(extensions, Image) { + return (extensions.Image = Image); +}); + +expose(["moxie/core/utils/Basic","moxie/core/utils/Encode","moxie/core/utils/Env","moxie/core/Exceptions","moxie/core/utils/Dom","moxie/core/EventTarget","moxie/runtime/Runtime","moxie/runtime/RuntimeClient","moxie/file/Blob","moxie/core/I18n","moxie/core/utils/Mime","moxie/file/FileInput","moxie/file/File","moxie/file/FileDrop","moxie/file/FileReader","moxie/core/utils/Url","moxie/runtime/RuntimeTarget","moxie/xhr/FormData","moxie/xhr/XMLHttpRequest","moxie/runtime/Transporter","moxie/image/Image","moxie/core/utils/Events","moxie/runtime/html5/image/ResizerCanvas"]); +})(this); +})); \ No newline at end of file diff --git a/static/js/uploader/plupload.dev.js b/static/js/uploader/plupload.dev.js new file mode 100644 index 00000000..24fc8565 --- /dev/null +++ b/static/js/uploader/plupload.dev.js @@ -0,0 +1,2476 @@ +/** + * Plupload - multi-runtime File Uploader + * v2.3.1 + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + * + * Date: 2017-02-06 + */ +;(function (global, factory) { + var extract = function() { + var ctx = {}; + factory.apply(ctx, arguments); + return ctx.plupload; + }; + + if (typeof define === "function" && define.amd) { + define("plupload", ['./moxie'], extract); + } else if (typeof module === "object" && module.exports) { + module.exports = extract(require('../../../js-sdk-master 2/src/moxie')); + } else { + global.plupload = extract(global.moxie); + } +}(this || window, function(moxie) { +/** + * Plupload.js + * + * Copyright 2013, Moxiecode Systems AB + * Released under GPL License. + * + * License: http://www.plupload.com/license + * Contributing: http://www.plupload.com/contributing + */ + +;(function(exports, o, undef) { + +var delay = window.setTimeout; +var fileFilters = {}; +var u = o.core.utils; +var Runtime = o.runtime.Runtime; + +// convert plupload features to caps acceptable by mOxie +function normalizeCaps(settings) { + var features = settings.required_features, caps = {}; + + function resolve(feature, value, strict) { + // Feature notation is deprecated, use caps (this thing here is required for backward compatibility) + var map = { + chunks: 'slice_blob', + jpgresize: 'send_binary_string', + pngresize: 'send_binary_string', + progress: 'report_upload_progress', + multi_selection: 'select_multiple', + dragdrop: 'drag_and_drop', + drop_element: 'drag_and_drop', + headers: 'send_custom_headers', + urlstream_upload: 'send_binary_string', + canSendBinary: 'send_binary', + triggerDialog: 'summon_file_dialog' + }; + + if (map[feature]) { + caps[map[feature]] = value; + } else if (!strict) { + caps[feature] = value; + } + } + + if (typeof(features) === 'string') { + plupload.each(features.split(/\s*,\s*/), function(feature) { + resolve(feature, true); + }); + } else if (typeof(features) === 'object') { + plupload.each(features, function(value, feature) { + resolve(feature, value); + }); + } else if (features === true) { + // check settings for required features + if (settings.chunk_size && settings.chunk_size > 0) { + caps.slice_blob = true; + } + + if (!plupload.isEmptyObj(settings.resize) || settings.multipart === false) { + caps.send_binary_string = true; + } + + if (settings.http_method) { + caps.use_http_method = settings.http_method; + } + + plupload.each(settings, function(value, feature) { + resolve(feature, !!value, true); // strict check + }); + } + + return caps; +} + +/** + * @module plupload + * @static + */ +var plupload = { + /** + * Plupload version will be replaced on build. + * + * @property VERSION + * @for Plupload + * @static + * @final + */ + VERSION : '2.3.1', + + /** + * The state of the queue before it has started and after it has finished + * + * @property STOPPED + * @static + * @final + */ + STOPPED : 1, + + /** + * Upload process is running + * + * @property STARTED + * @static + * @final + */ + STARTED : 2, + + /** + * File is queued for upload + * + * @property QUEUED + * @static + * @final + */ + QUEUED : 1, + + /** + * File is being uploaded + * + * @property UPLOADING + * @static + * @final + */ + UPLOADING : 2, + + /** + * File has failed to be uploaded + * + * @property FAILED + * @static + * @final + */ + FAILED : 4, + + /** + * File has been uploaded successfully + * + * @property DONE + * @static + * @final + */ + DONE : 5, + + // Error constants used by the Error event + + /** + * Generic error for example if an exception is thrown inside Silverlight. + * + * @property GENERIC_ERROR + * @static + * @final + */ + GENERIC_ERROR : -100, + + /** + * HTTP transport error. For example if the server produces a HTTP status other than 200. + * + * @property HTTP_ERROR + * @static + * @final + */ + HTTP_ERROR : -200, + + /** + * Generic I/O error. For example if it wasn't possible to open the file stream on local machine. + * + * @property IO_ERROR + * @static + * @final + */ + IO_ERROR : -300, + + /** + * @property SECURITY_ERROR + * @static + * @final + */ + SECURITY_ERROR : -400, + + /** + * Initialization error. Will be triggered if no runtime was initialized. + * + * @property INIT_ERROR + * @static + * @final + */ + INIT_ERROR : -500, + + /** + * File size error. If the user selects a file that is too large it will be blocked and an error of this type will be triggered. + * + * @property FILE_SIZE_ERROR + * @static + * @final + */ + FILE_SIZE_ERROR : -600, + + /** + * File extension error. If the user selects a file that isn't valid according to the filters setting. + * + * @property FILE_EXTENSION_ERROR + * @static + * @final + */ + FILE_EXTENSION_ERROR : -601, + + /** + * Duplicate file error. If prevent_duplicates is set to true and user selects the same file again. + * + * @property FILE_DUPLICATE_ERROR + * @static + * @final + */ + FILE_DUPLICATE_ERROR : -602, + + /** + * Runtime will try to detect if image is proper one. Otherwise will throw this error. + * + * @property IMAGE_FORMAT_ERROR + * @static + * @final + */ + IMAGE_FORMAT_ERROR : -700, + + /** + * While working on files runtime may run out of memory and will throw this error. + * + * @since 2.1.2 + * @property MEMORY_ERROR + * @static + * @final + */ + MEMORY_ERROR : -701, + + /** + * Each runtime has an upper limit on a dimension of the image it can handle. If bigger, will throw this error. + * + * @property IMAGE_DIMENSIONS_ERROR + * @static + * @final + */ + IMAGE_DIMENSIONS_ERROR : -702, + + /** + * Mime type lookup table. + * + * @property mimeTypes + * @type Object + * @final + */ + mimeTypes : u.Mime.mimes, + + /** + * In some cases sniffing is the only way around :( + */ + ua: u.Env, + + /** + * Gets the true type of the built-in object (better version of typeof). + * @credits Angus Croll (http://javascriptweblog.wordpress.com/) + * + * @method typeOf + * @static + * @param {Object} o Object to check. + * @return {String} Object [[Class]] + */ + typeOf: u.Basic.typeOf, + + /** + * Extends the specified object with another object. + * + * @method extend + * @static + * @param {Object} target Object to extend. + * @param {Object..} obj Multiple objects to extend with. + * @return {Object} Same as target, the extended object. + */ + extend : u.Basic.extend, + + /** + * Generates an unique ID. This is 99.99% unique since it takes the current time and 5 random numbers. + * The only way a user would be able to get the same ID is if the two persons at the same exact millisecond manages + * to get 5 the same random numbers between 0-65535 it also uses a counter so each call will be guaranteed to be page unique. + * It's more probable for the earth to be hit with an asteriod. You can also if you want to be 100% sure set the plupload.guidPrefix property + * to an user unique key. + * + * @method guid + * @static + * @return {String} Virtually unique id. + */ + guid : u.Basic.guid, + + /** + * Get array of DOM Elements by their ids. + * + * @method get + * @param {String} id Identifier of the DOM Element + * @return {Array} + */ + getAll : function get(ids) { + var els = [], el; + + if (plupload.typeOf(ids) !== 'array') { + ids = [ids]; + } + + var i = ids.length; + while (i--) { + el = plupload.get(ids[i]); + if (el) { + els.push(el); + } + } + + return els.length ? els : null; + }, + + /** + Get DOM element by id + + @method get + @param {String} id Identifier of the DOM Element + @return {Node} + */ + get: u.Dom.get, + + /** + * Executes the callback function for each item in array/object. If you return false in the + * callback it will break the loop. + * + * @method each + * @static + * @param {Object} obj Object to iterate. + * @param {function} callback Callback function to execute for each item. + */ + each : u.Basic.each, + + /** + * Returns the absolute x, y position of an Element. The position will be returned in a object with x, y fields. + * + * @method getPos + * @static + * @param {Element} node HTML element or element id to get x, y position from. + * @param {Element} root Optional root element to stop calculations at. + * @return {object} Absolute position of the specified element object with x, y fields. + */ + getPos : u.Dom.getPos, + + /** + * Returns the size of the specified node in pixels. + * + * @method getSize + * @static + * @param {Node} node Node to get the size of. + * @return {Object} Object with a w and h property. + */ + getSize : u.Dom.getSize, + + /** + * Encodes the specified string. + * + * @method xmlEncode + * @static + * @param {String} s String to encode. + * @return {String} Encoded string. + */ + xmlEncode : function(str) { + var xmlEncodeChars = {'<' : 'lt', '>' : 'gt', '&' : 'amp', '"' : 'quot', '\'' : '#39'}, xmlEncodeRegExp = /[<>&\"\']/g; + + return str ? ('' + str).replace(xmlEncodeRegExp, function(chr) { + return xmlEncodeChars[chr] ? '&' + xmlEncodeChars[chr] + ';' : chr; + }) : str; + }, + + /** + * Forces anything into an array. + * + * @method toArray + * @static + * @param {Object} obj Object with length field. + * @return {Array} Array object containing all items. + */ + toArray : u.Basic.toArray, + + /** + * Find an element in array and return its index if present, otherwise return -1. + * + * @method inArray + * @static + * @param {mixed} needle Element to find + * @param {Array} array + * @return {Int} Index of the element, or -1 if not found + */ + inArray : u.Basic.inArray, + + /** + Recieve an array of functions (usually async) to call in sequence, each function + receives a callback as first argument that it should call, when it completes. Finally, + after everything is complete, main callback is called. Passing truthy value to the + callback as a first argument will interrupt the sequence and invoke main callback + immediately. + + @method inSeries + @static + @param {Array} queue Array of functions to call in sequence + @param {Function} cb Main callback that is called in the end, or in case of error + */ + inSeries: u.Basic.inSeries, + + /** + * Extends the language pack object with new items. + * + * @method addI18n + * @static + * @param {Object} pack Language pack items to add. + * @return {Object} Extended language pack object. + */ + addI18n : o.core.I18n.addI18n, + + /** + * Translates the specified string by checking for the english string in the language pack lookup. + * + * @method translate + * @static + * @param {String} str String to look for. + * @return {String} Translated string or the input string if it wasn't found. + */ + translate : o.core.I18n.translate, + + /** + * Pseudo sprintf implementation - simple way to replace tokens with specified values. + * + * @param {String} str String with tokens + * @return {String} String with replaced tokens + */ + sprintf : u.Basic.sprintf, + + /** + * Checks if object is empty. + * + * @method isEmptyObj + * @static + * @param {Object} obj Object to check. + * @return {Boolean} + */ + isEmptyObj : u.Basic.isEmptyObj, + + /** + * Checks if specified DOM element has specified class. + * + * @method hasClass + * @static + * @param {Object} obj DOM element like object to add handler to. + * @param {String} name Class name + */ + hasClass : u.Dom.hasClass, + + /** + * Adds specified className to specified DOM element. + * + * @method addClass + * @static + * @param {Object} obj DOM element like object to add handler to. + * @param {String} name Class name + */ + addClass : u.Dom.addClass, + + /** + * Removes specified className from specified DOM element. + * + * @method removeClass + * @static + * @param {Object} obj DOM element like object to add handler to. + * @param {String} name Class name + */ + removeClass : u.Dom.removeClass, + + /** + * Returns a given computed style of a DOM element. + * + * @method getStyle + * @static + * @param {Object} obj DOM element like object. + * @param {String} name Style you want to get from the DOM element + */ + getStyle : u.Dom.getStyle, + + /** + * Adds an event handler to the specified object and store reference to the handler + * in objects internal Plupload registry (@see removeEvent). + * + * @method addEvent + * @static + * @param {Object} obj DOM element like object to add handler to. + * @param {String} name Name to add event listener to. + * @param {Function} callback Function to call when event occurs. + * @param {String} (optional) key that might be used to add specifity to the event record. + */ + addEvent : u.Events.addEvent, + + /** + * Remove event handler from the specified object. If third argument (callback) + * is not specified remove all events with the specified name. + * + * @method removeEvent + * @static + * @param {Object} obj DOM element to remove event listener(s) from. + * @param {String} name Name of event listener to remove. + * @param {Function|String} (optional) might be a callback or unique key to match. + */ + removeEvent: u.Events.removeEvent, + + /** + * Remove all kind of events from the specified object + * + * @method removeAllEvents + * @static + * @param {Object} obj DOM element to remove event listeners from. + * @param {String} (optional) unique key to match, when removing events. + */ + removeAllEvents: u.Events.removeAllEvents, + + /** + * Cleans the specified name from national characters (diacritics). The result will be a name with only a-z, 0-9 and _. + * + * @method cleanName + * @static + * @param {String} s String to clean up. + * @return {String} Cleaned string. + */ + cleanName : function(name) { + var i, lookup; + + // Replace diacritics + lookup = [ + /[\300-\306]/g, 'A', /[\340-\346]/g, 'a', + /\307/g, 'C', /\347/g, 'c', + /[\310-\313]/g, 'E', /[\350-\353]/g, 'e', + /[\314-\317]/g, 'I', /[\354-\357]/g, 'i', + /\321/g, 'N', /\361/g, 'n', + /[\322-\330]/g, 'O', /[\362-\370]/g, 'o', + /[\331-\334]/g, 'U', /[\371-\374]/g, 'u' + ]; + + for (i = 0; i < lookup.length; i += 2) { + name = name.replace(lookup[i], lookup[i + 1]); + } + + // Replace whitespace + name = name.replace(/\s+/g, '_'); + + // Remove anything else + name = name.replace(/[^a-z0-9_\-\.]+/gi, ''); + + return name; + }, + + /** + * Builds a full url out of a base URL and an object with items to append as query string items. + * + * @method buildUrl + * @static + * @param {String} url Base URL to append query string items to. + * @param {Object} items Name/value object to serialize as a querystring. + * @return {String} String with url + serialized query string items. + */ + buildUrl: function(url, items) { + var query = ''; + + plupload.each(items, function(value, name) { + query += (query ? '&' : '') + encodeURIComponent(name) + '=' + encodeURIComponent(value); + }); + + if (query) { + url += (url.indexOf('?') > 0 ? '&' : '?') + query; + } + + return url; + }, + + /** + * Formats the specified number as a size string for example 1024 becomes 1 KB. + * + * @method formatSize + * @static + * @param {Number} size Size to format as string. + * @return {String} Formatted size string. + */ + formatSize : function(size) { + + if (size === undef || /\D/.test(size)) { + return plupload.translate('N/A'); + } + + function round(num, precision) { + return Math.round(num * Math.pow(10, precision)) / Math.pow(10, precision); + } + + var boundary = Math.pow(1024, 4); + + // TB + if (size > boundary) { + return round(size / boundary, 1) + " " + plupload.translate('tb'); + } + + // GB + if (size > (boundary/=1024)) { + return round(size / boundary, 1) + " " + plupload.translate('gb'); + } + + // MB + if (size > (boundary/=1024)) { + return round(size / boundary, 1) + " " + plupload.translate('mb'); + } + + // KB + if (size > 1024) { + return Math.round(size / 1024) + " " + plupload.translate('kb'); + } + + return size + " " + plupload.translate('b'); + }, + + + /** + * Parses the specified size string into a byte value. For example 10kb becomes 10240. + * + * @method parseSize + * @static + * @param {String|Number} size String to parse or number to just pass through. + * @return {Number} Size in bytes. + */ + parseSize : u.Basic.parseSizeStr, + + + /** + * A way to predict what runtime will be choosen in the current environment with the + * specified settings. + * + * @method predictRuntime + * @static + * @param {Object|String} config Plupload settings to check + * @param {String} [runtimes] Comma-separated list of runtimes to check against + * @return {String} Type of compatible runtime + */ + predictRuntime : function(config, runtimes) { + var up, runtime; + + up = new plupload.Uploader(config); + runtime = Runtime.thatCan(up.getOption().required_features, runtimes || config.runtimes); + up.destroy(); + return runtime; + }, + + /** + * Registers a filter that will be executed for each file added to the queue. + * If callback returns false, file will not be added. + * + * Callback receives two arguments: a value for the filter as it was specified in settings.filters + * and a file to be filtered. Callback is executed in the context of uploader instance. + * + * @method addFileFilter + * @static + * @param {String} name Name of the filter by which it can be referenced in settings.filters + * @param {String} cb Callback - the actual routine that every added file must pass + */ + addFileFilter: function(name, cb) { + fileFilters[name] = cb; + } +}; + + +plupload.addFileFilter('mime_types', function(filters, file, cb) { + if (filters.length && !filters.regexp.test(file.name)) { + this.trigger('Error', { + code : plupload.FILE_EXTENSION_ERROR, + message : plupload.translate('File extension error.'), + file : file + }); + cb(false); + } else { + cb(true); + } +}); + + +plupload.addFileFilter('max_file_size', function(maxSize, file, cb) { + var undef; + + maxSize = plupload.parseSize(maxSize); + + // Invalid file size + if (file.size !== undef && maxSize && file.size > maxSize) { + this.trigger('Error', { + code : plupload.FILE_SIZE_ERROR, + message : plupload.translate('File size error.'), + file : file + }); + cb(false); + } else { + cb(true); + } +}); + + +plupload.addFileFilter('prevent_duplicates', function(value, file, cb) { + if (value) { + var ii = this.files.length; + while (ii--) { + // Compare by name and size (size might be 0 or undefined, but still equivalent for both) + if (file.name === this.files[ii].name && file.size === this.files[ii].size) { + this.trigger('Error', { + code : plupload.FILE_DUPLICATE_ERROR, + message : plupload.translate('Duplicate file error.'), + file : file + }); + cb(false); + return; + } + } + } + cb(true); +}); + + +/** +@class Uploader +@constructor + +@param {Object} settings For detailed information about each option check documentation. + @param {String|DOMElement} settings.browse_button id of the DOM element or DOM element itself to use as file dialog trigger. + @param {Number|String} [settings.chunk_size=0] Chunk size in bytes to slice the file into. Shorcuts with b, kb, mb, gb, tb suffixes also supported. `e.g. 204800 or "204800b" or "200kb"`. By default - disabled. + @param {String|DOMElement} [settings.container] id of the DOM element or DOM element itself that will be used to wrap uploader structures. Defaults to immediate parent of the `browse_button` element. + @param {String|DOMElement} [settings.drop_element] id of the DOM element or DOM element itself to use as a drop zone for Drag-n-Drop. + @param {String} [settings.file_data_name="file"] Name for the file field in Multipart formated message. + @param {Object} [settings.filters={}] Set of file type filters. + @param {String|Number} [settings.filters.max_file_size=0] Maximum file size that the user can pick, in bytes. Optionally supports b, kb, mb, gb, tb suffixes. `e.g. "10mb" or "1gb"`. By default - not set. Dispatches `plupload.FILE_SIZE_ERROR`. + @param {Array} [settings.filters.mime_types=[]] List of file types to accept, each one defined by title and list of extensions. `e.g. {title : "Image files", extensions : "jpg,jpeg,gif,png"}`. Dispatches `plupload.FILE_EXTENSION_ERROR` + @param {Boolean} [settings.filters.prevent_duplicates=false] Do not let duplicates into the queue. Dispatches `plupload.FILE_DUPLICATE_ERROR`. + @param {String} [settings.flash_swf_url] URL of the Flash swf. + @param {Object} [settings.headers] Custom headers to send with the upload. Hash of name/value pairs. + @param {String} [settings.http_method="POST"] HTTP method to use during upload (only PUT or POST allowed). + @param {Number} [settings.max_retries=0] How many times to retry the chunk or file, before triggering Error event. + @param {Boolean} [settings.multipart=true] Whether to send file and additional parameters as Multipart formated message. + @param {Object} [settings.multipart_params] Hash of key/value pairs to send with every file upload. + @param {Boolean} [settings.multi_selection=true] Enable ability to select multiple files at once in file dialog. + @param {String|Object} [settings.required_features] Either comma-separated list or hash of required features that chosen runtime should absolutely possess. + @param {Object} [settings.resize] Enable resizng of images on client-side. Applies to `image/jpeg` and `image/png` only. `e.g. {width : 200, height : 200, quality : 90, crop: true}` + @param {Number} [settings.resize.width] If image is bigger, it will be resized. + @param {Number} [settings.resize.height] If image is bigger, it will be resized. + @param {Number} [settings.resize.quality=90] Compression quality for jpegs (1-100). + @param {Boolean} [settings.resize.crop=false] Whether to crop images to exact dimensions. By default they will be resized proportionally. + @param {String} [settings.runtimes="html5,flash,silverlight,html4"] Comma separated list of runtimes, that Plupload will try in turn, moving to the next if previous fails. + @param {String} [settings.silverlight_xap_url] URL of the Silverlight xap. + @param {Boolean} [settings.send_chunk_number=true] Whether to send chunks and chunk numbers, or total and offset bytes. + @param {Boolean} [settings.send_file_name=true] Whether to send file name as additional argument - 'name' (required for chunked uploads and some other cases where file name cannot be sent via normal ways). + @param {String} settings.url URL of the server-side upload handler. + @param {Boolean} [settings.unique_names=false] If true will generate unique filenames for uploaded files. + +*/ +plupload.Uploader = function(options) { + /** + Fires when the current RunTime has been initialized. + + @event Init + @param {plupload.Uploader} uploader Uploader instance sending the event. + */ + + /** + Fires after the init event incase you need to perform actions there. + + @event PostInit + @param {plupload.Uploader} uploader Uploader instance sending the event. + */ + + /** + Fires when the option is changed in via uploader.setOption(). + + @event OptionChanged + @since 2.1 + @param {plupload.Uploader} uploader Uploader instance sending the event. + @param {String} name Name of the option that was changed + @param {Mixed} value New value for the specified option + @param {Mixed} oldValue Previous value of the option + */ + + /** + Fires when the silverlight/flash or other shim needs to move. + + @event Refresh + @param {plupload.Uploader} uploader Uploader instance sending the event. + */ + + /** + Fires when the overall state is being changed for the upload queue. + + @event StateChanged + @param {plupload.Uploader} uploader Uploader instance sending the event. + */ + + /** + Fires when browse_button is clicked and browse dialog shows. + + @event Browse + @since 2.1.2 + @param {plupload.Uploader} uploader Uploader instance sending the event. + */ + + /** + Fires for every filtered file before it is added to the queue. + + @event FileFiltered + @since 2.1 + @param {plupload.Uploader} uploader Uploader instance sending the event. + @param {plupload.File} file Another file that has to be added to the queue. + */ + + /** + Fires when the file queue is changed. In other words when files are added/removed to the files array of the uploader instance. + + @event QueueChanged + @param {plupload.Uploader} uploader Uploader instance sending the event. + */ + + /** + Fires after files were filtered and added to the queue. + + @event FilesAdded + @param {plupload.Uploader} uploader Uploader instance sending the event. + @param {Array} files Array of file objects that were added to queue by the user. + */ + + /** + Fires when file is removed from the queue. + + @event FilesRemoved + @param {plupload.Uploader} uploader Uploader instance sending the event. + @param {Array} files Array of files that got removed. + */ + + /** + Fires just before a file is uploaded. Can be used to cancel the upload for the specified file + by returning false from the handler. + + @event BeforeUpload + @param {plupload.Uploader} uploader Uploader instance sending the event. + @param {plupload.File} file File to be uploaded. + */ + + /** + Fires when a file is to be uploaded by the runtime. + + @event UploadFile + @param {plupload.Uploader} uploader Uploader instance sending the event. + @param {plupload.File} file File to be uploaded. + */ + + /** + Fires while a file is being uploaded. Use this event to update the current file upload progress. + + @event UploadProgress + @param {plupload.Uploader} uploader Uploader instance sending the event. + @param {plupload.File} file File that is currently being uploaded. + */ + + /** + * Fires just before a chunk is uploaded. This event enables you to override settings + * on the uploader instance before the chunk is uploaded. + * + * @event BeforeChunkUpload + * @param {plupload.Uploader} uploader Uploader instance sending the event. + * @param {plupload.File} file File to be uploaded. + * @param {Object} args POST params to be sent. + * @param {Blob} chunkBlob Current blob. + * @param {offset} offset Current offset. + */ + + /** + Fires when file chunk is uploaded. + + @event ChunkUploaded + @param {plupload.Uploader} uploader Uploader instance sending the event. + @param {plupload.File} file File that the chunk was uploaded for. + @param {Object} result Object with response properties. + @param {Number} result.offset The amount of bytes the server has received so far, including this chunk. + @param {Number} result.total The size of the file. + @param {String} result.response The response body sent by the server. + @param {Number} result.status The HTTP status code sent by the server. + @param {String} result.responseHeaders All the response headers as a single string. + */ + + /** + Fires when a file is successfully uploaded. + + @event FileUploaded + @param {plupload.Uploader} uploader Uploader instance sending the event. + @param {plupload.File} file File that was uploaded. + @param {Object} result Object with response properties. + @param {String} result.response The response body sent by the server. + @param {Number} result.status The HTTP status code sent by the server. + @param {String} result.responseHeaders All the response headers as a single string. + */ + + /** + Fires when all files in a queue are uploaded. + + @event UploadComplete + @param {plupload.Uploader} uploader Uploader instance sending the event. + @param {Array} files Array of file objects that was added to queue/selected by the user. + */ + + /** + Fires when a error occurs. + + @event Error + @param {plupload.Uploader} uploader Uploader instance sending the event. + @param {Object} error Contains code, message and sometimes file and other details. + @param {Number} error.code The plupload error code. + @param {String} error.message Description of the error (uses i18n). + */ + + /** + Fires when destroy method is called. + + @event Destroy + @param {plupload.Uploader} uploader Uploader instance sending the event. + */ + var uid = plupload.guid() + , settings + , files = [] + , preferred_caps = {} + , fileInputs = [] + , fileDrops = [] + , startTime + , total + , disabled = false + , xhr + ; + + + // Private methods + function uploadNext() { + var file, count = 0, i; + + if (this.state == plupload.STARTED) { + // Find first QUEUED file + for (i = 0; i < files.length; i++) { + if (!file && files[i].status == plupload.QUEUED) { + file = files[i]; + if (this.trigger("BeforeUpload", file)) { + file.status = plupload.UPLOADING; + this.trigger("UploadFile", file); + } + } else { + count++; + } + } + + // All files are DONE or FAILED + if (count == files.length) { + if (this.state !== plupload.STOPPED) { + this.state = plupload.STOPPED; + this.trigger("StateChanged"); + } + this.trigger("UploadComplete", files); + } + } + } + + + function calcFile(file) { + file.percent = file.size > 0 ? Math.ceil(file.loaded / file.size * 100) : 100; + calc(); + } + + + function calc() { + var i, file; + var loaded; + var loadedDuringCurrentSession = 0; + + // Reset stats + total.reset(); + + // Check status, size, loaded etc on all files + for (i = 0; i < files.length; i++) { + file = files[i]; + + if (file.size !== undef) { + // We calculate totals based on original file size + total.size += file.origSize; + + // Since we cannot predict file size after resize, we do opposite and + // interpolate loaded amount to match magnitude of total + loaded = file.loaded * file.origSize / file.size; + + if (!file.completeTimestamp || file.completeTimestamp > startTime) { + loadedDuringCurrentSession += loaded; + } + + total.loaded += loaded; + } else { + total.size = undef; + } + + if (file.status == plupload.DONE) { + total.uploaded++; + } else if (file.status == plupload.FAILED) { + total.failed++; + } else { + total.queued++; + } + } + + // If we couldn't calculate a total file size then use the number of files to calc percent + if (total.size === undef) { + total.percent = files.length > 0 ? Math.ceil(total.uploaded / files.length * 100) : 0; + } else { + total.bytesPerSec = Math.ceil(loadedDuringCurrentSession / ((+new Date() - startTime || 1) / 1000.0)); + total.percent = total.size > 0 ? Math.ceil(total.loaded / total.size * 100) : 0; + } + } + + + function getRUID() { + var ctrl = fileInputs[0] || fileDrops[0]; + if (ctrl) { + return ctrl.getRuntime().uid; + } + return false; + } + + + function runtimeCan(file, cap) { + if (file.ruid) { + var info = Runtime.getInfo(file.ruid); + if (info) { + return info.can(cap); + } + } + return false; + } + + + function bindEventListeners() { + this.bind('FilesAdded FilesRemoved', function(up) { + up.trigger('QueueChanged'); + up.refresh(); + }); + + this.bind('CancelUpload', onCancelUpload); + + this.bind('BeforeUpload', onBeforeUpload); + + this.bind('UploadFile', onUploadFile); + + this.bind('UploadProgress', onUploadProgress); + + this.bind('StateChanged', onStateChanged); + + this.bind('QueueChanged', calc); + + this.bind('Error', onError); + + this.bind('FileUploaded', onFileUploaded); + + this.bind('Destroy', onDestroy); + } + + + function initControls(settings, cb) { + var self = this, inited = 0, queue = []; + + // common settings + var options = { + runtime_order: settings.runtimes, + required_caps: settings.required_features, + preferred_caps: preferred_caps, + swf_url: settings.flash_swf_url, + xap_url: settings.silverlight_xap_url + }; + + // add runtime specific options if any + plupload.each(settings.runtimes.split(/\s*,\s*/), function(runtime) { + if (settings[runtime]) { + options[runtime] = settings[runtime]; + } + }); + + // initialize file pickers - there can be many + if (settings.browse_button) { + plupload.each(settings.browse_button, function(el) { + queue.push(function(cb) { + var fileInput = new o.file.FileInput(plupload.extend({}, options, { + accept: settings.filters.mime_types, + name: settings.file_data_name, + multiple: settings.multi_selection, + container: settings.container, + browse_button: el + })); + + fileInput.onready = function() { + var info = Runtime.getInfo(this.ruid); + + // for backward compatibility + plupload.extend(self.features, { + chunks: info.can('slice_blob'), + multipart: info.can('send_multipart'), + multi_selection: info.can('select_multiple') + }); + + inited++; + fileInputs.push(this); + cb(); + }; + + fileInput.onchange = function() { + self.addFile(this.files); + }; + + fileInput.bind('mouseenter mouseleave mousedown mouseup', function(e) { + if (!disabled) { + if (settings.browse_button_hover) { + if ('mouseenter' === e.type) { + plupload.addClass(el, settings.browse_button_hover); + } else if ('mouseleave' === e.type) { + plupload.removeClass(el, settings.browse_button_hover); + } + } + + if (settings.browse_button_active) { + if ('mousedown' === e.type) { + plupload.addClass(el, settings.browse_button_active); + } else if ('mouseup' === e.type) { + plupload.removeClass(el, settings.browse_button_active); + } + } + } + }); + + fileInput.bind('mousedown', function() { + self.trigger('Browse'); + }); + + fileInput.bind('error runtimeerror', function() { + fileInput = null; + cb(); + }); + + fileInput.init(); + }); + }); + } + + // initialize drop zones + if (settings.drop_element) { + plupload.each(settings.drop_element, function(el) { + queue.push(function(cb) { + var fileDrop = new o.file.FileDrop(plupload.extend({}, options, { + drop_zone: el + })); + + fileDrop.onready = function() { + var info = Runtime.getInfo(this.ruid); + + // for backward compatibility + plupload.extend(self.features, { + chunks: info.can('slice_blob'), + multipart: info.can('send_multipart'), + dragdrop: info.can('drag_and_drop') + }); + + inited++; + fileDrops.push(this); + cb(); + }; + + fileDrop.ondrop = function() { + self.addFile(this.files); + }; + + fileDrop.bind('error runtimeerror', function() { + fileDrop = null; + cb(); + }); + + fileDrop.init(); + }); + }); + } + + + plupload.inSeries(queue, function() { + if (typeof(cb) === 'function') { + cb(inited); + } + }); + } + + + function resizeImage(blob, params, cb) { + var img = new o.image.Image(); + + try { + img.onload = function() { + // no manipulation required if... + if (params.width > this.width && + params.height > this.height && + params.quality === undef && + params.preserve_headers && + !params.crop + ) { + this.destroy(); + return cb(blob); + } + // otherwise downsize + img.downsize(params.width, params.height, params.crop, params.preserve_headers); + }; + + img.onresize = function() { + cb(this.getAsBlob(blob.type, params.quality)); + this.destroy(); + }; + + img.onerror = function() { + cb(blob); + }; + + img.load(blob); + } catch(ex) { + cb(blob); + } + } + + + function setOption(option, value, init) { + var self = this, reinitRequired = false; + + function _setOption(option, value, init) { + var oldValue = settings[option]; + + switch (option) { + case 'max_file_size': + if (option === 'max_file_size') { + settings.max_file_size = settings.filters.max_file_size = value; + } + break; + + case 'chunk_size': + if (value = plupload.parseSize(value)) { + settings[option] = value; + settings.send_file_name = true; + } + break; + + case 'multipart': + settings[option] = value; + if (!value) { + settings.send_file_name = true; + } + break; + + case 'http_method': + settings[option] = value.toUpperCase() === 'PUT' ? 'PUT' : 'POST'; + break; + + case 'unique_names': + settings[option] = value; + if (value) { + settings.send_file_name = true; + } + break; + + case 'filters': + // for sake of backward compatibility + if (plupload.typeOf(value) === 'array') { + value = { + mime_types: value + }; + } + + if (init) { + plupload.extend(settings.filters, value); + } else { + settings.filters = value; + } + + // if file format filters are being updated, regenerate the matching expressions + if (value.mime_types) { + if (plupload.typeOf(value.mime_types) === 'string') { + value.mime_types = o.core.utils.Mime.mimes2extList(value.mime_types); + } + + value.mime_types.regexp = (function(filters) { + var extensionsRegExp = []; + + plupload.each(filters, function(filter) { + plupload.each(filter.extensions.split(/,/), function(ext) { + if (/^\s*\*\s*$/.test(ext)) { + extensionsRegExp.push('\\.*'); + } else { + extensionsRegExp.push('\\.' + ext.replace(new RegExp('[' + ('/^$.*+?|()[]{}\\'.replace(/./g, '\\$&')) + ']', 'g'), '\\$&')); + } + }); + }); + + return new RegExp('(' + extensionsRegExp.join('|') + ')$', 'i'); + }(value.mime_types)); + + settings.filters.mime_types = value.mime_types; + } + break; + + case 'resize': + if (value) { + settings.resize = plupload.extend({ + preserve_headers: true, + crop: false + }, value); + } else { + settings.resize = false; + } + break; + + case 'prevent_duplicates': + settings.prevent_duplicates = settings.filters.prevent_duplicates = !!value; + break; + + // options that require reinitialisation + case 'container': + case 'browse_button': + case 'drop_element': + value = 'container' === option + ? plupload.get(value) + : plupload.getAll(value) + ; + + case 'runtimes': + case 'multi_selection': + case 'flash_swf_url': + case 'silverlight_xap_url': + settings[option] = value; + if (!init) { + reinitRequired = true; + } + break; + + default: + settings[option] = value; + } + + if (!init) { + self.trigger('OptionChanged', option, value, oldValue); + } + } + + if (typeof(option) === 'object') { + plupload.each(option, function(value, option) { + _setOption(option, value, init); + }); + } else { + _setOption(option, value, init); + } + + if (init) { + // Normalize the list of required capabilities + settings.required_features = normalizeCaps(plupload.extend({}, settings)); + + // Come up with the list of capabilities that can affect default mode in a multi-mode runtimes + preferred_caps = normalizeCaps(plupload.extend({}, settings, { + required_features: true + })); + } else if (reinitRequired) { + self.trigger('Destroy'); + + initControls.call(self, settings, function(inited) { + if (inited) { + self.runtime = Runtime.getInfo(getRUID()).type; + self.trigger('Init', { runtime: self.runtime }); + self.trigger('PostInit'); + } else { + self.trigger('Error', { + code : plupload.INIT_ERROR, + message : plupload.translate('Init error.') + }); + } + }); + } + } + + + // Internal event handlers + function onBeforeUpload(up, file) { + // Generate unique target filenames + if (up.settings.unique_names) { + var matches = file.name.match(/\.([^.]+)$/), ext = "part"; + if (matches) { + ext = matches[1]; + } + file.target_name = file.id + '.' + ext; + } + } + + + function onUploadFile(up, file) { + var url = up.settings.url + , chunkSize = up.settings.chunk_size + , retries = up.settings.max_retries + , features = up.features + , offset = 0 + , blob + ; + + // make sure we start at a predictable offset + if (file.loaded) { + offset = file.loaded = chunkSize ? chunkSize * Math.floor(file.loaded / chunkSize) : 0; + } + + function handleError() { + if (retries-- > 0) { + delay(uploadNextChunk, 1000); + } else { + file.loaded = offset; // reset all progress + + up.trigger('Error', { + code : plupload.HTTP_ERROR, + message : plupload.translate('HTTP Error.'), + file : file, + response : xhr.responseText, + status : xhr.status, + responseHeaders: xhr.getAllResponseHeaders() + }); + } + } + + function uploadNextChunk() { + var chunkBlob, args = {}, curChunkSize; + + // make sure that file wasn't cancelled and upload is not stopped in general + if (file.status !== plupload.UPLOADING || up.state === plupload.STOPPED) { + return; + } + + // send additional 'name' parameter only if required + if (up.settings.send_file_name) { + args.name = file.target_name || file.name; + } + + if (chunkSize && features.chunks && blob.size > chunkSize) { // blob will be of type string if it was loaded in memory + curChunkSize = Math.min(chunkSize, blob.size - offset); + chunkBlob = blob.slice(offset, offset + curChunkSize); + } else { + curChunkSize = blob.size; + chunkBlob = blob; + } + + // If chunking is enabled add corresponding args, no matter if file is bigger than chunk or smaller + if (chunkSize && features.chunks) { + // Setup query string arguments + if (up.settings.send_chunk_number) { + args.chunk = Math.ceil(offset / chunkSize); + args.chunks = Math.ceil(blob.size / chunkSize); + } else { // keep support for experimental chunk format, just in case + args.offset = offset; + args.total = blob.size; + } + } + + if (up.trigger('BeforeChunkUpload', file, args, chunkBlob, offset)) { + uploadChunk(args, chunkBlob, curChunkSize); + } + } + + function uploadChunk(args, chunkBlob, curChunkSize) { + var formData; + + xhr = new o.xhr.XMLHttpRequest(); + + // Do we have upload progress support + if (xhr.upload) { + xhr.upload.onprogress = function(e) { + file.loaded = Math.min(file.size, offset + e.loaded); + up.trigger('UploadProgress', file); + }; + } + + xhr.onload = function() { + // check if upload made itself through + if (xhr.status >= 400) { + handleError(); + return; + } + + retries = up.settings.max_retries; // reset the counter + + // Handle chunk response + if (curChunkSize < blob.size) { + chunkBlob.destroy(); + + offset += curChunkSize; + file.loaded = Math.min(offset, blob.size); + + up.trigger('ChunkUploaded', file, { + offset : file.loaded, + total : blob.size, + response : xhr.responseText, + status : xhr.status, + responseHeaders: xhr.getAllResponseHeaders() + }); + + // stock Android browser doesn't fire upload progress events, but in chunking mode we can fake them + if (plupload.ua.browser === 'Android Browser') { + // doesn't harm in general, but is not required anywhere else + up.trigger('UploadProgress', file); + } + } else { + file.loaded = file.size; + } + + chunkBlob = formData = null; // Free memory + + // Check if file is uploaded + if (!offset || offset >= blob.size) { + // If file was modified, destory the copy + if (file.size != file.origSize) { + blob.destroy(); + blob = null; + } + + up.trigger('UploadProgress', file); + + file.status = plupload.DONE; + file.completeTimestamp = +new Date(); + + up.trigger('FileUploaded', file, { + response : xhr.responseText, + status : xhr.status, + responseHeaders: xhr.getAllResponseHeaders() + }); + } else { + // Still chunks left + delay(uploadNextChunk, 1); // run detached, otherwise event handlers interfere + } + }; + + xhr.onerror = function() { + handleError(); + }; + + xhr.onloadend = function() { + this.destroy(); + xhr = null; + }; + + // Build multipart request + if (up.settings.multipart && features.multipart) { + xhr.open(up.settings.http_method, url, true); + + // Set custom headers + plupload.each(up.settings.headers, function(value, name) { + xhr.setRequestHeader(name, value); + }); + + formData = new o.xhr.FormData(); + + // Add multipart params + plupload.each(plupload.extend(args, up.settings.multipart_params), function(value, name) { + formData.append(name, value); + }); + + // Add file and send it + formData.append(up.settings.file_data_name, chunkBlob); + xhr.send(formData, { + runtime_order: up.settings.runtimes, + required_caps: up.settings.required_features, + preferred_caps: preferred_caps, + swf_url: up.settings.flash_swf_url, + xap_url: up.settings.silverlight_xap_url + }); + } else { + // if no multipart, send as binary stream + url = plupload.buildUrl(up.settings.url, plupload.extend(args, up.settings.multipart_params)); + + xhr.open(up.settings.http_method, url, true); + + // Set custom headers + plupload.each(up.settings.headers, function(value, name) { + xhr.setRequestHeader(name, value); + }); + + // do not set Content-Type, if it was defined previously (see #1203) + if (!xhr.hasRequestHeader('Content-Type')) { + xhr.setRequestHeader('Content-Type', 'application/octet-stream'); // Binary stream header + } + + xhr.send(chunkBlob, { + runtime_order: up.settings.runtimes, + required_caps: up.settings.required_features, + preferred_caps: preferred_caps, + swf_url: up.settings.flash_swf_url, + xap_url: up.settings.silverlight_xap_url + }); + } + } + + + blob = file.getSource(); + + // Start uploading chunks + if (!plupload.isEmptyObj(up.settings.resize) && runtimeCan(blob, 'send_binary_string') && plupload.inArray(blob.type, ['image/jpeg', 'image/png']) !== -1) { + // Resize if required + resizeImage.call(this, blob, up.settings.resize, function(resizedBlob) { + blob = resizedBlob; + file.size = resizedBlob.size; + uploadNextChunk(); + }); + } else { + uploadNextChunk(); + } + } + + + function onUploadProgress(up, file) { + calcFile(file); + } + + + function onStateChanged(up) { + if (up.state == plupload.STARTED) { + // Get start time to calculate bps + startTime = (+new Date()); + } else if (up.state == plupload.STOPPED) { + // Reset currently uploading files + for (var i = up.files.length - 1; i >= 0; i--) { + if (up.files[i].status == plupload.UPLOADING) { + up.files[i].status = plupload.QUEUED; + calc(); + } + } + } + } + + + function onCancelUpload() { + if (xhr) { + xhr.abort(); + } + } + + + function onFileUploaded(up) { + calc(); + + // Upload next file but detach it from the error event + // since other custom listeners might want to stop the queue + delay(function() { + uploadNext.call(up); + }, 1); + } + + + function onError(up, err) { + if (err.code === plupload.INIT_ERROR) { + up.destroy(); + } + // Set failed status if an error occured on a file + else if (err.code === plupload.HTTP_ERROR) { + err.file.status = plupload.FAILED; + err.file.completeTimestamp = +new Date(); + calcFile(err.file); + + // Upload next file but detach it from the error event + // since other custom listeners might want to stop the queue + if (up.state == plupload.STARTED) { // upload in progress + up.trigger('CancelUpload'); + delay(function() { + uploadNext.call(up); + }, 1); + } + } + } + + + function onDestroy(up) { + up.stop(); + + // Purge the queue + plupload.each(files, function(file) { + file.destroy(); + }); + files = []; + + if (fileInputs.length) { + plupload.each(fileInputs, function(fileInput) { + fileInput.destroy(); + }); + fileInputs = []; + } + + if (fileDrops.length) { + plupload.each(fileDrops, function(fileDrop) { + fileDrop.destroy(); + }); + fileDrops = []; + } + + preferred_caps = {}; + disabled = false; + startTime = xhr = null; + total.reset(); + } + + + // Default settings + settings = { + chunk_size: 0, + file_data_name: 'file', + filters: { + mime_types: [], + prevent_duplicates: false, + max_file_size: 0 + }, + flash_swf_url: 'js/Moxie.swf', + http_method: 'POST', + max_retries: 0, + multipart: true, + multi_selection: true, + resize: false, + runtimes: Runtime.order, + send_file_name: true, + send_chunk_number: true, + silverlight_xap_url: 'js/Moxie.xap' + }; + + + setOption.call(this, options, null, true); + + // Inital total state + total = new plupload.QueueProgress(); + + // Add public methods + plupload.extend(this, { + + /** + * Unique id for the Uploader instance. + * + * @property id + * @type String + */ + id : uid, + uid : uid, // mOxie uses this to differentiate between event targets + + /** + * Current state of the total uploading progress. This one can either be plupload.STARTED or plupload.STOPPED. + * These states are controlled by the stop/start methods. The default value is STOPPED. + * + * @property state + * @type Number + */ + state : plupload.STOPPED, + + /** + * Map of features that are available for the uploader runtime. Features will be filled + * before the init event is called, these features can then be used to alter the UI for the end user. + * Some of the current features that might be in this map is: dragdrop, chunks, jpgresize, pngresize. + * + * @property features + * @type Object + */ + features : {}, + + /** + * Current runtime name. + * + * @property runtime + * @type String + */ + runtime : null, + + /** + * Current upload queue, an array of File instances. + * + * @property files + * @type Array + * @see plupload.File + */ + files : files, + + /** + * Object with name/value settings. + * + * @property settings + * @type Object + */ + settings : settings, + + /** + * Total progess information. How many files has been uploaded, total percent etc. + * + * @property total + * @type plupload.QueueProgress + */ + total : total, + + + /** + * Initializes the Uploader instance and adds internal event listeners. + * + * @method init + */ + init : function() { + var self = this, opt, preinitOpt, err; + + preinitOpt = self.getOption('preinit'); + if (typeof(preinitOpt) == "function") { + preinitOpt(self); + } else { + plupload.each(preinitOpt, function(func, name) { + self.bind(name, func); + }); + } + + bindEventListeners.call(self); + + // Check for required options + plupload.each(['container', 'browse_button', 'drop_element'], function(el) { + if (self.getOption(el) === null) { + err = { + code : plupload.INIT_ERROR, + message : plupload.sprintf(plupload.translate("%s specified, but cannot be found."), el) + } + return false; + } + }); + + if (err) { + return self.trigger('Error', err); + } + + + if (!settings.browse_button && !settings.drop_element) { + return self.trigger('Error', { + code : plupload.INIT_ERROR, + message : plupload.translate("You must specify either browse_button or drop_element.") + }); + } + + + initControls.call(self, settings, function(inited) { + var initOpt = self.getOption('init'); + if (typeof(initOpt) == "function") { + initOpt(self); + } else { + plupload.each(initOpt, function(func, name) { + self.bind(name, func); + }); + } + + if (inited) { + self.runtime = Runtime.getInfo(getRUID()).type; + self.trigger('Init', { runtime: self.runtime }); + self.trigger('PostInit'); + } else { + self.trigger('Error', { + code : plupload.INIT_ERROR, + message : plupload.translate('Init error.') + }); + } + }); + }, + + /** + * Set the value for the specified option(s). + * + * @method setOption + * @since 2.1 + * @param {String|Object} option Name of the option to change or the set of key/value pairs + * @param {Mixed} [value] Value for the option (is ignored, if first argument is object) + */ + setOption: function(option, value) { + setOption.call(this, option, value, !this.runtime); // until runtime not set we do not need to reinitialize + }, + + /** + * Get the value for the specified option or the whole configuration, if not specified. + * + * @method getOption + * @since 2.1 + * @param {String} [option] Name of the option to get + * @return {Mixed} Value for the option or the whole set + */ + getOption: function(option) { + if (!option) { + return settings; + } + return settings[option]; + }, + + /** + * Refreshes the upload instance by dispatching out a refresh event to all runtimes. + * This would for example reposition flash/silverlight shims on the page. + * + * @method refresh + */ + refresh : function() { + if (fileInputs.length) { + plupload.each(fileInputs, function(fileInput) { + fileInput.trigger('Refresh'); + }); + } + this.trigger('Refresh'); + }, + + /** + * Starts uploading the queued files. + * + * @method start + */ + start : function() { + if (this.state != plupload.STARTED) { + this.state = plupload.STARTED; + this.trigger('StateChanged'); + + uploadNext.call(this); + } + }, + + /** + * Stops the upload of the queued files. + * + * @method stop + */ + stop : function() { + if (this.state != plupload.STOPPED) { + this.state = plupload.STOPPED; + this.trigger('StateChanged'); + this.trigger('CancelUpload'); + } + }, + + + /** + * Disables/enables browse button on request. + * + * @method disableBrowse + * @param {Boolean} disable Whether to disable or enable (default: true) + */ + disableBrowse : function() { + disabled = arguments[0] !== undef ? arguments[0] : true; + + if (fileInputs.length) { + plupload.each(fileInputs, function(fileInput) { + fileInput.disable(disabled); + }); + } + + this.trigger('DisableBrowse', disabled); + }, + + /** + * Returns the specified file object by id. + * + * @method getFile + * @param {String} id File id to look for. + * @return {plupload.File} File object or undefined if it wasn't found; + */ + getFile : function(id) { + var i; + for (i = files.length - 1; i >= 0; i--) { + if (files[i].id === id) { + return files[i]; + } + } + }, + + /** + * Adds file to the queue programmatically. Can be native file, instance of Plupload.File, + * instance of mOxie.File, input[type="file"] element, or array of these. Fires FilesAdded, + * if any files were added to the queue. Otherwise nothing happens. + * + * @method addFile + * @since 2.0 + * @param {plupload.File|mOxie.File|File|Node|Array} file File or files to add to the queue. + * @param {String} [fileName] If specified, will be used as a name for the file + */ + addFile : function(file, fileName) { + var self = this + , queue = [] + , filesAdded = [] + , ruid + ; + + function filterFile(file, cb) { + var queue = []; + plupload.each(self.settings.filters, function(rule, name) { + if (fileFilters[name]) { + queue.push(function(cb) { + fileFilters[name].call(self, rule, file, function(res) { + cb(!res); + }); + }); + } + }); + plupload.inSeries(queue, cb); + } + + /** + * @method resolveFile + * @private + * @param {moxie.file.File|moxie.file.Blob|plupload.File|File|Blob|input[type="file"]} file + */ + function resolveFile(file) { + var type = plupload.typeOf(file); + + // moxie.file.File + if (file instanceof o.file.File) { + if (!file.ruid && !file.isDetached()) { + if (!ruid) { // weird case + return false; + } + file.ruid = ruid; + file.connectRuntime(ruid); + } + resolveFile(new plupload.File(file)); + } + // moxie.file.Blob + else if (file instanceof o.file.Blob) { + resolveFile(file.getSource()); + file.destroy(); + } + // plupload.File - final step for other branches + else if (file instanceof plupload.File) { + if (fileName) { + file.name = fileName; + } + + queue.push(function(cb) { + // run through the internal and user-defined filters, if any + filterFile(file, function(err) { + if (!err) { + // make files available for the filters by updating the main queue directly + files.push(file); + // collect the files that will be passed to FilesAdded event + filesAdded.push(file); + + self.trigger("FileFiltered", file); + } + delay(cb, 1); // do not build up recursions or eventually we might hit the limits + }); + }); + } + // native File or blob + else if (plupload.inArray(type, ['file', 'blob']) !== -1) { + resolveFile(new o.file.File(null, file)); + } + // input[type="file"] + else if (type === 'node' && plupload.typeOf(file.files) === 'filelist') { + // if we are dealing with input[type="file"] + plupload.each(file.files, resolveFile); + } + // mixed array of any supported types (see above) + else if (type === 'array') { + fileName = null; // should never happen, but unset anyway to avoid funny situations + plupload.each(file, resolveFile); + } + } + + ruid = getRUID(); + + resolveFile(file); + + if (queue.length) { + plupload.inSeries(queue, function() { + // if any files left after filtration, trigger FilesAdded + if (filesAdded.length) { + self.trigger("FilesAdded", filesAdded); + } + }); + } + }, + + /** + * Removes a specific file. + * + * @method removeFile + * @param {plupload.File|String} file File to remove from queue. + */ + removeFile : function(file) { + var id = typeof(file) === 'string' ? file : file.id; + + for (var i = files.length - 1; i >= 0; i--) { + if (files[i].id === id) { + return this.splice(i, 1)[0]; + } + } + }, + + /** + * Removes part of the queue and returns the files removed. This will also trigger the FilesRemoved and QueueChanged events. + * + * @method splice + * @param {Number} start (Optional) Start index to remove from. + * @param {Number} length (Optional) Lengh of items to remove. + * @return {Array} Array of files that was removed. + */ + splice : function(start, length) { + // Splice and trigger events + var removed = files.splice(start === undef ? 0 : start, length === undef ? files.length : length); + + // if upload is in progress we need to stop it and restart after files are removed + var restartRequired = false; + if (this.state == plupload.STARTED) { // upload in progress + plupload.each(removed, function(file) { + if (file.status === plupload.UPLOADING) { + restartRequired = true; // do not restart, unless file that is being removed is uploading + return false; + } + }); + + if (restartRequired) { + this.stop(); + } + } + + this.trigger("FilesRemoved", removed); + + // Dispose any resources allocated by those files + plupload.each(removed, function(file) { + file.destroy(); + }); + + if (restartRequired) { + this.start(); + } + + return removed; + }, + + /** + Dispatches the specified event name and its arguments to all listeners. + + @method trigger + @param {String} name Event name to fire. + @param {Object..} Multiple arguments to pass along to the listener functions. + */ + + // override the parent method to match Plupload-like event logic + dispatchEvent: function(type) { + var list, args, result; + + type = type.toLowerCase(); + + list = this.hasEventListener(type); + + if (list) { + // sort event list by priority + list.sort(function(a, b) { return b.priority - a.priority; }); + + // first argument should be current plupload.Uploader instance + args = [].slice.call(arguments); + args.shift(); + args.unshift(this); + + for (var i = 0; i < list.length; i++) { + // Fire event, break chain if false is returned + if (list[i].fn.apply(list[i].scope, args) === false) { + return false; + } + } + } + return true; + }, + + /** + Check whether uploader has any listeners to the specified event. + + @method hasEventListener + @param {String} name Event name to check for. + */ + + + /** + Adds an event listener by name. + + @method bind + @param {String} name Event name to listen for. + @param {function} fn Function to call ones the event gets fired. + @param {Object} [scope] Optional scope to execute the specified function in. + @param {Number} [priority=0] Priority of the event handler - handlers with higher priorities will be called first + */ + bind: function(name, fn, scope, priority) { + // adapt moxie EventTarget style to Plupload-like + plupload.Uploader.prototype.bind.call(this, name, fn, priority, scope); + }, + + /** + Removes the specified event listener. + + @method unbind + @param {String} name Name of event to remove. + @param {function} fn Function to remove from listener. + */ + + /** + Removes all event listeners. + + @method unbindAll + */ + + + /** + * Destroys Plupload instance and cleans after itself. + * + * @method destroy + */ + destroy : function() { + this.trigger('Destroy'); + settings = total = null; // purge these exclusively + this.unbindAll(); + } + }); +}; + +plupload.Uploader.prototype = o.core.EventTarget.instance; + +/** + * Constructs a new file instance. + * + * @class File + * @constructor + * + * @param {Object} file Object containing file properties + * @param {String} file.name Name of the file. + * @param {Number} file.size File size. + */ +plupload.File = (function() { + var filepool = {}; + + function PluploadFile(file) { + + plupload.extend(this, { + + /** + * File id this is a globally unique id for the specific file. + * + * @property id + * @type String + */ + id: plupload.guid(), + + /** + * File name for example "myfile.gif". + * + * @property name + * @type String + */ + name: file.name || file.fileName, + + /** + * File type, `e.g image/jpeg` + * + * @property type + * @type String + */ + type: file.type || '', + + /** + * File size in bytes (may change after client-side manupilation). + * + * @property size + * @type Number + */ + size: file.size || file.fileSize, + + /** + * Original file size in bytes. + * + * @property origSize + * @type Number + */ + origSize: file.size || file.fileSize, + + /** + * Number of bytes uploaded of the files total size. + * + * @property loaded + * @type Number + */ + loaded: 0, + + /** + * Number of percentage uploaded of the file. + * + * @property percent + * @type Number + */ + percent: 0, + + /** + * Status constant matching the plupload states QUEUED, UPLOADING, FAILED, DONE. + * + * @property status + * @type Number + * @see plupload + */ + status: plupload.QUEUED, + + /** + * Date of last modification. + * + * @property lastModifiedDate + * @type {String} + */ + lastModifiedDate: file.lastModifiedDate || (new Date()).toLocaleString(), // Thu Aug 23 2012 19:40:00 GMT+0400 (GET) + + + /** + * Set when file becomes plupload.DONE or plupload.FAILED. Is used to calculate proper plupload.QueueProgress.bytesPerSec. + * @private + * @property completeTimestamp + * @type {Number} + */ + completeTimestamp: 0, + + /** + * Returns native window.File object, when it's available. + * + * @method getNative + * @return {window.File} or null, if plupload.File is of different origin + */ + getNative: function() { + var file = this.getSource().getSource(); + return plupload.inArray(plupload.typeOf(file), ['blob', 'file']) !== -1 ? file : null; + }, + + /** + * Returns mOxie.File - unified wrapper object that can be used across runtimes. + * + * @method getSource + * @return {mOxie.File} or null + */ + getSource: function() { + if (!filepool[this.id]) { + return null; + } + return filepool[this.id]; + }, + + /** + * Destroys plupload.File object. + * + * @method destroy + */ + destroy: function() { + var src = this.getSource(); + if (src) { + src.destroy(); + delete filepool[this.id]; + } + } + }); + + filepool[this.id] = file; + } + + return PluploadFile; +}()); + + +/** + * Constructs a queue progress. + * + * @class QueueProgress + * @constructor + */ + plupload.QueueProgress = function() { + var self = this; // Setup alias for self to reduce code size when it's compressed + + /** + * Total queue file size. + * + * @property size + * @type Number + */ + self.size = 0; + + /** + * Total bytes uploaded. + * + * @property loaded + * @type Number + */ + self.loaded = 0; + + /** + * Number of files uploaded. + * + * @property uploaded + * @type Number + */ + self.uploaded = 0; + + /** + * Number of files failed to upload. + * + * @property failed + * @type Number + */ + self.failed = 0; + + /** + * Number of files yet to be uploaded. + * + * @property queued + * @type Number + */ + self.queued = 0; + + /** + * Total percent of the uploaded bytes. + * + * @property percent + * @type Number + */ + self.percent = 0; + + /** + * Bytes uploaded per second. + * + * @property bytesPerSec + * @type Number + */ + self.bytesPerSec = 0; + + /** + * Resets the progress to its initial values. + * + * @method reset + */ + self.reset = function() { + self.size = self.loaded = self.uploaded = self.failed = self.queued = self.percent = self.bytesPerSec = 0; + }; +}; + +exports.plupload = plupload; + +}(this, moxie)); + +})); \ No newline at end of file diff --git a/static/js/uploader/qiniu.js b/static/js/uploader/qiniu.js new file mode 100644 index 00000000..1f959b2f --- /dev/null +++ b/static/js/uploader/qiniu.js @@ -0,0 +1,1831 @@ +/*! + * qiniu-js-sdk v@VERSION + * + * Copyright 2015 by Qiniu + * Released under GPL V2 License. + * + * GitHub: http://github.com/qiniu/js-sdk + * + * Date: @DATE +*/ +/*! + * + * Rebuild By Aaron@2018 + * +*/ +/*global plupload ,mOxie*/ +/*global ActiveXObject */ +/*exported Qiniu */ +/*exported QiniuJsSDK */ + +function getCookieByString(cookieName){ + var start = document.cookie.indexOf(cookieName+'='); + if (start == -1) return false; + start = start+cookieName.length+1; + var end = document.cookie.indexOf(';', start); + if (end == -1) end=document.cookie.length; + return document.cookie.substring(start, end); +} +;(function( global ){ + +/** + * Creates new cookie or removes cookie with negative expiration + * @param key The key or identifier for the store + * @param value Contents of the store + * @param exp Expiration - creation defaults to 30 days + */ +function createCookie(key, value, exp) { + var date = new Date(); + date.setTime(date.getTime() + (exp * 24 * 60 * 60 * 1000)); + var expires = "; expires=" + date.toGMTString(); + document.cookie = key + "=" + value + expires + "; path=/"; +} + +/** + * Returns contents of cookie + * @param key The key or identifier for the store + */ +function readCookie(key) { + var nameEQ = key + "="; + var ca = document.cookie.split(';'); + for (var i = 0, max = ca.length; i < max; i++) { + var c = ca[i]; + while (c.charAt(0) === ' ') { + c = c.substring(1, c.length); + } + if (c.indexOf(nameEQ) === 0) { + return c.substring(nameEQ.length, c.length); + } + } + return null; +} + +// if current browser is not support localStorage +// use cookie to make a polyfill +if ( !window.localStorage ) { + window.localStorage = { + setItem: function (key, value) { + createCookie(key, value, 30); + }, + getItem: function (key) { + return readCookie(key); + }, + removeItem: function (key) { + createCookie(key, '', -1); + } + }; +} + +function QiniuJsSDK() { + + var that = this; + + /** + * detect IE version + * if current browser is not IE + * it will return false + * else + * it will return version of current IE browser + * @return {Number|Boolean} IE version or false + */ + this.detectIEVersion = function() { + var v = 4, + div = document.createElement('div'), + all = div.getElementsByTagName('i'); + while ( + div.innerHTML = '', + all[0] + ) { + v++; + } + return v > 4 ? v : false; + }; + + var logger = { + MUTE: 0, + FATA: 1, + ERROR: 2, + WARN: 3, + INFO: 4, + DEBUG: 5, + TRACE: 6, + level: 0 + }; + + function log(type, args){ + var header = "[Cloudreve-uploader]["+type+"]"; + var msg = header; + for (var i = 0; i < args.length; i++) { + if (typeof args[i] === "string") { + msg += " " + args[i]; + } else { + msg += " " + that.stringifyJSON(args[i]); + } + } + if (that.detectIEVersion()) { + // http://stackoverflow.com/questions/5538972/console-log-apply-not-working-in-ie9 + //var log = Function.prototype.bind.call(console.log, console); + //log.apply(console, args); + console.log(msg); + }else{ + args.unshift(header); + console.log.apply(console, args); + } + if (document.getElementById('qiniu-js-sdk-log')) { + document.getElementById('qiniu-js-sdk-log').innerHTML += '

'+msg+'

'; + } + } + + function makeLogFunc(code){ + var func = code.toLowerCase(); + logger[func] = function(){ + // logger[func].history = logger[func].history || []; + // logger[func].history.push(arguments); + if(window.console && window.console.log && logger.level>=logger[code]){ + var args = Array.prototype.slice.call(arguments); + log(func,args); + } + }; + } + + for (var property in logger){ + if (logger.hasOwnProperty(property) && (typeof logger[property]) === "number" && !logger.hasOwnProperty(property.toLowerCase())) { + makeLogFunc(property); + } + } + + + var qiniuUploadUrl; + + + /** + * qiniu upload urls + * 'qiniuUploadUrls' is used to change target when current url is not avaliable + * @type {Array} + */ + if(uploadConfig.saveType == "qiniu"){ + if (window.location.protocol === 'https:') { + qiniuUploadUrl = 'https://up.qbox.me'; + } else { + qiniuUploadUrl = 'http://upload.qiniu.com'; + } + var qiniuUploadUrls = [ + "http://upload.qiniu.com", + "http://up.qiniu.com" + ]; + + var qiniuUpHosts = { + "http": [ + "http://upload.qiniu.com", + "http://up.qiniu.com" + ], + "https": [ + "https://up.qbox.me" + ] + }; +}else if(uploadConfig.saveType == "local" || uploadConfig.saveType == "oss" ||uploadConfig.saveType == "upyun"||uploadConfig.saveType == "s3"|| uploadConfig.saveType == "remote"||uploadConfig.saveType == "onedrive"){ + qiniuUploadUrl = uploadConfig.upUrl; + var qiniuUploadUrls = [uploadConfig.upUrl,]; + var qiniuUpHosts = { + "http": [uploadConfig.upUrl,], + "https": [uploadConfig.upUrl,] + } +} + + var changeUrlTimes = 0; + + /** + * reset upload url + * if current page protocal is https + * it will always return 'https://up.qbox.me' + * else + * it will set 'qiniuUploadUrl' value with 'qiniuUploadUrls' looply + */ + this.resetUploadUrl = function(){ + var hosts = window.location.protocol === 'https:' ? qiniuUpHosts.https : qiniuUpHosts.http; + var i = changeUrlTimes % hosts.length; + qiniuUploadUrl = hosts[i]; + changeUrlTimes++; + logger.debug('resetUploadUrl: '+qiniuUploadUrl); + }; + + // this.resetUploadUrl(); + + + /** + * is image + * @param {String} url of a file + * @return {Boolean} file is a image or not + */ + this.isImage = function(url) { + url = url.split(/[?#]/)[0]; + return (/\.(png|jpg|jpeg|gif|bmp)$/i).test(url); + }; + + /** + * get file extension + * @param {String} filename + * @return {String} file extension + * @example + * input: test.txt + * output: txt + */ + this.getFileExtension = function(filename) { + var tempArr = filename.split("."); + var ext; + if (tempArr.length === 1 || (tempArr[0] === "" && tempArr.length === 2)) { + ext = ""; + } else { + ext = tempArr.pop().toLowerCase(); //get the extension and make it lower-case + } + return ext; + }; + + /** + * encode string by utf8 + * @param {String} string to encode + * @return {String} encoded string + */ + this.utf8_encode = function(argString) { + // http://kevin.vanzonneveld.net + // + original by: Webtoolkit.info (http://www.webtoolkit.info/) + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + improved by: sowberry + // + tweaked by: Jack + // + bugfixed by: Onno Marsman + // + improved by: Yves Sucaet + // + bugfixed by: Onno Marsman + // + bugfixed by: Ulrich + // + bugfixed by: Rafal Kukawski + // + improved by: kirilloid + // + bugfixed by: kirilloid + // * example 1: this.utf8_encode('Kevin van Zonneveld'); + // * returns 1: 'Kevin van Zonneveld' + + if (argString === null || typeof argString === 'undefined') { + return ''; + } + + var string = (argString + ''); // .replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + var utftext = '', + start, end, stringl = 0; + + start = end = 0; + stringl = string.length; + for (var n = 0; n < stringl; n++) { + var c1 = string.charCodeAt(n); + var enc = null; + + if (c1 < 128) { + end++; + } else if (c1 > 127 && c1 < 2048) { + enc = String.fromCharCode( + (c1 >> 6) | 192, (c1 & 63) | 128 + ); + } else if (c1 & 0xF800 ^ 0xD800 > 0) { + enc = String.fromCharCode( + (c1 >> 12) | 224, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128 + ); + } else { // surrogate pairs + if (c1 & 0xFC00 ^ 0xD800 > 0) { + throw new RangeError('Unmatched trail surrogate at ' + n); + } + var c2 = string.charCodeAt(++n); + if (c2 & 0xFC00 ^ 0xDC00 > 0) { + throw new RangeError('Unmatched lead surrogate at ' + (n - 1)); + } + c1 = ((c1 & 0x3FF) << 10) + (c2 & 0x3FF) + 0x10000; + enc = String.fromCharCode( + (c1 >> 18) | 240, ((c1 >> 12) & 63) | 128, ((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; + }; + + this.base64_decode = function (data) { + // http://kevin.vanzonneveld.net + // + original by: Tyler Akins (http://rumkin.com) + // + improved by: Thunder.m + // + input by: Aman Gupta + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + bugfixed by: Onno Marsman + // + bugfixed by: Pellentesque Malesuada + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + input by: Brett Zamir (http://brett-zamir.me) + // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // * example 1: base64_decode('S2V2aW4gdmFuIFpvbm5ldmVsZA=='); + // * returns 1: 'Kevin van Zonneveld' + // mozilla has this native + // - but breaks in 2.0.0.12! + //if (typeof this.window['atob'] == 'function') { + // return atob(data); + //} + var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, + ac = 0, + dec = "", + tmp_arr = []; + + if (!data) { + return data; + } + + data += ''; + + do { // unpack four hexets into three octets using index points in b64 + h1 = b64.indexOf(data.charAt(i++)); + h2 = b64.indexOf(data.charAt(i++)); + h3 = b64.indexOf(data.charAt(i++)); + h4 = b64.indexOf(data.charAt(i++)); + + bits = h1 << 18 | h2 << 12 | h3 << 6 | h4; + + o1 = bits >> 16 & 0xff; + o2 = bits >> 8 & 0xff; + o3 = bits & 0xff; + + if (h3 === 64) { + tmp_arr[ac++] = String.fromCharCode(o1); + } else if (h4 === 64) { + tmp_arr[ac++] = String.fromCharCode(o1, o2); + } else { + tmp_arr[ac++] = String.fromCharCode(o1, o2, o3); + } + } while (i < data.length); + + dec = tmp_arr.join(''); + + return dec; + }; + + /** + * encode data by base64 + * @param {String} data to encode + * @return {String} encoded data + */ + this.base64_encode = function(data) { + // http://kevin.vanzonneveld.net + // + original by: Tyler Akins (http://rumkin.com) + // + improved by: Bayron Guevara + // + improved by: Thunder.m + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + bugfixed by: Pellentesque Malesuada + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // - depends on: this.utf8_encode + // * example 1: this.base64_encode('Kevin van Zonneveld'); + // * returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA==' + // mozilla has this native + // - but breaks in 2.0.0.12! + //if (typeof this.window['atob'] == 'function') { + // return atob(data); + //} + var b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, + ac = 0, + enc = '', + tmp_arr = []; + + if (!data) { + return data; + } + + data = this.utf8_encode(data + ''); + + do { // pack three octets into four hexets + o1 = data.charCodeAt(i++); + o2 = data.charCodeAt(i++); + o3 = data.charCodeAt(i++); + + bits = o1 << 16 | o2 << 8 | o3; + + h1 = bits >> 18 & 0x3f; + h2 = bits >> 12 & 0x3f; + h3 = bits >> 6 & 0x3f; + h4 = bits & 0x3f; + + // use hexets to index into b64, and append result to encoded string + tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4); + } while (i < data.length); + + enc = tmp_arr.join(''); + + switch (data.length % 3) { + case 1: + enc = enc.slice(0, -2) + '=='; + break; + case 2: + enc = enc.slice(0, -1) + '='; + break; + } + + return enc; + }; + + /** + * encode string in url by base64 + * @param {String} string in url + * @return {String} encoded string + */ + this.URLSafeBase64Encode = function(v) { + v = this.base64_encode(v); + return v.replace(/\//g, '_').replace(/\+/g, '-'); + }; + + this.URLSafeBase64Decode = function(v) { + v = v.replace(/_/g, '/').replace(/-/g, '+'); + return this.base64_decode(v); + }; + + // TODO: use mOxie + /** + * craete object used to AJAX + * @return {Object} + */ + this.createAjax = function(argument) { + var xmlhttp = {}; + if (window.XMLHttpRequest) { + xmlhttp = new XMLHttpRequest(); + } else { + xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); + } + return xmlhttp; + }; + + // TODO: enhance IE compatibility + /** + * parse json string to javascript object + * @param {String} json string + * @return {Object} object + */ + this.parseJSON = function(data) { + // Attempt to parse using the native JSON parser first + if (window.JSON && window.JSON.parse) { + return window.JSON.parse(data); + } + + //var rx_one = /^[\],:{}\s]*$/, + // rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, + // rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, + // rx_four = /(?:^|:|,)(?:\s*\[)+/g, + var rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + + //var json; + + var text = String(data); + rx_dangerous.lastIndex = 0; + if(rx_dangerous.test(text)){ + text = text.replace(rx_dangerous, function(a){ + return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }); + } + + // todo 使用一下判断,增加安全性 + //if ( + // rx_one.test( + // text + // .replace(rx_two, '@') + // .replace(rx_three, ']') + // .replace(rx_four, '') + // ) + //) { + // return eval('(' + text + ')'); + //} + + return eval('('+text+')'); + }; + + /** + * parse javascript object to json string + * @param {Object} object + * @return {String} json string + */ + this.stringifyJSON = function(obj) { + // Attempt to parse using the native JSON parser first + if (window.JSON && window.JSON.stringify) { + return window.JSON.stringify(obj); + } + switch (typeof (obj)) { + case 'string': + return '"' + obj.replace(/(["\\])/g, '\\$1') + '"'; + case 'array': + return '[' + obj.map(that.stringifyJSON).join(',') + ']'; + case 'object': + if (obj instanceof Array) { + var strArr = []; + var len = obj.length; + for (var i = 0; i < len; i++) { + strArr.push(that.stringifyJSON(obj[i])); + } + return '[' + strArr.join(',') + ']'; + } else if (obj === null) { + return 'null'; + } else { + var string = []; + for (var property in obj) { + if (obj.hasOwnProperty(property)) { + string.push(that.stringifyJSON(property) + ':' + that.stringifyJSON(obj[property])); + } + } + return '{' + string.join(',') + '}'; + } + break; + case 'number': + return obj; + case false: + return obj; + case 'boolean': + return obj; + } + }; + + /** + * trim space beside text + * @param {String} untrimed string + * @return {String} trimed string + */ + this.trim = function(text) { + return text === null ? "" : text.replace(/^\s+|\s+$/g, ''); + }; + + /** + * create a uploader by QiniuJsSDK + * @param {object} options to create a new uploader + * @return {object} uploader + */ + this.uploader = function(op) { + + /********** inner function define start **********/ + + // according the different condition to reset chunk size + // and the upload strategy according with the chunk size + // when chunk size is zero will cause to direct upload + // see the statement binded on 'BeforeUpload' event + var reset_chunk_size = function() { + var ie = that.detectIEVersion(); + var BLOCK_BITS, MAX_CHUNK_SIZE, chunk_size; + // case Safari 5、Windows 7、iOS 7 set isSpecialSafari to true + var isSpecialSafari = (moxie.core.utils.Env.browser === "Safari" && moxie.core.utils.Env.version <= 5 && moxie.core.utils.Env.os === "Windows" && moxie.core.utils.Env.osVersion === "7") || (moxie.core.utils.Env.browser === "Safari" && moxie.core.utils.Env.os === "iOS" && moxie.core.utils.Env.osVersion === "7"); + // case IE 9-,chunk_size is not empty and flash is included in runtimes + // set op.chunk_size to zero + //if (ie && ie < 9 && op.chunk_size && op.runtimes.indexOf('flash') >= 0) { + if (ie && ie < 9 && op.chunk_size && op.runtimes.indexOf('flash') >= 0) { + // link: http://www.plupload.com/docs/Frequently-Asked-Questions#when-to-use-chunking-and-when-not + // when plupload chunk_size setting is't null ,it cause bug in ie8/9 which runs flash runtimes (not support html5) . + op.chunk_size = 0; + } else if (isSpecialSafari) { + // win7 safari / iOS7 safari have bug when in chunk upload mode + // reset chunk_size to 0 + // disable chunk in special version safari + op.chunk_size = 0; + } else { + BLOCK_BITS = 20; + MAX_CHUNK_SIZE = 4 << BLOCK_BITS; //4M + + chunk_size = plupload.parseSize(op.chunk_size); + if (chunk_size > MAX_CHUNK_SIZE) { + op.chunk_size = MAX_CHUNK_SIZE; + } + // qiniu service max_chunk_size is 4m + // reset chunk_size to max_chunk_size(4m) when chunk_size > 4m + } + // if op.chunk_size set 0 will be cause to direct upload + }; + + var getHosts = function(hosts) { + var result = []; + for (var i = 0; i < hosts.length; i++) { + var host = hosts[i]; + if (host.indexOf('-H') === 0) { + result.push(host.split(' ')[2]); + } else { + result.push(host); + } + } + return result; + }; + + var getPutPolicy = function (uptoken) { + if(uploadConfig.saveType =="oss" || uploadConfig.saveType =="upyun"||uploadConfig.saveType =="s3"||uploadConfig.saveType =="remote"){ + return "oss"; + }else{ + var segments = uptoken.split(":"); + var ak = segments[0]; + var putPolicy = that.parseJSON(that.URLSafeBase64Decode(segments[2])); + putPolicy.ak = ak; + if (putPolicy.scope.indexOf(":") >= 0) { + putPolicy.bucket = putPolicy.scope.split(":")[0]; + putPolicy.key = putPolicy.scope.split(":")[1]; + } else { + putPolicy.bucket = putPolicy.scope; + } + return putPolicy; + } + }; + + var getUpHosts = function(uptoken) { + var putPolicy = getPutPolicy(uptoken); + // var uphosts_url = "//uc.qbox.me/v1/query?ak="+ak+"&bucket="+putPolicy.scope; + // IE 9- is not support protocal relative url + var uphosts_url = window.location.protocol + "//uc.qbox.me/v1/query?ak=" + putPolicy.ak + "&bucket=" + putPolicy.bucket; + logger.debug("putPolicy: ", putPolicy); + logger.debug("get uphosts from: ", uphosts_url); + var ie = that.detectIEVersion(); + var ajax; + if (ie && ie <= 9) { + ajax = new moxie.XMLHttpRequest(); + moxie.core.utils.Env.swf_url = op.flash_swf_url; + }else{ + ajax = that.createAjax(); + } + if(uploadConfig.saveType != "qiniu"){ + qiniuUpHosts.http = [uploadConfig.upUrl]; + qiniuUpHosts.http = [uploadConfig.upUrl]; + that.resetUploadUrl(); + }else{ + ajax.open('GET', uphosts_url, false); + var onreadystatechange = function(){ + logger.debug("ajax.readyState: ", ajax.readyState); + if (ajax.readyState === 4) { + logger.debug("ajax.status: ", ajax.status); + if (ajax.status < 400) { + var res = that.parseJSON(ajax.responseText); + qiniuUpHosts.http = getHosts(res.http.up); + qiniuUpHosts.https = getHosts(res.https.up); + logger.debug("get new uphosts: ", qiniuUpHosts); + that.resetUploadUrl(); + } else { + logger.error("get uphosts error: ", ajax.responseText); + } + } + }; + if (ie && ie <= 9) { + ajax.bind('readystatechange', onreadystatechange); + }else{ + ajax.onreadystatechange = onreadystatechange; + } + ajax.send(); + // ajax.send(); + // if (ajax.status < 400) { + // var res = that.parseJSON(ajax.responseText); + // qiniuUpHosts.http = getHosts(res.http.up); + // qiniuUpHosts.https = getHosts(res.https.up); + // logger.debug("get new uphosts: ", qiniuUpHosts); + // that.resetUploadUrl(); + // } else { + // logger.error("get uphosts error: ", ajax.responseText); + // } + } + return; + }; + + var getUptoken = function(file) { + if(uploadConfig.saveType == "remote"){ + return that.token; + } + if (!that.token || (op.uptoken_url && that.tokenInfo.isExpired())) { + return getNewUpToken(file); + } else { + return that.token; + } + }; + + // getNewUptoken maybe called at Init Event or BeforeUpload Event + // case Init Event, the file param of getUptken will be set a null value + // if op.uptoken has value, set uptoken with op.uptoken + // else if op.uptoken_url has value, set uptoken from op.uptoken_url + // else if op.uptoken_func has value, set uptoken by result of op.uptoken_func + var getNewUpToken = function(file) { + if (op.uptoken) { + that.token = op.uptoken; + } else if (op.uptoken_url) { + logger.debug("get uptoken from: ", that.uptoken_url); + // TODO: use mOxie + var ajax = that.createAjax(); + ajax.open('GET', that.uptoken_url, false); + ajax.setRequestHeader("If-Modified-Since", "0"); + // ajax.onreadystatechange = function() { + // if (ajax.readyState === 4 && ajax.status === 200) { + // var res = that.parseJSON(ajax.responseText); + // that.token = res.uptoken; + // } + // }; + ajax.send(); + if (ajax.status === 200) { + var res = that.parseJSON(ajax.responseText); + that.token = res.uptoken; + if (uploadConfig.saveType == "oss"){ + var putPolicy = that.token; + that.sign = res.sign; + that.access = res.id; + that.file_name = res.key; + that.callback = res.callback; + }else if(uploadConfig.saveType == "s3"){ + var putPolicy = that.token; + that.sign = res.sign; + that.policy = res.policy; + that.file_name = res.key; + that.credential = res.credential; + that.x_amz_date = res.x_amz_date; + that.surl = res.siteUrl; + that.callbackKey = res.callBackKey; + }else if(uploadConfig.saveType == "upyun"){ + var putPolicy = that.token; + that.token = res.token; + that.policy = res.policy; + }else if(uploadConfig.saveType == "remote"){ + var putPolicy = that.token; + that.policy = res.uptoken; + }else{ + var segments = that.token.split(":"); + var putPolicy = that.parseJSON(that.URLSafeBase64Decode(segments[2])); + if (!that.tokenMap) { + that.tokenMap = {}; + } + var getTimestamp = function(time) { + return Math.ceil(time.getTime()/1000); + }; + var serverTime = getTimestamp(new Date(ajax.getResponseHeader("date"))); + var clientTime = getTimestamp(new Date()); + that.tokenInfo = { + serverDelay: clientTime - serverTime, + deadline: putPolicy.deadline, + isExpired: function() { + var leftTime = this.deadline - getTimestamp(new Date()) + this.serverDelay; + return leftTime < 600; + } + }; + logger.debug("get token info: ", that.tokenInfo); + } + + logger.debug("get new uptoken: ", that.token); + + } else { + logger.error("get uptoken error: ", ajax.responseText); + } + } else if (op.uptoken_func) { + logger.debug("get uptoken from uptoken_func"); + that.token = op.uptoken_func(file); + logger.debug("get new uptoken: ", that.token); + } else { + logger.error("one of [uptoken, uptoken_url, uptoken_func] settings in options is required!"); + } + if (that.token) { + getUpHosts(that.token); + } + return that.token; + }; + + // get file key according with the user passed options + var getFileKey = function(up, file, func) { + // WARNING + // When you set the key in putPolicy by "scope": "bucket:key" + // You should understand the risk of override a file in the bucket + // So the code below that automatically get key from uptoken has been commented + // var putPolicy = getPutPolicy(that.token) + // if (putPolicy.key) { + // logger.debug("key is defined in putPolicy.scope: ", putPolicy.key) + // return putPolicy.key + // } + var key = '', + unique_names = false; + if (!op.save_key) { + unique_names = up.getOption && up.getOption('unique_names'); + unique_names = unique_names || (up.settings && up.settings.unique_names); + if (unique_names) { + var ext = that.getFileExtension(file.name); + key = ext ? file.id + '.' + ext : file.id; + } else if (typeof func === 'function') { + key = func(up, file); + } else { + key = file.name; + } + } + if(uploadConfig.saveType == "qiniu"){ + return ""; + } + return key; + }; + + /********** inner function define end **********/ + + if (op.log_level) { + logger.level = op.log_level; + } + + if (!op.domain) { + throw 'domain setting in options is required!'; + } + + if (!op.browse_button) { + throw 'browse_button setting in options is required!'; + } + + if (!op.uptoken && !op.uptoken_url && !op.uptoken_func) { + throw 'one of [uptoken, uptoken_url, uptoken_func] settings in options is required!'; + } + + logger.debug("init uploader start"); + + logger.debug("environment: ", moxie.core.utils.Env); + + logger.debug("userAgent: ", navigator.userAgent); + + var option = {}; + + // hold the handler from user passed options + var _Error_Handler = op.init && op.init.Error; + var _FileUploaded_Handler = op.init && op.init.FileUploaded; + + // replace the handler for intercept + op.init.Error = function() {}; + op.init.FileUploaded = function() {}; + + that.uptoken_url = op.uptoken_url; + that.token = ''; + that.key_handler = typeof op.init.Key === 'function' ? op.init.Key : ''; + this.domain = op.domain; + // TODO: ctx is global in scope of a uploader instance + // this maybe cause error + var ctx = ''; + var speedCalInfo = { + isResumeUpload: false, + resumeFilesize: 0, + startTime: '', + currentTime: '' + }; + + reset_chunk_size(); + logger.debug("invoke reset_chunk_size()"); + logger.debug("op.chunk_size: ", op.chunk_size); + + var defaultSetting = { + url: qiniuUploadUrl, + multipart_params: { + token: '' + } + }; + var ie = that.detectIEVersion(); + // case IE 9- + // add accept in multipart params + if (ie && ie <= 9) { + defaultSetting.multipart_params.accept = 'text/plain; charset=utf-8'; + logger.debug("add accept text/plain in multipart params"); + } + + // compose options with user passed options and default setting + plupload.extend(option, op, defaultSetting); + + logger.debug("option: ", option); + + // create a new uploader with composed options + var uploader = new plupload.Uploader(option); + + logger.debug("new plupload.Uploader(option)"); + + // bind getNewUpToken to 'Init' event + uploader.bind('Init', function(up, params) { + logger.debug("Init event activated"); + // if op.get_new_uptoken is not true + // invoke getNewUptoken when uploader init + // else + // getNewUptoken everytime before a new file upload + if(!op.get_new_uptoken){ + getNewUpToken(null); + } + //getNewUpToken(null); + }); + + logger.debug("bind Init event"); + + // bind 'FilesAdded' event + // when file be added and auto_start has set value + // uploader will auto start upload the file + uploader.bind('FilesAdded', function(up, files) { + logger.debug("FilesAdded event activated"); + var auto_start = up.getOption && up.getOption('auto_start'); + auto_start = auto_start || (up.settings && up.settings.auto_start); + logger.debug("auto_start: ", auto_start); + logger.debug("files: ", files); + + + for (var i = 0; i < files.length; i++) { + var file = files[i]; + file.path = decodeURIComponent(getCookieByString("path_tmp")); + + } + + // detect is iOS + var is_ios = function (){ + if(moxie.core.utils.Env.OS.toLowerCase()==="ios") { + return true; + } else { + return false; + } + }; + + // if current env os is iOS change file name to [time].[ext] + if (is_ios()) { + for (var i = 0; i < files.length; i++) { + var file = files[i]; + var ext = that.getFileExtension(file.name); + file.name = file.id + "." + ext; + } + } + + if (auto_start) { + setTimeout(function(){ + up.start(); + logger.debug("invoke up.start()"); + }, 0); + // up.start(); + // plupload.each(files, function(i, file) { + // up.start(); + // logger.debug("invoke up.start()") + // logger.debug("file: ", file); + // }); + } + up.refresh(); // Reposition Flash/Silverlight + }); + + logger.debug("bind FilesAdded event"); + + // bind 'BeforeUpload' event + // intercept the process of upload + // - prepare uptoken + // - according the chunk size to make differnt upload strategy + // - resume upload with the last breakpoint of file + uploader.bind('BeforeUpload', function(up, file) { + logger.debug("BeforeUpload event activated"); + getNewUpToken(file); + // add a key named speed for file object + file.speed = file.speed || 0; + ctx = ''; + + var directUpload = function(up, file, func) { + speedCalInfo.startTime = new Date().getTime(); + + + + var multipart_params_obj; + if (op.save_key) { + multipart_params_obj = { + 'token': that.token + }; + } else { + multipart_params_obj = { + 'key': getFileKey(up, file, func), + 'token': that.token + }; + if(uploadConfig.saveType == "qiniu"){ + multipart_params_obj = { + 'token': that.token, + 'x:path': file.path + }; + }else if(uploadConfig.saveType == "local"||uploadConfig.saveType == "onedrive"){ + multipart_params_obj = { + 'path': file.path + }; + }else if(uploadConfig.saveType == "remote"){ + multipart_params_obj = { + 'path': file.path, + "token" :that.policy, + "MAX_FILE_SIZE":4194304, + }; + }else if(uploadConfig.saveType == "oss"){ + multipart_params_obj = { + 'policy': that.token, + 'x:path': file.path, + "signature":that.sign, + "OSSAccessKeyId":that.access, + 'x:fname':file.name, + "key" :that.file_name.replace("${filename}", file.name), + "callback":that.callback, + }; + }else if(uploadConfig.saveType == "s3"){ + multipart_params_obj = { + 'policy': that.policy, + "key" :that.file_name+"/"+(file.path.replace(",","/") == "" ? "" : file.path.replace(",","/")+"/")+file.name, + "success_action_redirect" :that.surl+"Callback/S3/key/"+that.callbackKey, + "x-amz-algorithm":"AWS4-HMAC-SHA256", + "x-amz-credential":that.credential, + "x-amz-date":that.x_amz_date, + "x-amz-signature":that.sign, + "Content-Type":file.type, + }; + }else if(uploadConfig.saveType == "upyun"){ + multipart_params_obj = { + 'authorization': that.token, + 'policy': that.policy, + }; + } + } + var ie = that.detectIEVersion(); + // case IE 9- + // add accept in multipart params + if (ie && ie <= 9) { + multipart_params_obj.accept = 'text/plain; charset=utf-8'; + logger.debug("add accept text/plain in multipart params"); + } + + logger.debug("directUpload multipart_params_obj: ", multipart_params_obj); + + var x_vars = op.x_vars; + if (x_vars !== undefined && typeof x_vars === 'object') { + for (var x_key in x_vars) { + if (x_vars.hasOwnProperty(x_key)) { + if (typeof x_vars[x_key] === 'function') { + multipart_params_obj['x:' + x_key] = x_vars[x_key](up, file); + } else if (typeof x_vars[x_key] !== 'object') { + multipart_params_obj['x:' + x_key] = x_vars[x_key]; + } + } + } + } + + up.setOption({ + 'url': qiniuUploadUrl, + 'multipart': true, + 'chunk_size': is_android_weixin_or_qq() ? op.max_file_size : undefined, + 'multipart_params': multipart_params_obj + }); + }; + + // detect is weixin or qq inner browser + var is_android_weixin_or_qq = function (){ + var ua = navigator.userAgent.toLowerCase(); + if((ua.match(/MicroMessenger/i) || moxie.core.utils.Env.browser === "QQBrowser" || ua.match(/V1_AND_SQ/i)) && moxie.core.utils.Env.OS.toLowerCase()==="android") { + return true; + } else { + return false; + } + }; + + var chunk_size = up.getOption && up.getOption('chunk_size'); + chunk_size = chunk_size || (up.settings && up.settings.chunk_size); + + logger.debug("uploader.runtime: ",uploader.runtime); + logger.debug("chunk_size: ",chunk_size); + + // TODO: flash support chunk upload + if ((uploader.runtime === 'html5' || uploader.runtime === 'flash') && chunk_size) { + if (file.size < chunk_size || is_android_weixin_or_qq()) { + logger.debug("directUpload because file.size < chunk_size || is_android_weixin_or_qq()"); + // direct upload if file size is less then the chunk size + directUpload(up, file, that.key_handler); + } else { + // TODO: need a polifill to make it work in IE 9- + // ISSUE: if file.name is existed in localStorage + // but not the same file maybe cause error + var localFileInfo = localStorage.getItem(file.name); + var blockSize = chunk_size; + if (localFileInfo) { + // TODO: although only the html5 runtime will enter this statement + // but need uniform way to make convertion between string and json + localFileInfo = that.parseJSON(localFileInfo); + var now = (new Date()).getTime(); + var before = localFileInfo.time || 0; + var aDay = 24 * 60 * 60 * 1000; // milliseconds of one day + // if the last upload time is within one day + // will upload continuously follow the last breakpoint + // else + // will reupload entire file + if (now - before < aDay) { + + if (localFileInfo.percent !== 100) { + if (file.size === localFileInfo.total) { + // TODO: if file.name and file.size is the same + // but not the same file will cause error + file.percent = localFileInfo.percent; + file.loaded = localFileInfo.offset; + ctx = localFileInfo.ctx; + + // set speed info + speedCalInfo.isResumeUpload = true; + speedCalInfo.resumeFilesize = localFileInfo.offset; + + // set block size + if (localFileInfo.offset + blockSize > file.size) { + blockSize = file.size - localFileInfo.offset; + } + } else { + // remove file info when file.size is conflict with file info + localStorage.removeItem(file.name); + } + + } else { + // remove file info when upload percent is 100% + // avoid 499 bug + localStorage.removeItem(file.name); + } + } else { + // remove file info when last upload time is over one day + localStorage.removeItem(file.name); + } + } + speedCalInfo.startTime = new Date().getTime(); + var multipart_params_obj = {}; + var ie = that.detectIEVersion(); + // case IE 9- + // add accept in multipart params + if (ie && ie <= 9) { + multipart_params_obj.accept = 'text/plain; charset=utf-8'; + logger.debug("add accept text/plain in multipart params"); + } + // TODO: to support bput + // http://developer.qiniu.com/docs/v6/api/reference/up/bput.html + if(uploadConfig.saveType == "remote"){ + up.setOption({ + 'url': qiniuUploadUrl + 'chunk.php', + 'multipart': false, + 'chunk_size': chunk_size, + 'required_features': "chunks", + 'headers': { + 'Authorization': getUptoken(file) + }, + 'multipart_params': multipart_params_obj + }); + }else{ + up.setOption({ + 'url': qiniuUploadUrl + '/mkblk/' + blockSize, + 'multipart': false, + 'chunk_size': chunk_size, + 'required_features': "chunks", + 'headers': { + 'Authorization': 'UpToken ' + getUptoken(file) + }, + 'multipart_params': multipart_params_obj + }); + } + } + } else { + logger.debug("directUpload because uploader.runtime !== 'html5' || uploader.runtime !== 'flash' || !chunk_size"); + // direct upload if runtime is not html5 + directUpload(up, file, that.key_handler); + } + }); + + logger.debug("bind BeforeUpload event"); + + // bind 'UploadProgress' event + // calculate upload speed + uploader.bind('UploadProgress', function(up, file) { + logger.trace("UploadProgress event activated"); + speedCalInfo.currentTime = new Date().getTime(); + var timeUsed = speedCalInfo.currentTime - speedCalInfo.startTime; // ms + var fileUploaded = file.loaded || 0; + if (speedCalInfo.isResumeUpload) { + fileUploaded = file.loaded - speedCalInfo.resumeFilesize; + } + file.speed = (fileUploaded / timeUsed * 1000).toFixed(0) || 0; // unit: byte/s + }); + + logger.debug("bind UploadProgress event"); + + // bind 'ChunkUploaded' event + // store the chunk upload info and set next chunk upload url + uploader.bind('ChunkUploaded', function(up, file, info) { + logger.debug("ChunkUploaded event activated"); + logger.debug("file: ", file); + logger.debug("info: ", info); + var res = that.parseJSON(info.response); + logger.debug("res: ", res); + // ctx should look like '[chunk01_ctx],[chunk02_ctx],[chunk03_ctx],...' + ctx = ctx ? ctx + ',' + res.ctx : res.ctx; + var leftSize = info.total - info.offset; + var chunk_size = up.getOption && up.getOption('chunk_size'); + chunk_size = chunk_size || (up.settings && up.settings.chunk_size); + if (leftSize < chunk_size) { + up.setOption({ + 'url': qiniuUploadUrl + '/mkblk/' + leftSize + }); + if(uploadConfig.saveType == "remote"){ + up.setOption({ + 'url': qiniuUploadUrl + 'chunk.php' + }); + } + logger.debug("up.setOption url: ", qiniuUploadUrl + '/mkblk/' + leftSize); + } + if(uploadConfig.saveType == "remote"){ + up.setOption({ + 'headers': { + 'Authorization': getUptoken(file) + } + }); + }else{ + up.setOption({ + 'headers': { + 'Authorization': 'UpToken ' + getUptoken(file) + } + }); + } + localStorage.setItem(file.name, that.stringifyJSON({ + ctx: ctx, + percent: file.percent, + total: info.total, + offset: info.offset, + time: (new Date()).getTime() + })); + }); + + logger.debug("bind ChunkUploaded event"); + + var retries = qiniuUploadUrls.length; + + // if error is unkown switch upload url and retry + var unknow_error_retry = function(file){ + if (retries-- > 0) { + setTimeout(function(){ + that.resetUploadUrl(); + file.status = plupload.QUEUED; + uploader.stop(); + uploader.start(); + }, 0); + return true; + }else{ + retries = qiniuUploadUrls.length; + return false; + } + }; + + // bind 'Error' event + // check the err.code and return the errTip + uploader.bind('Error', (function(_Error_Handler) { + return function(up, err) { + logger.error("Error event activated"); + logger.error("err: ", err); + var errTip = ''; + var file = err.file; + if (file) { + switch (err.code) { + case plupload.FAILED: + errTip = '上传失败。请稍后再试。'; + break; + case plupload.FILE_SIZE_ERROR: + var max_file_size = up.getOption && up.getOption('max_file_size'); + max_file_size = max_file_size || (up.settings && up.settings.max_file_size); + errTip = '文件过大,您当前用户组最多可上传' + max_file_size + '的文件'; + break; + case plupload.FILE_EXTENSION_ERROR: + errTip = '您当前的用户组不可上传此文件'; + break; + case plupload.HTTP_ERROR: + if (err.response === '') { + // Fix parseJSON error ,when http error is like net::ERR_ADDRESS_UNREACHABLE + errTip = err.message || '未知网络错误。'; + if (!unknow_error_retry(file)) { + return; + } + break; + } + if(uploadConfig.saveType == "oss"){ + var str = err.response + var a = $.parseXML(str); + $(a).find('Error').each(function () { + errTip = $(this).children('Message').text(); + var errorText = "Error"; + }); + if(err.status == 203){ + errTip = "上传失败,请检查空间容量、是否重名"; + } + }else if(uploadConfig.saveType == "s3" && err.status!=401){ + var str = err.response + var a = $.parseXML(str); + $(a).find('Error').each(function () { + errTip = $(this).children('Message').text(); + var errorText = "Error"; + }); + }else{ + var errorObj = that.parseJSON(err.response); + var errorText = errorObj.error; + if (err.status==579){ + var errorObj2 = that.parseJSON(errorText); + errorText=errorObj2.error; + } + switch (err.status) { + case 400: + errTip = "请求报文格式错误。"; + break; + case 401: + errTip = "客户端认证授权失败。请重试或提交反馈。"; + break; + case 405: + errTip = "客户端请求错误。请重试或提交反馈。"; + break; + case 579: + errTip = "资源上传成功,但回调失败。"; + break; + case 599: + errTip = "网络连接异常。请重试或提交反馈。"; + if (!unknow_error_retry(file)) { + return; + } + break; + case 614: + errTip = "文件已存在。"; + try { + errorObj = that.parseJSON(errorObj.error); + errorText = errorObj.error || 'file exists'; + } catch (e) { + errorText = errorObj.error || 'file exists'; + } + break; + case 631: + errTip = "指定空间不存在。"; + break; + case 701: + errTip = "上传数据块校验出错。请重试或提交反馈。"; + break; + default: + errTip = "未知错误。"; + if (!unknow_error_retry(file)) { + return; + } + break; + } + + } + errTip = errTip + '(' + err.status + ':' + errorText + ')'; + break; + case plupload.SECURITY_ERROR: + errTip = '安全配置错误。请联系网站管理员。'; + break; + case plupload.GENERIC_ERROR: + errTip = '上传失败。请稍后再试。'; + break; + case plupload.IO_ERROR: + errTip = '上传失败。请稍后再试。'; + break; + case plupload.INIT_ERROR: + errTip = '网站配置错误。请联系网站管理员。'; + uploader.destroy(); + break; + default: + errTip = err.message + err.details; + if (!unknow_error_retry(file)) { + return; + } + break; + } + if (_Error_Handler) { + _Error_Handler(up, err, errTip); + } + } + up.refresh(); // Reposition Flash/Silverlight + }; + })(_Error_Handler)); + + logger.debug("bind Error event"); + + // bind 'FileUploaded' event + // intercept the complete of upload + // - get downtoken from downtoken_url if bucket is private + // - invoke mkfile api to compose chunks if upload strategy is chunk upload + uploader.bind('FileUploaded', (function(_FileUploaded_Handler) { + return function(up, file, info) { + logger.debug("FileUploaded event activated"); + logger.debug("file: ", file); + logger.debug("info: ", info); + if(uploadConfig.saveType == "s3"){ + } + var last_step = function(up, file, info) { + if (op.downtoken_url) { + // if op.dowontoken_url is not empty + // need get downtoken before invoke the _FileUploaded_Handler + var ajax_downtoken = that.createAjax(); + ajax_downtoken.open('POST', op.downtoken_url, true); + ajax_downtoken.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + ajax_downtoken.onreadystatechange = function() { + if (ajax_downtoken.readyState === 4) { + + if (ajax_downtoken.status === 200 || ajax_downtoken.status === 204||ajax_downtoken.status === 303) { + + var res_downtoken; + try { + res_downtoken = that.parseJSON(ajax_downtoken.responseText); + } catch (e) { + throw ('invalid json format'); + } + var info_extended = {}; + plupload.extend(info_extended, that.parseJSON(info), res_downtoken); + if (_FileUploaded_Handler) { + _FileUploaded_Handler(up, file, that.stringifyJSON(info_extended)); + } + + } else { + uploader.trigger('Error', { + status: ajax_downtoken.status, + response: ajax_downtoken.responseText, + file: file, + code: plupload.HTTP_ERROR + }); + } + } + }; + ajax_downtoken.send('key=' + that.parseJSON(info).key + '&domain=' + op.domain); + } else if (_FileUploaded_Handler) { + _FileUploaded_Handler(up, file, info); + } + }; + if(uploadConfig.saveType == "oss" || uploadConfig.saveType == "upyun"){ + ctx = 0; + }else{ + var res = that.parseJSON(info.response); + ctx = ctx ? ctx : res.ctx; + } + // if ctx is not empty + // that means the upload strategy is chunk upload + // befroe the invoke the last_step + // we need request the mkfile to compose all uploaded chunks + // else + // invalke the last_step + logger.debug("ctx: ", ctx); + if (ctx) { + var key = ''; + logger.debug("save_key: ", op.save_key); + if (!op.save_key) { + key = getFileKey(up, file, that.key_handler); + key = key ? '/key/' + that.URLSafeBase64Encode(key) : ''; + } + + var fname = '/fname/' + that.URLSafeBase64Encode(file.name); + if(uploadConfig.saveType=="remote"){ + if (!op.save_key) { + key = getFileKey(up, file, that.key_handler); + key = key ? that.URLSafeBase64Encode(key) : ''; + } + fname = '' + that.URLSafeBase64Encode(file.name); + op.x_vars= { + 'path': file.path, + }; + } + logger.debug("op.x_vars: ", op.x_vars); + if(uploadConfig.saveType == "qiniu"){ + op.x_vars= { + 'path': file.path, + }; + } + var x_vars = op.x_vars, + x_val = '', + x_vars_url = ''; + if (x_vars !== undefined && typeof x_vars === 'object') { + for (var x_key in x_vars) { + if (x_vars.hasOwnProperty(x_key)) { + if (typeof x_vars[x_key] === 'function') { + x_val = that.URLSafeBase64Encode(x_vars[x_key](up, file)); + } else if (typeof x_vars[x_key] !== 'object') { + x_val = that.URLSafeBase64Encode(x_vars[x_key]); + } + x_vars_url += '/x:' + x_key + '/' + x_val; + } + } + } + local_path = ""; + if(uploadConfig.saveType == "local"||uploadConfig.saveType == "onedrive"){ + pathTmp = file.path; + if(file.path == ""){ + pathTmp = "ROOTDIR"; + } + local_path = '/path/'+that.URLSafeBase64Encode(pathTmp); + } + if(uploadConfig.saveType == "remote"){ + pathTmp = file.path; + local_path = that.URLSafeBase64Encode(pathTmp); + var url = qiniuUploadUrl + 'mkfile.php?size=' + file.size +"&key="+ key+"&fname="+ fname +"&path="+local_path; + }else{ + var url = qiniuUploadUrl + '/mkfile/' + file.size + key + fname + x_vars_url+local_path; + } + var ie = that.detectIEVersion(); + var ajax; + if (ie && ie <= 9) { + ajax = new moxie.xhr.XMLHttpRequest(); + moxie.core.utils.Env.swf_url = op.flash_swf_url; + }else{ + ajax = that.createAjax(); + } + ajax.open('POST', url, true); + ajax.setRequestHeader('Content-Type', 'text/plain;charset=UTF-8'); + if(uploadConfig.saveType == "remote"){ + ajax.setRequestHeader('Authorization',that.token); + }else{ + ajax.setRequestHeader('Authorization', 'UpToken ' + that.token); + } + var onreadystatechange = function(){ + logger.debug("ajax.readyState: ", ajax.readyState); + if (ajax.readyState === 4) { + localStorage.removeItem(file.name); + var info; + if (ajax.status === 200) { + info = ajax.responseText; + logger.debug("mkfile is success: ", info); + last_step(up, file, info); + } else { + info = { + status: ajax.status, + response: ajax.responseText, + file: file, + code: -200, + responseHeaders: ajax.getAllResponseHeaders() + }; + logger.debug("mkfile is error: ", info); + uploader.trigger('Error', info); + } + } + }; + if (ie && ie <= 9) { + ajax.bind('readystatechange', onreadystatechange); + }else{ + ajax.onreadystatechange = onreadystatechange; + } + ajax.send(ctx); + logger.debug("mkfile: ", url); + } else { + last_step(up, file, info.response); + } + + }; + })(_FileUploaded_Handler)); + + logger.debug("bind FileUploaded event"); + + // init uploader + uploader.init(); + + logger.debug("invoke uploader.init()"); + + logger.debug("init uploader end"); + + return uploader; + }; + + /** + * get url by key + * @param {String} key of file + * @return {String} url of file + */ + this.getUrl = function(key) { + if (!key) { + return false; + } + key = encodeURI(key); + var domain = this.domain; + if (domain.slice(domain.length - 1) !== '/') { + domain = domain + '/'; + } + return domain + key; + }; + + /** + * invoke the imageView2 api of Qiniu + * @param {Object} api params + * @param {String} key of file + * @return {String} url of processed image + */ + this.imageView2 = function(op, key) { + + if (!/^\d$/.test(op.mode)) { + return false; + } + + var mode = op.mode, + w = op.w || '', + h = op.h || '', + q = op.q || '', + format = op.format || ''; + + if (!w && !h) { + return false; + } + + var imageUrl = 'imageView2/' + mode; + imageUrl += w ? '/w/' + w : ''; + imageUrl += h ? '/h/' + h : ''; + imageUrl += q ? '/q/' + q : ''; + imageUrl += format ? '/format/' + format : ''; + if (key) { + imageUrl = this.getUrl(key) + '?' + imageUrl; + } + return imageUrl; + }; + + /** + * invoke the imageMogr2 api of Qiniu + * @param {Object} api params + * @param {String} key of file + * @return {String} url of processed image + */ + this.imageMogr2 = function(op, key) { + var auto_orient = op['auto-orient'] || '', + thumbnail = op.thumbnail || '', + strip = op.strip || '', + gravity = op.gravity || '', + crop = op.crop || '', + quality = op.quality || '', + rotate = op.rotate || '', + format = op.format || '', + blur = op.blur || ''; + //Todo check option + + var imageUrl = 'imageMogr2'; + + imageUrl += auto_orient ? '/auto-orient' : ''; + imageUrl += thumbnail ? '/thumbnail/' + thumbnail : ''; + imageUrl += strip ? '/strip' : ''; + imageUrl += gravity ? '/gravity/' + gravity : ''; + imageUrl += quality ? '/quality/' + quality : ''; + imageUrl += crop ? '/crop/' + crop : ''; + imageUrl += rotate ? '/rotate/' + rotate : ''; + imageUrl += format ? '/format/' + format : ''; + imageUrl += blur ? '/blur/' + blur : ''; + + if (key) { + imageUrl = this.getUrl(key) + '?' + imageUrl; + } + return imageUrl; + }; + + /** + * invoke the watermark api of Qiniu + * @param {Object} api params + * @param {String} key of file + * @return {String} url of processed image + */ + this.watermark = function(op, key) { + var mode = op.mode; + if (!mode) { + return false; + } + + var imageUrl = 'watermark/' + mode; + + if (mode === 1) { + var image = op.image || ''; + if (!image) { + return false; + } + imageUrl += image ? '/image/' + this.URLSafeBase64Encode(image) : ''; + } else if (mode === 2) { + var text = op.text ? op.text : '', + font = op.font ? op.font : '', + fontsize = op.fontsize ? op.fontsize : '', + fill = op.fill ? op.fill : ''; + if (!text) { + return false; + } + imageUrl += text ? '/text/' + this.URLSafeBase64Encode(text) : ''; + imageUrl += font ? '/font/' + this.URLSafeBase64Encode(font) : ''; + imageUrl += fontsize ? '/fontsize/' + fontsize : ''; + imageUrl += fill ? '/fill/' + this.URLSafeBase64Encode(fill) : ''; + } else { + // Todo mode3 + return false; + } + + var dissolve = op.dissolve || '', + gravity = op.gravity || '', + dx = op.dx || '', + dy = op.dy || ''; + + imageUrl += dissolve ? '/dissolve/' + dissolve : ''; + imageUrl += gravity ? '/gravity/' + gravity : ''; + imageUrl += dx ? '/dx/' + dx : ''; + imageUrl += dy ? '/dy/' + dy : ''; + + if (key) { + imageUrl = this.getUrl(key) + '?' + imageUrl; + } + return imageUrl; + }; + + /** + * invoke the imageInfo api of Qiniu + * @param {String} key of file + * @return {Object} image info + */ + this.imageInfo = function(key) { + if (!key) { + return false; + } + var url = this.getUrl(key) + '?imageInfo'; + var xhr = this.createAjax(); + var info; + var that = this; + xhr.open('GET', url, false); + xhr.onreadystatechange = function() { + if (xhr.readyState === 4 && xhr.status === 200) { + info = that.parseJSON(xhr.responseText); + } + }; + xhr.send(); + return info; + }; + + /** + * invoke the exif api of Qiniu + * @param {String} key of file + * @return {Object} image exif + */ + this.exif = function(key) { + if (!key) { + return false; + } + var url = this.getUrl(key) + '?exif'; + var xhr = this.createAjax(); + var info; + var that = this; + xhr.open('GET', url, false); + xhr.onreadystatechange = function() { + if (xhr.readyState === 4 && xhr.status === 200) { + info = that.parseJSON(xhr.responseText); + } + }; + xhr.send(); + return info; + }; + + /** + * invoke the exif or imageInfo api of Qiniu + * according with type param + * @param {String} ['exif'|'imageInfo']type of info + * @param {String} key of file + * @return {Object} image exif or info + */ + this.get = function(type, key) { + if (!key || !type) { + return false; + } + if (type === 'exif') { + return this.exif(key); + } else if (type === 'imageInfo') { + return this.imageInfo(key); + } + return false; + }; + + /** + * invoke api of Qiniu like a pipeline + * @param {Array of Object} params of a series api call + * each object in array is options of api which name is set as 'fop' property + * each api's output will be next api's input + * @param {String} key of file + * @return {String|Boolean} url of processed image + */ + this.pipeline = function(arr, key) { + var isArray = Object.prototype.toString.call(arr) === '[object Array]'; + var option, errOp, imageUrl = ''; + if (isArray) { + for (var i = 0, len = arr.length; i < len; i++) { + option = arr[i]; + if (!option.fop) { + return false; + } + switch (option.fop) { + case 'watermark': + imageUrl += this.watermark(option) + '|'; + break; + case 'imageView2': + imageUrl += this.imageView2(option) + '|'; + break; + case 'imageMogr2': + imageUrl += this.imageMogr2(option) + '|'; + break; + default: + errOp = true; + break; + } + if (errOp) { + return false; + } + } + if (key) { + imageUrl = this.getUrl(key) + '?' + imageUrl; + var length = imageUrl.length; + if (imageUrl.slice(length - 1) === '|') { + imageUrl = imageUrl.slice(0, length - 1); + } + } + return imageUrl; + } + return false; + }; +} + +var Qiniu = new QiniuJsSDK(); + +global.Qiniu = Qiniu; + +global.QiniuJsSDK = QiniuJsSDK; + +})( window ); \ No newline at end of file diff --git a/static/js/uploader/ui.js b/static/js/uploader/ui.js new file mode 100644 index 00000000..9f78b446 --- /dev/null +++ b/static/js/uploader/ui.js @@ -0,0 +1,278 @@ +/*global plupload */ +/*global qiniu */ +function FileProgress(file, targetID) { + this.fileProgressID = file.id; + this.file = file; + + this.opacity = 100; + this.height = 0; + this.fileProgressWrapper = $('#' + this.fileProgressID); + if (!this.fileProgressWrapper.length) { + //
+ //
+ // 20% Complete + //
+ //
+ + this.fileProgressWrapper = $(''); + var Wrappeer = this.fileProgressWrapper; + Wrappeer.attr('id', this.fileProgressID).addClass('progressContainer'); + + var progressText = $(""); + progressText.addClass('progressName').text(file.name); + + + var fileSize = plupload.formatSize(file.size).toUpperCase(); + var progressSize = $(""); + progressSize.addClass("progressFileSize").text(fileSize); + + var progressBarTd = $(""); + var progressBarBox = $("
"); + progressBarBox.addClass('info'); + var progressBarWrapper = $("
"); + progressBarWrapper.addClass("progress progress-striped"); + + var progressBar = $("
"); + progressBar.addClass("progress-bar progress-bar-info") + .attr('role', 'progressbar') + .attr('aria-valuemax', 100) + .attr('aria-valuenow', 0) + .attr('aria-valuein', 0) + .width('0%'); + + var progressBarPercent = $(''); + progressBarPercent.text(fileSize); + + var progressCancel = $(''); + progressCancel.show().addClass('progressCancel').text('×'); + + progressBar.append(progressBarPercent); + progressBarWrapper.append(progressBar); + progressBarBox.append(progressBarWrapper); + progressText.append(progressCancel); + + var progressBarStatus = $('
'); + progressBarBox.append(progressBarStatus); + progressBarTd.append(progressBarBox); + + Wrappeer.append(progressText); + Wrappeer.append(progressSize); + Wrappeer.append(progressBarTd); + + $('#' + targetID).append(Wrappeer); + } else { + this.reset(); + } + + this.height = this.fileProgressWrapper.offset().top; + this.setTimer(null); +} + +FileProgress.prototype.setTimer = function(timer) { + this.fileProgressWrapper.FP_TIMER = timer; +}; + +FileProgress.prototype.getTimer = function(timer) { + return this.fileProgressWrapper.FP_TIMER || null; +}; + +FileProgress.prototype.reset = function() { + this.fileProgressWrapper.attr('class', "progressContainer"); + this.fileProgressWrapper.find('td .progress .progress-bar-info').attr('aria-valuenow', 0).width('0%').find('span').text(''); + this.appear(); +}; + +FileProgress.prototype.setChunkProgess = function(chunk_size) { + var chunk_amount = Math.ceil(this.file.size / chunk_size); + if (chunk_amount === 1) { + return false; + } + + var viewProgess = $(''); + + var progressBarChunkTr = $(''); + var progressBarChunk = $('
'); + for (var i = 1; i <= chunk_amount; i++) { + var col = $('
'); + var progressBarWrapper = $('
"); + progressBar.addClass("progress-bar progress-bar-info text-left") + .attr('role', 'progressbar') + .attr('aria-valuemax', 100) + .attr('aria-valuenow', 0) + .attr('aria-valuein', 0) + .width('0%') + .attr('id', this.file.id + '_' + i) + .text(''); + + var progressBarStatus = $(''); + progressBarStatus.addClass('chunk-status').text(); + + progressBarWrapper.append(progressBar); + progressBarWrapper.append(progressBarStatus); + + col.append(progressBarWrapper); + progressBarChunk.append(col); + } + + if(!this.fileProgressWrapper.find('td:eq(2) .btn-default').length){ + this.fileProgressWrapper.find('td>div').append(viewProgess); + } + progressBarChunkTr.hide().find('td').append(progressBarChunk); + progressBarChunkTr.insertAfter(this.fileProgressWrapper); + +}; + +FileProgress.prototype.setProgress = function(percentage, speed, chunk_size) { + this.fileProgressWrapper.attr('class', "progressContainer green"); + + var file = this.file; + var uploaded = file.loaded; + + var size = plupload.formatSize(uploaded).toUpperCase(); + var formatSpeed = plupload.formatSize(speed).toUpperCase(); + var progressbar = this.fileProgressWrapper.find('td .progress').find('.progress-bar-info'); + if (this.fileProgressWrapper.find('.status').text() === '取消上传'){ + return; + } + this.fileProgressWrapper.find('.status').text("已上传: " + size + " 上传速度: " + formatSpeed + "/s"); + percentage = parseInt(percentage, 10); + if (file.status !== plupload.DONE && percentage === 100) { + percentage = 99; + } + + progressbar.attr('aria-valuenow', percentage).css('width', percentage + '%'); + + if (chunk_size) { + var chunk_amount = Math.ceil(file.size / chunk_size); + if (chunk_amount === 1) { + return false; + } + var current_uploading_chunk = Math.ceil(uploaded / chunk_size); + var pre_chunk, text; + + for (var index = 0; index < current_uploading_chunk; index++) { + pre_chunk = $('#' + file.id + "_" + index); + pre_chunk.width('100%').removeClass().addClass('alert alert-success').attr('aria-valuenow', 100); + text = "块" + index + "上传进度100%"; + pre_chunk.next().html(text); + } + + var currentProgessBar = $('#' + file.id + "_" + current_uploading_chunk); + var current_chunk_percent; + if (current_uploading_chunk < chunk_amount) { + if (uploaded % chunk_size) { + current_chunk_percent = ((uploaded % chunk_size) / chunk_size * 100).toFixed(2); + } else { + current_chunk_percent = 100; + currentProgessBar.removeClass().addClass('alert alert-success'); + } + } else { + var last_chunk_size = file.size - chunk_size * (chunk_amount - 1); + var left_file_size = file.size - uploaded; + if (left_file_size % last_chunk_size) { + current_chunk_percent = ((uploaded % chunk_size) / last_chunk_size * 100).toFixed(2); + } else { + current_chunk_percent = 100; + currentProgessBar.removeClass().addClass('alert alert-success'); + } + } + currentProgessBar.width(current_chunk_percent + '%'); + currentProgessBar.attr('aria-valuenow', current_chunk_percent); + text = "块" + current_uploading_chunk + "上传进度" + current_chunk_percent + '%'; + currentProgessBar.next().html(text); + } + + this.appear(); +}; + +FileProgress.prototype.setComplete = function(up, info) { + var td = this.fileProgressWrapper.find('td:eq(2)'), + tdProgress = td.find('.progress'); + + var res; + var url; + if(uploadConfig.saveType == "oss"){ + url = "oss"; + str = "
上传成功
"; + }else{ + res = $.parseJSON(info); + if (res.url) { + url = res.url; + str = "
上传成功
"; + } else { + var domain = up.getOption('domain'); + url = domain + encodeURI(res.key); + var link = domain + res.key; + str = "
上传成功
"; + } + } + + tdProgress.html(str).removeClass().next().next('.status').hide(); + this.fileProgressWrapper.find('td:eq(0) .progressCancel').hide(); + td.find('.status').hide(); + angular.element(document.querySelector('angular-filemanager > div')).scope().fileNavigator.refresh(); +}; +FileProgress.prototype.setError = function() { + this.fileProgressWrapper.find('td:eq(2)').attr('class', 'text-warning'); + this.fileProgressWrapper.find('td:eq(2) .progress').css('width', 0).hide(); + this.fileProgressWrapper.find('button').hide(); + this.fileProgressWrapper.next('.chunk-status-tr').hide(); +}; + +FileProgress.prototype.setCancelled = function(manual) { + var progressContainer = 'progressContainer'; + if (!manual) { + progressContainer += ' red'; + } + this.fileProgressWrapper.attr('class', progressContainer); + this.fileProgressWrapper.find('td .progress').remove(); + this.fileProgressWrapper.find('td:eq(2) .btn-default').hide(); + this.fileProgressWrapper.find('td:eq(0) .progressCancel').hide(); +}; + +FileProgress.prototype.setStatus = function(status, isUploading) { + if (!isUploading) { + this.fileProgressWrapper.find('.status').text(status).attr('class', 'status text-left'); + } +}; + +// 绑定取消上传事件 +FileProgress.prototype.bindUploadCancel = function(up) { + var self = this; + if (up) { + self.fileProgressWrapper.find('td:eq(0) .progressCancel').on('click', function(){ + self.setCancelled(false); + self.setStatus("取消上传"); + self.fileProgressWrapper.find('.status').css('left', '0'); + up.removeFile(self.file); + }); + } + +}; + +FileProgress.prototype.appear = function() { + if (this.getTimer() !== null) { + clearTimeout(this.getTimer()); + this.setTimer(null); + } + + if (this.fileProgressWrapper[0].filters) { + try { + this.fileProgressWrapper[0].filters.item("DXImageTransform.Microsoft.Alpha").opacity = 100; + } catch (e) { + // If it is not set initially, the browser will throw an error. This will set it if it is not set yet. + this.fileProgressWrapper.css('filter', "progid:DXImageTransform.Microsoft.Alpha(opacity=100)"); + } + } else { + this.fileProgressWrapper.css('opacity', 1); + } + + this.fileProgressWrapper.css('height', ''); + + this.height = this.fileProgressWrapper.offset().top; + this.opacity = 100; + this.fileProgressWrapper.show(); + +}; diff --git a/thinkphp/.gitignore b/thinkphp/.gitignore new file mode 100644 index 00000000..7e31ef51 --- /dev/null +++ b/thinkphp/.gitignore @@ -0,0 +1,4 @@ +/composer.lock +/vendor +.idea +.DS_Store diff --git a/thinkphp/CONTRIBUTING.md b/thinkphp/CONTRIBUTING.md index 6cefcb38..dc8e91cd 100644 --- a/thinkphp/CONTRIBUTING.md +++ b/thinkphp/CONTRIBUTING.md @@ -7,7 +7,7 @@ ThinkPHP 目前使用 Git 来控制程序版本,如果你想为 ThinkPHP 贡献源代码,请先大致了解 Git 的使用方法。我们目前把项目托管在 GitHub 上,任何 GitHub 用户都可以向我们贡献代码。 -参与的方式很简单,`fork`一份 ThinkPHP 的代码到你的仓库中,修改后提交,并向我们发起`pull request`申请,我们会及时对代码进行审查并处理你的申请并。审查通过后,你的代码将被`merge`进我们的仓库中,这样你就会自动出现在贡献者名单里了,非常方便。 +参与的方式很简单,`fork`一份 ThinkPHP 的代码到你的仓库中,修改后提交,并向我们发起`pull request`申请,我们会及时对代码进行审查并处理你的申请。审查通过后,你的代码将被`merge`进我们的仓库中,这样你就会自动出现在贡献者名单里了,非常方便。 我们希望你贡献的代码符合: @@ -60,7 +60,7 @@ GitHub 提供了 Issue 功能,该功能可以用于: 6. 变基(衍合 `rebase`)你的分支到上游 master 分支; 7. `push` 你的本地仓库到 GitHub; 8. 提交 `pull request`; -9. 等待 CI 验证(若不通过则重复 5~7,GitHub 会自动更新你的 `pull request`); +9. 等待 CI 验证(若不通过则重复 5~7,不需要重新提交 `pull request`,GitHub 会自动更新你的 `pull request`); 10. 等待管理员处理,并及时 `rebase` 你的分支到上游 master 分支(若上游 master 分支有修改)。 *若有必要,可以 `git push -f` 强行推送 rebase 后的分支到自己的 `fork`* diff --git a/thinkphp/LICENSE.txt b/thinkphp/LICENSE.txt index 574a39c4..2cb9a8a9 100644 --- a/thinkphp/LICENSE.txt +++ b/thinkphp/LICENSE.txt @@ -1,6 +1,6 @@ ThinkPHP遵循Apache2开源协议发布,并提供免费使用。 -版权所有Copyright © 2006-2016 by ThinkPHP (http://thinkphp.cn) +版权所有Copyright © 2006-2017 by ThinkPHP (http://thinkphp.cn) All rights reserved。 ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。 diff --git a/thinkphp/README.md b/thinkphp/README.md new file mode 100644 index 00000000..f01fd2b9 --- /dev/null +++ b/thinkphp/README.md @@ -0,0 +1,114 @@ +ThinkPHP 5.0 +=============== + +[![StyleCI](https://styleci.io/repos/48530411/shield?style=flat&branch=master)](https://styleci.io/repos/48530411) +[![Build Status](https://travis-ci.org/top-think/framework.svg?branch=master)](https://travis-ci.org/top-think/framework) +[![codecov.io](http://codecov.io/github/top-think/framework/coverage.svg?branch=master)](http://codecov.io/github/github/top-think/framework?branch=master) +[![Total Downloads](https://poser.pugx.org/topthink/framework/downloads)](https://packagist.org/packages/topthink/framework) +[![Latest Stable Version](https://poser.pugx.org/topthink/framework/v/stable)](https://packagist.org/packages/topthink/framework) +[![Latest Unstable Version](https://poser.pugx.org/topthink/framework/v/unstable)](https://packagist.org/packages/topthink/framework) +[![License](https://poser.pugx.org/topthink/framework/license)](https://packagist.org/packages/topthink/framework) + +ThinkPHP5在保持快速开发和大道至简的核心理念不变的同时,PHP版本要求提升到5.4,优化核心,减少依赖,基于全新的架构思想和命名空间实现,是ThinkPHP突破原有框架思路的颠覆之作,其主要特性包括: + + + 基于命名空间和众多PHP新特性 + + 核心功能组件化 + + 强化路由功能 + + 更灵活的控制器 + + 重构的模型和数据库类 + + 配置文件可分离 + + 重写的自动验证和完成 + + 简化扩展机制 + + API支持完善 + + 改进的Log类 + + 命令行访问支持 + + REST支持 + + 引导文件支持 + + 方便的自动生成定义 + + 真正惰性加载 + + 分布式环境支持 + + 支持Composer + + 支持MongoDb + +> ThinkPHP5的运行环境要求PHP5.4以上。 + +详细开发文档参考 [ThinkPHP5完全开发手册](http://www.kancloud.cn/manual/thinkphp5) 以及[ThinkPHP5入门系列教程](http://www.kancloud.cn/special/thinkphp5_quickstart) + +## 目录结构 + +初始的目录结构如下: + +~~~ +www WEB部署目录(或者子目录) +├─application 应用目录 +│ ├─common 公共模块目录(可以更改) +│ ├─module_name 模块目录 +│ │ ├─config.php 模块配置文件 +│ │ ├─common.php 模块函数文件 +│ │ ├─controller 控制器目录 +│ │ ├─model 模型目录 +│ │ ├─view 视图目录 +│ │ └─ ... 更多类库目录 +│ │ +│ ├─command.php 命令行工具配置文件 +│ ├─common.php 公共函数文件 +│ ├─config.php 公共配置文件 +│ ├─route.php 路由配置文件 +│ ├─tags.php 应用行为扩展定义文件 +│ └─database.php 数据库配置文件 +│ +├─public WEB目录(对外访问目录) +│ ├─index.php 入口文件 +│ ├─router.php 快速测试文件 +│ └─.htaccess 用于apache的重写 +│ +├─thinkphp 框架系统目录 +│ ├─lang 语言文件目录 +│ ├─library 框架类库目录 +│ │ ├─think Think类库包目录 +│ │ └─traits 系统Trait目录 +│ │ +│ ├─tpl 系统模板目录 +│ ├─base.php 基础定义文件 +│ ├─console.php 控制台入口文件 +│ ├─convention.php 框架惯例配置文件 +│ ├─helper.php 助手函数文件 +│ ├─phpunit.xml phpunit配置文件 +│ └─start.php 框架入口文件 +│ +├─extend 扩展类库目录 +├─runtime 应用的运行时目录(可写,可定制) +├─vendor 第三方类库目录(Composer依赖库) +├─build.php 自动生成定义文件(参考) +├─composer.json composer 定义文件 +├─LICENSE.txt 授权说明文件 +├─README.md README 文件 +├─think 命令行入口文件 +~~~ + +> router.php用于php自带webserver支持,可用于快速测试 +> 切换到public目录后,启动命令:php -S localhost:8888 router.php +> 上面的目录结构和名称是可以改变的,这取决于你的入口文件和配置参数。 + +## 命名规范 + +ThinkPHP5的命名规范遵循`PSR-2`规范以及`PSR-4`自动加载规范。 + +## 参与开发 +注册并登录 Github 帐号, fork 本项目并进行改动。 + +更多细节参阅 [CONTRIBUTING.md](CONTRIBUTING.md) + +## 版权信息 + +ThinkPHP遵循Apache2开源协议发布,并提供免费使用。 + +本项目包含的第三方源码和二进制文件之版权信息另行标注。 + +版权所有Copyright © 2006-2018 by ThinkPHP (http://thinkphp.cn) + +All rights reserved。 + +ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。 + +更多细节参阅 [LICENSE.txt](LICENSE.txt) diff --git a/thinkphp/base.php b/thinkphp/base.php index 9744f01d..92c4fa55 100644 --- a/thinkphp/base.php +++ b/thinkphp/base.php @@ -2,14 +2,14 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- // | Author: liu21st // +---------------------------------------------------------------------- -define('THINK_VERSION', '5.0.9'); +define('THINK_VERSION', '5.0.24'); define('THINK_START_TIME', microtime(true)); define('THINK_START_MEM', memory_get_usage()); define('EXT', '.php'); @@ -40,8 +40,10 @@ require CORE_PATH . 'Loader.php'; // 加载环境变量配置文件 if (is_file(ROOT_PATH . '.env')) { $env = parse_ini_file(ROOT_PATH . '.env', true); + foreach ($env as $key => $val) { $name = ENV_PREFIX . strtoupper($key); + if (is_array($val)) { foreach ($val as $k => $v) { $item = $name . '_' . strtoupper($k); diff --git a/thinkphp/convention.php b/thinkphp/convention.php index 801c90a8..31a0a0c1 100644 --- a/thinkphp/convention.php +++ b/thinkphp/convention.php @@ -7,7 +7,7 @@ return [ // 默认Host地址 'app_host' => '', // 应用调试模式 - 'app_debug' => true, + 'app_debug' => false, // 应用Trace 'app_trace' => false, // 应用模式状态 @@ -74,6 +74,8 @@ return [ 'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'], // pathinfo分隔符 'pathinfo_depr' => '/', + // HTTPS代理标识 + 'https_agent_name' => '', // URL伪静态后缀 'url_html_suffix' => 'html', // URL普通方式参数 用于自动生成 @@ -114,6 +116,8 @@ return [ // +---------------------------------------------------------------------- 'template' => [ + // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 + 'auto_rule' => 1, // 模板引擎类型 支持 php think 支持扩展 'type' => 'Think', // 视图基础目录,配置目录为所有模块的视图起始目录 @@ -153,6 +157,8 @@ return [ 'show_error_msg' => false, // 异常处理handle类 留空使用 \think\exception\Handle 'exception_handle' => '', + // 是否记录trace信息到日志 + 'record_trace' => false, // +---------------------------------------------------------------------- // | 日志设置 @@ -282,4 +288,11 @@ return [ 'list_rows' => 15, ], + //控制台配置 + 'console' => [ + 'name' => 'Think Console', + 'version' => '0.1', + 'user' => null, + ], + ]; diff --git a/thinkphp/helper.php b/thinkphp/helper.php index a23b6794..12683cfd 100644 --- a/thinkphp/helper.php +++ b/thinkphp/helper.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -127,7 +127,7 @@ if (!function_exists('input')) { if ($pos = strpos($key, '.')) { // 指定参数来源 list($method, $key) = explode('.', $key, 2); - if (!in_array($method, ['get', 'post', 'put', 'patch', 'delete', 'param', 'request', 'session', 'cookie', 'server', 'env', 'path', 'file'])) { + if (!in_array($method, ['get', 'post', 'put', 'patch', 'delete', 'route', 'param', 'request', 'session', 'cookie', 'server', 'env', 'path', 'file'])) { $key = $method . '.' . $key; $method = 'param'; } diff --git a/thinkphp/lang/zh-cn.php b/thinkphp/lang/zh-cn.php index c6c5a50f..eb7a9142 100644 --- a/thinkphp/lang/zh-cn.php +++ b/thinkphp/lang/zh-cn.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -12,57 +12,125 @@ // 核心中文语言包 return [ // 系统错误提示 - 'Undefined variable' => '未定义变量', - 'Undefined index' => '未定义数组索引', - 'Undefined offset' => '未定义数组下标', - 'Parse error' => '语法解析错误', - 'Type error' => '类型错误', - 'Fatal error' => '致命错误', - 'syntax error' => '语法错误', + 'Undefined variable' => '未定义变量', + 'Undefined index' => '未定义数组索引', + 'Undefined offset' => '未定义数组下标', + 'Parse error' => '语法解析错误', + 'Type error' => '类型错误', + 'Fatal error' => '致命错误', + 'syntax error' => '语法错误', // 框架核心错误提示 - 'dispatch type not support' => '不支持的调度类型', - 'method param miss' => '方法参数错误', - 'method not exists' => '方法不存在', - 'module not exists' => '模块不存在', - 'controller not exists' => '控制器不存在', - 'class not exists' => '类不存在', - 'property not exists' => '类的属性不存在', - 'template not exists' => '模板文件不存在', - 'illegal controller name' => '非法的控制器名称', - 'illegal action name' => '非法的操作名称', - 'url suffix deny' => '禁止的URL后缀访问', - 'Route Not Found' => '当前访问路由未定义', - 'Underfined db type' => '未定义数据库类型', - 'variable type error' => '变量类型错误', - 'PSR-4 error' => 'PSR-4 规范错误', - 'not support total' => '简洁模式下不能获取数据总数', - 'not support last' => '简洁模式下不能获取最后一页', - 'error session handler' => '错误的SESSION处理器类', - 'not allow php tag' => '模板不允许使用PHP语法', - 'not support' => '不支持', - 'redisd master' => 'Redisd 主服务器错误', - 'redisd slave' => 'Redisd 从服务器错误', - 'must run at sae' => '必须在SAE运行', - 'memcache init error' => '未开通Memcache服务,请在SAE管理平台初始化Memcache服务', - 'KVDB init error' => '没有初始化KVDB,请在SAE管理平台初始化KVDB服务', - 'fields not exists' => '数据表字段不存在', - 'where express error' => '查询表达式错误', - 'no data to update' => '没有任何数据需要更新', - 'miss data to insert' => '缺少需要写入的数据', - 'miss complex primary data' => '缺少复合主键数据', - 'miss update condition' => '缺少更新条件', - 'model data Not Found' => '模型数据不存在', - 'table data not Found' => '表数据不存在', - 'delete without condition' => '没有条件不会执行删除操作', - 'miss relation data' => '缺少关联表数据', - 'tag attr must' => '模板标签属性必须', - 'tag error' => '模板标签错误', - 'cache write error' => '缓存写入失败', - 'sae mc write error' => 'SAE mc 写入错误', - 'route name not exists' => '路由标识不存在(或参数不够)', - 'invalid request' => '非法请求', - 'bind attr has exists' => '模型的属性已经存在', - 'relation data not exists' => '关联数据不存在', - 'relation not support' => '关联不支持', + 'dispatch type not support' => '不支持的调度类型', + 'method param miss' => '方法参数错误', + 'method not exists' => '方法不存在', + 'module not exists' => '模块不存在', + 'controller not exists' => '控制器不存在', + 'class not exists' => '类不存在', + 'property not exists' => '类的属性不存在', + 'template not exists' => '模板文件不存在', + 'illegal controller name' => '非法的控制器名称', + 'illegal action name' => '非法的操作名称', + 'url suffix deny' => '禁止的URL后缀访问', + 'Route Not Found' => '当前访问路由未定义', + 'Undefined db type' => '未定义数据库类型', + 'variable type error' => '变量类型错误', + 'PSR-4 error' => 'PSR-4 规范错误', + 'not support total' => '简洁模式下不能获取数据总数', + 'not support last' => '简洁模式下不能获取最后一页', + 'error session handler' => '错误的SESSION处理器类', + 'not allow php tag' => '模板不允许使用PHP语法', + 'not support' => '不支持', + 'redisd master' => 'Redisd 主服务器错误', + 'redisd slave' => 'Redisd 从服务器错误', + 'must run at sae' => '必须在SAE运行', + 'memcache init error' => '未开通Memcache服务,请在SAE管理平台初始化Memcache服务', + 'KVDB init error' => '没有初始化KVDB,请在SAE管理平台初始化KVDB服务', + 'fields not exists' => '数据表字段不存在', + 'where express error' => '查询表达式错误', + 'not support data' => '不支持的数据表达式', + 'no data to update' => '没有任何数据需要更新', + 'miss data to insert' => '缺少需要写入的数据', + 'miss complex primary data' => '缺少复合主键数据', + 'miss update condition' => '缺少更新条件', + 'model data Not Found' => '模型数据不存在', + 'table data not Found' => '表数据不存在', + 'delete without condition' => '没有条件不会执行删除操作', + 'miss relation data' => '缺少关联表数据', + 'tag attr must' => '模板标签属性必须', + 'tag error' => '模板标签错误', + 'cache write error' => '缓存写入失败', + 'sae mc write error' => 'SAE mc 写入错误', + 'route name not exists' => '路由标识不存在(或参数不够)', + 'invalid request' => '非法请求', + 'bind attr has exists' => '模型的属性已经存在', + 'relation data not exists' => '关联数据不存在', + 'relation not support' => '关联不支持', + 'chunk not support order' => 'Chunk不支持调用order方法', + 'closure not support cache(true)' => '使用闭包查询不支持cache(true),请指定缓存Key', + + // 上传错误信息 + 'unknown upload error' => '未知上传错误!', + 'file write error' => '文件写入失败!', + 'upload temp dir not found' => '找不到临时文件夹!', + 'no file to uploaded' => '没有文件被上传!', + 'only the portion of file is uploaded' => '文件只有部分被上传!', + 'upload File size exceeds the maximum value' => '上传文件大小超过了最大值!', + 'upload write error' => '文件上传保存错误!', + 'has the same filename: {:filename}' => '存在同名文件:{:filename}', + 'upload illegal files' => '非法上传文件', + 'illegal image files' => '非法图片文件', + 'extensions to upload is not allowed' => '上传文件后缀不允许', + 'mimetype to upload is not allowed' => '上传文件MIME类型不允许!', + 'filesize not match' => '上传文件大小不符!', + 'directory {:path} creation failed' => '目录 {:path} 创建失败!', + + // Validate Error Message + ':attribute require' => ':attribute不能为空', + ':attribute must be numeric' => ':attribute必须是数字', + ':attribute must be integer' => ':attribute必须是整数', + ':attribute must be float' => ':attribute必须是浮点数', + ':attribute must be bool' => ':attribute必须是布尔值', + ':attribute not a valid email address' => ':attribute格式不符', + ':attribute not a valid mobile' => ':attribute格式不符', + ':attribute must be a array' => ':attribute必须是数组', + ':attribute must be yes,on or 1' => ':attribute必须是yes、on或者1', + ':attribute not a valid datetime' => ':attribute不是一个有效的日期或时间格式', + ':attribute not a valid file' => ':attribute不是有效的上传文件', + ':attribute not a valid image' => ':attribute不是有效的图像文件', + ':attribute must be alpha' => ':attribute只能是字母', + ':attribute must be alpha-numeric' => ':attribute只能是字母和数字', + ':attribute must be alpha-numeric, dash, underscore' => ':attribute只能是字母、数字和下划线_及破折号-', + ':attribute not a valid domain or ip' => ':attribute不是有效的域名或者IP', + ':attribute must be chinese' => ':attribute只能是汉字', + ':attribute must be chinese or alpha' => ':attribute只能是汉字、字母', + ':attribute must be chinese,alpha-numeric' => ':attribute只能是汉字、字母和数字', + ':attribute must be chinese,alpha-numeric,underscore, dash' => ':attribute只能是汉字、字母、数字和下划线_及破折号-', + ':attribute not a valid url' => ':attribute不是有效的URL地址', + ':attribute not a valid ip' => ':attribute不是有效的IP地址', + ':attribute must be dateFormat of :rule' => ':attribute必须使用日期格式 :rule', + ':attribute must be in :rule' => ':attribute必须在 :rule 范围内', + ':attribute be notin :rule' => ':attribute不能在 :rule 范围内', + ':attribute must between :1 - :2' => ':attribute只能在 :1 - :2 之间', + ':attribute not between :1 - :2' => ':attribute不能在 :1 - :2 之间', + 'size of :attribute must be :rule' => ':attribute长度不符合要求 :rule', + 'max size of :attribute must be :rule' => ':attribute长度不能超过 :rule', + 'min size of :attribute must be :rule' => ':attribute长度不能小于 :rule', + ':attribute cannot be less than :rule' => ':attribute日期不能小于 :rule', + ':attribute cannot exceed :rule' => ':attribute日期不能超过 :rule', + ':attribute not within :rule' => '不在有效期内 :rule', + 'access IP is not allowed' => '不允许的IP访问', + 'access IP denied' => '禁止的IP访问', + ':attribute out of accord with :2' => ':attribute和确认字段:2不一致', + ':attribute cannot be same with :2' => ':attribute和比较字段:2不能相同', + ':attribute must greater than or equal :rule' => ':attribute必须大于等于 :rule', + ':attribute must greater than :rule' => ':attribute必须大于 :rule', + ':attribute must less than or equal :rule' => ':attribute必须小于等于 :rule', + ':attribute must less than :rule' => ':attribute必须小于 :rule', + ':attribute must equal :rule' => ':attribute必须等于 :rule', + ':attribute has exists' => ':attribute已存在', + ':attribute not conform to the rules' => ':attribute不符合指定规则', + 'invalid Request method' => '无效的请求类型', + 'invalid token' => '令牌数据无效', + 'not conform to the rules' => '规则错误', ]; diff --git a/thinkphp/library/think/App.php b/thinkphp/library/think/App.php index 7b27a15d..f572b907 100644 --- a/thinkphp/library/think/App.php +++ b/thinkphp/library/think/App.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -11,13 +11,14 @@ namespace think; +use think\exception\ClassNotFoundException; use think\exception\HttpException; use think\exception\HttpResponseException; use think\exception\RouteNotFoundException; /** * App 应用管理 - * @author liu21st + * @author liu21st */ class App { @@ -56,24 +57,32 @@ class App */ protected static $routeMust; + /** + * @var array 请求调度分发 + */ protected static $dispatch; + + /** + * @var array 额外加载文件 + */ protected static $file = []; /** * 执行应用程序 * @access public - * @param Request $request Request对象 + * @param Request $request 请求对象 * @return Response * @throws Exception */ public static function run(Request $request = null) { - is_null($request) && $request = Request::instance(); + $request = is_null($request) ? Request::instance() : $request; try { $config = self::initCommon(); + + // 模块/控制器绑定 if (defined('BIND_MODULE')) { - // 模块/控制器绑定 BIND_MODULE && Route::bind(BIND_MODULE); } elseif ($config['auto_bind_module']) { // 入口自动绑定 @@ -87,10 +96,8 @@ class App // 默认语言 Lang::range($config['default_lang']); - if ($config['lang_switch_on']) { - // 开启多语言机制 检测当前语言 - Lang::detect(); - } + // 开启多语言机制 检测当前语言 + $config['lang_switch_on'] && Lang::detect(); $request->langset(Lang::range()); // 加载系统语言包 @@ -99,12 +106,16 @@ class App APP_PATH . 'lang' . DS . $request->langset() . EXT, ]); + // 监听 app_dispatch + Hook::listen('app_dispatch', self::$dispatch); // 获取应用调度信息 $dispatch = self::$dispatch; + + // 未设置调度信息则进行 URL 路由检测 if (empty($dispatch)) { - // 进行URL路由检测 $dispatch = self::routeCheck($request, $config); } + // 记录当前调度信息 $request->dispatch($dispatch); @@ -115,10 +126,15 @@ class App Log::record('[ PARAM ] ' . var_export($request->param(), true), 'info'); } - // 监听app_begin + // 监听 app_begin Hook::listen('app_begin', $dispatch); + // 请求缓存检查 - $request->cache($config['request_cache'], $config['request_cache_expire'], $config['request_cache_except']); + $request->cache( + $config['request_cache'], + $config['request_cache_expire'], + $config['request_cache_except'] + ); $data = self::exec($dispatch, $config); } catch (HttpResponseException $exception) { @@ -133,24 +149,151 @@ class App $response = $data; } elseif (!is_null($data)) { // 默认自动识别响应输出类型 - $isAjax = $request->isAjax(); - $type = $isAjax ? Config::get('default_ajax_return') : Config::get('default_return_type'); + $type = $request->isAjax() ? + Config::get('default_ajax_return') : + Config::get('default_return_type'); + $response = Response::create($data, $type); } else { $response = Response::create(); } - // 监听app_end + // 监听 app_end Hook::listen('app_end', $response); return $response; } + /** + * 初始化应用,并返回配置信息 + * @access public + * @return array + */ + public static function initCommon() + { + if (empty(self::$init)) { + if (defined('APP_NAMESPACE')) { + self::$namespace = APP_NAMESPACE; + } + + Loader::addNamespace(self::$namespace, APP_PATH); + + // 初始化应用 + $config = self::init(); + self::$suffix = $config['class_suffix']; + + // 应用调试模式 + self::$debug = Env::get('app_debug', Config::get('app_debug')); + + if (!self::$debug) { + ini_set('display_errors', 'Off'); + } elseif (!IS_CLI) { + // 重新申请一块比较大的 buffer + if (ob_get_level() > 0) { + $output = ob_get_clean(); + } + + ob_start(); + + if (!empty($output)) { + echo $output; + } + + } + + if (!empty($config['root_namespace'])) { + Loader::addNamespace($config['root_namespace']); + } + + // 加载额外文件 + if (!empty($config['extra_file_list'])) { + foreach ($config['extra_file_list'] as $file) { + $file = strpos($file, '.') ? $file : APP_PATH . $file . EXT; + if (is_file($file) && !isset(self::$file[$file])) { + include $file; + self::$file[$file] = true; + } + } + } + + // 设置系统时区 + date_default_timezone_set($config['default_timezone']); + + // 监听 app_init + Hook::listen('app_init'); + + self::$init = true; + } + + return Config::get(); + } + + /** + * 初始化应用或模块 + * @access public + * @param string $module 模块名 + * @return array + */ + private static function init($module = '') + { + // 定位模块目录 + $module = $module ? $module . DS : ''; + + // 加载初始化文件 + if (is_file(APP_PATH . $module . 'init' . EXT)) { + include APP_PATH . $module . 'init' . EXT; + } elseif (is_file(RUNTIME_PATH . $module . 'init' . EXT)) { + include RUNTIME_PATH . $module . 'init' . EXT; + } else { + // 加载模块配置 + $config = Config::load(CONF_PATH . $module . 'config' . CONF_EXT); + + // 读取数据库配置文件 + $filename = CONF_PATH . $module . 'database' . CONF_EXT; + Config::load($filename, 'database'); + + // 读取扩展配置文件 + if (is_dir(CONF_PATH . $module . 'extra')) { + $dir = CONF_PATH . $module . 'extra'; + $files = scandir($dir); + foreach ($files as $file) { + if ('.' . pathinfo($file, PATHINFO_EXTENSION) === CONF_EXT) { + $filename = $dir . DS . $file; + Config::load($filename, pathinfo($file, PATHINFO_FILENAME)); + } + } + } + + // 加载应用状态配置 + if ($config['app_status']) { + Config::load(CONF_PATH . $module . $config['app_status'] . CONF_EXT); + } + + // 加载行为扩展文件 + if (is_file(CONF_PATH . $module . 'tags' . EXT)) { + Hook::import(include CONF_PATH . $module . 'tags' . EXT); + } + + // 加载公共文件 + $path = APP_PATH . $module; + if (is_file($path . 'common' . EXT)) { + include $path . 'common' . EXT; + } + + // 加载当前模块语言包 + if ($module) { + Lang::load($path . 'lang' . DS . Request::instance()->langset() . EXT); + } + } + + return Config::get(); + } + /** * 设置当前请求的调度信息 * @access public * @param array|string $dispatch 调度信息 - * @param string $type 调度类型 + * @param string $type 调度类型 * @return void */ public static function dispatch($dispatch, $type = 'module') @@ -169,8 +312,10 @@ class App { $reflect = new \ReflectionFunction($function); $args = self::bindParams($reflect, $vars); + // 记录执行信息 self::$debug && Log::record('[ RUN ] ' . $reflect->__toString(), 'info'); + return $reflect->invokeArgs($args); } @@ -190,28 +335,27 @@ class App // 静态方法 $reflect = new \ReflectionMethod($method); } + $args = self::bindParams($reflect, $vars); self::$debug && Log::record('[ RUN ] ' . $reflect->class . '->' . $reflect->name . '[ ' . $reflect->getFileName() . ' ]', 'info'); + return $reflect->invokeArgs(isset($class) ? $class : null, $args); } /** * 调用反射执行类的实例化 支持依赖注入 * @access public - * @param string $class 类名 - * @param array $vars 变量 + * @param string $class 类名 + * @param array $vars 变量 * @return mixed */ public static function invokeClass($class, $vars = []) { $reflect = new \ReflectionClass($class); $constructor = $reflect->getConstructor(); - if ($constructor) { - $args = self::bindParams($constructor, $vars); - } else { - $args = []; - } + $args = $constructor ? self::bindParams($constructor, $vars) : []; + return $reflect->newInstanceArgs($args); } @@ -224,52 +368,58 @@ class App */ private static function bindParams($reflect, $vars = []) { + // 自动获取请求变量 if (empty($vars)) { - // 自动获取请求变量 - if (Config::get('url_param_type')) { - $vars = Request::instance()->route(); - } else { - $vars = Request::instance()->param(); - } + $vars = Config::get('url_param_type') ? + Request::instance()->route() : + Request::instance()->param(); } + $args = []; if ($reflect->getNumberOfParameters() > 0) { // 判断数组类型 数字数组时按顺序绑定参数 reset($vars); - $type = key($vars) === 0 ? 1 : 0; - $params = $reflect->getParameters(); - foreach ($params as $param) { + $type = key($vars) === 0 ? 1 : 0; + + foreach ($reflect->getParameters() as $param) { $args[] = self::getParamValue($param, $vars, $type); } } + return $args; } /** * 获取参数值 * @access private - * @param \ReflectionParameter $param - * @param array $vars 变量 - * @param string $type + * @param \ReflectionParameter $param 参数 + * @param array $vars 变量 + * @param string $type 类别 * @return array */ private static function getParamValue($param, &$vars, $type) { $name = $param->getName(); $class = $param->getClass(); + if ($class) { $className = $class->getName(); $bind = Request::instance()->$name; + if ($bind instanceof $className) { $result = $bind; } else { if (method_exists($className, 'invoke')) { $method = new \ReflectionMethod($className, 'invoke'); + if ($method->isPublic() && $method->isStatic()) { return $className::invoke(Request::instance()); } } - $result = method_exists($className, 'instance') ? $className::instance() : new $className; + + $result = method_exists($className, 'instance') ? + $className::instance() : + new $className; } } elseif (1 == $type && !empty($vars)) { $result = array_shift($vars); @@ -280,65 +430,85 @@ class App } else { throw new \InvalidArgumentException('method param miss:' . $name); } + return $result; } + /** + * 执行调用分发 + * @access protected + * @param array $dispatch 调用信息 + * @param array $config 配置信息 + * @return Response|mixed + * @throws \InvalidArgumentException + */ protected static function exec($dispatch, $config) { switch ($dispatch['type']) { - case 'redirect': - // 执行重定向跳转 - $data = Response::create($dispatch['url'], 'redirect')->code($dispatch['status']); + case 'redirect': // 重定向跳转 + $data = Response::create($dispatch['url'], 'redirect') + ->code($dispatch['status']); break; - case 'module': - // 模块/控制器/操作 - $data = self::module($dispatch['module'], $config, isset($dispatch['convert']) ? $dispatch['convert'] : null); + case 'module': // 模块/控制器/操作 + $data = self::module( + $dispatch['module'], + $config, + isset($dispatch['convert']) ? $dispatch['convert'] : null + ); break; - case 'controller': - // 执行控制器操作 + case 'controller': // 执行控制器操作 $vars = array_merge(Request::instance()->param(), $dispatch['var']); - $data = Loader::action($dispatch['controller'], $vars, $config['url_controller_layer'], $config['controller_suffix']); + $data = Loader::action( + $dispatch['controller'], + $vars, + $config['url_controller_layer'], + $config['controller_suffix'] + ); break; - case 'method': - // 执行回调方法 + case 'method': // 回调方法 $vars = array_merge(Request::instance()->param(), $dispatch['var']); $data = self::invokeMethod($dispatch['method'], $vars); break; - case 'function': - // 执行闭包 + case 'function': // 闭包 $data = self::invokeFunction($dispatch['function']); break; - case 'response': + case 'response': // Response 实例 $data = $dispatch['response']; break; default: throw new \InvalidArgumentException('dispatch type not support'); } + return $data; } /** * 执行模块 * @access public - * @param array $result 模块/控制器/操作 - * @param array $config 配置参数 + * @param array $result 模块/控制器/操作 + * @param array $config 配置参数 * @param bool $convert 是否自动转换控制器和操作名 * @return mixed + * @throws HttpException */ public static function module($result, $config, $convert = null) { if (is_string($result)) { $result = explode('/', $result); } + $request = Request::instance(); + if ($config['app_multi_module']) { // 多模块部署 $module = strip_tags(strtolower($result[0] ?: $config['default_module'])); $bind = Route::getBind('module'); $available = false; + if ($bind) { // 绑定模块 list($bindModule) = explode('/', $bind); + if (empty($result[0])) { $module = $bindModule; $available = true; @@ -354,8 +524,13 @@ class App // 初始化模块 $request->module($module); $config = self::init($module); + // 模块请求缓存检查 - $request->cache($config['request_cache'], $config['request_cache_expire'], $config['request_cache_except']); + $request->cache( + $config['request_cache'], + $config['request_cache_expire'], + $config['request_cache_except'] + ); } else { throw new HttpException(404, 'module not exists:' . $module); } @@ -364,22 +539,32 @@ class App $module = ''; $request->module($module); } + + // 设置默认过滤机制 + $request->filter($config['default_filter']); + // 当前模块路径 App::$modulePath = APP_PATH . ($module ? $module . DS : ''); // 是否自动转换控制器和操作名 $convert = is_bool($convert) ? $convert : $config['url_convert']; + // 获取控制器名 $controller = strip_tags($result[1] ?: $config['default_controller']); - $controller = $convert ? strtolower($controller) : $controller; - + if (!preg_match('/^[A-Za-z](\w|\.)*$/', $controller)) { throw new HttpException(404, 'controller not exists:' . $controller); } - + + $controller = $convert ? strtolower($controller) : $controller; + // 获取操作名 $actionName = strip_tags($result[2] ?: $config['default_action']); - $actionName = $convert ? strtolower($actionName) : $actionName; + if (!empty($config['action_convert'])) { + $actionName = Loader::parseName($actionName, 1); + } else { + $actionName = $convert ? strtolower($actionName) : $actionName; + } // 设置当前请求的控制器、操作 $request->controller(Loader::parseName($controller, 1))->action($actionName); @@ -387,10 +572,17 @@ class App // 监听module_init Hook::listen('module_init', $request); - $instance = Loader::controller($controller, $config['url_controller_layer'], $config['controller_suffix'], $config['empty_controller']); - if (is_null($instance)) { - throw new HttpException(404, 'controller not exists:' . Loader::parseName($controller, 1)); + try { + $instance = Loader::controller( + $controller, + $config['url_controller_layer'], + $config['controller_suffix'], + $config['empty_controller'] + ); + } catch (ClassNotFoundException $e) { + throw new HttpException(404, 'controller not exists:' . $e->getClass()); } + // 获取当前操作名 $action = $actionName . $config['action_suffix']; @@ -398,6 +590,13 @@ class App if (is_callable([$instance, $action])) { // 执行操作方法 $call = [$instance, $action]; + // 严格获取当前操作方法名 + $reflect = new \ReflectionMethod($instance, $action); + $methodName = $reflect->getName(); + $suffix = $config['action_suffix']; + $actionName = $suffix ? substr($methodName, 0, -strlen($suffix)) : $methodName; + $request->action($actionName); + } elseif (is_callable([$instance, '_empty'])) { // 空操作 $call = [$instance, '_empty']; @@ -412,125 +611,11 @@ class App return self::invokeMethod($call, $vars); } - /** - * 初始化应用 - */ - public static function initCommon() - { - if (empty(self::$init)) { - if (defined('APP_NAMESPACE')) { - self::$namespace = APP_NAMESPACE; - } - Loader::addNamespace(self::$namespace, APP_PATH); - - // 初始化应用 - $config = self::init(); - self::$suffix = $config['class_suffix']; - - // 应用调试模式 - self::$debug = Env::get('app_debug', Config::get('app_debug')); - if (!self::$debug) { - ini_set('display_errors', 'Off'); - } elseif (!IS_CLI) { - //重新申请一块比较大的buffer - if (ob_get_level() > 0) { - $output = ob_get_clean(); - } - ob_start(); - if (!empty($output)) { - echo $output; - } - } - - if (!empty($config['root_namespace'])) { - Loader::addNamespace($config['root_namespace']); - } - - // 加载额外文件 - if (!empty($config['extra_file_list'])) { - foreach ($config['extra_file_list'] as $file) { - $file = strpos($file, '.') ? $file : APP_PATH . $file . EXT; - if (is_file($file) && !isset(self::$file[$file])) { - include $file; - self::$file[$file] = true; - } - } - } - - // 设置系统时区 - date_default_timezone_set($config['default_timezone']); - - // 监听app_init - Hook::listen('app_init'); - - self::$init = true; - } - return Config::get(); - } - - /** - * 初始化应用或模块 - * @access public - * @param string $module 模块名 - * @return array - */ - private static function init($module = '') - { - // 定位模块目录 - $module = $module ? $module . DS : ''; - - // 加载初始化文件 - if (is_file(APP_PATH . $module . 'init' . EXT)) { - include APP_PATH . $module . 'init' . EXT; - } elseif (is_file(RUNTIME_PATH . $module . 'init' . EXT)) { - include RUNTIME_PATH . $module . 'init' . EXT; - } else { - $path = APP_PATH . $module; - // 加载模块配置 - $config = Config::load(CONF_PATH . $module . 'config' . CONF_EXT); - // 读取数据库配置文件 - $filename = CONF_PATH . $module . 'database' . CONF_EXT; - Config::load($filename, 'database'); - // 读取扩展配置文件 - if (is_dir(CONF_PATH . $module . 'extra')) { - $dir = CONF_PATH . $module . 'extra'; - $files = scandir($dir); - foreach ($files as $file) { - if ('.' . pathinfo($file, PATHINFO_EXTENSION) === CONF_EXT) { - $filename = $dir . DS . $file; - Config::load($filename, pathinfo($file, PATHINFO_FILENAME)); - } - } - } - - // 加载应用状态配置 - if ($config['app_status']) { - $config = Config::load(CONF_PATH . $module . $config['app_status'] . CONF_EXT); - } - - // 加载行为扩展文件 - if (is_file(CONF_PATH . $module . 'tags' . EXT)) { - Hook::import(include CONF_PATH . $module . 'tags' . EXT); - } - - // 加载公共文件 - if (is_file($path . 'common' . EXT)) { - include $path . 'common' . EXT; - } - - // 加载当前模块语言包 - if ($module) { - Lang::load($path . 'lang' . DS . Request::instance()->langset() . EXT); - } - } - return Config::get(); - } - /** * URL路由检测(根据PATH_INFO) * @access public - * @param \think\Request $request - * @param array $config + * @param \think\Request $request 请求实例 + * @param array $config 配置信息 * @return array * @throws \think\Exception */ @@ -539,6 +624,7 @@ class App $path = $request->path(); $depr = $config['pathinfo_depr']; $result = false; + // 路由检测 $check = !is_null(self::$routeCheck) ? self::$routeCheck : $config['url_route_on']; if ($check) { @@ -546,18 +632,14 @@ class App if (is_file(RUNTIME_PATH . 'route.php')) { // 读取路由缓存 $rules = include RUNTIME_PATH . 'route.php'; - if (is_array($rules)) { - Route::rules($rules); - } + is_array($rules) && Route::rules($rules); } else { $files = $config['route_config_file']; foreach ($files as $file) { if (is_file(CONF_PATH . $file . CONF_EXT)) { // 导入路由配置 $rules = include CONF_PATH . $file . CONF_EXT; - if (is_array($rules)) { - Route::import($rules); - } + is_array($rules) && Route::import($rules); } } } @@ -565,15 +647,18 @@ class App // 路由检测(根据路由定义返回不同的URL调度) $result = Route::check($request, $path, $depr, $config['url_domain_deploy']); $must = !is_null(self::$routeMust) ? self::$routeMust : $config['url_route_must']; + if ($must && false === $result) { // 路由无效 throw new RouteNotFoundException(); } } + + // 路由无效 解析模块/控制器/操作/参数... 支持控制器自动搜索 if (false === $result) { - // 路由无效 解析模块/控制器/操作/参数... 支持控制器自动搜索 $result = Route::parseUrl($path, $depr, $config['controller_auto_search']); } + return $result; } diff --git a/thinkphp/library/think/Build.php b/thinkphp/library/think/Build.php index d08dc50b..de7c3275 100644 --- a/thinkphp/library/think/Build.php +++ b/thinkphp/library/think/Build.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -14,36 +14,44 @@ namespace think; class Build { /** - * 根据传入的build资料创建目录和文件 - * @access protected - * @param array $build build列表 + * 根据传入的 build 资料创建目录和文件 + * @access public + * @param array $build build 列表 * @param string $namespace 应用类库命名空间 - * @param bool $suffix 类库后缀 + * @param bool $suffix 类库后缀 * @return void + * @throws Exception */ public static function run(array $build = [], $namespace = 'app', $suffix = false) { // 锁定 - $lockfile = APP_PATH . 'build.lock'; - if (is_writable($lockfile)) { - return; - } elseif (!touch($lockfile)) { - throw new Exception('应用目录[' . APP_PATH . ']不可写,目录无法自动生成!
请手动生成项目目录~', 10006); - } - foreach ($build as $module => $list) { - if ('__dir__' == $module) { - // 创建目录列表 - self::buildDir($list); - } elseif ('__file__' == $module) { - // 创建文件列表 - self::buildFile($list); - } else { - // 创建模块 - self::module($module, $list, $namespace, $suffix); + $lock = APP_PATH . 'build.lock'; + + // 如果锁定文件不可写(不存在)则进行处理,否则表示已经有程序在处理了 + if (!is_writable($lock)) { + if (!touch($lock)) { + throw new Exception( + '应用目录[' . APP_PATH . ']不可写,目录无法自动生成!
请手动生成项目目录~', + 10006 + ); } + + foreach ($build as $module => $list) { + if ('__dir__' == $module) { + // 创建目录列表 + self::buildDir($list); + } elseif ('__file__' == $module) { + // 创建文件列表 + self::buildFile($list); + } else { + // 创建模块 + self::module($module, $list, $namespace, $suffix); + } + } + + // 解除锁定 + unlink($lock); } - // 解除锁定 - unlink($lockfile); } /** @@ -55,10 +63,8 @@ class Build protected static function buildDir($list) { foreach ($list as $dir) { - if (!is_dir(APP_PATH . $dir)) { - // 创建目录 - mkdir(APP_PATH . $dir, 0755, true); - } + // 目录不存在则创建目录 + !is_dir(APP_PATH . $dir) && mkdir(APP_PATH . $dir, 0755, true); } } @@ -71,12 +77,17 @@ class Build protected static function buildFile($list) { foreach ($list as $file) { + // 先创建目录 if (!is_dir(APP_PATH . dirname($file))) { - // 创建目录 mkdir(APP_PATH . dirname($file), 0755, true); } + + // 再创建文件 if (!is_file(APP_PATH . $file)) { - file_put_contents(APP_PATH . $file, 'php' == pathinfo($file, PATHINFO_EXTENSION) ? " ['config.php', 'common.php'], '__dir__' => ['controller', 'model', 'view'], ]; } + // 创建子目录和文件 foreach ($list as $path => $file) { $modulePath = APP_PATH . $module . DS; + if ('__dir__' == $path) { // 生成子目录 foreach ($file as $dir) { - if (!is_dir($modulePath . $dir)) { - // 创建目录 - mkdir($modulePath . $dir, 0755, true); - } + self::checkDirBuild($modulePath . $dir); } } elseif ('__file__' == $path) { // 生成(空白)文件 foreach ($file as $name) { if (!is_file($modulePath . $name)) { - file_put_contents($modulePath . $name, 'php' == pathinfo($name, PATHINFO_EXTENSION) ? "has($name); } /** * 读取缓存 * @access public - * @param string $name 缓存标识 - * @param mixed $default 默认值 + * @param string $name 缓存标识 + * @param mixed $default 默认值 * @return mixed */ public static function get($name, $default = false) { self::$readTimes++; + return self::init()->get($name, $default); } /** * 写入缓存 * @access public - * @param string $name 缓存标识 - * @param mixed $value 存储数据 - * @param int|null $expire 有效时间 0为永久 + * @param string $name 缓存标识 + * @param mixed $value 存储数据 + * @param int|null $expire 有效时间 0为永久 * @return boolean */ public static function set($name, $value, $expire = null) { self::$writeTimes++; + return self::init()->set($name, $value, $expire); } /** * 自增缓存(针对数值缓存) * @access public - * @param string $name 缓存变量名 - * @param int $step 步长 + * @param string $name 缓存变量名 + * @param int $step 步长 * @return false|int */ public static function inc($name, $step = 1) { self::$writeTimes++; + return self::init()->inc($name, $step); } /** * 自减缓存(针对数值缓存) * @access public - * @param string $name 缓存变量名 - * @param int $step 步长 + * @param string $name 缓存变量名 + * @param int $step 步长 * @return false|int */ public static function dec($name, $step = 1) { self::$writeTimes++; + return self::init()->dec($name, $step); } /** * 删除缓存 * @access public - * @param string $name 缓存标识 + * @param string $name 缓存标识 * @return boolean */ public static function rm($name) { self::$writeTimes++; + return self::init()->rm($name); } /** * 清除缓存 * @access public - * @param string $tag 标签名 + * @param string $tag 标签名 * @return boolean */ public static function clear($tag = null) { self::$writeTimes++; + return self::init()->clear($tag); } /** * 读取缓存并删除 * @access public - * @param string $name 缓存变量名 + * @param string $name 缓存变量名 * @return mixed */ public static function pull($name) { self::$readTimes++; self::$writeTimes++; + return self::init()->pull($name); } /** * 如果不存在则写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param int $expire 有效时间 0为永久 + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int $expire 有效时间 0为永久 * @return mixed */ public static function remember($name, $value, $expire = null) { self::$readTimes++; + return self::init()->remember($name, $value, $expire); } /** * 缓存标签 * @access public - * @param string $name 标签名 - * @param string|array $keys 缓存标识 - * @param bool $overlay 是否覆盖 + * @param string $name 标签名 + * @param string|array $keys 缓存标识 + * @param bool $overlay 是否覆盖 * @return Driver */ public static function tag($name, $keys = null, $overlay = false) diff --git a/thinkphp/library/think/Collection.php b/thinkphp/library/think/Collection.php index 41b42759..f872476f 100644 --- a/thinkphp/library/think/Collection.php +++ b/thinkphp/library/think/Collection.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -19,20 +19,35 @@ use JsonSerializable; class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable { + /** + * @var array 数据 + */ protected $items = []; + /** + * Collection constructor. + * @access public + * @param array $items 数据 + */ public function __construct($items = []) { $this->items = $this->convertToArray($items); } + /** + * 创建 Collection 实例 + * @access public + * @param array $items 数据 + * @return static + */ public static function make($items = []) { return new static($items); } /** - * 是否为空 + * 判断数据是否为空 + * @access public * @return bool */ public function isEmpty() @@ -40,74 +55,96 @@ class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSeria return empty($this->items); } + /** + * 将数据转成数组 + * @access public + * @return array + */ public function toArray() { return array_map(function ($value) { - return ($value instanceof Model || $value instanceof self) ? $value->toArray() : $value; + return ($value instanceof Model || $value instanceof self) ? + $value->toArray() : + $value; }, $this->items); } + /** + * 获取全部的数据 + * @access public + * @return array + */ public function all() { return $this->items; } /** - * 合并数组 - * - * @param mixed $items + * 交换数组中的键和值 + * @access public * @return static */ - public function merge($items) + public function flip() { - return new static(array_merge($this->items, $this->convertToArray($items))); + return new static(array_flip($this->items)); } /** - * 比较数组,返回差集 - * - * @param mixed $items + * 返回数组中所有的键名组成的新 Collection 实例 + * @access public * @return static */ - public function diff($items) + public function keys() { - return new static(array_diff($this->items, $this->convertToArray($items))); + return new static(array_keys($this->items)); } /** - * 交换数组中的键和值 - * + * 返回数组中所有的值组成的新 Collection 实例 + * @access public * @return static */ - public function flip() + public function values() { - return new static(array_flip($this->items)); + return new static(array_values($this->items)); } /** - * 比较数组,返回交集 - * - * @param mixed $items + * 合并数组并返回一个新的 Collection 实例 + * @access public + * @param mixed $items 新的数据 * @return static */ - public function intersect($items) + public function merge($items) { - return new static(array_intersect($this->items, $this->convertToArray($items))); + return new static(array_merge($this->items, $this->convertToArray($items))); } /** - * 返回数组中所有的键名 - * + * 比较数组,返回差集生成的新 Collection 实例 + * @access public + * @param mixed $items 做比较的数据 * @return static */ - public function keys() + public function diff($items) { - return new static(array_keys($this->items)); + return new static(array_diff($this->items, $this->convertToArray($items))); + } + + /** + * 比较数组,返回交集组成的 Collection 新实例 + * @access public + * @param mixed $items 比较数据 + * @return static + */ + public function intersect($items) + { + return new static(array_intersect($this->items, $this->convertToArray($items))); } /** - * 删除数组的最后一个元素(出栈) - * + * 返回并删除数据中的的最后一个元素(出栈) + * @access public * @return mixed */ public function pop() @@ -116,42 +153,74 @@ class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSeria } /** - * 通过使用用户自定义函数,以字符串返回数组 - * - * @param callable $callback - * @param mixed $initial + * 返回并删除数据中首个元素 + * @access public * @return mixed */ - public function reduce(callable $callback, $initial = null) + public function shift() { - return array_reduce($this->items, $callback, $initial); + return array_shift($this->items); } /** - * 以相反的顺序返回数组。 - * - * @return static + * 在数组开头插入一个元素 + * @access public + * @param mixed $value 值 + * @param mixed $key 键名 + * @return void */ - public function reverse() + public function unshift($value, $key = null) { - return new static(array_reverse($this->items)); + if (is_null($key)) { + array_unshift($this->items, $value); + } else { + $this->items = [$key => $value] + $this->items; + } + } + + /** + * 在数组结尾插入一个元素 + * @access public + * @param mixed $value 值 + * @param mixed $key 键名 + * @return void + */ + public function push($value, $key = null) + { + if (is_null($key)) { + $this->items[] = $value; + } else { + $this->items[$key] = $value; + } } /** - * 删除数组中首个元素,并返回被删除元素的值 - * + * 通过使用用户自定义函数,以字符串返回数组 + * @access public + * @param callable $callback 回调函数 + * @param mixed $initial 初始值 * @return mixed */ - public function shift() + public function reduce(callable $callback, $initial = null) { - return array_shift($this->items); + return array_reduce($this->items, $callback, $initial); + } + + /** + * 以相反的顺序创建一个新的 Collection 实例 + * @access public + * @return static + */ + public function reverse() + { + return new static(array_reverse($this->items)); } /** - * 把一个数组分割为新的数组块. - * - * @param int $size - * @param bool $preserveKeys + * 把数据分割为新的数组块 + * @access public + * @param int $size 分隔长度 + * @param bool $preserveKeys 是否保持原数据索引 * @return static */ public function chunk($size, $preserveKeys = false) @@ -166,78 +235,70 @@ class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSeria } /** - * 在数组开头插入一个元素 - * @param mixed $value - * @param null $key - * @return int - */ - public function unshift($value, $key = null) - { - if (is_null($key)) { - array_unshift($this->items, $value); - } else { - $this->items = [$key => $value] + $this->items; - } - } - - /** - * 给每个元素执行个回调 - * - * @param callable $callback + * 给数据中的每个元素执行回调 + * @access public + * @param callable $callback 回调函数 * @return $this */ public function each(callable $callback) { foreach ($this->items as $key => $item) { - if ($callback($item, $key) === false) { + $result = $callback($item, $key); + + if (false === $result) { break; } + + if (!is_object($item)) { + $this->items[$key] = $result; + } } return $this; } /** - * 用回调函数过滤数组中的元素 - * @param callable|null $callback + * 用回调函数过滤数据中的元素 + * @access public + * @param callable|null $callback 回调函数 * @return static */ public function filter(callable $callback = null) { - if ($callback) { - return new static(array_filter($this->items, $callback)); - } - - return new static(array_filter($this->items)); + return new static(array_filter($this->items, $callback ?: null)); } /** - * 返回数组中指定的一列 - * @param $column_key - * @param null $index_key + * 返回数据中指定的一列 + * @access public + * @param mixed $columnKey 键名 + * @param null $indexKey 作为索引值的列 * @return array */ - public function column($column_key, $index_key = null) + public function column($columnKey, $indexKey = null) { if (function_exists('array_column')) { - return array_column($this->items, $column_key, $index_key); + return array_column($this->items, $columnKey, $indexKey); } $result = []; foreach ($this->items as $row) { - $key = $value = null; + $key = $value = null; $keySet = $valueSet = false; - if (null !== $index_key && array_key_exists($index_key, $row)) { + + if (null !== $indexKey && array_key_exists($indexKey, $row)) { + $key = (string) $row[$indexKey]; $keySet = true; - $key = (string) $row[$index_key]; } - if (null === $column_key) { + + if (null === $columnKey) { $valueSet = true; $value = $row; - } elseif (is_array($row) && array_key_exists($column_key, $row)) { + } elseif (is_array($row) && array_key_exists($columnKey, $row)) { $valueSet = true; - $value = $row[$column_key]; + $value = $row[$columnKey]; } + if ($valueSet) { if ($keySet) { $result[$key] = $value; @@ -246,34 +307,30 @@ class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSeria } } } + return $result; } /** - * 对数组排序 - * - * @param callable|null $callback + * 对数据排序,并返回排序后的数据组成的新 Collection 实例 + * @access public + * @param callable|null $callback 回调函数 * @return static */ public function sort(callable $callback = null) { - $items = $this->items; - - $callback ? uasort($items, $callback) : uasort($items, function ($a, $b) { - - if ($a == $b) { - return 0; - } - - return ($a < $b) ? -1 : 1; - }); + $items = $this->items; + $callback = $callback ?: function ($a, $b) { + return $a == $b ? 0 : (($a < $b) ? -1 : 1); + }; + uasort($items, $callback); return new static($items); } /** - * 将数组打乱 - * + * 将数据打乱后组成新的 Collection 实例 + * @access public * @return static */ public function shuffle() @@ -281,16 +338,15 @@ class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSeria $items = $this->items; shuffle($items); - return new static($items); } /** - * 截取数组 - * - * @param int $offset - * @param int $length - * @param bool $preserveKeys + * 截取数据并返回新的 Collection 实例 + * @access public + * @param int $offset 起始位置 + * @param int $length 截取长度 + * @param bool $preserveKeys 是否保持原先的键名 * @return static */ public function slice($offset, $length = null, $preserveKeys = false) @@ -298,17 +354,35 @@ class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSeria return new static(array_slice($this->items, $offset, $length, $preserveKeys)); } - // ArrayAccess + /** + * 指定的键是否存在 + * @access public + * @param mixed $offset 键名 + * @return bool + */ public function offsetExists($offset) { return array_key_exists($offset, $this->items); } + /** + * 获取指定键对应的值 + * @access public + * @param mixed $offset 键名 + * @return mixed + */ public function offsetGet($offset) { return $this->items[$offset]; } + /** + * 设置键值 + * @access public + * @param mixed $offset 键名 + * @param mixed $value 值 + * @return void + */ public function offsetSet($offset, $value) { if (is_null($offset)) { @@ -318,33 +392,51 @@ class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSeria } } + /** + * 删除指定键值 + * @access public + * @param mixed $offset 键名 + * @return void + */ public function offsetUnset($offset) { unset($this->items[$offset]); } - //Countable + /** + * 统计数据的个数 + * @access public + * @return int + */ public function count() { return count($this->items); } - //IteratorAggregate + /** + * 获取数据的迭代器 + * @access public + * @return ArrayIterator + */ public function getIterator() { return new ArrayIterator($this->items); } - //JsonSerializable + /** + * 将数据反序列化成数组 + * @access public + * @return array + */ public function jsonSerialize() { return $this->toArray(); } /** - * 转换当前数据集为JSON字符串 + * 转换当前数据集为 JSON 字符串 * @access public - * @param integer $options json参数 + * @param integer $options json 参数 * @return string */ public function toJson($options = JSON_UNESCAPED_UNICODE) @@ -352,22 +444,24 @@ class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSeria return json_encode($this->toArray(), $options); } + /** + * 将数据转换成字符串 + * @access public + * @return string + */ public function __toString() { return $this->toJson(); } /** - * 转换成数组 - * - * @param mixed $items + * 将数据转换成数组 + * @access protected + * @param mixed $items 数据 * @return array */ protected function convertToArray($items) { - if ($items instanceof self) { - return $items->all(); - } - return (array) $items; + return $items instanceof self ? $items->all() : (array) $items; } } diff --git a/thinkphp/library/think/Config.php b/thinkphp/library/think/Config.php index 2b808434..8fa668d1 100644 --- a/thinkphp/library/think/Config.php +++ b/thinkphp/library/think/Config.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -13,70 +13,88 @@ namespace think; class Config { - // 配置参数 + /** + * @var array 配置参数 + */ private static $config = []; - // 参数作用域 + + /** + * @var string 参数作用域 + */ private static $range = '_sys_'; - // 设定配置参数的作用域 + /** + * 设定配置参数的作用域 + * @access public + * @param string $range 作用域 + * @return void + */ public static function range($range) { self::$range = $range; - if (!isset(self::$config[$range])) { - self::$config[$range] = []; - } + + if (!isset(self::$config[$range])) self::$config[$range] = []; } /** * 解析配置文件或内容 - * @param string $config 配置文件路径或内容 - * @param string $type 配置解析类型 - * @param string $name 配置名(如设置即表示二级配置) - * @param string $range 作用域 + * @access public + * @param string $config 配置文件路径或内容 + * @param string $type 配置解析类型 + * @param string $name 配置名(如设置即表示二级配置) + * @param string $range 作用域 * @return mixed */ public static function parse($config, $type = '', $name = '', $range = '') { $range = $range ?: self::$range; - if (empty($type)) { - $type = pathinfo($config, PATHINFO_EXTENSION); - } - $class = false !== strpos($type, '\\') ? $type : '\\think\\config\\driver\\' . ucwords($type); + + if (empty($type)) $type = pathinfo($config, PATHINFO_EXTENSION); + + $class = false !== strpos($type, '\\') ? + $type : + '\\think\\config\\driver\\' . ucwords($type); + return self::set((new $class())->parse($config), $name, $range); } /** * 加载配置文件(PHP格式) - * @param string $file 配置文件名 - * @param string $name 配置名(如设置即表示二级配置) - * @param string $range 作用域 + * @access public + * @param string $file 配置文件名 + * @param string $name 配置名(如设置即表示二级配置) + * @param string $range 作用域 * @return mixed */ public static function load($file, $name = '', $range = '') { $range = $range ?: self::$range; - if (!isset(self::$config[$range])) { - self::$config[$range] = []; - } + + if (!isset(self::$config[$range])) self::$config[$range] = []; + if (is_file($file)) { $name = strtolower($name); $type = pathinfo($file, PATHINFO_EXTENSION); + if ('php' == $type) { return self::set(include $file, $name, $range); - } elseif ('yaml' == $type && function_exists('yaml_parse_file')) { + } + + if ('yaml' == $type && function_exists('yaml_parse_file')) { return self::set(yaml_parse_file($file), $name, $range); - } else { - return self::parse($file, $type, $name, $range); } - } else { - return self::$config[$range]; + + return self::parse($file, $type, $name, $range); } + + return self::$config[$range]; } /** * 检测配置是否存在 - * @param string $name 配置参数名(支持二级配置 .号分割) - * @param string $range 作用域 + * @access public + * @param string $name 配置参数名(支持二级配置 . 号分割) + * @param string $range 作用域 * @return bool */ public static function has($name, $range = '') @@ -85,82 +103,108 @@ class Config if (!strpos($name, '.')) { return isset(self::$config[$range][strtolower($name)]); - } else { - // 二维数组设置和获取支持 - $name = explode('.', $name, 2); - return isset(self::$config[$range][strtolower($name[0])][$name[1]]); } + + // 二维数组设置和获取支持 + $name = explode('.', $name, 2); + return isset(self::$config[$range][strtolower($name[0])][$name[1]]); } /** * 获取配置参数 为空则获取所有配置 - * @param string $name 配置参数名(支持二级配置 .号分割) - * @param string $range 作用域 + * @access public + * @param string $name 配置参数名(支持二级配置 . 号分割) + * @param string $range 作用域 * @return mixed */ public static function get($name = null, $range = '') { $range = $range ?: self::$range; + // 无参数时获取所有 if (empty($name) && isset(self::$config[$range])) { return self::$config[$range]; } + // 非二级配置时直接返回 if (!strpos($name, '.')) { $name = strtolower($name); return isset(self::$config[$range][$name]) ? self::$config[$range][$name] : null; - } else { - // 二维数组设置和获取支持 - $name = explode('.', $name, 2); - $name[0] = strtolower($name[0]); - return isset(self::$config[$range][$name[0]][$name[1]]) ? self::$config[$range][$name[0]][$name[1]] : null; } + + // 二维数组设置和获取支持 + $name = explode('.', $name, 2); + $name[0] = strtolower($name[0]); + + if (!isset(self::$config[$range][$name[0]])) { + // 动态载入额外配置 + $module = Request::instance()->module(); + $file = CONF_PATH . ($module ? $module . DS : '') . 'extra' . DS . $name[0] . CONF_EXT; + + is_file($file) && self::load($file, $name[0]); + } + + return isset(self::$config[$range][$name[0]][$name[1]]) ? + self::$config[$range][$name[0]][$name[1]] : + null; } /** - * 设置配置参数 name为数组则为批量设置 - * @param string|array $name 配置参数名(支持二级配置 .号分割) - * @param mixed $value 配置值 - * @param string $range 作用域 + * 设置配置参数 name 为数组则为批量设置 + * @access public + * @param string|array $name 配置参数名(支持二级配置 . 号分割) + * @param mixed $value 配置值 + * @param string $range 作用域 * @return mixed */ public static function set($name, $value = null, $range = '') { $range = $range ?: self::$range; - if (!isset(self::$config[$range])) { - self::$config[$range] = []; - } + + if (!isset(self::$config[$range])) self::$config[$range] = []; + + // 字符串则表示单个配置设置 if (is_string($name)) { if (!strpos($name, '.')) { self::$config[$range][strtolower($name)] = $value; } else { - // 二维数组设置和获取支持 - $name = explode('.', $name, 2); + // 二维数组 + $name = explode('.', $name, 2); self::$config[$range][strtolower($name[0])][$name[1]] = $value; } - return; - } elseif (is_array($name)) { - // 批量设置 + + return $value; + } + + // 数组则表示批量设置 + if (is_array($name)) { if (!empty($value)) { self::$config[$range][$value] = isset(self::$config[$range][$value]) ? - array_merge(self::$config[$range][$value], $name) : - self::$config[$range][$value] = $name; + array_merge(self::$config[$range][$value], $name) : + $name; + return self::$config[$range][$value]; - } else { - return self::$config[$range] = array_merge(self::$config[$range], array_change_key_case($name)); } - } else { - // 为空直接返回 已有配置 - return self::$config[$range]; + + return self::$config[$range] = array_merge( + self::$config[$range], array_change_key_case($name) + ); } + + // 为空直接返回已有配置 + return self::$config[$range]; } /** * 重置配置参数 + * @access public + * @param string $range 作用域 + * @return void */ public static function reset($range = '') { $range = $range ?: self::$range; + if (true === $range) { self::$config = []; } else { diff --git a/thinkphp/library/think/Console.php b/thinkphp/library/think/Console.php index 1d97ab2b..32b25725 100644 --- a/thinkphp/library/think/Console.php +++ b/thinkphp/library/think/Console.php @@ -20,20 +20,49 @@ use think\console\output\driver\Buffer; class Console { - + /** + * @var string 命令名称 + */ private $name; + + /** + * @var string 命令版本 + */ private $version; - /** @var Command[] */ + /** + * @var Command[] 命令 + */ private $commands = []; + /** + * @var bool 是否需要帮助信息 + */ private $wantHelps = false; + /** + * @var bool 是否捕获异常 + */ private $catchExceptions = true; - private $autoExit = true; + + /** + * @var bool 是否自动退出执行 + */ + private $autoExit = true; + + /** + * @var InputDefinition 输入定义 + */ private $definition; + + /** + * @var string 默认执行的命令 + */ private $defaultCommand; + /** + * @var array 默认提供的命令 + */ private static $defaultCommands = [ "think\\console\\command\\Help", "think\\console\\command\\Lists", @@ -47,11 +76,22 @@ class Console "think\\console\\command\\optimize\\Schema", ]; - public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') + /** + * Console constructor. + * @access public + * @param string $name 名称 + * @param string $version 版本 + * @param null|string $user 执行用户 + */ + public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN', $user = null) { $this->name = $name; $this->version = $version; + if ($user) { + $this->setUser($user); + } + $this->defaultCommand = 'list'; $this->definition = $this->getDefaultInputDefinition(); @@ -60,38 +100,58 @@ class Console } } + /** + * 设置执行用户 + * @param $user + */ + public function setUser($user) + { + $user = posix_getpwnam($user); + if ($user) { + posix_setuid($user['uid']); + posix_setgid($user['gid']); + } + } + + /** + * 初始化 Console + * @access public + * @param bool $run 是否运行 Console + * @return int|Console + */ public static function init($run = true) { static $console; + if (!$console) { - // 实例化console - $console = new self('Think Console', '0.1'); + $config = Config::get('console'); + // 实例化 console + $console = new self($config['name'], $config['version'], $config['user']); + // 读取指令集 if (is_file(CONF_PATH . 'command' . EXT)) { $commands = include CONF_PATH . 'command' . EXT; + if (is_array($commands)) { foreach ($commands as $command) { - if (class_exists($command) && is_subclass_of($command, "\\think\\console\\Command")) { - // 注册指令 - $console->add(new $command()); - } + class_exists($command) && + is_subclass_of($command, "\\think\\console\\Command") && + $console->add(new $command()); // 注册指令 } } } } - if ($run) { - // 运行 - return $console->run(); - } else { - return $console; - } + + return $run ? $console->run() : $console; } /** - * @param $command - * @param array $parameters - * @param string $driver - * @return Output|Buffer + * 调用命令 + * @access public + * @param string $command + * @param array $parameters + * @param string $driver + * @return Output */ public static function call($command, array $parameters = [], $driver = 'buffer') { @@ -110,9 +170,9 @@ class Console /** * 执行当前的指令 + * @access public * @return int * @throws \Exception - * @api */ public function run() { @@ -124,27 +184,21 @@ class Console try { $exitCode = $this->doRun($input, $output); } catch (\Exception $e) { - if (!$this->catchExceptions) { - throw $e; - } + if (!$this->catchExceptions) throw $e; $output->renderException($e); $exitCode = $e->getCode(); + if (is_numeric($exitCode)) { - $exitCode = (int) $exitCode; - if (0 === $exitCode) { - $exitCode = 1; - } + $exitCode = ((int) $exitCode) ?: 1; } else { $exitCode = 1; } } if ($this->autoExit) { - if ($exitCode > 255) { - $exitCode = 255; - } + if ($exitCode > 255) $exitCode = 255; exit($exitCode); } @@ -154,12 +208,14 @@ class Console /** * 执行指令 - * @param Input $input - * @param Output $output + * @access public + * @param Input $input 输入 + * @param Output $output 输出 * @return int */ public function doRun(Input $input, Output $output) { + // 获取版本信息 if (true === $input->hasParameterOption(['--version', '-V'])) { $output->writeln($this->getLongVersion()); @@ -168,6 +224,7 @@ class Console $name = $this->getCommandName($input); + // 获取帮助信息 if (true === $input->hasParameterOption(['--help', '-h'])) { if (!$name) { $name = 'help'; @@ -182,25 +239,26 @@ class Console $input = new Input([$this->defaultCommand]); } - $command = $this->find($name); - - $exitCode = $this->doRunCommand($command, $input, $output); - - return $exitCode; + return $this->doRunCommand($this->find($name), $input, $output); } /** * 设置输入参数定义 - * @param InputDefinition $definition + * @access public + * @param InputDefinition $definition 输入定义 + * @return $this; */ public function setDefinition(InputDefinition $definition) { $this->definition = $definition; + + return $this; } /** * 获取输入参数定义 - * @return InputDefinition The InputDefinition instance + * @access public + * @return InputDefinition */ public function getDefinition() { @@ -208,8 +266,9 @@ class Console } /** - * Gets the help message. - * @return string A help message. + * 获取帮助信息 + * @access public + * @return string */ public function getHelp() { @@ -217,27 +276,34 @@ class Console } /** - * 是否捕获异常 - * @param bool $boolean - * @api + * 设置是否捕获异常 + * @access public + * @param bool $boolean 是否捕获 + * @return $this */ public function setCatchExceptions($boolean) { $this->catchExceptions = (bool) $boolean; + + return $this; } /** - * 是否自动退出 - * @param bool $boolean - * @api + * 设置是否自动退出 + * @access public + * @param bool $boolean 是否自动退出 + * @return $this */ public function setAutoExit($boolean) { $this->autoExit = (bool) $boolean; + + return $this; } /** * 获取名称 + * @access public * @return string */ public function getName() @@ -247,17 +313,21 @@ class Console /** * 设置名称 - * @param string $name + * @access public + * @param string $name 名称 + * @return $this */ public function setName($name) { $this->name = $name; + + return $this; } /** * 获取版本 + * @access public * @return string - * @api */ public function getVersion() { @@ -266,21 +336,30 @@ class Console /** * 设置版本 - * @param string $version + * @access public + * @param string $version 版本信息 + * @return $this */ public function setVersion($version) { $this->version = $version; + + return $this; } /** * 获取完整的版本号 + * @access public * @return string */ public function getLongVersion() { if ('UNKNOWN' !== $this->getName() && 'UNKNOWN' !== $this->getVersion()) { - return sprintf('%s version %s', $this->getName(), $this->getVersion()); + return sprintf( + '%s version %s', + $this->getName(), + $this->getVersion() + ); } return 'Console Tool'; @@ -288,7 +367,8 @@ class Console /** * 注册一个指令 - * @param string $name + * @access public + * @param string $name 指令名称 * @return Command */ public function register($name) @@ -297,32 +377,37 @@ class Console } /** - * 添加指令 - * @param Command[] $commands + * 批量添加指令 + * @access public + * @param Command[] $commands 指令实例 + * @return $this */ public function addCommands(array $commands) { - foreach ($commands as $command) { - $this->add($command); - } + foreach ($commands as $command) $this->add($command); + + return $this; } /** * 添加一个指令 - * @param Command $command - * @return Command + * @access public + * @param Command $command 命令实例 + * @return Command|bool */ public function add(Command $command) { - $command->setConsole($this); - if (!$command->isEnabled()) { $command->setConsole(null); - return; + return false; } + $command->setConsole($this); + if (null === $command->getDefinition()) { - throw new \LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', get_class($command))); + throw new \LogicException( + sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', get_class($command)) + ); } $this->commands[$command->getName()] = $command; @@ -336,14 +421,17 @@ class Console /** * 获取指令 - * @param string $name 指令名称 + * @access public + * @param string $name 指令名称 * @return Command * @throws \InvalidArgumentException */ public function get($name) { if (!isset($this->commands[$name])) { - throw new \InvalidArgumentException(sprintf('The command "%s" does not exist.', $name)); + throw new \InvalidArgumentException( + sprintf('The command "%s" does not exist.', $name) + ); } $command = $this->commands[$name]; @@ -363,7 +451,8 @@ class Console /** * 某个指令是否存在 - * @param string $name 指令名称 + * @access public + * @param string $name 指令名称 * @return bool */ public function has($name) @@ -373,16 +462,22 @@ class Console /** * 获取所有的命名空间 + * @access public * @return array */ public function getNamespaces() { $namespaces = []; + foreach ($this->commands as $command) { - $namespaces = array_merge($namespaces, $this->extractAllNamespaces($command->getName())); + $namespaces = array_merge( + $namespaces, $this->extractAllNamespaces($command->getName()) + ); foreach ($command->getAliases() as $alias) { - $namespaces = array_merge($namespaces, $this->extractAllNamespaces($alias)); + $namespaces = array_merge( + $namespaces, $this->extractAllNamespaces($alias) + ); } } @@ -390,21 +485,25 @@ class Console } /** - * 查找注册命名空间中的名称或缩写。 + * 查找注册命名空间中的名称或缩写 + * @access public * @param string $namespace * @return string * @throws \InvalidArgumentException */ public function findNamespace($namespace) { - $allNamespaces = $this->getNamespaces(); - $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { + $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]) . '[^:]*'; }, $namespace); - $namespaces = preg_grep('{^' . $expr . '}', $allNamespaces); + + $allNamespaces = $this->getNamespaces(); + $namespaces = preg_grep('{^' . $expr . '}', $allNamespaces); if (empty($namespaces)) { - $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace); + $message = sprintf( + 'There are no commands defined in the "%s" namespace.', $namespace + ); if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) { if (1 == count($alternatives)) { @@ -420,8 +519,14 @@ class Console } $exact = in_array($namespace, $namespaces, true); + if (count($namespaces) > 1 && !$exact) { - throw new \InvalidArgumentException(sprintf('The namespace "%s" is ambiguous (%s).', $namespace, $this->getAbbreviationSuggestions(array_values($namespaces)))); + throw new \InvalidArgumentException( + sprintf( + 'The namespace "%s" is ambiguous (%s).', + $namespace, + $this->getAbbreviationSuggestions(array_values($namespaces))) + ); } return $exact ? $namespace : reset($namespaces); @@ -429,20 +534,22 @@ class Console /** * 查找指令 - * @param string $name 名称或者别名 + * @access public + * @param string $name 名称或者别名 * @return Command * @throws \InvalidArgumentException */ public function find($name) { - $allCommands = array_keys($this->commands); - $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { + $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]) . '[^:]*'; }, $name); - $commands = preg_grep('{^' . $expr . '}', $allCommands); + + $allCommands = array_keys($this->commands); + $commands = preg_grep('{^' . $expr . '}', $allCommands); if (empty($commands) || count(preg_grep('{^' . $expr . '$}', $commands)) < 1) { - if (false !== $pos = strrpos($name, ':')) { + if (false !== ($pos = strrpos($name, ':'))) { $this->findNamespace(substr($name, 0, $pos)); } @@ -473,7 +580,9 @@ class Console if (count($commands) > 1 && !$exact) { $suggestions = $this->getAbbreviationSuggestions(array_values($commands)); - throw new \InvalidArgumentException(sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions)); + throw new \InvalidArgumentException( + sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions) + ); } return $this->get($exact ? $name : reset($commands)); @@ -481,21 +590,20 @@ class Console /** * 获取所有的指令 - * @param string $namespace 命名空间 + * @access public + * @param string $namespace 命名空间 * @return Command[] - * @api */ public function all($namespace = null) { - if (null === $namespace) { - return $this->commands; - } + if (null === $namespace) return $this->commands; $commands = []; + foreach ($this->commands as $name => $command) { - if ($this->extractNamespace($name, substr_count($namespace, ':') + 1) === $namespace) { - $commands[$name] = $command; - } + $ext = $this->extractNamespace($name, substr_count($namespace, ':') + 1); + + if ($ext === $namespace) $commands[$name] = $command; } return $commands; @@ -503,7 +611,8 @@ class Console /** * 获取可能的指令名 - * @param array $names + * @access public + * @param array $names 指令名 * @return array */ public static function getAbbreviations($names) @@ -520,9 +629,11 @@ class Console } /** - * 配置基于用户的参数和选项的输入和输出实例。 - * @param Input $input 输入实例 - * @param Output $output 输出实例 + * 配置基于用户的参数和选项的输入和输出实例 + * @access protected + * @param Input $input 输入实例 + * @param Output $output 输出实例 + * @return void */ protected function configureIO(Input $input, Output $output) { @@ -551,9 +662,10 @@ class Console /** * 执行指令 - * @param Command $command 指令实例 - * @param Input $input 输入实例 - * @param Output $output 输出实例 + * @access protected + * @param Command $command 指令实例 + * @param Input $input 输入实例 + * @param Output $output 输出实例 * @return int * @throws \Exception */ @@ -563,8 +675,9 @@ class Console } /** - * 获取指令的基础名称 - * @param Input $input + * 获取指令的名称 + * @access protected + * @param Input $input 输入实例 * @return string */ protected function getCommandName(Input $input) @@ -574,6 +687,7 @@ class Console /** * 获取默认输入定义 + * @access protected * @return InputDefinition */ protected function getDefaultInputDefinition() @@ -591,41 +705,55 @@ class Console } /** - * 设置默认命令 - * @return Command[] An array of default Command instances + * 获取默认命令 + * @access protected + * @return Command[] */ protected function getDefaultCommands() { $defaultCommands = []; - foreach (self::$defaultCommands as $classname) { - if (class_exists($classname) && is_subclass_of($classname, "think\\console\\Command")) { - $defaultCommands[] = new $classname(); + foreach (self::$defaultCommands as $class) { + if (class_exists($class) && is_subclass_of($class, "think\\console\\Command")) { + $defaultCommands[] = new $class(); } } return $defaultCommands; } - public static function addDefaultCommands(array $classnames) + /** + * 添加默认指令 + * @access public + * @param array $classes 指令 + * @return void + */ + public static function addDefaultCommands(array $classes) { - self::$defaultCommands = array_merge(self::$defaultCommands, $classnames); + self::$defaultCommands = array_merge(self::$defaultCommands, $classes); } /** * 获取可能的建议 - * @param array $abbrevs + * @access private + * @param array $abbrevs * @return string */ private function getAbbreviationSuggestions($abbrevs) { - return sprintf('%s, %s%s', $abbrevs[0], $abbrevs[1], count($abbrevs) > 2 ? sprintf(' and %d more', count($abbrevs) - 2) : ''); + return sprintf( + '%s, %s%s', + $abbrevs[0], + $abbrevs[1], + count($abbrevs) > 2 ? sprintf(' and %d more', count($abbrevs) - 2) : '' + ); } /** - * 返回命名空间部分 - * @param string $name 指令 - * @param string $limit 部分的命名空间的最大数量 + * 返回指令的命名空间部分 + * @access public + * @param string $name 指令名称 + * @param string $limit 部分的命名空间的最大数量 * @return string */ public function extractNamespace($name, $limit = null) @@ -638,16 +766,17 @@ class Console /** * 查找可替代的建议 - * @param string $name - * @param array|\Traversable $collection + * @access private + * @param string $name 指令名称 + * @param array|\Traversable $collection 建议集合 * @return array */ private function findAlternatives($name, $collection) { - $threshold = 1e3; - $alternatives = []; - + $threshold = 1e3; + $alternatives = []; $collectionParts = []; + foreach ($collection as $item) { $collectionParts[$item] = explode(':', $item); } @@ -655,6 +784,7 @@ class Console foreach (explode(':', $name) as $i => $subname) { foreach ($collectionParts as $collectionName => $parts) { $exists = isset($alternatives[$collectionName]); + if (!isset($parts[$i]) && $exists) { $alternatives[$collectionName] += $threshold; continue; @@ -663,8 +793,14 @@ class Console } $lev = levenshtein($subname, $parts[$i]); - if ($lev <= strlen($subname) / 3 || '' !== $subname && false !== strpos($parts[$i], $subname)) { - $alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev; + + if ($lev <= strlen($subname) / 3 || + '' !== $subname && + false !== strpos($parts[$i], $subname) + ) { + $alternatives[$collectionName] = $exists ? + $alternatives[$collectionName] + $lev : + $lev; } elseif ($exists) { $alternatives[$collectionName] += $threshold; } @@ -673,14 +809,18 @@ class Console foreach ($collection as $item) { $lev = levenshtein($name, $item); + if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) { - $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev; + $alternatives[$item] = isset($alternatives[$item]) ? + $alternatives[$item] - $lev : + $lev; } } $alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; }); + asort($alternatives); return array_keys($alternatives); @@ -688,24 +828,28 @@ class Console /** * 设置默认的指令 - * @param string $commandName The Command name + * @access public + * @param string $commandName 指令名称 + * @return $this */ public function setDefaultCommand($commandName) { $this->defaultCommand = $commandName; + + return $this; } /** * 返回所有的命名空间 - * @param string $name + * @access private + * @param string $name 指令名称 * @return array */ private function extractAllNamespaces($name) { - $parts = explode(':', $name, -1); $namespaces = []; - foreach ($parts as $part) { + foreach (explode(':', $name, -1) as $part) { if (count($namespaces)) { $namespaces[] = end($namespaces) . ':' . $part; } else { diff --git a/thinkphp/library/think/Controller.php b/thinkphp/library/think/Controller.php index 7aba39cd..77225b73 100644 --- a/thinkphp/library/think/Controller.php +++ b/thinkphp/library/think/Controller.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -11,46 +11,49 @@ namespace think; -\think\Loader::import('controller/Jump', TRAIT_PATH, EXT); - use think\exception\ValidateException; +use traits\controller\Jump; + +Loader::import('controller/Jump', TRAIT_PATH, EXT); class Controller { - use \traits\controller\Jump; + use Jump; /** * @var \think\View 视图类实例 */ protected $view; + /** - * @var \think\Request Request实例 + * @var \think\Request Request 实例 */ protected $request; - // 验证失败是否抛出异常 + + /** + * @var bool 验证失败是否抛出异常 + */ protected $failException = false; - // 是否批量验证 + + /** + * @var bool 是否批量验证 + */ protected $batchValidate = false; /** - * 前置操作方法列表 - * @var array $beforeActionList - * @access protected + * @var array 前置操作方法列表 */ protected $beforeActionList = []; /** * 构造方法 - * @param Request $request Request对象 * @access public + * @param Request $request Request 对象 */ public function __construct(Request $request = null) { - if (is_null($request)) { - $request = Request::instance(); - } $this->view = View::instance(Config::get('template'), Config::get('view_replace_str')); - $this->request = $request; + $this->request = is_null($request) ? Request::instance() : $request; // 控制器初始化 $this->_initialize(); @@ -65,7 +68,10 @@ class Controller } } - // 初始化 + /** + * 初始化操作 + * @access protected + */ protected function _initialize() { } @@ -73,8 +79,9 @@ class Controller /** * 前置操作 * @access protected - * @param string $method 前置操作方法名 - * @param array $options 调用参数 ['only'=>[...]] 或者['except'=>[...]] + * @param string $method 前置操作方法名 + * @param array $options 调用参数 ['only'=>[...]] 或者 ['except'=>[...]] + * @return void */ protected function beforeAction($method, $options = []) { @@ -82,6 +89,7 @@ class Controller if (is_string($options['only'])) { $options['only'] = explode(',', $options['only']); } + if (!in_array($this->request->action(), $options['only'])) { return; } @@ -89,6 +97,7 @@ class Controller if (is_string($options['except'])) { $options['except'] = explode(',', $options['except']); } + if (in_array($this->request->action(), $options['except'])) { return; } @@ -100,10 +109,10 @@ class Controller /** * 加载模板输出 * @access protected - * @param string $template 模板文件名 - * @param array $vars 模板输出变量 - * @param array $replace 模板替换 - * @param array $config 模板参数 + * @param string $template 模板文件名 + * @param array $vars 模板输出变量 + * @param array $replace 模板替换 + * @param array $config 模板参数 * @return mixed */ protected function fetch($template = '', $vars = [], $replace = [], $config = []) @@ -114,10 +123,10 @@ class Controller /** * 渲染内容输出 * @access protected - * @param string $content 模板内容 - * @param array $vars 模板输出变量 - * @param array $replace 替换内容 - * @param array $config 模板参数 + * @param string $content 模板内容 + * @param array $vars 模板输出变量 + * @param array $replace 替换内容 + * @param array $config 模板参数 * @return mixed */ protected function display($content = '', $vars = [], $replace = [], $config = []) @@ -128,24 +137,28 @@ class Controller /** * 模板变量赋值 * @access protected - * @param mixed $name 要显示的模板变量 - * @param mixed $value 变量的值 - * @return void + * @param mixed $name 要显示的模板变量 + * @param mixed $value 变量的值 + * @return $this */ protected function assign($name, $value = '') { $this->view->assign($name, $value); + + return $this; } /** * 初始化模板引擎 * @access protected * @param array|string $engine 引擎参数 - * @return void + * @return $this */ protected function engine($engine) { $this->view->engine($engine); + + return $this; } /** @@ -157,17 +170,18 @@ class Controller protected function validateFailException($fail = true) { $this->failException = $fail; + return $this; } /** * 验证数据 * @access protected - * @param array $data 数据 - * @param string|array $validate 验证器名或者验证规则数组 - * @param array $message 提示信息 - * @param bool $batch 是否批量验证 - * @param mixed $callback 回调方法(闭包) + * @param array $data 数据 + * @param string|array $validate 验证器名或者验证规则数组 + * @param array $message 提示信息 + * @param bool $batch 是否批量验证 + * @param mixed $callback 回调方法(闭包) * @return array|string|true * @throws ValidateException */ @@ -177,24 +191,27 @@ class Controller $v = Loader::validate(); $v->rule($validate); } else { + // 支持场景 if (strpos($validate, '.')) { - // 支持场景 list($validate, $scene) = explode('.', $validate); } + $v = Loader::validate($validate); - if (!empty($scene)) { - $v->scene($scene); - } + + !empty($scene) && $v->scene($scene); } - // 是否批量验证 + + // 批量验证 if ($batch || $this->batchValidate) { $v->batch(true); } + // 设置错误信息 if (is_array($message)) { $v->message($message); } + // 使用回调验证 if ($callback && is_callable($callback)) { call_user_func_array($callback, [$v, &$data]); } @@ -202,11 +219,11 @@ class Controller if (!$v->check($data)) { if ($this->failException) { throw new ValidateException($v->getError()); - } else { - return $v->getError(); } - } else { - return true; + + return $v->getError(); } + + return true; } } diff --git a/thinkphp/library/think/Cookie.php b/thinkphp/library/think/Cookie.php index 3205fcd9..61b47cce 100644 --- a/thinkphp/library/think/Cookie.php +++ b/thinkphp/library/think/Cookie.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -13,28 +13,28 @@ namespace think; class Cookie { + /** + * @var array cookie 设置参数 + */ protected static $config = [ - // cookie 名称前缀 - 'prefix' => '', - // cookie 保存时间 - 'expire' => 0, - // cookie 保存路径 - 'path' => '/', - // cookie 有效域名 - 'domain' => '', - // cookie 启用安全传输 - 'secure' => false, - // httponly设置 - 'httponly' => '', - // 是否使用 setcookie - 'setcookie' => true, + 'prefix' => '', // cookie 名称前缀 + 'expire' => 0, // cookie 保存时间 + 'path' => '/', // cookie 保存路径 + 'domain' => '', // cookie 有效域名 + 'secure' => false, // cookie 启用安全传输 + 'httponly' => false, // httponly 设置 + 'setcookie' => true, // 是否使用 setcookie ]; + /** + * @var bool 是否完成初始化了 + */ protected static $init; /** * Cookie初始化 - * @param array $config + * @access public + * @param array $config 配置参数 * @return void */ public static function init(array $config = []) @@ -42,39 +42,43 @@ class Cookie if (empty($config)) { $config = Config::get('cookie'); } + self::$config = array_merge(self::$config, array_change_key_case($config)); + if (!empty(self::$config['httponly'])) { ini_set('session.cookie_httponly', 1); } + self::$init = true; } /** - * 设置或者获取cookie作用域(前缀) - * @param string $prefix - * @return string|void + * 设置或者获取 cookie 作用域(前缀) + * @access public + * @param string $prefix 前缀 + * @return string| */ public static function prefix($prefix = '') { if (empty($prefix)) { return self::$config['prefix']; } - self::$config['prefix'] = $prefix; + + return self::$config['prefix'] = $prefix; } /** * Cookie 设置、获取、删除 - * - * @param string $name cookie名称 - * @param mixed $value cookie值 - * @param mixed $option 可选参数 可能会是 null|integer|string - * - * @return mixed - * @internal param mixed $options cookie参数 + * @access public + * @param string $name cookie 名称 + * @param mixed $value cookie 值 + * @param mixed $option 可选参数 可能会是 null|integer|string + * @return void */ public static function set($name, $value = '', $option = null) { !isset(self::$init) && self::init(); + // 参数设置(会覆盖黙认设置) if (!is_null($option)) { if (is_numeric($option)) { @@ -82,28 +86,40 @@ class Cookie } elseif (is_string($option)) { parse_str($option, $option); } + $config = array_merge(self::$config, array_change_key_case($option)); } else { $config = self::$config; } + $name = $config['prefix'] . $name; - // 设置cookie + + // 设置 cookie if (is_array($value)) { array_walk_recursive($value, 'self::jsonFormatProtect', 'encode'); $value = 'think:' . json_encode($value); } - $expire = !empty($config['expire']) ? $_SERVER['REQUEST_TIME'] + intval($config['expire']) : 0; + + $expire = !empty($config['expire']) ? + $_SERVER['REQUEST_TIME'] + intval($config['expire']) : + 0; + if ($config['setcookie']) { - setcookie($name, $value, $expire, $config['path'], $config['domain'], $config['secure'], $config['httponly']); + setcookie( + $name, $value, $expire, $config['path'], $config['domain'], + $config['secure'], $config['httponly'] + ); } + $_COOKIE[$name] = $value; } /** - * 永久保存Cookie数据 - * @param string $name cookie名称 - * @param mixed $value cookie值 - * @param mixed $option 可选参数 可能会是 null|integer|string + * 永久保存 Cookie 数据 + * @access public + * @param string $name cookie 名称 + * @param mixed $value cookie 值 + * @param mixed $option 可选参数 可能会是 null|integer|string * @return void */ public static function forever($name, $value = '', $option = null) @@ -111,33 +127,39 @@ class Cookie if (is_null($option) || is_numeric($option)) { $option = []; } + $option['expire'] = 315360000; + self::set($name, $value, $option); } /** - * 判断Cookie数据 - * @param string $name cookie名称 - * @param string|null $prefix cookie前缀 + * 判断是否有 Cookie 数据 + * @access public + * @param string $name cookie 名称 + * @param string|null $prefix cookie 前缀 * @return bool */ public static function has($name, $prefix = null) { !isset(self::$init) && self::init(); + $prefix = !is_null($prefix) ? $prefix : self::$config['prefix']; - $name = $prefix . $name; - return isset($_COOKIE[$name]); + + return isset($_COOKIE[$prefix . $name]); } /** - * Cookie获取 - * @param string $name cookie名称 - * @param string|null $prefix cookie前缀 + * 获取 Cookie 的值 + * @access public + * @param string $name cookie 名称 + * @param string|null $prefix cookie 前缀 * @return mixed */ public static function get($name = '', $prefix = null) { !isset(self::$init) && self::init(); + $prefix = !is_null($prefix) ? $prefix : self::$config['prefix']; $key = $prefix . $name; @@ -145,80 +167,102 @@ class Cookie // 获取全部 if ($prefix) { $value = []; + foreach ($_COOKIE as $k => $val) { if (0 === strpos($k, $prefix)) { $value[$k] = $val; } + } } else { $value = $_COOKIE; } } elseif (isset($_COOKIE[$key])) { $value = $_COOKIE[$key]; + if (0 === strpos($value, 'think:')) { - $value = substr($value, 6); - $value = json_decode($value, true); + $value = json_decode(substr($value, 6), true); array_walk_recursive($value, 'self::jsonFormatProtect', 'decode'); } } else { $value = null; } + return $value; } /** - * Cookie删除 - * @param string $name cookie名称 - * @param string|null $prefix cookie前缀 - * @return mixed + * 删除 Cookie + * @access public + * @param string $name cookie 名称 + * @param string|null $prefix cookie 前缀 + * @return void */ public static function delete($name, $prefix = null) { !isset(self::$init) && self::init(); + $config = self::$config; $prefix = !is_null($prefix) ? $prefix : $config['prefix']; $name = $prefix . $name; + if ($config['setcookie']) { - setcookie($name, '', $_SERVER['REQUEST_TIME'] - 3600, $config['path'], $config['domain'], $config['secure'], $config['httponly']); + setcookie( + $name, '', $_SERVER['REQUEST_TIME'] - 3600, $config['path'], + $config['domain'], $config['secure'], $config['httponly'] + ); } - // 删除指定cookie + + // 删除指定 cookie unset($_COOKIE[$name]); } /** - * Cookie清空 - * @param string|null $prefix cookie前缀 - * @return mixed + * 清除指定前缀的所有 cookie + * @access public + * @param string|null $prefix cookie 前缀 + * @return void */ public static function clear($prefix = null) { - // 清除指定前缀的所有cookie if (empty($_COOKIE)) { return; } + !isset(self::$init) && self::init(); - // 要删除的cookie前缀,不指定则删除config设置的指定前缀 + + // 要删除的 cookie 前缀,不指定则删除 config 设置的指定前缀 $config = self::$config; $prefix = !is_null($prefix) ? $prefix : $config['prefix']; + if ($prefix) { - // 如果前缀为空字符串将不作处理直接返回 foreach ($_COOKIE as $key => $val) { if (0 === strpos($key, $prefix)) { if ($config['setcookie']) { - setcookie($key, '', $_SERVER['REQUEST_TIME'] - 3600, $config['path'], $config['domain'], $config['secure'], $config['httponly']); + setcookie( + $key, '', $_SERVER['REQUEST_TIME'] - 3600, $config['path'], + $config['domain'], $config['secure'], $config['httponly'] + ); } + unset($_COOKIE[$key]); } } } - return; } - private static function jsonFormatProtect(&$val, $key, $type = 'encode') + /** + * json 转换时的格式保护 + * @access protected + * @param mixed $val 要转换的值 + * @param string $key 键名 + * @param string $type 转换类别 + * @return void + */ + protected static function jsonFormatProtect(&$val, $key, $type = 'encode') { if (!empty($val) && true !== $val) { $val = 'decode' == $type ? urldecode($val) : urlencode($val); } } - } diff --git a/thinkphp/library/think/Db.php b/thinkphp/library/think/Db.php index 3e613f7a..80f08d24 100644 --- a/thinkphp/library/think/Db.php +++ b/thinkphp/library/think/Db.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -49,19 +49,26 @@ use think\db\Query; */ class Db { - // 数据库连接实例 + /** + * @var Connection[] 数据库连接实例 + */ private static $instance = []; - // 查询次数 + + /** + * @var int 查询次数 + */ public static $queryTimes = 0; - // 执行次数 + + /** + * @var int 执行次数 + */ public static $executeTimes = 0; /** - * 数据库初始化 并取得数据库类实例 - * @static + * 数据库初始化,并取得数据库类实例 * @access public - * @param mixed $config 连接配置 - * @param bool|string $name 连接标识 true 强制重新连接 + * @param mixed $config 连接配置 + * @param bool|string $name 连接标识 true 强制重新连接 * @return Connection * @throws Exception */ @@ -70,31 +77,48 @@ class Db if (false === $name) { $name = md5(serialize($config)); } + if (true === $name || !isset(self::$instance[$name])) { // 解析连接参数 支持数组和字符串 $options = self::parseConfig($config); + if (empty($options['type'])) { - throw new \InvalidArgumentException('Underfined db type'); + throw new \InvalidArgumentException('Undefined db type'); } - $class = false !== strpos($options['type'], '\\') ? $options['type'] : '\\think\\db\\connector\\' . ucwords($options['type']); + + $class = false !== strpos($options['type'], '\\') ? + $options['type'] : + '\\think\\db\\connector\\' . ucwords($options['type']); + // 记录初始化信息 if (App::$debug) { Log::record('[ DB ] INIT ' . $options['type'], 'info'); } + if (true === $name) { - return new $class($options); - } else { - self::$instance[$name] = new $class($options); + $name = md5(serialize($config)); } + + self::$instance[$name] = new $class($options); } + return self::$instance[$name]; } + /** + * 清除连接实例 + * @access public + * @return void + */ + public static function clear() + { + self::$instance = []; + } + /** * 数据库连接参数解析 - * @static * @access private - * @param mixed $config + * @param mixed $config 连接参数 * @return array */ private static function parseConfig($config) @@ -102,30 +126,27 @@ class Db if (empty($config)) { $config = Config::get('database'); } elseif (is_string($config) && false === strpos($config, '/')) { - // 支持读取配置参数 - $config = Config::get($config); - } - if (is_string($config)) { - return self::parseDsn($config); - } else { - return $config; + $config = Config::get($config); // 支持读取配置参数 } + + return is_string($config) ? self::parseDsn($config) : $config; } /** - * DSN解析 + * DSN 解析 * 格式: mysql://username:passwd@localhost:3306/DbName?param1=val1¶m2=val2#utf8 - * @static * @access private - * @param string $dsnStr + * @param string $dsnStr 数据库 DSN 字符串解析 * @return array */ private static function parseDsn($dsnStr) { $info = parse_url($dsnStr); + if (!$info) { return []; } + $dsn = [ 'type' => $info['scheme'], 'username' => isset($info['user']) ? $info['user'] : '', @@ -141,13 +162,19 @@ class Db } else { $dsn['params'] = []; } + return $dsn; } - // 调用驱动类的方法 + /** + * 调用驱动类的方法 + * @access public + * @param string $method 方法名 + * @param array $params 参数 + * @return mixed + */ public static function __callStatic($method, $params) { - // 自动初始化数据库 return call_user_func_array([self::connect(), $method], $params); } } diff --git a/thinkphp/library/think/Debug.php b/thinkphp/library/think/Debug.php index 9994e20c..df487485 100644 --- a/thinkphp/library/think/Debug.php +++ b/thinkphp/library/think/Debug.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -16,21 +16,27 @@ use think\response\Redirect; class Debug { - // 区间时间信息 + /** + * @var array 区间时间信息 + */ protected static $info = []; - // 区间内存信息 + + /** + * @var array 区间内存信息 + */ protected static $mem = []; /** * 记录时间(微秒)和内存使用情况 - * @param string $name 标记位置 - * @param mixed $value 标记值 留空则取当前 time 表示仅记录时间 否则同时记录时间和内存 - * @return mixed + * @access public + * @param string $name 标记位置 + * @param mixed $value 标记值(留空则取当前 time 表示仅记录时间 否则同时记录时间和内存) + * @return void */ public static function remark($name, $value = '') { - // 记录时间和内存使用 self::$info[$name] = is_float($value) ? $value : microtime(true); + if ('time' != $value) { self::$mem['mem'][$name] = is_float($value) ? $value : memory_get_usage(); self::$mem['peak'][$name] = memory_get_peak_usage(); @@ -39,23 +45,26 @@ class Debug /** * 统计某个区间的时间(微秒)使用情况 返回值以秒为单位 - * @param string $start 开始标签 - * @param string $end 结束标签 - * @param integer|string $dec 小数位 - * @return integer + * @access public + * @param string $start 开始标签 + * @param string $end 结束标签 + * @param integer $dec 小数位 + * @return string */ public static function getRangeTime($start, $end, $dec = 6) { if (!isset(self::$info[$end])) { self::$info[$end] = microtime(true); } + return number_format((self::$info[$end] - self::$info[$start]), $dec); } /** * 统计从开始到统计时的时间(微秒)使用情况 返回值以秒为单位 - * @param integer|string $dec 小数位 - * @return integer + * @access public + * @param integer $dec 小数位 + * @return string */ public static function getUseTime($dec = 6) { @@ -64,6 +73,7 @@ class Debug /** * 获取当前访问的吞吐率情况 + * @access public * @return string */ public static function getThroughputRate() @@ -73,9 +83,10 @@ class Debug /** * 记录区间的内存使用情况 - * @param string $start 开始标签 - * @param string $end 结束标签 - * @param integer|string $dec 小数位 + * @access public + * @param string $start 开始标签 + * @param string $end 结束标签 + * @param integer $dec 小数位 * @return string */ public static function getRangeMem($start, $end, $dec = 2) @@ -83,19 +94,23 @@ class Debug if (!isset(self::$mem['mem'][$end])) { self::$mem['mem'][$end] = memory_get_usage(); } + $size = self::$mem['mem'][$end] - self::$mem['mem'][$start]; $a = ['B', 'KB', 'MB', 'GB', 'TB']; $pos = 0; + while ($size >= 1024) { $size /= 1024; $pos++; } + return round($size, $dec) . " " . $a[$pos]; } /** * 统计从开始到统计时的内存使用情况 - * @param integer|string $dec 小数位 + * @access public + * @param integer $dec 小数位 * @return string */ public static function getUseMem($dec = 2) @@ -103,103 +118,128 @@ class Debug $size = memory_get_usage() - THINK_START_MEM; $a = ['B', 'KB', 'MB', 'GB', 'TB']; $pos = 0; + while ($size >= 1024) { $size /= 1024; $pos++; } + return round($size, $dec) . " " . $a[$pos]; } /** * 统计区间的内存峰值情况 - * @param string $start 开始标签 - * @param string $end 结束标签 - * @param integer|string $dec 小数位 - * @return mixed + * @access public + * @param string $start 开始标签 + * @param string $end 结束标签 + * @param integer $dec 小数位 + * @return string */ public static function getMemPeak($start, $end, $dec = 2) { if (!isset(self::$mem['peak'][$end])) { self::$mem['peak'][$end] = memory_get_peak_usage(); } + $size = self::$mem['peak'][$end] - self::$mem['peak'][$start]; $a = ['B', 'KB', 'MB', 'GB', 'TB']; $pos = 0; + while ($size >= 1024) { $size /= 1024; $pos++; } + return round($size, $dec) . " " . $a[$pos]; } /** * 获取文件加载信息 - * @param bool $detail 是否显示详细 + * @access public + * @param bool $detail 是否显示详细 * @return integer|array */ public static function getFile($detail = false) { + $files = get_included_files(); + if ($detail) { - $files = get_included_files(); - $info = []; - foreach ($files as $key => $file) { + $info = []; + + foreach ($files as $file) { $info[] = $file . ' ( ' . number_format(filesize($file) / 1024, 2) . ' KB )'; } + return $info; } - return count(get_included_files()); + + return count($files); } /** * 浏览器友好的变量输出 - * @param mixed $var 变量 - * @param boolean $echo 是否输出 默认为true 如果为false 则返回输出字符串 - * @param string $label 标签 默认为空 - * @param integer $flags htmlspecialchars flags - * @return void|string + * @access public + * @param mixed $var 变量 + * @param boolean $echo 是否输出(默认为 true,为 false 则返回输出字符串) + * @param string|null $label 标签(默认为空) + * @param integer $flags htmlspecialchars 的标志 + * @return null|string */ public static function dump($var, $echo = true, $label = null, $flags = ENT_SUBSTITUTE) { $label = (null === $label) ? '' : rtrim($label) . ':'; + ob_start(); var_dump($var); - $output = ob_get_clean(); - $output = preg_replace('/\]\=\>\n(\s+)/m', '] => ', $output); + $output = preg_replace('/\]\=\>\n(\s+)/m', '] => ', ob_get_clean()); + if (IS_CLI) { $output = PHP_EOL . $label . $output . PHP_EOL; } else { if (!extension_loaded('xdebug')) { $output = htmlspecialchars($output, $flags); } + $output = '
' . $label . $output . '
'; } + if ($echo) { echo($output); return; - } else { - return $output; } + + return $output; } + /** + * 调试信息注入到响应中 + * @access public + * @param Response $response 响应实例 + * @param string $content 返回的字符串 + * @return void + */ public static function inject(Response $response, &$content) { - $config = Config::get('trace'); - $type = isset($config['type']) ? $config['type'] : 'Html'; - $request = Request::instance(); - $class = false !== strpos($type, '\\') ? $type : '\\think\\debug\\' . ucwords($type); + $config = Config::get('trace'); + $type = isset($config['type']) ? $config['type'] : 'Html'; + $class = false !== strpos($type, '\\') ? $type : '\\think\\debug\\' . ucwords($type); + unset($config['type']); - if (class_exists($class)) { - $trace = new $class($config); - } else { + + if (!class_exists($class)) { throw new ClassNotFoundException('class not exists:' . $class, $class); } + /** @var \think\debug\Console|\think\debug\Html $trace */ + $trace = new $class($config); + if ($response instanceof Redirect) { - //TODO 记录 + // TODO 记录 } else { $output = $trace->output($response, Log::getLog()); + if (is_string($output)) { - // trace调试信息注入 + // trace 调试信息注入 $pos = strripos($content, ''); if (false !== $pos) { $content = substr($content, 0, $pos) . $output . substr($content, $pos); diff --git a/thinkphp/library/think/Env.php b/thinkphp/library/think/Env.php index fa87897c..0a8b2509 100644 --- a/thinkphp/library/think/Env.php +++ b/thinkphp/library/think/Env.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -15,22 +15,25 @@ class Env { /** * 获取环境变量值 - * @param string $name 环境变量名(支持二级 .号分割) - * @param string $default 默认值 + * @access public + * @param string $name 环境变量名(支持二级 . 号分割) + * @param string $default 默认值 * @return mixed */ public static function get($name, $default = null) { $result = getenv(ENV_PREFIX . strtoupper(str_replace('.', '_', $name))); + if (false !== $result) { if ('false' === $result) { $result = false; } elseif ('true' === $result) { $result = true; } + return $result; - } else { - return $default; } + + return $default; } } diff --git a/thinkphp/library/think/Error.php b/thinkphp/library/think/Error.php index c17292bf..5f361d58 100644 --- a/thinkphp/library/think/Error.php +++ b/thinkphp/library/think/Error.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -20,6 +20,7 @@ class Error { /** * 注册异常处理 + * @access public * @return void */ public static function register() @@ -31,8 +32,10 @@ class Error } /** - * Exception Handler - * @param \Exception|\Throwable $e + * 异常处理 + * @access public + * @param \Exception|\Throwable $e 异常 + * @return void */ public static function appException($e) { @@ -40,44 +43,50 @@ class Error $e = new ThrowableError($e); } - self::getExceptionHandler()->report($e); + $handler = self::getExceptionHandler(); + $handler->report($e); + if (IS_CLI) { - self::getExceptionHandler()->renderForConsole(new ConsoleOutput, $e); + $handler->renderForConsole(new ConsoleOutput, $e); } else { - self::getExceptionHandler()->render($e)->send(); + $handler->render($e)->send(); } } /** - * Error Handler - * @param integer $errno 错误编号 - * @param integer $errstr 详细错误信息 - * @param string $errfile 出错的文件 - * @param integer $errline 出错行号 - * @param array $errcontext + * 错误处理 + * @access public + * @param integer $errno 错误编号 + * @param integer $errstr 详细错误信息 + * @param string $errfile 出错的文件 + * @param integer $errline 出错行号 + * @return void * @throws ErrorException */ - public static function appError($errno, $errstr, $errfile = '', $errline = 0, $errcontext = []) + public static function appError($errno, $errstr, $errfile = '', $errline = 0) { - $exception = new ErrorException($errno, $errstr, $errfile, $errline, $errcontext); + $exception = new ErrorException($errno, $errstr, $errfile, $errline); + + // 符合异常处理的则将错误信息托管至 think\exception\ErrorException if (error_reporting() & $errno) { - // 将错误信息托管至 think\exception\ErrorException throw $exception; - } else { - self::getExceptionHandler()->report($exception); } + + self::getExceptionHandler()->report($exception); } /** - * Shutdown Handler + * 异常中止处理 + * @access public + * @return void */ public static function appShutdown() { + // 将错误信息托管至 think\ErrorException if (!is_null($error = error_get_last()) && self::isFatal($error['type'])) { - // 将错误信息托管至think\ErrorException - $exception = new ErrorException($error['type'], $error['message'], $error['file'], $error['line']); - - self::appException($exception); + self::appException(new ErrorException( + $error['type'], $error['message'], $error['file'], $error['line'] + )); } // 写入日志 @@ -86,8 +95,8 @@ class Error /** * 确定错误类型是否致命 - * - * @param int $type + * @access protected + * @param int $type 错误类型 * @return bool */ protected static function isFatal($type) @@ -96,22 +105,32 @@ class Error } /** - * Get an instance of the exception handler. - * + * 获取异常处理的实例 + * @access public * @return Handle */ public static function getExceptionHandler() { static $handle; + if (!$handle) { - // 异常处理handle + // 异常处理 handle $class = Config::get('exception_handle'); - if ($class && class_exists($class) && is_subclass_of($class, "\\think\\exception\\Handle")) { + + if ($class && is_string($class) && class_exists($class) && + is_subclass_of($class, "\\think\\exception\\Handle") + ) { $handle = new $class; } else { $handle = new Handle; + + if ($class instanceof \Closure) { + $handle->setRender($class); + } + } } + return $handle; } } diff --git a/thinkphp/library/think/Exception.php b/thinkphp/library/think/Exception.php index 034c85b6..1ef06bdb 100644 --- a/thinkphp/library/think/Exception.php +++ b/thinkphp/library/think/Exception.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -13,15 +13,13 @@ namespace think; class Exception extends \Exception { - /** - * 保存异常页面显示的额外Debug数据 - * @var array + * @var array 保存异常页面显示的额外 Debug 数据 */ protected $data = []; /** - * 设置异常额外的Debug数据 + * 设置异常额外的 Debug 数据 * 数据将会显示为下面的格式 * * Exception Data @@ -33,8 +31,10 @@ class Exception extends \Exception * key1 value1 * key2 value2 * - * @param string $label 数据分类,用于异常页面显示 - * @param array $data 需要显示的数据,必须为关联数组 + * @access protected + * @param string $label 数据分类,用于异常页面显示 + * @param array $data 需要显示的数据,必须为关联数组 + * @return void */ final protected function setData($label, array $data) { @@ -42,13 +42,14 @@ class Exception extends \Exception } /** - * 获取异常额外Debug数据 + * 获取异常额外 Debug 数据 * 主要用于输出到异常页面便于调试 - * @return array 由setData设置的Debug数据 + * @access public + * @return array */ final public function getData() { return $this->data; } - + } diff --git a/thinkphp/library/think/File.php b/thinkphp/library/think/File.php index 90eac4e2..d2ed2208 100644 --- a/thinkphp/library/think/File.php +++ b/thinkphp/library/think/File.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -16,25 +16,51 @@ use SplFileObject; class File extends SplFileObject { /** - * 错误信息 - * @var string + * @var string 错误信息 */ private $error = ''; - // 当前完整文件名 + + /** + * @var string 当前完整文件名 + */ protected $filename; - // 上传文件名 + + /** + * @var string 上传文件名 + */ protected $saveName; - // 文件上传命名规则 + + /** + * @var string 文件上传命名规则 + */ protected $rule = 'date'; - // 文件上传验证规则 + + /** + * @var array 文件上传验证规则 + */ protected $validate = []; - // 单元测试 + + /** + * @var bool 单元测试 + */ protected $isTest; - // 上传文件信息 + + /** + * @var array 上传文件信息 + */ protected $info; - // 文件hash信息 + + /** + * @var array 文件 hash 信息 + */ protected $hash = []; + /** + * File constructor. + * @access public + * @param string $filename 文件名称 + * @param string $mode 访问模式 + */ public function __construct($filename, $mode = 'r') { parent::__construct($filename, $mode); @@ -42,30 +68,35 @@ class File extends SplFileObject } /** - * 是否测试 - * @param bool $test 是否测试 + * 设置是否是单元测试 + * @access public + * @param bool $test 是否是测试 * @return $this */ public function isTest($test = false) { $this->isTest = $test; + return $this; } /** * 设置上传信息 - * @param array $info 上传文件信息 + * @access public + * @param array $info 上传文件信息 * @return $this */ public function setUploadInfo($info) { $this->info = $info; + return $this; } /** * 获取上传文件的信息 - * @param string $name + * @access public + * @param string $name 信息名称 * @return array|string */ public function getInfo($name = '') @@ -75,106 +106,111 @@ class File extends SplFileObject /** * 获取上传文件的文件名 + * @access public * @return string */ public function getSaveName() { return $this->saveName; } - public function getFileName() - { - return $this->filename; - } /** * 设置上传文件的保存文件名 - * @param string $saveName + * @access public + * @param string $saveName 保存名称 * @return $this */ public function setSaveName($saveName) { $this->saveName = $saveName; + return $this; } /** * 获取文件的哈希散列值 - * @return $string + * @access public + * @param string $type 类型 + * @return string */ public function hash($type = 'sha1') { if (!isset($this->hash[$type])) { $this->hash[$type] = hash_file($type, $this->filename); } + return $this->hash[$type]; } /** * 检查目录是否可写 - * @param string $path 目录 + * @access protected + * @param string $path 目录 * @return boolean */ protected function checkPath($path) { - if (is_dir($path)) { + if (is_dir($path) || mkdir($path, 0755, true)) { return true; } - if (mkdir($path, 0755, true)) { - return true; - } else { - $this->error = "目录 {$path} 创建失败!"; - return false; - } + $this->error = ['directory {:path} creation failed', ['path' => $path]]; + + return false; } /** * 获取文件类型信息 + * @access public * @return string */ public function getMime() { $finfo = finfo_open(FILEINFO_MIME_TYPE); + return finfo_file($finfo, $this->filename); } /** * 设置文件的命名规则 - * @param string $rule 文件命名规则 + * @access public + * @param string $rule 文件命名规则 * @return $this */ public function rule($rule) { $this->rule = $rule; + return $this; } /** * 设置上传文件的验证规则 - * @param array $rule 验证规则 + * @access public + * @param array $rule 验证规则 * @return $this */ - public function validate($rule = []) + public function validate(array $rule = []) { $this->validate = $rule; + return $this; } /** * 检测是否合法的上传文件 + * @access public * @return bool */ public function isValid() { - if ($this->isTest) { - return is_file($this->filename); - } - return is_uploaded_file($this->filename); + return $this->isTest ? is_file($this->filename) : is_uploaded_file($this->filename); } /** * 检测上传文件 - * @param array $rule 验证规则 + * @access public + * @param array $rule 验证规则 * @return bool */ public function check($rule = []) @@ -183,25 +219,25 @@ class File extends SplFileObject /* 检查文件大小 */ if (isset($rule['size']) && !$this->checkSize($rule['size'])) { - $this->error = '上传文件大小不符!'; + $this->error = 'filesize not match'; return false; } - /* 检查文件Mime类型 */ + /* 检查文件 Mime 类型 */ if (isset($rule['type']) && !$this->checkMime($rule['type'])) { - $this->error = '上传文件MIME类型不允许!'; + $this->error = 'mimetype to upload is not allowed'; return false; } /* 检查文件后缀 */ if (isset($rule['ext']) && !$this->checkExt($rule['ext'])) { - $this->error = '上传文件后缀不允许'; + $this->error = 'extensions to upload is not allowed'; return false; } /* 检查图像文件 */ if (!$this->checkImg()) { - $this->error = '非法图像文件!'; + $this->error = 'illegal image files'; return false; } @@ -210,7 +246,8 @@ class File extends SplFileObject /** * 检测上传文件后缀 - * @param array|string $ext 允许后缀 + * @access public + * @param array|string $ext 允许后缀 * @return bool */ public function checkExt($ext) @@ -218,73 +255,76 @@ class File extends SplFileObject if (is_string($ext)) { $ext = explode(',', $ext); } + $extension = strtolower(pathinfo($this->getInfo('name'), PATHINFO_EXTENSION)); - if (!in_array($extension, $ext)) { - return false; - } - return true; + + return in_array($extension, $ext); } /** * 检测图像文件 + * @access public * @return bool */ public function checkImg() { $extension = strtolower(pathinfo($this->getInfo('name'), PATHINFO_EXTENSION)); - /* 对图像文件进行严格检测 */ - if (in_array($extension, ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'swf']) && !in_array($this->getImageType($this->filename), [1, 2, 3, 4, 6])) { - return false; - } - return true; + + // 如果上传的不是图片,或者是图片而且后缀确实符合图片类型则返回 true + return !in_array($extension, ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'swf']) || in_array($this->getImageType($this->filename), [1, 2, 3, 4, 6, 13]); } - // 判断图像类型 + /** + * 判断图像类型 + * @access protected + * @param string $image 图片名称 + * @return bool|int + */ protected function getImageType($image) { if (function_exists('exif_imagetype')) { return exif_imagetype($image); - } else { + } + + try { $info = getimagesize($image); - return $info[2]; + return $info ? $info[2] : false; + } catch (\Exception $e) { + return false; } } /** * 检测上传文件大小 - * @param integer $size 最大大小 + * @access public + * @param integer $size 最大大小 * @return bool */ public function checkSize($size) { - if ($this->getSize() > $size) { - return false; - } - return true; + return $this->getSize() <= $size; } /** * 检测上传文件类型 - * @param array|string $mime 允许类型 + * @access public + * @param array|string $mime 允许类型 * @return bool */ public function checkMime($mime) { - if (is_string($mime)) { - $mime = explode(',', $mime); - } - if (!in_array(strtolower($this->getMime()), $mime)) { - return false; - } - return true; + $mime = is_string($mime) ? explode(',', $mime) : $mime; + + return in_array(strtolower($this->getMime()), $mime); } /** * 移动文件 - * @param string $path 保存路径 - * @param string|bool $savename 保存的文件名 默认自动生成 - * @param boolean $replace 同名文件是否覆盖 - * @return false|File false-失败 否则返回File实例 + * @access public + * @param string $path 保存路径 + * @param string|bool $savename 保存的文件名 默认自动生成 + * @param boolean $replace 同名文件是否覆盖 + * @return false|File */ public function move($path, $savename = true, $replace = true) { @@ -296,7 +336,7 @@ class File extends SplFileObject // 检测合法性 if (!$this->isValid()) { - $this->error = '非法上传文件'; + $this->error = 'upload illegal files'; return false; } @@ -304,6 +344,7 @@ class File extends SplFileObject if (!$this->check()) { return false; } + $path = rtrim($path, DS) . DS; // 文件保存命名规则 $saveName = $this->buildSaveName($savename); @@ -314,9 +355,9 @@ class File extends SplFileObject return false; } - /* 不覆盖同名文件 */ + // 不覆盖同名文件 if (!$replace && is_file($filename)) { - $this->error = '存在同名文件'; + $this->error = ['has the same filename: {:filename}', ['filename' => $filename]]; return false; } @@ -324,25 +365,27 @@ class File extends SplFileObject if ($this->isTest) { rename($this->filename, $filename); } elseif (!move_uploaded_file($this->filename, $filename)) { - $this->error = '文件上传保存错误!'; + $this->error = 'upload write error'; return false; } - // 返回 File对象实例 + + // 返回 File 对象实例 $file = new self($filename); - $file->setSaveName($saveName); - $file->setUploadInfo($this->info); + $file->setSaveName($saveName)->setUploadInfo($this->info); + return $file; } /** * 获取保存文件名 - * @param string|bool $savename 保存的文件名 默认自动生成 + * @access protected + * @param string|bool $savename 保存的文件名 默认自动生成 * @return string */ protected function buildSaveName($savename) { + // 自动生成文件名 if (true === $savename) { - // 自动生成文件名 if ($this->rule instanceof \Closure) { $savename = call_user_func_array($this->rule, [$this]); } else { @@ -361,52 +404,73 @@ class File extends SplFileObject } } } - } elseif ('' === $savename) { + } elseif ('' === $savename || false === $savename) { $savename = $this->getInfo('name'); } + if (!strpos($savename, '.')) { $savename .= '.' . pathinfo($this->getInfo('name'), PATHINFO_EXTENSION); } + return $savename; } /** * 获取错误代码信息 - * @param int $errorNo 错误号 + * @access private + * @param int $errorNo 错误号 + * @return $this */ private function error($errorNo) { switch ($errorNo) { case 1: case 2: - $this->error = '上传文件大小超过了最大值!'; + $this->error = 'upload File size exceeds the maximum value'; break; case 3: - $this->error = '文件只有部分被上传!'; + $this->error = 'only the portion of file is uploaded'; break; case 4: - $this->error = '没有文件被上传!'; + $this->error = 'no file to uploaded'; break; case 6: - $this->error = '找不到临时文件夹!'; + $this->error = 'upload temp dir not found'; break; case 7: - $this->error = '文件写入失败!'; + $this->error = 'file write error'; break; default: - $this->error = '未知上传错误!'; + $this->error = 'unknown upload error'; } + + return $this; } /** - * 获取错误信息 - * @return mixed + * 获取错误信息(支持多语言) + * @access public + * @return string */ public function getError() { - return $this->error; + if (is_array($this->error)) { + list($msg, $vars) = $this->error; + } else { + $msg = $this->error; + $vars = []; + } + + return Lang::has($msg) ? Lang::get($msg, $vars) : $msg; } + /** + * 魔法方法,获取文件的 hash 值 + * @access public + * @param string $method 方法名 + * @param mixed $args 调用参数 + * @return string + */ public function __call($method, $args) { return $this->hash($method); diff --git a/thinkphp/library/think/Hook.php b/thinkphp/library/think/Hook.php index f06196e4..a69ce546 100644 --- a/thinkphp/library/think/Hook.php +++ b/thinkphp/library/think/Hook.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -13,19 +13,23 @@ namespace think; class Hook { - + /** + * @var array 标签 + */ private static $tags = []; /** * 动态添加行为扩展到某个标签 - * @param string $tag 标签名称 - * @param mixed $behavior 行为名称 - * @param bool $first 是否放到开头执行 + * @access public + * @param string $tag 标签名称 + * @param mixed $behavior 行为名称 + * @param bool $first 是否放到开头执行 * @return void */ public static function add($tag, $behavior, $first = false) { isset(self::$tags[$tag]) || self::$tags[$tag] = []; + if (is_array($behavior) && !is_callable($behavior)) { if (!array_key_exists('_overlay', $behavior) || !$behavior['_overlay']) { unset($behavior['_overlay']); @@ -43,8 +47,10 @@ class Hook /** * 批量导入插件 - * @param array $tags 插件信息 - * @param boolean $recursive 是否递归合并 + * @access public + * @param array $tags 插件信息 + * @param boolean $recursive 是否递归合并 + * @return void */ public static function import(array $tags, $recursive = true) { @@ -59,55 +65,59 @@ class Hook /** * 获取插件信息 - * @param string $tag 插件位置 留空获取全部 + * @access public + * @param string $tag 插件位置(留空获取全部) * @return array */ public static function get($tag = '') { if (empty($tag)) { - //获取全部的插件信息 return self::$tags; - } else { - return array_key_exists($tag, self::$tags) ? self::$tags[$tag] : []; } + + return array_key_exists($tag, self::$tags) ? self::$tags[$tag] : []; } /** * 监听标签的行为 - * @param string $tag 标签名称 - * @param mixed $params 传入参数 - * @param mixed $extra 额外参数 - * @param bool $once 只获取一个有效返回值 + * @access public + * @param string $tag 标签名称 + * @param mixed $params 传入参数 + * @param mixed $extra 额外参数 + * @param bool $once 只获取一个有效返回值 * @return mixed */ public static function listen($tag, &$params = null, $extra = null, $once = false) { $results = []; - $tags = static::get($tag); - foreach ($tags as $key => $name) { + + foreach (static::get($tag) as $key => $name) { $results[$key] = self::exec($name, $tag, $params, $extra); - if (false === $results[$key]) { - // 如果返回false 则中断行为执行 - break; - } elseif (!is_null($results[$key]) && $once) { + + // 如果返回 false,或者仅获取一个有效返回则中断行为执行 + if (false === $results[$key] || (!is_null($results[$key]) && $once)) { break; } } + return $once ? end($results) : $results; } /** * 执行某个行为 - * @param mixed $class 要执行的行为 - * @param string $tag 方法名(标签名) - * @param Mixed $params 传人的参数 - * @param mixed $extra 额外参数 + * @access public + * @param mixed $class 要执行的行为 + * @param string $tag 方法名(标签名) + * @param mixed $params 传人的参数 + * @param mixed $extra 额外参数 * @return mixed */ public static function exec($class, $tag = '', &$params = null, $extra = null) { App::$debug && Debug::remark('behavior_start', 'time'); + $method = Loader::parseName($tag, 1, false); + if ($class instanceof \Closure) { $result = call_user_func_array($class, [ & $params, $extra]); $class = 'Closure'; @@ -126,10 +136,12 @@ class Hook $method = ($tag && is_callable([$obj, $method])) ? $method : 'run'; $result = $obj->$method($params, $extra); } + if (App::$debug) { Debug::remark('behavior_end', 'time'); Log::record('[ BEHAVIOR ] Run ' . $class . ' @' . $tag . ' [ RunTime:' . Debug::getRangeTime('behavior_start', 'behavior_end') . 's ]', 'info'); } + return $result; } diff --git a/thinkphp/library/think/Lang.php b/thinkphp/library/think/Lang.php index 2df3767c..a50d838d 100644 --- a/thinkphp/library/think/Lang.php +++ b/thinkphp/library/think/Lang.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -13,114 +13,148 @@ namespace think; class Lang { - // 语言数据 + /** + * @var array 语言数据 + */ private static $lang = []; - // 语言作用域 + + /** + * @var string 语言作用域 + */ private static $range = 'zh-cn'; - // 语言自动侦测的变量 + + /** + * @var string 语言自动侦测的变量 + */ protected static $langDetectVar = 'lang'; - // 语言Cookie变量 + + /** + * @var string 语言 Cookie 变量 + */ protected static $langCookieVar = 'think_var'; - // 语言Cookie的过期时间 + + /** + * @var int 语言 Cookie 的过期时间 + */ protected static $langCookieExpire = 3600; - // 允许语言列表 + + /** + * @var array 允许语言列表 + */ protected static $allowLangList = []; - // Accept-Language转义为对应语言包名称 系统默认配置 - protected static $acceptLanguage = [ - 'zh-hans-cn' => 'zh-cn', - ]; - // 设定当前的语言 + /** + * @var array Accept-Language 转义为对应语言包名称 系统默认配置 + */ + protected static $acceptLanguage = ['zh-hans-cn' => 'zh-cn']; + + /** + * 设定当前的语言 + * @access public + * @param string $range 语言作用域 + * @return string + */ public static function range($range = '') { - if ('' == $range) { - return self::$range; - } else { + if ($range) { self::$range = $range; } + return self::$range; } /** * 设置语言定义(不区分大小写) - * @param string|array $name 语言变量 - * @param string $value 语言值 - * @param string $range 语言作用域 + * @access public + * @param string|array $name 语言变量 + * @param string $value 语言值 + * @param string $range 语言作用域 * @return mixed */ public static function set($name, $value = null, $range = '') { $range = $range ?: self::$range; - // 批量定义 + if (!isset(self::$lang[$range])) { self::$lang[$range] = []; } + if (is_array($name)) { return self::$lang[$range] = array_change_key_case($name) + self::$lang[$range]; - } else { - return self::$lang[$range][strtolower($name)] = $value; } + + return self::$lang[$range][strtolower($name)] = $value; } /** * 加载语言定义(不区分大小写) - * @param string $file 语言文件 - * @param string $range 语言作用域 + * @access public + * @param array|string $file 语言文件 + * @param string $range 语言作用域 * @return mixed */ public static function load($file, $range = '') { $range = $range ?: self::$range; + $file = is_string($file) ? [$file] : $file; + if (!isset(self::$lang[$range])) { self::$lang[$range] = []; } - // 批量定义 - if (is_string($file)) { - $file = [$file]; - } + $lang = []; + foreach ($file as $_file) { if (is_file($_file)) { // 记录加载信息 App::$debug && Log::record('[ LANG ] ' . $_file, 'info'); + $_lang = include $_file; + if (is_array($_lang)) { $lang = array_change_key_case($_lang) + $lang; } } } + if (!empty($lang)) { self::$lang[$range] = $lang + self::$lang[$range]; } + return self::$lang[$range]; } /** * 获取语言定义(不区分大小写) - * @param string|null $name 语言变量 - * @param string $range 语言作用域 + * @access public + * @param string|null $name 语言变量 + * @param string $range 语言作用域 * @return mixed */ public static function has($name, $range = '') { $range = $range ?: self::$range; + return isset(self::$lang[$range][strtolower($name)]); } /** * 获取语言定义(不区分大小写) - * @param string|null $name 语言变量 - * @param array $vars 变量替换 - * @param string $range 语言作用域 + * @access public + * @param string|null $name 语言变量 + * @param array $vars 变量替换 + * @param string $range 语言作用域 * @return mixed */ public static function get($name = null, $vars = [], $range = '') { $range = $range ?: self::$range; + // 空参数返回所有定义 if (empty($name)) { return self::$lang[$range]; } + $key = strtolower($name); $value = isset(self::$lang[$range][$key]) ? self::$lang[$range][$key] : $name; @@ -145,42 +179,50 @@ class Lang } } + return $value; } /** * 自动侦测设置获取语言选择 + * @access public * @return string */ public static function detect() { - // 自动侦测设置获取语言选择 $langSet = ''; if (isset($_GET[self::$langDetectVar])) { - // url中设置了语言变量 + // url 中设置了语言变量 $langSet = strtolower($_GET[self::$langDetectVar]); + } elseif (isset($_COOKIE[self::$langCookieVar])) { + // Cookie 中设置了语言变量 + $langSet = strtolower($_COOKIE[self::$langCookieVar]); } elseif (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // 自动侦测浏览器语言 preg_match('/^([a-z\d\-]+)/i', $_SERVER['HTTP_ACCEPT_LANGUAGE'], $matches); $langSet = strtolower($matches[1]); $acceptLangs = Config::get('header_accept_lang'); + if (isset($acceptLangs[$langSet])) { $langSet = $acceptLangs[$langSet]; } elseif (isset(self::$acceptLanguage[$langSet])) { $langSet = self::$acceptLanguage[$langSet]; } } + + // 合法的语言 if (empty(self::$allowLangList) || in_array($langSet, self::$allowLangList)) { - // 合法的语言 self::$range = $langSet ?: self::$range; } + return self::$range; } /** * 设置语言自动侦测的变量 - * @param string $var 变量名称 + * @access public + * @param string $var 变量名称 * @return void */ public static function setLangDetectVar($var) @@ -189,8 +231,9 @@ class Lang } /** - * 设置语言的cookie保存变量 - * @param string $var 变量名称 + * 设置语言的 cookie 保存变量 + * @access public + * @param string $var 变量名称 * @return void */ public static function setLangCookieVar($var) @@ -199,8 +242,9 @@ class Lang } /** - * 设置语言的cookie的过期时间 - * @param string $expire 过期时间 + * 设置语言的 cookie 的过期时间 + * @access public + * @param string $expire 过期时间 * @return void */ public static function setLangCookieExpire($expire) @@ -210,7 +254,8 @@ class Lang /** * 设置允许的语言列表 - * @param array $list 语言列表 + * @access public + * @param array $list 语言列表 * @return void */ public static function setAllowLangList($list) diff --git a/thinkphp/library/think/Loader.php b/thinkphp/library/think/Loader.php index a0727fef..d813a5d7 100644 --- a/thinkphp/library/think/Loader.php +++ b/thinkphp/library/think/Loader.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -15,26 +15,57 @@ use think\exception\ClassNotFoundException; class Loader { + /** + * @var array 实例数组 + */ protected static $instance = []; - // 类名映射 - protected static $map = []; - // 命名空间别名 + /** + * @var array 类名映射 + */ + protected static $classMap = []; + + /** + * @var array 命名空间别名 + */ protected static $namespaceAlias = []; - // PSR-4 + /** + * @var array PSR-4 命名空间前缀长度映射 + */ private static $prefixLengthsPsr4 = []; - private static $prefixDirsPsr4 = []; - private static $fallbackDirsPsr4 = []; - // PSR-0 - private static $prefixesPsr0 = []; + /** + * @var array PSR-4 的加载目录 + */ + private static $prefixDirsPsr4 = []; + + /** + * @var array PSR-4 加载失败的回退目录 + */ + private static $fallbackDirsPsr4 = []; + + /** + * @var array PSR-0 命名空间前缀映射 + */ + private static $prefixesPsr0 = []; + + /** + * @var array PSR-0 加载失败的回退目录 + */ private static $fallbackDirsPsr0 = []; - // 自动加载的文件 - private static $autoloadFiles = []; + /** + * @var array 需要加载的文件 + */ + private static $files = []; - // 自动加载 + /** + * 自动加载 + * @access public + * @param string $class 类名 + * @return bool + */ public static function autoload($class) { // 检测命名空间别名 @@ -49,33 +80,33 @@ class Loader } if ($file = self::findFile($class)) { - - // Win环境严格区分大小写 - if (IS_WIN && pathinfo($file, PATHINFO_FILENAME) != pathinfo(realpath($file), PATHINFO_FILENAME)) { - return false; + // 非 Win 环境不严格区分大小写 + if (!IS_WIN || pathinfo($file, PATHINFO_FILENAME) == pathinfo(realpath($file), PATHINFO_FILENAME)) { + __include_file($file); + return true; } - - __include_file($file); - return true; } + + return false; } /** * 查找文件 - * @param $class - * @return bool + * @access private + * @param string $class 类名 + * @return bool|string */ private static function findFile($class) { - if (!empty(self::$map[$class])) { - // 类库映射 - return self::$map[$class]; + // 类库映射 + if (!empty(self::$classMap[$class])) { + return self::$classMap[$class]; } // 查找 PSR-4 $logicalPathPsr4 = strtr($class, '\\', DS) . EXT; + $first = $class[0]; - $first = $class[0]; if (isset(self::$prefixLengthsPsr4[$first])) { foreach (self::$prefixLengthsPsr4[$first] as $prefix => $length) { if (0 === strpos($class, $prefix)) { @@ -97,7 +128,7 @@ class Loader // 查找 PSR-0 if (false !== $pos = strrpos($class, '\\')) { - // namespaced class name + // namespace class name $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) . strtr(substr($logicalPathPsr4, $pos + 1), '_', DS); } else { @@ -124,20 +155,33 @@ class Loader } } - return self::$map[$class] = false; + // 找不到则设置映射为 false 并返回 + return self::$classMap[$class] = false; } - // 注册classmap + /** + * 注册 classmap + * @access public + * @param string|array $class 类名 + * @param string $map 映射 + * @return void + */ public static function addClassMap($class, $map = '') { if (is_array($class)) { - self::$map = array_merge(self::$map, $class); + self::$classMap = array_merge(self::$classMap, $class); } else { - self::$map[$class] = $map; + self::$classMap[$class] = $map; } } - // 注册命名空间 + /** + * 注册命名空间 + * @access public + * @param string|array $namespace 命名空间 + * @param string $path 路径 + * @return void + */ public static function addNamespace($namespace, $path = '') { if (is_array($namespace)) { @@ -149,84 +193,77 @@ class Loader } } - // 添加Ps0空间 + /** + * 添加 PSR-0 命名空间 + * @access private + * @param array|string $prefix 空间前缀 + * @param array $paths 路径 + * @param bool $prepend 预先设置的优先级更高 + * @return void + */ private static function addPsr0($prefix, $paths, $prepend = false) { if (!$prefix) { - if ($prepend) { - self::$fallbackDirsPsr0 = array_merge( - (array) $paths, - self::$fallbackDirsPsr0 - ); + self::$fallbackDirsPsr0 = $prepend ? + array_merge((array) $paths, self::$fallbackDirsPsr0) : + array_merge(self::$fallbackDirsPsr0, (array) $paths); + } else { + $first = $prefix[0]; + + if (!isset(self::$prefixesPsr0[$first][$prefix])) { + self::$prefixesPsr0[$first][$prefix] = (array) $paths; } else { - self::$fallbackDirsPsr0 = array_merge( - self::$fallbackDirsPsr0, - (array) $paths - ); + self::$prefixesPsr0[$first][$prefix] = $prepend ? + array_merge((array) $paths, self::$prefixesPsr0[$first][$prefix]) : + array_merge(self::$prefixesPsr0[$first][$prefix], (array) $paths); } - - return; - } - - $first = $prefix[0]; - if (!isset(self::$prefixesPsr0[$first][$prefix])) { - self::$prefixesPsr0[$first][$prefix] = (array) $paths; - - return; - } - if ($prepend) { - self::$prefixesPsr0[$first][$prefix] = array_merge( - (array) $paths, - self::$prefixesPsr0[$first][$prefix] - ); - } else { - self::$prefixesPsr0[$first][$prefix] = array_merge( - self::$prefixesPsr0[$first][$prefix], - (array) $paths - ); } } - // 添加Psr4空间 + /** + * 添加 PSR-4 空间 + * @access private + * @param array|string $prefix 空间前缀 + * @param string $paths 路径 + * @param bool $prepend 预先设置的优先级更高 + * @return void + */ private static function addPsr4($prefix, $paths, $prepend = false) { if (!$prefix) { // Register directories for the root namespace. - if ($prepend) { - self::$fallbackDirsPsr4 = array_merge( - (array) $paths, - self::$fallbackDirsPsr4 - ); - } else { - self::$fallbackDirsPsr4 = array_merge( - self::$fallbackDirsPsr4, - (array) $paths - ); - } + self::$fallbackDirsPsr4 = $prepend ? + array_merge((array) $paths, self::$fallbackDirsPsr4) : + array_merge(self::$fallbackDirsPsr4, (array) $paths); + } elseif (!isset(self::$prefixDirsPsr4[$prefix])) { // Register directories for a new namespace. $length = strlen($prefix); if ('\\' !== $prefix[$length - 1]) { - throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + throw new \InvalidArgumentException( + "A non-empty PSR-4 prefix must end with a namespace separator." + ); } + self::$prefixLengthsPsr4[$prefix[0]][$prefix] = $length; self::$prefixDirsPsr4[$prefix] = (array) $paths; - } elseif ($prepend) { - // Prepend directories for an already registered namespace. - self::$prefixDirsPsr4[$prefix] = array_merge( - (array) $paths, - self::$prefixDirsPsr4[$prefix] - ); + } else { + self::$prefixDirsPsr4[$prefix] = $prepend ? + // Prepend directories for an already registered namespace. + array_merge((array) $paths, self::$prefixDirsPsr4[$prefix]) : // Append directories for an already registered namespace. - self::$prefixDirsPsr4[$prefix] = array_merge( - self::$prefixDirsPsr4[$prefix], - (array) $paths - ); + array_merge(self::$prefixDirsPsr4[$prefix], (array) $paths); } } - // 注册命名空间别名 + /** + * 注册命名空间别名 + * @access public + * @param array|string $namespace 命名空间 + * @param string $original 源文件 + * @return void + */ public static function addNamespaceAlias($namespace, $original = '') { if (is_array($namespace)) { @@ -236,32 +273,58 @@ class Loader } } - // 注册自动加载机制 - public static function register($autoload = '') + /** + * 注册自动加载机制 + * @access public + * @param callable $autoload 自动加载处理方法 + * @return void + */ + public static function register($autoload = null) { // 注册系统自动加载 spl_autoload_register($autoload ?: 'think\\Loader::autoload', true, true); + + // Composer 自动加载支持 + if (is_dir(VENDOR_PATH . 'composer')) { + if (PHP_VERSION_ID >= 50600 && is_file(VENDOR_PATH . 'composer' . DS . 'autoload_static.php')) { + require VENDOR_PATH . 'composer' . DS . 'autoload_static.php'; + + $declaredClass = get_declared_classes(); + $composerClass = array_pop($declaredClass); + + foreach (['prefixLengthsPsr4', 'prefixDirsPsr4', 'fallbackDirsPsr4', 'prefixesPsr0', 'fallbackDirsPsr0', 'classMap', 'files'] as $attr) { + if (property_exists($composerClass, $attr)) { + self::${$attr} = $composerClass::${$attr}; + } + } + } else { + self::registerComposerLoader(); + } + } + // 注册命名空间定义 self::addNamespace([ 'think' => LIB_PATH . 'think' . DS, 'behavior' => LIB_PATH . 'behavior' . DS, 'traits' => LIB_PATH . 'traits' . DS, ]); + // 加载类库映射文件 if (is_file(RUNTIME_PATH . 'classmap' . EXT)) { self::addClassMap(__include_file(RUNTIME_PATH . 'classmap' . EXT)); } - // Composer自动加载支持 - if (is_dir(VENDOR_PATH . 'composer')) { - self::registerComposerLoader(); - } + self::loadComposerAutoloadFiles(); - // 自动加载extend目录 + // 自动加载 extend 目录 self::$fallbackDirsPsr4[] = rtrim(EXTEND_PATH, DS); } - // 注册composer自动加载 + /** + * 注册 composer 自动加载 + * @access private + * @return void + */ private static function registerComposerLoader() { if (is_file(VENDOR_PATH . 'composer/autoload_namespaces.php')) { @@ -286,28 +349,36 @@ class Loader } if (is_file(VENDOR_PATH . 'composer/autoload_files.php')) { - $includeFiles = require VENDOR_PATH . 'composer/autoload_files.php'; - foreach ($includeFiles as $fileIdentifier => $file) { - if (empty(self::$autoloadFiles[$fileIdentifier])) { - __require_file($file); - self::$autoloadFiles[$fileIdentifier] = true; - } + self::$files = require VENDOR_PATH . 'composer/autoload_files.php'; + } + } + + // 加载composer autofile文件 + public static function loadComposerAutoloadFiles() + { + foreach (self::$files as $fileIdentifier => $file) { + if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { + __require_file($file); + + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; } } } /** - * 导入所需的类库 同java的Import 本函数有缓存功能 - * @param string $class 类库命名空间字符串 - * @param string $baseUrl 起始路径 - * @param string $ext 导入的文件扩展名 - * @return boolean + * 导入所需的类库 同 Java 的 Import 本函数有缓存功能 + * @access public + * @param string $class 类库命名空间字符串 + * @param string $baseUrl 起始路径 + * @param string $ext 导入的文件扩展名 + * @return bool */ public static function import($class, $baseUrl = '', $ext = EXT) { static $_file = []; $key = $class . $baseUrl; $class = str_replace(['.', '#'], [DS, '.'], $class); + if (isset($_file[$key])) { return true; } @@ -319,7 +390,7 @@ class Loader // 注册的命名空间 $baseUrl = self::$prefixDirsPsr4[$name . '\\']; } elseif ('@' == $name) { - //加载当前模块应用类库 + // 加载当前模块应用类库 $baseUrl = App::$modulePath; } elseif (is_dir(EXTEND_PATH . $name)) { $baseUrl = EXTEND_PATH . $name . DS; @@ -330,11 +401,11 @@ class Loader } elseif (substr($baseUrl, -1) != DS) { $baseUrl .= DS; } - // 如果类存在 则导入类库文件 + + // 如果类存在则导入类库文件 if (is_array($baseUrl)) { foreach ($baseUrl as $path) { - $filename = $path . DS . $class . $ext; - if (is_file($filename)) { + if (is_file($filename = $path . DS . $class . $ext)) { break; } } @@ -342,135 +413,154 @@ class Loader $filename = $baseUrl . $class . $ext; } - if (!empty($filename) && is_file($filename)) { - // 开启调试模式Win环境严格区分大小写 - if (IS_WIN && pathinfo($filename, PATHINFO_FILENAME) != pathinfo(realpath($filename), PATHINFO_FILENAME)) { - return false; - } + if (!empty($filename) && + is_file($filename) && + (!IS_WIN || pathinfo($filename, PATHINFO_FILENAME) == pathinfo(realpath($filename), PATHINFO_FILENAME)) + ) { __include_file($filename); $_file[$key] = true; + return true; } + return false; } /** * 实例化(分层)模型 - * @param string $name Model名称 - * @param string $layer 业务层名称 - * @param bool $appendSuffix 是否添加类名后缀 - * @param string $common 公共模块名 - * @return Object + * @access public + * @param string $name Model名称 + * @param string $layer 业务层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @param string $common 公共模块名 + * @return object * @throws ClassNotFoundException */ public static function model($name = '', $layer = 'model', $appendSuffix = false, $common = 'common') { - $guid = $name . $layer; - if (isset(self::$instance[$guid])) { - return self::$instance[$guid]; - } - if (false !== strpos($name, '\\')) { - $class = $name; - $module = Request::instance()->module(); - } else { - if (strpos($name, '/')) { - list($module, $name) = explode('/', $name, 2); - } else { - $module = Request::instance()->module(); - } - $class = self::parseClass($module, $layer, $name, $appendSuffix); + $uid = $name . $layer; + + if (isset(self::$instance[$uid])) { + return self::$instance[$uid]; } + + list($module, $class) = self::getModuleAndClass($name, $layer, $appendSuffix); + if (class_exists($class)) { $model = new $class(); } else { $class = str_replace('\\' . $module . '\\', '\\' . $common . '\\', $class); + if (class_exists($class)) { $model = new $class(); } else { throw new ClassNotFoundException('class not exists:' . $class, $class); } } - self::$instance[$guid] = $model; - return $model; + + return self::$instance[$uid] = $model; } /** * 实例化(分层)控制器 格式:[模块名/]控制器名 - * @param string $name 资源地址 - * @param string $layer 控制层名称 - * @param bool $appendSuffix 是否添加类名后缀 - * @param string $empty 空控制器名称 - * @return Object|false + * @access public + * @param string $name 资源地址 + * @param string $layer 控制层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @param string $empty 空控制器名称 + * @return object * @throws ClassNotFoundException */ public static function controller($name, $layer = 'controller', $appendSuffix = false, $empty = '') { - if (false !== strpos($name, '\\')) { - $class = $name; - $module = Request::instance()->module(); - } else { - if (strpos($name, '/')) { - list($module, $name) = explode('/', $name); - } else { - $module = Request::instance()->module(); - } - $class = self::parseClass($module, $layer, $name, $appendSuffix); - } + list($module, $class) = self::getModuleAndClass($name, $layer, $appendSuffix); + if (class_exists($class)) { return App::invokeClass($class); - } elseif ($empty && class_exists($emptyClass = self::parseClass($module, $layer, $empty, $appendSuffix))) { - return new $emptyClass(Request::instance()); } + + if ($empty) { + $emptyClass = self::parseClass($module, $layer, $empty, $appendSuffix); + + if (class_exists($emptyClass)) { + return new $emptyClass(Request::instance()); + } + } + + throw new ClassNotFoundException('class not exists:' . $class, $class); } /** * 实例化验证类 格式:[模块名/]验证器名 - * @param string $name 资源地址 - * @param string $layer 验证层名称 - * @param bool $appendSuffix 是否添加类名后缀 - * @param string $common 公共模块名 - * @return Object|false + * @access public + * @param string $name 资源地址 + * @param string $layer 验证层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @param string $common 公共模块名 + * @return object|false * @throws ClassNotFoundException */ public static function validate($name = '', $layer = 'validate', $appendSuffix = false, $common = 'common') { $name = $name ?: Config::get('default_validate'); + if (empty($name)) { return new Validate; } - $guid = $name . $layer; - if (isset(self::$instance[$guid])) { - return self::$instance[$guid]; - } - if (false !== strpos($name, '\\')) { - $class = $name; - $module = Request::instance()->module(); - } else { - if (strpos($name, '/')) { - list($module, $name) = explode('/', $name); - } else { - $module = Request::instance()->module(); - } - $class = self::parseClass($module, $layer, $name, $appendSuffix); + + $uid = $name . $layer; + if (isset(self::$instance[$uid])) { + return self::$instance[$uid]; } + + list($module, $class) = self::getModuleAndClass($name, $layer, $appendSuffix); + if (class_exists($class)) { $validate = new $class; } else { $class = str_replace('\\' . $module . '\\', '\\' . $common . '\\', $class); + if (class_exists($class)) { $validate = new $class; } else { throw new ClassNotFoundException('class not exists:' . $class, $class); } } - self::$instance[$guid] = $validate; - return $validate; + + return self::$instance[$uid] = $validate; + } + + /** + * 解析模块和类名 + * @access protected + * @param string $name 资源地址 + * @param string $layer 验证层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return array + */ + protected static function getModuleAndClass($name, $layer, $appendSuffix) + { + if (false !== strpos($name, '\\')) { + $module = Request::instance()->module(); + $class = $name; + } else { + if (strpos($name, '/')) { + list($module, $name) = explode('/', $name, 2); + } else { + $module = Request::instance()->module(); + } + + $class = self::parseClass($module, $layer, $name, $appendSuffix); + } + + return [$module, $class]; } /** * 数据库初始化 并取得数据库类实例 - * @param mixed $config 数据库配置 - * @param bool|string $name 连接标识 true 强制重新连接 + * @access public + * @param mixed $config 数据库配置 + * @param bool|string $name 连接标识 true 强制重新连接 * @return \think\db\Connection */ public static function db($config = [], $name = false) @@ -480,10 +570,11 @@ class Loader /** * 远程调用模块的操作方法 参数格式 [模块/控制器/]操作 - * @param string $url 调用地址 - * @param string|array $vars 调用参数 支持字符串和数组 - * @param string $layer 要调用的控制层名称 - * @param bool $appendSuffix 是否添加类名后缀 + * @access public + * @param string $url 调用地址 + * @param string|array $vars 调用参数 支持字符串和数组 + * @param string $layer 要调用的控制层名称 + * @param bool $appendSuffix 是否添加类名后缀 * @return mixed */ public static function action($url, $vars = [], $layer = 'controller', $appendSuffix = false) @@ -492,6 +583,7 @@ class Loader $action = $info['basename']; $module = '.' != $info['dirname'] ? $info['dirname'] : Request::instance()->controller(); $class = self::controller($module, $layer, $appendSuffix); + if ($class) { if (is_scalar($vars)) { if (strpos($vars, '=')) { @@ -500,16 +592,20 @@ class Loader $vars = [$vars]; } } + return App::invokeMethod([$class, $action . Config::get('action_suffix')], $vars); } + + return false; } /** * 字符串命名风格转换 - * type 0 将Java风格转换为C的风格 1 将C风格转换为Java的风格 - * @param string $name 字符串 - * @param integer $type 转换类型 - * @param bool $ucfirst 首字母是否大写(驼峰规则) + * type 0 将 Java 风格转换为 C 的风格 1 将 C 风格转换为 Java 的风格 + * @access public + * @param string $name 字符串 + * @param integer $type 转换类型 + * @param bool $ucfirst 首字母是否大写(驼峰规则) * @return string */ public static function parseName($name, $type = 0, $ucfirst = true) @@ -518,31 +614,38 @@ class Loader $name = preg_replace_callback('/_([a-zA-Z])/', function ($match) { return strtoupper($match[1]); }, $name); + return $ucfirst ? ucfirst($name) : lcfirst($name); - } else { - return strtolower(trim(preg_replace("/[A-Z]/", "_\\0", $name), "_")); } + + return strtolower(trim(preg_replace("/[A-Z]/", "_\\0", $name), "_")); } /** * 解析应用类的类名 - * @param string $module 模块名 - * @param string $layer 层名 controller model ... - * @param string $name 类名 - * @param bool $appendSuffix + * @access public + * @param string $module 模块名 + * @param string $layer 层名 controller model ... + * @param string $name 类名 + * @param bool $appendSuffix 是否添加类名后缀 * @return string */ public static function parseClass($module, $layer, $name, $appendSuffix = false) { - $name = str_replace(['/', '.'], '\\', $name); - $array = explode('\\', $name); - $class = self::parseName(array_pop($array), 1) . (App::$suffix || $appendSuffix ? ucfirst($layer) : ''); + + $array = explode('\\', str_replace(['/', '.'], '\\', $name)); + $class = self::parseName(array_pop($array), 1); + $class = $class . (App::$suffix || $appendSuffix ? ucfirst($layer) : ''); $path = $array ? implode('\\', $array) . '\\' : ''; - return App::$namespace . '\\' . ($module ? $module . '\\' : '') . $layer . '\\' . $path . $class; + + return App::$namespace . '\\' . + ($module ? $module . '\\' : '') . + $layer . '\\' . $path . $class; } /** * 初始化类的实例 + * @access public * @return void */ public static function clearInstance() @@ -551,10 +654,11 @@ class Loader } } +// 作用范围隔离 + /** - * 作用范围隔离 - * - * @param $file + * include + * @param string $file 文件路径 * @return mixed */ function __include_file($file) @@ -562,6 +666,11 @@ function __include_file($file) return include $file; } +/** + * require + * @param string $file 文件路径 + * @return mixed + */ function __require_file($file) { return require $file; diff --git a/thinkphp/library/think/Log.php b/thinkphp/library/think/Log.php index a20ab262..c064306c 100644 --- a/thinkphp/library/think/Log.php +++ b/thinkphp/library/think/Log.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -17,12 +17,12 @@ use think\exception\ClassNotFoundException; * Class Log * @package think * - * @method void log($msg) static - * @method void error($msg) static - * @method void info($msg) static - * @method void sql($msg) static - * @method void notice($msg) static - * @method void alert($msg) static + * @method void log($msg) static 记录一般日志 + * @method void error($msg) static 记录错误日志 + * @method void info($msg) static 记录一般信息日志 + * @method void sql($msg) static 记录 SQL 查询日志 + * @method void notice($msg) static 记录提示日志 + * @method void alert($msg) static 记录报警日志 */ class Log { @@ -34,41 +34,60 @@ class Log const ALERT = 'alert'; const DEBUG = 'debug'; - // 日志信息 + /** + * @var array 日志信息 + */ protected static $log = []; - // 配置参数 + + /** + * @var array 配置参数 + */ protected static $config = []; - // 日志类型 + + /** + * @var array 日志类型 + */ protected static $type = ['log', 'error', 'info', 'sql', 'notice', 'alert', 'debug']; - // 日志写入驱动 + + /** + * @var log\driver\File|log\driver\Test|log\driver\Socket 日志写入驱动 + */ protected static $driver; - // 当前日志授权key + /** + * @var string 当前日志授权 key + */ protected static $key; /** * 日志初始化 - * @param array $config + * @access public + * @param array $config 配置参数 + * @return void */ public static function init($config = []) { - $type = isset($config['type']) ? $config['type'] : 'File'; - $class = false !== strpos($type, '\\') ? $type : '\\think\\log\\driver\\' . ucwords($type); + $type = isset($config['type']) ? $config['type'] : 'File'; + $class = false !== strpos($type, '\\') ? $type : '\\think\\log\\driver\\' . ucwords($type); + self::$config = $config; unset($config['type']); + if (class_exists($class)) { self::$driver = new $class($config); } else { throw new ClassNotFoundException('class not exists:' . $class, $class); } + // 记录初始化信息 App::$debug && Log::record('[ LOG ] INIT ' . $type, 'info'); } /** * 获取日志信息 - * @param string $type 信息类型 - * @return array + * @access public + * @param string $type 信息类型 + * @return array|string */ public static function getLog($type = '') { @@ -77,21 +96,22 @@ class Log /** * 记录调试信息 - * @param mixed $msg 调试信息 - * @param string $type 信息类型 + * @access public + * @param mixed $msg 调试信息 + * @param string $type 信息类型 * @return void */ public static function record($msg, $type = 'log') { self::$log[$type][] = $msg; - if (IS_CLI) { - // 命令行下面日志写入改进 - self::save(); - } + + // 命令行下面日志写入改进 + IS_CLI && self::save(); } /** * 清空日志信息 + * @access public * @return void */ public static function clear() @@ -100,8 +120,9 @@ class Log } /** - * 当前日志记录的授权key - * @param string $key 授权key + * 设置当前日志记录的授权 key + * @access public + * @param string $key 授权 key * @return void */ public static function key($key) @@ -111,102 +132,105 @@ class Log /** * 检查日志写入权限 - * @param array $config 当前日志配置参数 + * @access public + * @param array $config 当前日志配置参数 * @return bool */ public static function check($config) { - if (self::$key && !empty($config['allow_key']) && !in_array(self::$key, $config['allow_key'])) { - return false; - } - return true; + return !self::$key || empty($config['allow_key']) || in_array(self::$key, $config['allow_key']); } /** * 保存调试信息 + * @access public * @return bool */ public static function save() { - if (!empty(self::$log)) { - if (is_null(self::$driver)) { - self::init(Config::get('log')); - } + // 没有需要保存的记录则直接返回 + if (empty(self::$log)) { + return true; + } - if (!self::check(self::$config)) { - // 检测日志写入权限 - return false; - } + is_null(self::$driver) && self::init(Config::get('log')); - if (empty(self::$config['level'])) { - // 获取全部日志 - $log = self::$log; - if (!App::$debug && isset($log['debug'])) { - unset($log['debug']); - } - } else { - // 记录允许级别 - $log = []; - foreach (self::$config['level'] as $level) { - if (isset(self::$log[$level])) { - $log[$level] = self::$log[$level]; - } + // 检测日志写入权限 + if (!self::check(self::$config)) { + return false; + } + + if (empty(self::$config['level'])) { + // 获取全部日志 + $log = self::$log; + if (!App::$debug && isset($log['debug'])) { + unset($log['debug']); + } + } else { + // 记录允许级别 + $log = []; + foreach (self::$config['level'] as $level) { + if (isset(self::$log[$level])) { + $log[$level] = self::$log[$level]; } } + } - $result = self::$driver->save($log); - if ($result) { - self::$log = []; - } - Hook::listen('log_write_done', $log); - return $result; + if ($result = self::$driver->save($log, true)) { + self::$log = []; } - return true; + + Hook::listen('log_write_done', $log); + + return $result; } /** * 实时写入日志信息 并支持行为 - * @param mixed $msg 调试信息 - * @param string $type 信息类型 - * @param bool $force 是否强制写入 + * @access public + * @param mixed $msg 调试信息 + * @param string $type 信息类型 + * @param bool $force 是否强制写入 * @return bool */ public static function write($msg, $type = 'log', $force = false) { $log = self::$log; - // 封装日志信息 - if (true === $force || empty(self::$config['level'])) { - $log[$type][] = $msg; - } elseif (in_array($type, self::$config['level'])) { - $log[$type][] = $msg; - } else { + + // 如果不是强制写入,而且信息类型不在可记录的类别中则直接返回 false 不做记录 + if (true !== $force && !empty(self::$config['level']) && !in_array($type, self::$config['level'])) { return false; } - // 监听log_write + // 封装日志信息 + $log[$type][] = $msg; + + // 监听 log_write Hook::listen('log_write', $log); - if (is_null(self::$driver)) { - self::init(Config::get('log')); - } + + is_null(self::$driver) && self::init(Config::get('log')); + // 写入日志 - $result = self::$driver->save($log); - if ($result) { + if ($result = self::$driver->save($log, false)) { self::$log = []; } + return $result; } /** - * 静态调用 - * @param $method - * @param $args - * @return mixed + * 静态方法调用 + * @access public + * @param string $method 调用方法 + * @param mixed $args 参数 + * @return void */ public static function __callStatic($method, $args) { if (in_array($method, self::$type)) { array_push($args, $method); - return call_user_func_array('\\think\\Log::record', $args); + + call_user_func_array('\\think\\Log::record', $args); } } diff --git a/thinkphp/library/think/Model.php b/thinkphp/library/think/Model.php index 6fc8cabe..2dc27b48 100644 --- a/thinkphp/library/think/Model.php +++ b/thinkphp/library/think/Model.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -11,6 +11,7 @@ namespace think; +use BadMethodCallException; use InvalidArgumentException; use think\db\Query; use think\exception\ValidateException; @@ -56,6 +57,10 @@ abstract class Model implements \JsonSerializable, \ArrayAccess protected $pk; // 数据表字段信息 留空则自动获取 protected $field = []; + // 数据排除字段 + protected $except = []; + // 数据废弃字段 + protected $disuse = []; // 只读字段 protected $readonly = []; // 显示属性 @@ -89,6 +94,10 @@ abstract class Model implements \JsonSerializable, \ArrayAccess protected $type = []; // 是否为更新数据 protected $isUpdate = false; + // 是否使用Replace + protected $replace = false; + // 是否强制更新所有数据 + protected $force = false; // 更新条件 protected $updateWhere; // 验证失败是否抛出异常 @@ -109,6 +118,12 @@ abstract class Model implements \JsonSerializable, \ArrayAccess */ protected static $initialized = []; + /** + * 是否从主库读取(主从分布式有效) + * @var array + */ + protected static $readMaster; + /** * 构造方法 * @access public @@ -121,6 +136,16 @@ abstract class Model implements \JsonSerializable, \ArrayAccess } else { $this->data = $data; } + + if ($this->disuse) { + // 废弃字段 + foreach ((array) $this->disuse as $key) { + if (array_key_exists($key, $this->data)) { + unset($this->data[$key]); + } + } + } + // 记录原始数据 $this->origin = $this->data; @@ -154,6 +179,20 @@ abstract class Model implements \JsonSerializable, \ArrayAccess $this->initialize(); } + /** + * 是否从主库读取数据(主从分布有效) + * @access public + * @param bool $all 是否所有模型生效 + * @return $this + */ + public function readMaster($all = false) + { + $model = $all ? '*' : $this->class; + + static::$readMaster[$model] = true; + return $this; + } + /** * 创建模型的查询对象 * @access protected @@ -175,7 +214,11 @@ abstract class Model implements \JsonSerializable, \ArrayAccess $con = Db::connect($connection); // 设置当前模型 确保查询返回模型对象 $queryClass = $this->query ?: $con->getConfig('query'); - $query = new $queryClass($con, $this->class); + $query = new $queryClass($con, $this); + + if (isset(static::$readMaster['*']) || isset(static::$readMaster[$this->class])) { + $query->master(true); + } // 设置当前数据表和模型名 if (!empty($this->table)) { @@ -191,6 +234,19 @@ abstract class Model implements \JsonSerializable, \ArrayAccess return $query; } + /** + * 创建新的模型实例 + * @access public + * @param array|object $data 数据 + * @param bool $isUpdate 是否为更新 + * @param mixed $where 更新条件 + * @return Model + */ + public function newInstance($data = [], $isUpdate = false, $where = null) + { + return (new static($data))->isUpdate($isUpdate, $where); + } + /** * 获取当前模型的查询对象 * @access public @@ -261,7 +317,6 @@ abstract class Model implements \JsonSerializable, \ArrayAccess public function setParent($model) { $this->parent = $model; - return $this; } @@ -336,6 +391,18 @@ abstract class Model implements \JsonSerializable, \ArrayAccess return $this; } + /** + * 更新是否强制写入数据 而不做比较 + * @access public + * @param bool $force + * @return $this + */ + public function force($force = true) + { + $this->force = $force; + return $this; + } + /** * 修改器 设置数据对象值 * @access public @@ -353,7 +420,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess // 检测修改器 $method = 'set' . Loader::parseName($name, 1) . 'Attr'; if (method_exists($this, $method)) { - $value = $this->$method($value, array_merge($this->data, $data)); + $value = $this->$method($value, array_merge($this->data, $data), $this->relation); } elseif (isset($this->type[$name])) { // 类型转换 $value = $this->writeTransform($value, $this->type[$name]); @@ -532,7 +599,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess // 检测属性获取器 $method = 'get' . Loader::parseName($name, 1) . 'Attr'; if (method_exists($this, $method)) { - $value = $this->$method($value, $this->data); + $value = $this->$method($value, $this->data, $this->relation); } elseif (isset($this->type[$name])) { // 类型转换 $value = $this->readTransform($value, $this->type[$name]); @@ -567,14 +634,19 @@ abstract class Model implements \JsonSerializable, \ArrayAccess * @access public * @param Relation $modelRelation 模型关联对象 * @return mixed + * @throws BadMethodCallException */ protected function getRelationData(Relation $modelRelation) { - if ($this->parent && get_class($this->parent) == $modelRelation->getModel()) { + if ($this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent)) { $value = $this->parent; } else { // 首先获取关联数据 - $value = $modelRelation->getRelation(); + if (method_exists($modelRelation, 'getRelation')) { + $value = $modelRelation->getRelation(); + } else { + throw new BadMethodCallException('method not exists:' . get_class($modelRelation) . '-> getRelation'); + } } return $value; } @@ -633,7 +705,11 @@ abstract class Model implements \JsonSerializable, \ArrayAccess $value = empty($value) ? new \stdClass() : json_decode($value); break; case 'serialize': - $value = unserialize($value); + try { + $value = unserialize($value); + } catch (\Exception $e) { + $value = null; + } break; default: if (false !== strpos($type, '\\')) { @@ -686,7 +762,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess if (isset($this->data[$key])) { throw new Exception('bind attr has exists:' . $key); } else { - $this->data[$key] = $model->$attr; + $this->data[$key] = $model->getAttr($attr); } } } @@ -820,7 +896,29 @@ abstract class Model implements \JsonSerializable, \ArrayAccess $relation = $this->getAttr($key); $item[$key] = $relation->append([$attr])->toArray(); } else { - $item[$name] = $this->getAttr($name); + $relation = Loader::parseName($name, 1, false); + if (method_exists($this, $relation)) { + $modelRelation = $this->$relation(); + $value = $this->getRelationData($modelRelation); + + if (method_exists($modelRelation, 'getBindAttr')) { + $bindAttr = $modelRelation->getBindAttr(); + if ($bindAttr) { + foreach ($bindAttr as $key => $attr) { + $key = is_numeric($key) ? $attr : $key; + if (isset($this->data[$key])) { + throw new Exception('bind attr has exists:' . $key); + } else { + $item[$key] = $value ? $value->getAttr($attr) : null; + } + } + continue; + } + } + $item[$name] = $value; + } else { + $item[$name] = $this->getAttr($name); + } } } } @@ -838,6 +936,17 @@ abstract class Model implements \JsonSerializable, \ArrayAccess return json_encode($this->toArray(), $options); } + /** + * 移除当前模型的关联属性 + * @access public + * @return $this + */ + public function removeRelation() + { + $this->relation = []; + return $this; + } + /** * 转换当前模型数据集为数据集对象 * @access public @@ -906,6 +1015,18 @@ abstract class Model implements \JsonSerializable, \ArrayAccess return false; } + /** + * 新增数据是否使用Replace + * @access public + * @param bool $replace + * @return $this + */ + public function replace($replace = true) + { + $this->replace = $replace; + return $this; + } + /** * 保存当前数据对象 * @access public @@ -916,18 +1037,26 @@ abstract class Model implements \JsonSerializable, \ArrayAccess */ public function save($data = [], $where = [], $sequence = null) { + if (is_string($data)) { + $sequence = $data; + $data = []; + } + + // 数据自动验证 if (!empty($data)) { - // 数据自动验证 if (!$this->validateData($data)) { return false; } + // 数据对象赋值 foreach ($data as $key => $value) { $this->setAttr($key, $value, $data); } - if (!empty($where)) { - $this->isUpdate = true; - } + } + + if (!empty($where)) { + $this->isUpdate = true; + $this->updateWhere = $where; } // 自动关联写入 @@ -964,9 +1093,6 @@ abstract class Model implements \JsonSerializable, \ArrayAccess } $pk = $this->getPk(); if ($this->isUpdate) { - // 检测字段 - $this->checkAllowField($this->data, array_merge($this->auto, $this->update)); - // 自动更新 $this->autoCompleteData($this->update); @@ -986,7 +1112,8 @@ abstract class Model implements \JsonSerializable, \ArrayAccess return 0; } elseif ($this->autoWriteTimestamp && $this->updateTime && !isset($data[$this->updateTime])) { // 自动写入更新时间 - $data[$this->updateTime] = $this->autoWriteTimestamp($this->updateTime); + $data[$this->updateTime] = $this->autoWriteTimestamp($this->updateTime); + $this->data[$this->updateTime] = $data[$this->updateTime]; } if (empty($where) && !empty($this->updateWhere)) { @@ -1000,16 +1127,28 @@ abstract class Model implements \JsonSerializable, \ArrayAccess } } - if (is_string($pk) && isset($data[$pk])) { - if (!isset($where[$pk])) { - unset($where); - $where[$pk] = $data[$pk]; + $array = []; + + foreach ((array) $pk as $key) { + if (isset($data[$key])) { + $array[$key] = $data[$key]; + unset($data[$key]); } - unset($data[$pk]); } + if (!empty($array)) { + $where = $array; + } + + // 检测字段 + $allowFields = $this->checkAllowField(array_merge($this->auto, $this->update)); + // 模型更新 - $result = $this->getQuery()->where($where)->update($data); + if (!empty($allowFields)) { + $result = $this->getQuery()->where($where)->strict(false)->field($allowFields)->update($data); + } else { + $result = $this->getQuery()->where($where)->update($data); + } // 关联更新 if (isset($relation)) { @@ -1020,9 +1159,6 @@ abstract class Model implements \JsonSerializable, \ArrayAccess $this->trigger('after_update', $this); } else { - // 检测字段 - $this->checkAllowField($this->data, array_merge($this->auto, $this->insert)); - // 自动写入 $this->autoCompleteData($this->insert); @@ -1040,13 +1176,20 @@ abstract class Model implements \JsonSerializable, \ArrayAccess return false; } - $result = $this->getQuery()->insert($this->data); + // 检测字段 + $allowFields = $this->checkAllowField(array_merge($this->auto, $this->insert)); + if (!empty($allowFields)) { + $result = $this->getQuery()->strict(false)->field($allowFields)->insert($this->data, $this->replace, false, $sequence); + } else { + $result = $this->getQuery()->insert($this->data, $this->replace, false, $sequence); + } // 获取自动增长主键 - if ($result && is_string($pk) && (!isset($this->data[$pk]) || '' == $this->data[$pk])) { - $insertId = $this->getQuery()->getLastInsID($sequence); - if ($insertId) { - $this->data[$pk] = $insertId; + if ($result && $insertId = $this->getQuery()->getLastInsID($sequence)) { + foreach ((array) $pk as $key) { + if (!isset($this->data[$key]) || '' == $this->data[$key]) { + $this->data[$key] = $insertId; + } } } @@ -1073,22 +1216,29 @@ abstract class Model implements \JsonSerializable, \ArrayAccess return $result; } - protected function checkAllowField(&$data, $auto = []) + protected function checkAllowField($auto = []) { - if (!empty($this->field)) { - if (true === $this->field) { - $this->field = $this->getQuery()->getTableInfo('', 'fields'); - $field = $this->field; - } else { - $field = array_merge($this->field, $auto); + if (true === $this->field) { + $this->field = $this->getQuery()->getTableInfo('', 'fields'); + $field = $this->field; + } elseif (!empty($this->field)) { + $field = array_merge($this->field, $auto); + if ($this->autoWriteTimestamp) { + array_push($field, $this->createTime, $this->updateTime); } + } elseif (!empty($this->except)) { + $fields = $this->getQuery()->getTableInfo('', 'fields'); + $field = array_diff($fields, (array) $this->except); + $this->field = $field; + } else { + $field = []; + } - foreach ($data as $key => $val) { - if (!in_array($key, $field)) { - unset($data[$key]); - } - } + if ($this->disuse) { + // 废弃字段 + $field = array_diff($field, (array) $this->disuse); } + return $field; } protected function autoRelationUpdate($relation) @@ -1113,12 +1263,16 @@ abstract class Model implements \JsonSerializable, \ArrayAccess */ public function getChangedData() { - $data = array_udiff_assoc($this->data, $this->origin, function ($a, $b) { - if ((empty($b) || empty($b)) && $a !== $b) { - return 1; - } - return is_object($a) || $a != $b ? 1 : 0; - }); + if ($this->force) { + $data = $this->data; + } else { + $data = array_udiff_assoc($this->data, $this->origin, function ($a, $b) { + if ((empty($a) || empty($b)) && $a !== $b) { + return 1; + } + return is_object($a) || $a != $b ? 1 : 0; + }); + } if (!empty($this->readonly)) { // 只读字段不允许更新 @@ -1143,16 +1297,8 @@ abstract class Model implements \JsonSerializable, \ArrayAccess */ public function setInc($field, $step = 1, $lazyTime = 0) { - // 删除条件 - $pk = $this->getPk(); - - if (is_string($pk) && isset($this->data[$pk])) { - $where = [$pk => $this->data[$pk]]; - } elseif (!empty($this->updateWhere)) { - $where = $this->updateWhere; - } else { - $where = null; - } + // 更新条件 + $where = $this->getWhere(); $result = $this->getQuery()->where($where)->setInc($field, $step, $lazyTime); if (true !== $result) { @@ -1172,6 +1318,23 @@ abstract class Model implements \JsonSerializable, \ArrayAccess * @throws Exception */ public function setDec($field, $step = 1, $lazyTime = 0) + { + // 更新条件 + $where = $this->getWhere(); + $result = $this->getQuery()->where($where)->setDec($field, $step, $lazyTime); + if (true !== $result) { + $this->data[$field] -= $step; + } + + return $result; + } + + /** + * 获取更新条件 + * @access protected + * @return mixed + */ + protected function getWhere() { // 删除条件 $pk = $this->getPk(); @@ -1183,13 +1346,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess } else { $where = null; } - - $result = $this->getQuery()->where($where)->setDec($field, $step, $lazyTime); - if (true !== $result) { - $this->data[$field] -= $step; - } - - return $result; + return $where; } /** @@ -1221,14 +1378,14 @@ abstract class Model implements \JsonSerializable, \ArrayAccess $auto = true; } foreach ($dataSet as $key => $data) { - if (!empty($auto) && isset($data[$pk])) { + if ($this->isUpdate || (!empty($auto) && isset($data[$pk]))) { $result[$key] = self::update($data, [], $this->field); } else { $result[$key] = self::create($data, $this->field); } } $db->commit(); - return $result; + return $this->toCollection($result); } catch (\Exception $e) { $db->rollback(); throw $e; @@ -1238,7 +1395,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess /** * 设置允许写入的字段 * @access public - * @param mixed $field 允许写入的字段 如果为true只允许写入数据表字段 + * @param string|array $field 允许写入的字段 如果为true只允许写入数据表字段 * @return $this */ public function allowField($field) @@ -1250,6 +1407,21 @@ abstract class Model implements \JsonSerializable, \ArrayAccess return $this; } + /** + * 设置排除写入的字段 + * @access public + * @param string|array $field 排除允许写入的字段 + * @return $this + */ + public function except($field) + { + if (is_string($field)) { + $field = explode(',', $field); + } + $this->except = $field; + return $this; + } + /** * 设置只读字段 * @access public @@ -1317,14 +1489,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess } // 删除条件 - $pk = $this->getPk(); - if (is_string($pk) && isset($this->data[$pk])) { - $where = [$pk => $this->data[$pk]]; - } elseif (!empty($this->updateWhere)) { - $where = $this->updateWhere; - } else { - $where = null; - } + $where = $this->getWhere(); // 删除当前模型数据 $result = $this->getQuery()->where($where)->delete(); @@ -1595,14 +1760,14 @@ abstract class Model implements \JsonSerializable, \ArrayAccess { $model = new static(); $query = $model->db(); - if (is_array($data) && key($data) !== 0) { + if (empty($data) && 0 !== $data) { + return 0; + } elseif (is_array($data) && key($data) !== 0) { $query->where($data); $data = null; } elseif ($data instanceof \Closure) { call_user_func_array($data, [ & $query]); $data = null; - } elseif (empty($data) && 0 !== $data) { - return 0; } $resultSet = $query->select($data); $count = 0; @@ -1627,6 +1792,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess $model = new static(); $query = $model->db(); $params = func_get_args(); + array_shift($params); array_unshift($params, $query); if ($name instanceof \Closure) { call_user_func_array($name, $params); @@ -1648,7 +1814,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess * 设置是否使用全局查询范围 * @param bool $use 是否启用全局查询范围 * @access public - * @return Model + * @return Query */ public static function useGlobalScope($use) { @@ -1677,13 +1843,14 @@ abstract class Model implements \JsonSerializable, \ArrayAccess /** * 根据关联条件查询当前模型 * @access public - * @param string $relation 关联方法名 - * @param mixed $where 查询条件(数组或者闭包) + * @param string $relation 关联方法名 + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 * @return Relation|Query */ - public static function hasWhere($relation, $where = []) + public static function hasWhere($relation, $where = [], $fields = null) { - return (new static())->$relation()->hasWhere($where); + return (new static())->$relation()->hasWhere($where, $fields); } /** @@ -1840,7 +2007,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess * @access public * @param string $model 模型名 * @param string $foreignKey 关联外键 - * @param string $localKey 关联主键 + * @param string $localKey 当前模型主键 * @param array $alias 别名定义(已经废弃) * @param string $joinType JOIN类型 * @return HasOne @@ -1880,7 +2047,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess * @access public * @param string $model 模型名 * @param string $foreignKey 关联外键 - * @param string $localKey 关联主键 + * @param string $localKey 当前模型主键 * @return HasMany */ public function hasMany($model, $foreignKey = '', $localKey = '') @@ -1899,7 +2066,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess * @param string $through 中间模型名 * @param string $foreignKey 关联外键 * @param string $throughKey 关联外键 - * @param string $localKey 关联主键 + * @param string $localKey 当前模型主键 * @return HasManyThrough */ public function hasManyThrough($model, $through, $foreignKey = '', $throughKey = '', $localKey = '') @@ -1927,7 +2094,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess // 记录当前关联信息 $model = $this->parseModel($model); $name = Loader::parseName(basename(str_replace('\\', '/', $model))); - $table = $table ?: $this->getQuery()->getTable(Loader::parseName($this->name) . '_' . $name); + $table = $table ?: Loader::parseName($this->name) . '_' . $name; $foreignKey = $foreignKey ?: $name . '_id'; $localKey = $localKey ?: $this->getForeignKey($this->name); return new BelongsToMany($this, $model, $table, $foreignKey, $localKey); @@ -1949,7 +2116,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess $trace = debug_backtrace(false, 2); $morph = Loader::parseName($trace[1]['function']); } - $type = $type ?: Loader::parseName($this->name); + $type = $type ?: get_class($this); if (is_array($morph)) { list($morphType, $foreignKey) = $morph; } else { @@ -1975,7 +2142,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess $trace = debug_backtrace(false, 2); $morph = Loader::parseName($trace[1]['function']); } - $type = $type ?: Loader::parseName($this->name); + $type = $type ?: get_class($this); if (is_array($morph)) { list($morphType, $foreignKey) = $morph; } else { @@ -2013,7 +2180,6 @@ abstract class Model implements \JsonSerializable, \ArrayAccess public function __call($method, $args) { $query = $this->db(true, false); - if (method_exists($this, 'scope' . $method)) { // 动态调用命名范围 $method = 'scope' . $method; @@ -2029,7 +2195,6 @@ abstract class Model implements \JsonSerializable, \ArrayAccess { $model = new static(); $query = $model->db(); - if (method_exists($model, 'scope' . $method)) { // 动态调用命名范围 $method = 'scope' . $method; diff --git a/thinkphp/library/think/Paginator.php b/thinkphp/library/think/Paginator.php index 5a8fa20e..36555678 100644 --- a/thinkphp/library/think/Paginator.php +++ b/thinkphp/library/think/Paginator.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -49,6 +49,9 @@ abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, J 'fragment' => '', ]; + /** @var mixed simple模式下的下个元素 */ + protected $nextItem; + public function __construct($items, $listRows, $currentPage = null, $total = null, $simple = false, $options = []) { $this->options = array_merge($this->options, $options); @@ -65,7 +68,10 @@ abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, J if ($simple) { $this->currentPage = $this->setCurrentPage($currentPage); $this->hasMore = count($items) > ($this->listRows); - $items = $items->slice(0, $this->listRows); + if ($this->hasMore) { + $this->nextItem = $items->slice($this->listRows, 1); + } + $items = $items->slice(0, $this->listRows); } else { $this->total = $total; $this->lastPage = (int) ceil($total / $listRows); @@ -122,7 +128,7 @@ abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, J } $url = $path; if (!empty($parameters)) { - $url .= '?' . urldecode(http_build_query($parameters, null, '&')); + $url .= '?' . http_build_query($parameters, null, '&'); } return $url . $this->buildFragment(); } @@ -135,9 +141,9 @@ abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, J */ public static function getCurrentPage($varPage = 'page', $default = 1) { - $page = Request::instance()->request($varPage); + $page = (int) Request::instance()->param($varPage); - if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int) $page >= 1) { + if (filter_var($page, FILTER_VALIDATE_INT) !== false && $page >= 1) { return $page; } @@ -282,8 +288,11 @@ abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, J public function each(callable $callback) { foreach ($this->items as $key => $item) { - if ($callback($item, $key) === false) { + $result = $callback($item, $key); + if (false === $result) { break; + } elseif (!is_object($item)) { + $this->items[$key] = $result; } } @@ -356,19 +365,24 @@ abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, J public function toArray() { - try { - $total = $this->total(); - } catch (\DomainException $e) { - $total = null; + if ($this->simple) { + return [ + 'per_page' => $this->listRows, + 'current_page' => $this->currentPage, + 'has_more' => $this->hasMore, + 'next_item' => $this->nextItem, + 'data' => $this->items->toArray(), + ]; + } else { + return [ + 'total' => $this->total, + 'per_page' => $this->listRows, + 'current_page' => $this->currentPage, + 'last_page' => $this->lastPage, + 'data' => $this->items->toArray(), + ]; } - return [ - 'total' => $total, - 'per_page' => $this->listRows(), - 'current_page' => $this->currentPage(), - 'last_page' => $this->lastPage, - 'data' => $this->items->toArray(), - ]; } /** @@ -381,7 +395,15 @@ abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, J public function __call($name, $arguments) { - return call_user_func_array([$this->getCollection(), $name], $arguments); + $collection = $this->getCollection(); + + $result = call_user_func_array([$collection, $name], $arguments); + + if ($result === $collection) { + return $this; + } + + return $result; } } diff --git a/thinkphp/library/think/Request.php b/thinkphp/library/think/Request.php index e3fa3a4f..5997a763 100644 --- a/thinkphp/library/think/Request.php +++ b/thinkphp/library/think/Request.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -59,6 +59,11 @@ class Request */ protected $routeInfo = []; + /** + * @var array 环境变量 + */ + protected $env; + /** * @var array 当前调度信息 */ @@ -116,6 +121,11 @@ class Request protected $cache; // 缓存是否检查 protected $isCheckCache; + /** + * 是否合并Param + * @var bool + */ + protected $mergeParam = false; /** * 构造函数 @@ -150,8 +160,8 @@ class Request /** * Hook 方法注入 * @access public - * @param string|array $method 方法名 - * @param mixed $callback callable + * @param string|array $method 方法名 + * @param mixed $callback callable * @return void */ public static function hook($method, $callback = null) @@ -177,16 +187,28 @@ class Request return self::$instance; } + /** + * 销毁当前请求对象 + * @access public + * @return void + */ + public static function destroy() + { + if (!is_null(self::$instance)) { + self::$instance = null; + } + } + /** * 创建一个URL请求 * @access public - * @param string $uri URL地址 - * @param string $method 请求类型 - * @param array $params 请求参数 - * @param array $cookie - * @param array $files - * @param array $server - * @param string $content + * @param string $uri URL地址 + * @param string $method 请求类型 + * @param array $params 请求参数 + * @param array $cookie + * @param array $files + * @param array $server + * @param string $content * @return \think\Request */ public static function create($uri, $method = 'GET', $params = [], $cookie = [], $files = [], $server = [], $content = null) @@ -227,7 +249,7 @@ class Request parse_str(html_entity_decode($info['query']), $query); if (!empty($params)) { $params = array_replace($query, $params); - $queryString = http_build_query($query, '', '&'); + $queryString = http_build_query($params, '', '&'); } else { $params = $query; $queryString = $info['query']; @@ -474,8 +496,8 @@ class Request /** * 设置资源类型 * @access public - * @param string|array $type 资源类型名 - * @param string $val 资源类型 + * @param string|array $type 资源类型名 + * @param string $val 资源类型 * @return void */ public function mimeType($type, $val = '') @@ -490,22 +512,28 @@ class Request /** * 当前的请求类型 * @access public - * @param bool $method true 获取原始请求类型 + * @param bool $method true 获取原始请求类型 * @return string */ public function method($method = false) { if (true === $method) { // 获取原始请求类型 - return IS_CLI ? 'GET' : (isset($this->server['REQUEST_METHOD']) ? $this->server['REQUEST_METHOD'] : $_SERVER['REQUEST_METHOD']); + return $this->server('REQUEST_METHOD') ?: 'GET'; } elseif (!$this->method) { if (isset($_POST[Config::get('var_method')])) { - $this->method = strtoupper($_POST[Config::get('var_method')]); - $this->{$this->method}($_POST); + $method = strtoupper($_POST[Config::get('var_method')]); + if (in_array($method, ['GET', 'POST', 'DELETE', 'PUT', 'PATCH'])) { + $this->method = $method; + $this->{$this->method}($_POST); + } else { + $this->method = 'POST'; + } + unset($_POST[Config::get('var_method')]); } elseif (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) { $this->method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']); } else { - $this->method = IS_CLI ? 'GET' : (isset($this->server['REQUEST_METHOD']) ? $this->server['REQUEST_METHOD'] : $_SERVER['REQUEST_METHOD']); + $this->method = $this->server('REQUEST_METHOD') ?: 'GET'; } } return $this->method; @@ -604,14 +632,14 @@ class Request /** * 获取当前请求的参数 * @access public - * @param string|array $name 变量名 - * @param mixed $default 默认值 - * @param string|array $filter 过滤方法 + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ public function param($name = '', $default = null, $filter = '') { - if (empty($this->param)) { + if (empty($this->mergeParam)) { $method = $this->method(true); // 自动获取请求变量 switch ($method) { @@ -627,7 +655,8 @@ class Request $vars = []; } // 当前请求参数和URL地址中的参数合并 - $this->param = array_merge($this->get(false), $vars, $this->route(false)); + $this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false)); + $this->mergeParam = true; } if (true === $name) { // 获取包含文件上传信息的数组 @@ -641,15 +670,16 @@ class Request /** * 设置获取路由参数 * @access public - * @param string|array $name 变量名 - * @param mixed $default 默认值 - * @param string|array $filter 过滤方法 + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ public function route($name = '', $default = null, $filter = '') { if (is_array($name)) { $this->param = []; + $this->mergeParam = false; return $this->route = array_merge($this->route, $name); } return $this->input($this->route, $name, $default, $filter); @@ -658,9 +688,9 @@ class Request /** * 设置获取GET参数 * @access public - * @param string|array $name 变量名 - * @param mixed $default 默认值 - * @param string|array $filter 过滤方法 + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ public function get($name = '', $default = null, $filter = '') @@ -670,6 +700,7 @@ class Request } if (is_array($name)) { $this->param = []; + $this->mergeParam = false; return $this->get = array_merge($this->get, $name); } return $this->input($this->get, $name, $default, $filter); @@ -678,9 +709,9 @@ class Request /** * 设置获取POST参数 * @access public - * @param string $name 变量名 - * @param mixed $default 默认值 - * @param string|array $filter 过滤方法 + * @param string $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ public function post($name = '', $default = null, $filter = '') @@ -695,6 +726,7 @@ class Request } if (is_array($name)) { $this->param = []; + $this->mergeParam = false; return $this->post = array_merge($this->post, $name); } return $this->input($this->post, $name, $default, $filter); @@ -703,9 +735,9 @@ class Request /** * 设置获取PUT参数 * @access public - * @param string|array $name 变量名 - * @param mixed $default 默认值 - * @param string|array $filter 过滤方法 + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ public function put($name = '', $default = null, $filter = '') @@ -720,6 +752,7 @@ class Request } if (is_array($name)) { $this->param = []; + $this->mergeParam = false; return $this->put = is_null($this->put) ? $name : array_merge($this->put, $name); } @@ -729,9 +762,9 @@ class Request /** * 设置获取DELETE参数 * @access public - * @param string|array $name 变量名 - * @param mixed $default 默认值 - * @param string|array $filter 过滤方法 + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ public function delete($name = '', $default = null, $filter = '') @@ -742,9 +775,9 @@ class Request /** * 设置获取PATCH参数 * @access public - * @param string|array $name 变量名 - * @param mixed $default 默认值 - * @param string|array $filter 过滤方法 + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ public function patch($name = '', $default = null, $filter = '') @@ -754,9 +787,9 @@ class Request /** * 获取request变量 - * @param string $name 数据名称 - * @param string $default 默认值 - * @param string|array $filter 过滤方法 + * @param string $name 数据名称 + * @param string $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ public function request($name = '', $default = null, $filter = '') @@ -766,6 +799,7 @@ class Request } if (is_array($name)) { $this->param = []; + $this->mergeParam = false; return $this->request = array_merge($this->request, $name); } return $this->input($this->request, $name, $default, $filter); @@ -774,9 +808,9 @@ class Request /** * 获取session数据 * @access public - * @param string|array $name 数据名称 - * @param string $default 默认值 - * @param string|array $filter 过滤方法 + * @param string|array $name 数据名称 + * @param string $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ public function session($name = '', $default = null, $filter = '') @@ -793,9 +827,9 @@ class Request /** * 获取cookie参数 * @access public - * @param string|array $name 数据名称 - * @param string $default 默认值 - * @param string|array $filter 过滤方法 + * @param string|array $name 数据名称 + * @param string $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ public function cookie($name = '', $default = null, $filter = '') @@ -826,9 +860,9 @@ class Request /** * 获取server参数 * @access public - * @param string|array $name 数据名称 - * @param string $default 默认值 - * @param string|array $filter 过滤方法 + * @param string|array $name 数据名称 + * @param string $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ public function server($name = '', $default = null, $filter = '') @@ -904,9 +938,9 @@ class Request /** * 获取环境变量 - * @param string|array $name 数据名称 - * @param string $default 默认值 - * @param string|array $filter 过滤方法 + * @param string|array $name 数据名称 + * @param string $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ public function env($name = '', $default = null, $filter = '') @@ -923,8 +957,8 @@ class Request /** * 设置或者获取当前的Header * @access public - * @param string|array $name header名称 - * @param string $default 默认值 + * @param string|array $name header名称 + * @param string $default 默认值 * @return string */ public function header($name = '', $default = null) @@ -962,10 +996,10 @@ class Request /** * 获取变量 支持过滤和默认值 - * @param array $data 数据源 - * @param string|false $name 字段名 - * @param mixed $default 默认值 - * @param string|array $filter 过滤函数 + * @param array $data 数据源 + * @param string|false $name 字段名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤函数 * @return mixed */ public function input($data = [], $name = '', $default = null, $filter = '') @@ -1046,9 +1080,9 @@ class Request /** * 递归过滤给定的值 - * @param mixed $value 键值 - * @param mixed $key 键名 - * @param array $filters 过滤方法+默认值 + * @param mixed $value 键值 + * @param mixed $key 键名 + * @param array $filters 过滤方法+默认值 * @return mixed */ private function filterValue(&$value, $key, $filters) @@ -1088,7 +1122,7 @@ class Request public function filterExp(&$value) { // 过滤查询特殊字符 - if (is_string($value) && preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i', $value)) { + if (is_string($value) && preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT LIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOT EXISTS|NOTEXISTS|EXISTS|NOT NULL|NOTNULL|NULL|BETWEEN TIME|NOT BETWEEN TIME|NOTBETWEEN TIME|NOTIN|NOT IN|IN)$/i', $value)) { $value .= ' '; } // TODO 其他安全过滤 @@ -1133,9 +1167,9 @@ class Request /** * 是否存在某个请求参数 * @access public - * @param string $name 变量名 - * @param string $type 变量类型 - * @param bool $checkEmpty 是否检测空值 + * @param string $name 变量名 + * @param string $type 变量类型 + * @param bool $checkEmpty 是否检测空值 * @return mixed */ public function has($name, $type = 'param', $checkEmpty = false) @@ -1159,8 +1193,8 @@ class Request /** * 获取指定的参数 * @access public - * @param string|array $name 变量名 - * @param string $type 变量类型 + * @param string|array $name 变量名 + * @param string $type 变量类型 * @return mixed */ public function only($name, $type = 'param') @@ -1181,8 +1215,8 @@ class Request /** * 排除指定参数获取 * @access public - * @param string|array $name 变量名 - * @param string $type 变量类型 + * @param string|array $name 变量名 + * @param string $type 变量类型 * @return mixed */ public function except($name, $type = 'param') @@ -1215,6 +1249,8 @@ class Request return true; } elseif (isset($server['HTTP_X_FORWARDED_PROTO']) && 'https' == $server['HTTP_X_FORWARDED_PROTO']) { return true; + } elseif (Config::get('https_agent_name') && isset($server[Config::get('https_agent_name')])) { + return true; } return false; } @@ -1222,7 +1258,7 @@ class Request /** * 当前是否Ajax请求 * @access public - * @param bool $ajax true 获取原始ajax请求 + * @param bool $ajax true 获取原始ajax请求 * @return bool */ public function isAjax($ajax = false) @@ -1232,14 +1268,16 @@ class Request if (true === $ajax) { return $result; } else { - return $this->param(Config::get('var_ajax')) ? true : $result; + $result = $this->param(Config::get('var_ajax')) ? true : $result; + $this->mergeParam = false; + return $result; } } /** * 当前是否Pjax请求 * @access public - * @param bool $pjax true 获取原始pjax请求 + * @param bool $pjax true 获取原始pjax请求 * @return bool */ public function isPjax($pjax = false) @@ -1248,17 +1286,19 @@ class Request if (true === $pjax) { return $result; } else { - return $this->param(Config::get('var_pjax')) ? true : $result; + $result = $this->param(Config::get('var_pjax')) ? true : $result; + $this->mergeParam = false; + return $result; } } /** * 获取客户端IP地址 - * @param integer $type 返回类型 0 返回IP地址 1 返回IPV4地址数字 - * @param boolean $adv 是否进行高级模式获取(有可能被伪装) + * @param integer $type 返回类型 0 返回IP地址 1 返回IPV4地址数字 + * @param boolean $adv 是否进行高级模式获取(有可能被伪装) * @return mixed */ - public function ip($type = 0, $adv = false) + public function ip($type = 0, $adv = true) { $type = $type ? 1 : 0; static $ip = null; @@ -1266,7 +1306,11 @@ class Request return $ip[$type]; } - if ($adv) { + $httpAgentIp = Config::get('http_agent_ip'); + + if ($httpAgentIp && isset($_SERVER[$httpAgentIp])) { + $ip = $_SERVER[$httpAgentIp]; + } elseif ($adv) { if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { $arr = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); $pos = array_search('unknown', $arr); @@ -1331,11 +1375,18 @@ class Request /** * 当前请求的host * @access public + * @param bool $strict true 仅仅获取HOST * @return string */ - public function host() + public function host($strict = false) { - return $this->server('HTTP_HOST'); + if (isset($_SERVER['HTTP_X_REAL_HOST'])) { + $host = $_SERVER['HTTP_X_REAL_HOST']; + } else { + $host = $this->server('HTTP_HOST'); + } + + return true === $strict && strpos($host, ':') ? strstr($host, ':', true) : $host; } /** @@ -1405,7 +1456,7 @@ class Request /** * 设置或者获取当前请求的调度信息 * @access public - * @param array $dispatch 调度信息 + * @param array $dispatch 调度信息 * @return array */ public function dispatch($dispatch = null) @@ -1456,11 +1507,12 @@ class Request */ public function action($action = null) { - if (!is_null($action)) { + if (!is_null($action) && !is_bool($action)) { $this->action = $action; return $this; } else { - return $this->action ?: ''; + $name = $this->action ?: ''; + return true === $action ? $name : strtolower($name); } } @@ -1524,13 +1576,19 @@ class Request /** * 设置当前地址的请求缓存 * @access public - * @param string $key 缓存标识,支持变量规则 ,例如 item/:name/:id + * @param string $key 缓存标识,支持变量规则 ,例如 item/:name/:id * @param mixed $expire 缓存有效期 * @param array $except 缓存排除 + * @param string $tag 缓存标签 * @return void */ - public function cache($key, $expire = null, $except = []) + public function cache($key, $expire = null, $except = [], $tag = null) { + if (!is_array($except)) { + $tag = $except; + $except = []; + } + if (false !== $key && $this->isGet() && !$this->isCheckCache) { // 标记请求缓存检查 $this->isCheckCache = true; @@ -1542,7 +1600,7 @@ class Request $key = call_user_func_array($key, [$this]); } elseif (true === $key) { foreach ($except as $rule) { - if (0 === strpos($this->url(), $rule)) { + if (0 === stripos($this->url(), $rule)) { return; } } @@ -1584,7 +1642,7 @@ class Request $response = Response::create($content)->header($header); throw new \think\exception\HttpResponseException($response); } else { - $this->cache = [$key, $expire]; + $this->cache = [$key, $expire, $tag]; } } } @@ -1602,8 +1660,8 @@ class Request /** * 设置当前请求绑定的对象实例 * @access public - * @param string $name 绑定的对象标识 - * @param mixed $obj 绑定的对象实例 + * @param string|array $name 绑定的对象标识 + * @param mixed $obj 绑定的对象实例 * @return mixed */ public function bind($name, $obj = null) diff --git a/thinkphp/library/think/Response.php b/thinkphp/library/think/Response.php index 96737dcd..c5c15209 100644 --- a/thinkphp/library/think/Response.php +++ b/thinkphp/library/think/Response.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -69,9 +69,7 @@ class Response */ public static function create($data = '', $type = '', $code = 200, array $header = [], $options = []) { - $type = empty($type) ? 'null' : strtolower($type); - - $class = false !== strpos($type, '\\') ? $type : '\\think\\response\\' . ucfirst($type); + $class = false !== strpos($type, '\\') ? $type : '\\think\\response\\' . ucfirst(strtolower($type)); if (class_exists($class)) { $response = new $class($data, $code, $header, $options); } else { @@ -89,6 +87,9 @@ class Response */ public function send() { + // 监听response_send + Hook::listen('response_send', $this); + // 处理输出数据 $data = $this->getContent(); @@ -103,7 +104,7 @@ class Response $this->header['Cache-Control'] = 'max-age=' . $cache[1] . ',must-revalidate'; $this->header['Last-Modified'] = gmdate('D, d M Y H:i:s') . ' GMT'; $this->header['Expires'] = gmdate('D, d M Y H:i:s', $_SERVER['REQUEST_TIME'] + $cache[1]) . ' GMT'; - Cache::set($cache[0], [$data, $this->header], $cache[1]); + Cache::tag($cache[2])->set($cache[0], [$data, $this->header], $cache[1]); } } diff --git a/thinkphp/library/think/Route.php b/thinkphp/library/think/Route.php index a5b19327..ab53aa20 100644 --- a/thinkphp/library/think/Route.php +++ b/thinkphp/library/think/Route.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -68,8 +68,8 @@ class Route /** * 注册变量规则 * @access public - * @param string|array $name 变量名 - * @param string $rule 变量规则 + * @param string|array $name 变量名 + * @param string $rule 变量规则 * @return void */ public static function pattern($name = null, $rule = '') @@ -84,10 +84,10 @@ class Route /** * 注册子域名部署规则 * @access public - * @param string|array $domain 子域名 - * @param mixed $rule 路由规则 - * @param array $option 路由参数 - * @param array $pattern 变量规则 + * @param string|array $domain 子域名 + * @param mixed $rule 路由规则 + * @param array $option 路由参数 + * @param array $pattern 变量规则 * @return void */ public static function domain($domain, $rule = '', $option = [], $pattern = []) @@ -121,8 +121,8 @@ class Route /** * 设置路由绑定 * @access public - * @param mixed $bind 绑定信息 - * @param string $type 绑定类型 默认为module 支持 namespace class controller + * @param mixed $bind 绑定信息 + * @param string $type 绑定类型 默认为module 支持 namespace class controller * @return mixed */ public static function bind($bind, $type = 'module') @@ -133,8 +133,8 @@ class Route /** * 设置或者获取路由标识 * @access public - * @param string|array $name 路由命名标识 数组表示批量设置 - * @param array $value 路由地址及变量信息 + * @param string|array $name 路由命名标识 数组表示批量设置 + * @param array $value 路由地址及变量信息 * @return array */ public static function name($name = '', $value = null) @@ -154,7 +154,7 @@ class Route /** * 读取路由绑定 * @access public - * @param string $type 绑定类型 + * @param string $type 绑定类型 * @return mixed */ public static function getBind($type) @@ -165,8 +165,8 @@ class Route /** * 导入配置文件的路由规则 * @access public - * @param array $rule 路由规则 - * @param string $type 请求类型 + * @param array $rule 路由规则 + * @param string $type 请求类型 * @return void */ public static function import(array $rule, $type = '*') @@ -222,11 +222,11 @@ class Route /** * 注册路由规则 * @access public - * @param string $rule 路由规则 - * @param string $route 路由地址 - * @param string $type 请求类型 - * @param array $option 路由参数 - * @param array $pattern 变量规则 + * @param string|array $rule 路由规则 + * @param string $route 路由地址 + * @param string $type 请求类型 + * @param array $option 路由参数 + * @param array $pattern 变量规则 * @return void */ public static function rule($rule, $route = '', $type = '*', $option = [], $pattern = []) @@ -255,9 +255,11 @@ class Route $option1 = array_merge($option, $val[1]); $pattern1 = array_merge($pattern, isset($val[2]) ? $val[2] : []); } else { - $route = $val; + $option1 = null; + $pattern1 = null; + $route = $val; } - self::setRule($key, $route, $type, isset($option1) ? $option1 : $option, isset($pattern1) ? $pattern1 : $pattern, $group); + self::setRule($key, $route, $type, !is_null($option1) ? $option1 : $option, !is_null($pattern1) ? $pattern1 : $pattern, $group); } } else { self::setRule($rule, $route, $type, $option, $pattern, $group); @@ -268,12 +270,12 @@ class Route /** * 设置路由规则 * @access public - * @param string $rule 路由规则 - * @param string $route 路由地址 - * @param string $type 请求类型 - * @param array $option 路由参数 - * @param array $pattern 变量规则 - * @param string $group 所属分组 + * @param string|array $rule 路由规则 + * @param string $route 路由地址 + * @param string $type 请求类型 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @param string $group 所属分组 * @return void */ protected static function setRule($rule, $route, $type = '*', $option = [], $pattern = [], $group = '') @@ -333,9 +335,9 @@ class Route if ('*' == $type) { // 注册路由快捷方式 foreach (['get', 'post', 'put', 'delete', 'patch', 'head', 'options'] as $method) { - if (self::$domain) { + if (self::$domain && !isset(self::$rules['domain'][self::$domain][$method][$rule])) { self::$rules['domain'][self::$domain][$method][$rule] = true; - } else { + } elseif (!self::$domain && !isset(self::$rules[$method][$rule])) { self::$rules[$method][$rule] = true; } } @@ -346,7 +348,7 @@ class Route /** * 设置当前执行的参数信息 * @access public - * @param array $options 参数信息 + * @param array $options 参数信息 * @return mixed */ protected static function setOption($options = []) @@ -367,7 +369,7 @@ class Route /** * 获取当前的分组信息 * @access public - * @param string $type 分组信息名称 name option pattern + * @param string $type 分组信息名称 name option pattern * @return mixed */ public static function getGroup($type) @@ -382,9 +384,9 @@ class Route /** * 设置当前的路由分组 * @access public - * @param string $name 分组名称 - * @param array $option 分组路由参数 - * @param array $pattern 分组变量规则 + * @param string $name 分组名称 + * @param array $option 分组路由参数 + * @param array $pattern 分组变量规则 * @return void */ public static function setGroup($name, $option = [], $pattern = []) @@ -397,10 +399,10 @@ class Route /** * 注册路由分组 * @access public - * @param string|array $name 分组名称或者参数 - * @param array|\Closure $routes 路由地址 - * @param array $option 路由参数 - * @param array $pattern 变量规则 + * @param string|array $name 分组名称或者参数 + * @param array|\Closure $routes 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 * @return void */ public static function group($name, $routes, $option = [], $pattern = []) @@ -428,7 +430,8 @@ class Route self::$rules['*'][$name]['pattern'] = $pattern; } } else { - $item = []; + $item = []; + $completeMatch = Config::get('route_complete_match'); foreach ($routes as $key => $val) { if (is_numeric($key)) { $key = array_shift($val); @@ -447,6 +450,8 @@ class Route // 是否完整匹配 $options['complete_match'] = true; $key = substr($key, 0, -1); + } elseif ($completeMatch) { + $options['complete_match'] = true; } $key = trim($key, '/'); $vars = self::parseVar($key); @@ -482,10 +487,10 @@ class Route /** * 注册路由 * @access public - * @param string $rule 路由规则 - * @param string $route 路由地址 - * @param array $option 路由参数 - * @param array $pattern 变量规则 + * @param string|array $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 * @return void */ public static function any($rule, $route = '', $option = [], $pattern = []) @@ -496,10 +501,10 @@ class Route /** * 注册GET路由 * @access public - * @param string $rule 路由规则 - * @param string $route 路由地址 - * @param array $option 路由参数 - * @param array $pattern 变量规则 + * @param string|array $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 * @return void */ public static function get($rule, $route = '', $option = [], $pattern = []) @@ -510,10 +515,10 @@ class Route /** * 注册POST路由 * @access public - * @param string $rule 路由规则 - * @param string $route 路由地址 - * @param array $option 路由参数 - * @param array $pattern 变量规则 + * @param string|array $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 * @return void */ public static function post($rule, $route = '', $option = [], $pattern = []) @@ -524,10 +529,10 @@ class Route /** * 注册PUT路由 * @access public - * @param string $rule 路由规则 - * @param string $route 路由地址 - * @param array $option 路由参数 - * @param array $pattern 变量规则 + * @param string|array $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 * @return void */ public static function put($rule, $route = '', $option = [], $pattern = []) @@ -538,10 +543,10 @@ class Route /** * 注册DELETE路由 * @access public - * @param string $rule 路由规则 - * @param string $route 路由地址 - * @param array $option 路由参数 - * @param array $pattern 变量规则 + * @param string|array $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 * @return void */ public static function delete($rule, $route = '', $option = [], $pattern = []) @@ -552,10 +557,10 @@ class Route /** * 注册PATCH路由 * @access public - * @param string $rule 路由规则 - * @param string $route 路由地址 - * @param array $option 路由参数 - * @param array $pattern 变量规则 + * @param string|array $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 * @return void */ public static function patch($rule, $route = '', $option = [], $pattern = []) @@ -566,10 +571,10 @@ class Route /** * 注册资源路由 * @access public - * @param string $rule 路由规则 - * @param string $route 路由地址 - * @param array $option 路由参数 - * @param array $pattern 变量规则 + * @param string|array $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 * @return void */ public static function resource($rule, $route = '', $option = [], $pattern = []) @@ -613,10 +618,10 @@ class Route /** * 注册控制器路由 操作方法对应不同的请求后缀 * @access public - * @param string $rule 路由规则 - * @param string $route 路由地址 - * @param array $option 路由参数 - * @param array $pattern 变量规则 + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 * @return void */ public static function controller($rule, $route = '', $option = [], $pattern = []) @@ -629,9 +634,9 @@ class Route /** * 注册别名路由 * @access public - * @param string|array $rule 路由别名 - * @param string $route 路由地址 - * @param array $option 路由参数 + * @param string|array $rule 路由别名 + * @param string $route 路由地址 + * @param array $option 路由参数 * @return void */ public static function alias($rule = null, $route = '', $option = []) @@ -646,8 +651,8 @@ class Route /** * 设置不同请求类型下面的方法前缀 * @access public - * @param string $method 请求类型 - * @param string $prefix 类型前缀 + * @param string $method 请求类型 + * @param string $prefix 类型前缀 * @return void */ public static function setMethodPrefix($method, $prefix = '') @@ -662,8 +667,8 @@ class Route /** * rest方法定义和修改 * @access public - * @param string $name 方法名称 - * @param array|bool $resource 资源 + * @param string|array $name 方法名称 + * @param array|bool $resource 资源 * @return void */ public static function rest($name, $resource = []) @@ -678,9 +683,9 @@ class Route /** * 注册未匹配路由规则后的处理 * @access public - * @param string $route 路由地址 - * @param string $method 请求类型 - * @param array $option 路由参数 + * @param string $route 路由地址 + * @param string $method 请求类型 + * @param array $option 路由参数 * @return void */ public static function miss($route, $method = '*', $option = []) @@ -691,7 +696,7 @@ class Route /** * 注册一个自动解析的URL路由 * @access public - * @param string $route 路由地址 + * @param string $route 路由地址 * @return void */ public static function auto($route) @@ -721,9 +726,9 @@ class Route /** * 检测子域名部署 * @access public - * @param Request $request Request请求对象 - * @param array $currentRules 当前路由规则 - * @param string $method 请求类型 + * @param Request $request Request请求对象 + * @param array $currentRules 当前路由规则 + * @param string $method 请求类型 * @return void */ public static function checkDomain($request, &$currentRules, $method = 'get') @@ -732,7 +737,7 @@ class Route $rules = self::$rules['domain']; // 开启子域名部署 支持二级和三级域名 if (!empty($rules)) { - $host = $request->host(); + $host = $request->host(true); if (isset($rules[$host])) { // 完整域名或者IP配置 $item = $rules[$host]; @@ -822,14 +827,23 @@ class Route /** * 检测URL路由 * @access public - * @param Request $request Request请求对象 - * @param string $url URL地址 - * @param string $depr URL分隔符 - * @param bool $checkDomain 是否检测域名规则 + * @param Request $request Request请求对象 + * @param string $url URL地址 + * @param string $depr URL分隔符 + * @param bool $checkDomain 是否检测域名规则 * @return false|array */ public static function check($request, $url, $depr = '/', $checkDomain = false) { + //检查解析缓存 + if (!App::$debug && Config::get('route_check_cache')) { + $key = self::getCheckCacheKey($request); + if (Cache::has($key)) { + list($rule, $route, $pathinfo, $option, $matches) = Cache::get($key); + return self::parseRule($rule, $route, $pathinfo, $option, $matches, true); + } + } + // 分隔符替换 确保路由定义使用统一的分隔符 $url = str_replace($depr, '|', $url); @@ -883,12 +897,12 @@ class Route /** * 检测路由规则 * @access private - * @param Request $request - * @param array $rules 路由规则 - * @param string $url URL地址 - * @param string $depr URL分割符 - * @param string $group 路由分组名 - * @param array $options 路由参数(分组) + * @param Request $request + * @param array $rules 路由规则 + * @param string $url URL地址 + * @param string $depr URL分割符 + * @param string $group 路由分组名 + * @param array $options 路由参数(分组) * @return mixed */ private static function checkRoute($request, $rules, $url, $depr = '/', $group = '', $options = []) @@ -966,9 +980,9 @@ class Route /** * 检测路由别名 * @access private - * @param Request $request - * @param string $url URL地址 - * @param string $depr URL分隔符 + * @param Request $request + * @param string $url URL地址 + * @param string $depr URL分隔符 * @return mixed */ private static function checkRouteAlias($request, $url, $depr) @@ -1013,9 +1027,9 @@ class Route /** * 检测URL绑定 * @access private - * @param string $url URL地址 - * @param array $rules 路由规则 - * @param string $depr URL分隔符 + * @param string $url URL地址 + * @param array $rules 路由规则 + * @param string $depr URL分隔符 * @return mixed */ private static function checkUrlBind(&$url, &$rules, $depr = '/') @@ -1044,9 +1058,9 @@ class Route /** * 绑定到类 * @access public - * @param string $url URL地址 - * @param string $class 类名(带命名空间) - * @param string $depr URL分隔符 + * @param string $url URL地址 + * @param string $class 类名(带命名空间) + * @param string $depr URL分隔符 * @return array */ public static function bindToClass($url, $class, $depr = '/') @@ -1063,9 +1077,9 @@ class Route /** * 绑定到命名空间 * @access public - * @param string $url URL地址 - * @param string $namespace 命名空间 - * @param string $depr URL分隔符 + * @param string $url URL地址 + * @param string $namespace 命名空间 + * @param string $depr URL分隔符 * @return array */ public static function bindToNamespace($url, $namespace, $depr = '/') @@ -1083,9 +1097,9 @@ class Route /** * 绑定到控制器类 * @access public - * @param string $url URL地址 - * @param string $controller 控制器名 (支持带模块名 index/user ) - * @param string $depr URL分隔符 + * @param string $url URL地址 + * @param string $controller 控制器名 (支持带模块名 index/user ) + * @param string $depr URL分隔符 * @return array */ public static function bindToController($url, $controller, $depr = '/') @@ -1102,9 +1116,9 @@ class Route /** * 绑定到模块/控制器 * @access public - * @param string $url URL地址 - * @param string $controller 控制器类名(带命名空间) - * @param string $depr URL分隔符 + * @param string $url URL地址 + * @param string $controller 控制器类名(带命名空间) + * @param string $depr URL分隔符 * @return array */ public static function bindToModule($url, $controller, $depr = '/') @@ -1121,8 +1135,8 @@ class Route /** * 路由参数有效性检查 * @access private - * @param array $option 路由参数 - * @param Request $request Request对象 + * @param array $option 路由参数 + * @param Request $request Request对象 * @return bool */ private static function checkOption($option, $request) @@ -1148,18 +1162,18 @@ class Route /** * 检测路由规则 * @access private - * @param string $rule 路由规则 - * @param string $route 路由地址 - * @param string $url URL地址 - * @param array $pattern 变量规则 - * @param array $option 路由参数 - * @param string $depr URL分隔符(全局) + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param string $url URL地址 + * @param array $pattern 变量规则 + * @param array $option 路由参数 + * @param string $depr URL分隔符(全局) * @return array|false */ private static function checkRule($rule, $route, $url, $pattern, $option, $depr) { // 检查完整规则定义 - if (isset($pattern['__url__']) && !preg_match('/^' . $pattern['__url__'] . '/', str_replace('|', $depr, $url))) { + if (isset($pattern['__url__']) && !preg_match(0 === strpos($pattern['__url__'], '/') ? $pattern['__url__'] : '/^' . $pattern['__url__'] . '/', str_replace('|', $depr, $url))) { return false; } // 检查路由的参数分隔符 @@ -1195,9 +1209,9 @@ class Route /** * 解析模块的URL地址 [模块/控制器/操作?]参数1=值1&参数2=值2... * @access public - * @param string $url URL地址 - * @param string $depr URL分隔符 - * @param bool $autoSearch 是否自动深度搜索控制器 + * @param string $url URL地址 + * @param string $depr URL分隔符 + * @param bool $autoSearch 是否自动深度搜索控制器 * @return array */ public static function parseUrl($url, $depr = '/', $autoSearch = false) @@ -1264,7 +1278,7 @@ class Route /** * 解析URL的pathinfo参数和变量 * @access private - * @param string $url URL地址 + * @param string $url URL地址 * @return array */ private static function parseUrlPath($url) @@ -1290,9 +1304,9 @@ class Route /** * 检测URL和规则路由是否匹配 * @access private - * @param string $url URL地址 - * @param string $rule 路由规则 - * @param array $pattern 变量规则 + * @param string $url URL地址 + * @param string $rule 路由规则 + * @param array $pattern 变量规则 * @return array|false */ private static function match($url, $rule, $pattern) @@ -1349,7 +1363,7 @@ class Route if (false === $result) { return false; } - } elseif (!preg_match('/^' . $pattern[$name] . '$/', $m1[$key])) { + } elseif (!preg_match(0 === strpos($pattern[$name], '/') ? $pattern[$name] : '/^' . $pattern[$name] . '$/', $m1[$key])) { return false; } } @@ -1365,16 +1379,28 @@ class Route /** * 解析规则路由 * @access private - * @param string $rule 路由规则 - * @param string $route 路由地址 - * @param string $pathinfo URL地址 - * @param array $option 路由参数 - * @param array $matches 匹配的变量 + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param string $pathinfo URL地址 + * @param array $option 路由参数 + * @param array $matches 匹配的变量 + * @param bool $fromCache 通过缓存解析 * @return array */ - private static function parseRule($rule, $route, $pathinfo, $option = [], $matches = []) + private static function parseRule($rule, $route, $pathinfo, $option = [], $matches = [], $fromCache = false) { $request = Request::instance(); + + //保存解析缓存 + if (Config::get('route_check_cache') && !$fromCache) { + try { + $key = self::getCheckCacheKey($request); + Cache::tag('route_check')->set($key, [$rule, $route, $pathinfo, $option, $matches]); + } catch (\Exception $e) { + + } + } + // 解析路由规则 if ($rule) { $rule = explode('/', $rule); @@ -1449,6 +1475,10 @@ class Route $request->bind($bind); } + if (!empty($option['response'])) { + Hook::add('response_send', $option['response']); + } + // 解析额外参数 self::parseUrlParams(empty($paths) ? '' : implode('|', $paths), $matches); // 记录匹配的路由信息 @@ -1497,18 +1527,19 @@ class Route App::$modulePath = APP_PATH . (Config::get('app_multi_module') ? $request->module() . DS : ''); } else { // 路由到模块/控制器/操作 - $result = self::parseModule($route); + $result = self::parseModule($route, isset($option['convert']) ? $option['convert'] : false); } // 开启请求缓存 if ($request->isGet() && isset($option['cache'])) { $cache = $option['cache']; if (is_array($cache)) { - list($key, $expire) = $cache; + list($key, $expire, $tag) = array_pad($cache, 3, null); } else { $key = str_replace('|', '/', $pathinfo); $expire = $cache; + $tag = null; } - $request->cache($key, $expire); + $request->cache($key, $expire, $tag); } return $result; } @@ -1516,10 +1547,11 @@ class Route /** * 解析URL地址为 模块/控制器/操作 * @access private - * @param string $url URL地址 + * @param string $url URL地址 + * @param bool $convert 是否自动转换URL地址 * @return array */ - private static function parseModule($url) + private static function parseModule($url, $convert = false) { list($path, $var) = self::parseUrlPath($url); $action = array_pop($path); @@ -1533,14 +1565,14 @@ class Route // 设置当前请求的路由变量 Request::instance()->route($var); // 路由到模块/控制器/操作 - return ['type' => 'module', 'module' => [$module, $controller, $action], 'convert' => false]; + return ['type' => 'module', 'module' => [$module, $controller, $action], 'convert' => $convert]; } /** * 解析URL地址中的参数Request对象 * @access private - * @param string $rule 路由规则 - * @param array $var 变量 + * @param string $url 路由规则 + * @param array $var 变量 * @return void */ private static function parseUrlParams($url, &$var = []) @@ -1590,4 +1622,24 @@ class Route } return $var; } + + /** + * 获取路由解析缓存的key + * @param Request $request + * @return string + */ + private static function getCheckCacheKey(Request $request) + { + static $key; + + if (empty($key)) { + if ($callback = Config::get('route_check_cache_key')) { + $key = call_user_func($callback, $request); + } else { + $key = "{$request->host(true)}|{$request->method()}|{$request->path()}"; + } + } + + return $key; + } } diff --git a/thinkphp/library/think/Session.php b/thinkphp/library/think/Session.php index d5a6a5a5..61150bca 100644 --- a/thinkphp/library/think/Session.php +++ b/thinkphp/library/think/Session.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -25,6 +25,7 @@ class Session */ public static function prefix($prefix = '') { + empty(self::$init) && self::boot(); if (empty($prefix) && null !== $prefix) { return self::$prefix; } else { @@ -56,7 +57,7 @@ class Session $isDoStart = true; } - if (isset($config['prefix']) && (self::$prefix === '' || self::$prefix === null)) { + if (isset($config['prefix']) && ('' === self::$prefix || null === self::$prefix)) { self::$prefix = $config['prefix']; } if (isset($config['var_session_id']) && isset($_REQUEST[$config['var_session_id']])) { @@ -347,7 +348,7 @@ class Session * @param bool $delete 是否删除关联会话文件 * @return void */ - private static function regenerate($delete = false) + public static function regenerate($delete = false) { session_regenerate_id($delete); } diff --git a/thinkphp/library/think/Template.php b/thinkphp/library/think/Template.php index 4abf21ce..9ba0ff35 100644 --- a/thinkphp/library/think/Template.php +++ b/thinkphp/library/think/Template.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -12,6 +12,7 @@ namespace think; use think\exception\TemplateNotFoundException; +use think\template\TagLib; /** * ThinkPHP分离出来的模板引擎 @@ -59,15 +60,20 @@ class Template /** * 构造函数 * @access public + * @param array $config */ public function __construct(array $config = []) { - $this->config['cache_path'] = TEMP_PATH; - $this->config = array_merge($this->config, $config); - $this->config['taglib_begin'] = $this->stripPreg($this->config['taglib_begin']); - $this->config['taglib_end'] = $this->stripPreg($this->config['taglib_end']); - $this->config['tpl_begin'] = $this->stripPreg($this->config['tpl_begin']); - $this->config['tpl_end'] = $this->stripPreg($this->config['tpl_end']); + $this->config['cache_path'] = TEMP_PATH; + $this->config = array_merge($this->config, $config); + + $this->config['taglib_begin_origin'] = $this->config['taglib_begin']; + $this->config['taglib_end_origin'] = $this->config['taglib_end']; + + $this->config['taglib_begin'] = preg_quote($this->config['taglib_begin'], '/'); + $this->config['taglib_end'] = preg_quote($this->config['taglib_end'], '/'); + $this->config['tpl_begin'] = preg_quote($this->config['tpl_begin'], '/'); + $this->config['tpl_end'] = preg_quote($this->config['tpl_end'], '/'); // 初始化模板编译存储器 $type = $this->config['compile_type'] ? $this->config['compile_type'] : 'File'; @@ -75,20 +81,6 @@ class Template $this->storage = new $class(); } - /** - * 字符串替换 避免正则混淆 - * @access private - * @param string $str - * @return string - */ - private function stripPreg($str) - { - return str_replace( - ['{', '}', '(', ')', '|', '[', ']', '-', '+', '*', '.', '^', '?'], - ['\{', '\}', '\(', '\)', '\|', '\[', '\]', '\-', '\+', '\*', '\.', '\^', '\?'], - $str); - } - /** * 模板变量赋值 * @access public @@ -120,7 +112,7 @@ class Template * 模板引擎配置项 * @access public * @param array|string $config - * @return void|array + * @return string|void|array */ public function config($config) { @@ -183,7 +175,7 @@ class Template } $template = $this->parseTemplateFile($template); if ($template) { - $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($template) . '.' . ltrim($this->config['cache_suffix'], '.'); + $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($this->config['layout_name'] . $template) . '.' . ltrim($this->config['cache_suffix'], '.'); if (!$this->checkCache($cacheFile)) { // 缓存无效 重新模板编译 $content = file_get_contents($template); @@ -234,7 +226,7 @@ class Template * @access public * @param mixed $name 布局模板名称 false 则关闭布局 * @param string $replace 布局模板内容替换标识 - * @return object + * @return Template */ public function layout($name, $replace = '') { @@ -688,6 +680,7 @@ class Template } else { $className = '\\think\\template\\taglib\\' . ucwords($tagLib); } + /** @var Taglib $tLib */ $tLib = new $className($this); $tLib->parseTag($content, $hide ? '' : $tagLib); return; @@ -763,31 +756,26 @@ class Template } else { if (isset($array[1])) { $this->parseVar($array[2]); - $_name = ' && ' . $name . $array[1] . $array[2]; + $express = $name . $array[1] . $array[2]; } else { - $_name = ''; + $express = false; } // $name为数组 switch ($first) { case '?': // {$varname??'xxx'} $varname有定义则输出$varname,否则输出xxx - $str = ''; + $str = ''; break; case '=': // {$varname?='xxx'} $varname为真时才输出xxx - $str = ''; + $str = ''; break; case ':': // {$varname?:'xxx'} $varname为真时输出$varname,否则输出xxx - $str = ''; + $str = ''; break; default: - if (strpos($str, ':')) { - // {$varname ? 'a' : 'b'} $varname为真时输出a,否则输出b - $str = ''; - } else { - $str = ''; - } + $str = ''; } } } else { @@ -1075,7 +1063,7 @@ class Template } else { $path = isset($module) ? APP_PATH . $module . DS . basename($this->config['view_path']) . DS : $this->config['view_path']; } - $template = $path . $template . '.' . ltrim($this->config['view_suffix'], '.'); + $template = realpath($path . $template . '.' . ltrim($this->config['view_suffix'], '.')); } if (is_file($template)) { diff --git a/thinkphp/library/think/Url.php b/thinkphp/library/think/Url.php index 9f725e2c..53a545f9 100644 --- a/thinkphp/library/think/Url.php +++ b/thinkphp/library/think/Url.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -118,7 +118,7 @@ class Url $type = Route::getBind('type'); if ($type) { $bind = Route::getBind($type); - if (0 === strpos($url, $bind)) { + if ($bind && 0 === strpos($url, $bind)) { $url = substr($url, strlen($bind) + 1); } } @@ -135,7 +135,7 @@ class Url if (!empty($vars)) { // 添加参数 if (Config::get('url_common_param')) { - $vars = urldecode(http_build_query($vars)); + $vars = http_build_query($vars); $url .= $suffix . '?' . $vars . $anchor; } else { $paramType = Config::get('url_param_type'); @@ -210,17 +210,21 @@ class Url } $module = $module ? $module . '/' : ''; - $controller = Loader::parseName($request->controller()); + $controller = $request->controller(); if ('' == $url) { // 空字符串输出当前的 模块/控制器/操作 - $url = $module . $controller . '/' . $request->action(); + $action = $request->action(); } else { $path = explode('/', $url); - $action = Config::get('url_convert') ? strtolower(array_pop($path)) : array_pop($path); - $controller = empty($path) ? $controller : (Config::get('url_convert') ? Loader::parseName(array_pop($path)) : array_pop($path)); + $action = array_pop($path); + $controller = empty($path) ? $controller : array_pop($path); $module = empty($path) ? $module : array_pop($path) . '/'; - $url = $module . $controller . '/' . $action; } + if (Config::get('url_convert')) { + $action = strtolower($action); + $controller = Loader::parseName($controller); + } + $url = $module . $controller . '/' . $action; } return $url; } @@ -272,7 +276,7 @@ class Url $domain .= '.' . $rootDomain; } } - if (false !== strpos($domain, ':')) { + if (false !== strpos($domain, '://')) { $scheme = ''; } else { $scheme = $request->isSsl() || Config::get('is_https') ? 'https://' : 'http://'; @@ -298,11 +302,12 @@ class Url foreach ($rule as $item) { list($url, $pattern, $domain, $suffix) = $item; if (empty($pattern)) { - return [$url, $domain, $suffix]; + return [rtrim($url, '$'), $domain, $suffix]; } + $type = Config::get('url_common_param'); foreach ($pattern as $key => $val) { if (isset($vars[$key])) { - $url = str_replace(['[:' . $key . ']', '<' . $key . '?>', ':' . $key . '', '<' . $key . '>'], urlencode($vars[$key]), $url); + $url = str_replace(['[:' . $key . ']', '<' . $key . '?>', ':' . $key . '', '<' . $key . '>'], $type ? $vars[$key] : urlencode($vars[$key]), $url); unset($vars[$key]); $result = [$url, $domain, $suffix]; } elseif (2 == $val) { diff --git a/thinkphp/library/think/Validate.php b/thinkphp/library/think/Validate.php index 8d1cb3b5..608e1e4a 100644 --- a/thinkphp/library/think/Validate.php +++ b/thinkphp/library/think/Validate.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -36,54 +36,56 @@ class Validate // 验证规则默认提示信息 protected static $typeMsg = [ - 'require' => ':attribute不能为空', - 'number' => ':attribute必须是数字', - 'float' => ':attribute必须是浮点数', - 'boolean' => ':attribute必须是布尔值', - 'email' => ':attribute格式不符', - 'array' => ':attribute必须是数组', - 'accepted' => ':attribute必须是yes、on或者1', - 'date' => ':attribute格式不符合', - 'file' => ':attribute不是有效的上传文件', - 'image' => ':attribute不是有效的图像文件', - 'alpha' => ':attribute只能是字母', - 'alphaNum' => ':attribute只能是字母和数字', - 'alphaDash' => ':attribute只能是字母、数字和下划线_及破折号-', - 'activeUrl' => ':attribute不是有效的域名或者IP', - 'chs' => ':attribute只能是汉字', - 'chsAlpha' => ':attribute只能是汉字、字母', - 'chsAlphaNum' => ':attribute只能是汉字、字母和数字', - 'chsDash' => ':attribute只能是汉字、字母、数字和下划线_及破折号-', - 'url' => ':attribute不是有效的URL地址', - 'ip' => ':attribute不是有效的IP地址', - 'dateFormat' => ':attribute必须使用日期格式 :rule', - 'in' => ':attribute必须在 :rule 范围内', - 'notIn' => ':attribute不能在 :rule 范围内', - 'between' => ':attribute只能在 :1 - :2 之间', - 'notBetween' => ':attribute不能在 :1 - :2 之间', - 'length' => ':attribute长度不符合要求 :rule', - 'max' => ':attribute长度不能超过 :rule', - 'min' => ':attribute长度不能小于 :rule', - 'after' => ':attribute日期不能小于 :rule', - 'before' => ':attribute日期不能超过 :rule', - 'expire' => '不在有效期内 :rule', - 'allowIp' => '不允许的IP访问', - 'denyIp' => '禁止的IP访问', - 'confirm' => ':attribute和确认字段:2不一致', - 'different' => ':attribute和比较字段:2不能相同', - 'egt' => ':attribute必须大于等于 :rule', - 'gt' => ':attribute必须大于 :rule', - 'elt' => ':attribute必须小于等于 :rule', - 'lt' => ':attribute必须小于 :rule', - 'eq' => ':attribute必须等于 :rule', - 'unique' => ':attribute已存在', - 'regex' => ':attribute不符合指定规则', - 'method' => '无效的请求类型', - 'token' => '令牌数据无效', - 'fileSize' => '上传文件大小不符', - 'fileExt' => '上传文件后缀不符', - 'fileMime' => '上传文件类型不符', - + 'require' => ':attribute require', + 'number' => ':attribute must be numeric', + 'integer' => ':attribute must be integer', + 'float' => ':attribute must be float', + 'boolean' => ':attribute must be bool', + 'email' => ':attribute not a valid email address', + 'array' => ':attribute must be a array', + 'accepted' => ':attribute must be yes,on or 1', + 'date' => ':attribute not a valid datetime', + 'file' => ':attribute not a valid file', + 'image' => ':attribute not a valid image', + 'alpha' => ':attribute must be alpha', + 'alphaNum' => ':attribute must be alpha-numeric', + 'alphaDash' => ':attribute must be alpha-numeric, dash, underscore', + 'activeUrl' => ':attribute not a valid domain or ip', + 'chs' => ':attribute must be chinese', + 'chsAlpha' => ':attribute must be chinese or alpha', + 'chsAlphaNum' => ':attribute must be chinese,alpha-numeric', + 'chsDash' => ':attribute must be chinese,alpha-numeric,underscore, dash', + 'url' => ':attribute not a valid url', + 'ip' => ':attribute not a valid ip', + 'dateFormat' => ':attribute must be dateFormat of :rule', + 'in' => ':attribute must be in :rule', + 'notIn' => ':attribute be notin :rule', + 'between' => ':attribute must between :1 - :2', + 'notBetween' => ':attribute not between :1 - :2', + 'length' => 'size of :attribute must be :rule', + 'max' => 'max size of :attribute must be :rule', + 'min' => 'min size of :attribute must be :rule', + 'after' => ':attribute cannot be less than :rule', + 'before' => ':attribute cannot exceed :rule', + 'afterWith' => ':attribute cannot be less than :rule', + 'beforeWith' => ':attribute cannot exceed :rule', + 'expire' => ':attribute not within :rule', + 'allowIp' => 'access IP is not allowed', + 'denyIp' => 'access IP denied', + 'confirm' => ':attribute out of accord with :2', + 'different' => ':attribute cannot be same with :2', + 'egt' => ':attribute must greater than or equal :rule', + 'gt' => ':attribute must greater than :rule', + 'elt' => ':attribute must less than or equal :rule', + 'lt' => ':attribute must less than :rule', + 'eq' => ':attribute must equal :rule', + 'unique' => ':attribute has exists', + 'regex' => ':attribute not conform to the rules', + 'method' => 'invalid Request method', + 'token' => 'invalid token', + 'fileSize' => 'filesize not match', + 'fileExt' => 'extensions to upload is not allowed', + 'fileMime' => 'mimetype to upload is not allowed', ]; // 当前验证场景 @@ -339,6 +341,41 @@ class Validate return !empty($this->error) ? false : true; } + /** + * 根据验证规则验证数据 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rules 验证规则 + * @return bool + */ + protected function checkRule($value, $rules) + { + if ($rules instanceof \Closure) { + return call_user_func_array($rules, [$value]); + } elseif (is_string($rules)) { + $rules = explode('|', $rules); + } + + foreach ($rules as $key => $rule) { + if ($rule instanceof \Closure) { + $result = call_user_func_array($rule, [$value]); + } else { + // 判断验证类型 + list($type, $rule) = $this->getValidateType($key, $rule); + + $callback = isset(self::$type[$type]) ? self::$type[$type] : [$this, $type]; + + $result = call_user_func_array($callback, [$value, $rule]); + } + + if (true !== $result) { + return $result; + } + } + + return true; + } + /** * 验证单个字段规则 * @access protected @@ -363,25 +400,7 @@ class Validate $info = is_numeric($key) ? '' : $key; } else { // 判断验证类型 - if (is_numeric($key)) { - if (strpos($rule, ':')) { - list($type, $rule) = explode(':', $rule, 2); - if (isset($this->alias[$type])) { - // 判断别名 - $type = $this->alias[$type]; - } - $info = $type; - } elseif (method_exists($this, $rule)) { - $type = $rule; - $info = $rule; - $rule = ''; - } else { - $type = 'is'; - $info = $rule; - } - } else { - $info = $type = $key; - } + list($type, $rule, $info) = $this->getValidateType($key, $rule); // 如果不是require 有数据才会行验证 if (0 === strpos($info, 'require') || (!is_null($value) && '' !== $value)) { @@ -417,6 +436,39 @@ class Validate return $result; } + /** + * 获取当前验证类型及规则 + * @access public + * @param mixed $key + * @param mixed $rule + * @return array + */ + protected function getValidateType($key, $rule) + { + // 判断验证类型 + if (!is_numeric($key)) { + return [$key, $rule, $key]; + } + + if (strpos($rule, ':')) { + list($type, $rule) = explode(':', $rule, 2); + if (isset($this->alias[$type])) { + // 判断别名 + $type = $this->alias[$type]; + } + $info = $type; + } elseif (method_exists($this, $rule)) { + $type = $rule; + $info = $rule; + $rule = ''; + } else { + $type = 'is'; + $info = $rule; + } + + return [$type, $rule, $info]; + } + /** * 验证是否和某个字段的值一致 * @access protected @@ -568,7 +620,7 @@ class Validate break; case 'chsDash': // 只允许汉字、字母、数字和下划线_及破折号- - $result = $this->regex($value, '/^[\x{4e00}-\x{9fa5}a-zA-Z0-9\_\-\.]+$/u'); + $result = $this->regex($value, '/^[\x{4e00}-\x{9fa5}a-zA-Z0-9\_\-]+$/u'); break; case 'activeUrl': // 是否为有效的网址 @@ -632,8 +684,12 @@ class Validate if (function_exists('exif_imagetype')) { return exif_imagetype($image); } else { - $info = getimagesize($image); - return $info[2]; + try { + $info = getimagesize($image); + return $info ? $info[2] : false; + } catch (\Exception $e) { + return false; + } } } @@ -676,21 +732,17 @@ class Validate */ protected function fileExt($file, $rule) { - if (!($file instanceof File)) { - return false; - } - if (is_string($rule)) { - $rule = explode(',', $rule); - } if (is_array($file)) { foreach ($file as $item) { - if (!$item->checkExt($rule)) { + if (!($item instanceof File) || !$item->checkExt($rule)) { return false; } } return true; - } else { + } elseif ($file instanceof File) { return $file->checkExt($rule); + } else { + return false; } } @@ -703,21 +755,17 @@ class Validate */ protected function fileMime($file, $rule) { - if (!($file instanceof File)) { - return false; - } - if (is_string($rule)) { - $rule = explode(',', $rule); - } if (is_array($file)) { foreach ($file as $item) { - if (!$item->checkMime($rule)) { + if (!($item instanceof File) || !$item->checkMime($rule)) { return false; } } return true; - } else { + } elseif ($file instanceof File) { return $file->checkMime($rule); + } else { + return false; } } @@ -730,18 +778,17 @@ class Validate */ protected function fileSize($file, $rule) { - if (!($file instanceof File)) { - return false; - } if (is_array($file)) { foreach ($file as $item) { - if (!$item->checkSize($rule)) { + if (!($item instanceof File) || !$item->checkSize($rule)) { return false; } } return true; - } else { + } elseif ($file instanceof File) { return $file->checkSize($rule); + } else { + return false; } } @@ -833,21 +880,26 @@ class Validate // 支持多个字段验证 $fields = explode('^', $key); foreach ($fields as $key) { - $map[$key] = $data[$key]; + if (isset($data[$key])) { + $map[$key] = $data[$key]; + } } } elseif (strpos($key, '=')) { parse_str($key, $map); - } else { + } elseif (isset($data[$field])) { $map[$key] = $data[$field]; + } else { + $map = []; } - $pk = strval(isset($rule[3]) ? $rule[3] : $db->getPk()); - if (isset($rule[2])) { - $map[$pk] = ['neq', $rule[2]]; - } elseif (isset($data[$pk])) { - $map[$pk] = ['neq', $data[$pk]]; + $pk = isset($rule[3]) ? $rule[3] : $db->getPk(); + if (is_string($pk)) { + if (isset($rule[2])) { + $map[$pk] = ['neq', $rule[2]]; + } elseif (isset($data[$pk])) { + $map[$pk] = ['neq', $data[$pk]]; + } } - if ($db->where($map)->field($pk)->find()) { return false; } @@ -899,7 +951,7 @@ class Validate { list($field, $val) = explode(',', $rule); if ($this->getDataValue($data, $field) == $val) { - return !empty($value); + return !empty($value) || '0' == $value; } else { return true; } @@ -917,7 +969,7 @@ class Validate { $result = call_user_func_array($rule, [$value, $data]); if ($result) { - return !empty($value); + return !empty($value) || '0' == $value; } else { return true; } @@ -935,7 +987,7 @@ class Validate { $val = $this->getDataValue($data, $rule); if (!empty($val)) { - return !empty($value); + return !empty($value) || '0' == $value; } else { return true; } @@ -1067,9 +1119,10 @@ class Validate * @access protected * @param mixed $value 字段值 * @param mixed $rule 验证规则 + * @param array $data 数据 * @return bool */ - protected function after($value, $rule) + protected function after($value, $rule, $data) { return strtotime($value) >= strtotime($rule); } @@ -1079,13 +1132,42 @@ class Validate * @access protected * @param mixed $value 字段值 * @param mixed $rule 验证规则 + * @param array $data 数据 * @return bool */ - protected function before($value, $rule) + protected function before($value, $rule, $data) { return strtotime($value) <= strtotime($rule); } + /** + * 验证日期字段 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + protected function afterWith($value, $rule, $data) + { + $rule = $this->getDataValue($data, $rule); + return !is_null($rule) && strtotime($value) >= strtotime($rule); + } + + /** + * 验证日期字段 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + protected function beforeWith($value, $rule, $data) + { + $rule = $this->getDataValue($data, $rule); + return !is_null($rule) && strtotime($value) <= strtotime($rule); + } + /** * 验证有效期 * @access protected @@ -1149,7 +1231,7 @@ class Validate // 不是正则表达式则两端补上/ $rule = '/^' . $rule . '$/'; } - return 1 === preg_match($rule, (string) $value); + return is_scalar($value) && 1 === preg_match($rule, (string) $value); } /** @@ -1225,12 +1307,16 @@ class Validate $msg = $this->message[$attribute]; } elseif (isset(self::$typeMsg[$type])) { $msg = self::$typeMsg[$type]; + } elseif (0 === strpos($type, 'require')) { + $msg = self::$typeMsg['require']; } else { - $msg = $title . '规则错误'; + $msg = $title . Lang::get('not conform to the rules'); } if (is_string($msg) && 0 === strpos($msg, '{%')) { $msg = Lang::get(substr($msg, 2, -1)); + } elseif (Lang::has($msg)) { + $msg = Lang::get($msg); } if (is_string($msg) && is_scalar($rule) && false !== strpos($msg, ':')) { diff --git a/thinkphp/library/think/View.php b/thinkphp/library/think/View.php index 96ae56c9..ca2dadbb 100644 --- a/thinkphp/library/think/View.php +++ b/thinkphp/library/think/View.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -33,7 +33,7 @@ class View public function __construct($engine = [], $replace = []) { // 初始化模板引擎 - $this->engine((array) $engine); + $this->engine($engine); // 基础替换字符串 $request = Request::instance(); $base = $request->root(); @@ -127,7 +127,7 @@ class View * @access private * @param string|array $name 参数名 * @param mixed $value 参数值 - * @return void + * @return $this */ public function config($name, $value = null) { @@ -155,18 +155,21 @@ class View ob_implicit_flush(0); // 渲染输出 - $method = $renderContent ? 'display' : 'fetch'; - $this->engine->$method($template, $vars, $config); + try { + $method = $renderContent ? 'display' : 'fetch'; + // 允许用户自定义模板的字符串替换 + $replace = array_merge($this->replace, $replace, (array) $this->engine->config('tpl_replace_string')); + $this->engine->config('tpl_replace_string', $replace); + $this->engine->$method($template, $vars, $config); + } catch (\Exception $e) { + ob_end_clean(); + throw $e; + } // 获取并清空缓存 $content = ob_get_clean(); // 内容过滤标签 Hook::listen('view_filter', $content); - // 允许用户自定义模板的字符串替换 - $replace = array_merge($this->replace, $replace); - if (!empty($replace)) { - $content = strtr($content, $replace); - } return $content; } diff --git a/thinkphp/library/think/cache/Driver.php b/thinkphp/library/think/cache/Driver.php index 688507a8..07805e48 100644 --- a/thinkphp/library/think/cache/Driver.php +++ b/thinkphp/library/think/cache/Driver.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -120,10 +120,29 @@ abstract class Driver public function remember($name, $value, $expire = null) { if (!$this->has($name)) { - if ($value instanceof \Closure) { - $value = call_user_func($value); + $time = time(); + while ($time + 5 > time() && $this->has($name . '_lock')) { + // 存在锁定则等待 + usleep(200000); + } + + try { + // 锁定 + $this->set($name . '_lock', true); + if ($value instanceof \Closure) { + $value = call_user_func($value); + } + $this->set($name, $value, $expire); + // 解锁 + $this->rm($name . '_lock'); + } catch (\Exception $e) { + // 解锁 + $this->rm($name . '_lock'); + throw $e; + } catch (\throwable $e) { + $this->rm($name . '_lock'); + throw $e; } - $this->set($name, $value, $expire); } else { $value = $this->get($name); } @@ -140,7 +159,9 @@ abstract class Driver */ public function tag($name, $keys = null, $overlay = false) { - if (is_null($keys)) { + if (is_null($name)) { + + } elseif (is_null($keys)) { $this->tag = $name; } else { $key = 'tag_' . md5($name); @@ -153,7 +174,7 @@ abstract class Driver } else { $value = array_unique(array_merge($this->getTagItem($name), $keys)); } - $this->set($key, implode(',', $value)); + $this->set($key, implode(',', $value), 0); } return $this; } @@ -170,12 +191,13 @@ abstract class Driver $key = 'tag_' . md5($this->tag); $this->tag = null; if ($this->has($key)) { - $value = $this->get($key); - $value .= ',' . $name; + $value = explode(',', $this->get($key)); + $value[] = $name; + $value = implode(',', array_unique($value)); } else { $value = $name; } - $this->set($key, $value); + $this->set($key, $value, 0); } } @@ -190,7 +212,7 @@ abstract class Driver $key = 'tag_' . md5($tag); $value = $this->get($key); if ($value) { - return explode(',', $value); + return array_filter(explode(',', $value)); } else { return []; } diff --git a/thinkphp/library/think/cache/driver/File.php b/thinkphp/library/think/cache/driver/File.php index 5aadb2b1..fee64894 100644 --- a/thinkphp/library/think/cache/driver/File.php +++ b/thinkphp/library/think/cache/driver/File.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -27,6 +27,8 @@ class File extends Driver 'data_compress' => false, ]; + protected $expire; + /** * 构造函数 * @param array $options @@ -61,10 +63,11 @@ class File extends Driver /** * 取得变量的存储文件名 * @access protected - * @param string $name 缓存变量名 + * @param string $name 缓存变量名 + * @param bool $auto 是否自动创建目录 * @return string */ - protected function getCacheKey($name) + protected function getCacheKey($name, $auto = false) { $name = md5($name); if ($this->options['cache_subdir']) { @@ -76,7 +79,8 @@ class File extends Driver } $filename = $this->options['path'] . $name . '.php'; $dir = dirname($filename); - if (!is_dir($dir)) { + + if ($auto && !is_dir($dir)) { mkdir($dir, 0755, true); } return $filename; @@ -106,15 +110,15 @@ class File extends Driver if (!is_file($filename)) { return $default; } - $content = file_get_contents($filename); + $content = file_get_contents($filename); + $this->expire = null; if (false !== $content) { $expire = (int) substr($content, 8, 12); - if (0 != $expire && $_SERVER['REQUEST_TIME'] > filemtime($filename) + $expire) { - //缓存过期删除缓存文件 - $this->unlink($filename); + if (0 != $expire && time() > filemtime($filename) + $expire) { return $default; } - $content = substr($content, 20, -3); + $this->expire = $expire; + $content = substr($content, 32); if ($this->options['data_compress'] && function_exists('gzcompress')) { //启用数据压缩 $content = gzuncompress($content); @@ -129,9 +133,9 @@ class File extends Driver /** * 写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param int $expire 有效时间 0为永久 + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer|\DateTime $expire 有效时间(秒) * @return boolean */ public function set($name, $value, $expire = null) @@ -139,7 +143,10 @@ class File extends Driver if (is_null($expire)) { $expire = $this->options['expire']; } - $filename = $this->getCacheKey($name); + if ($expire instanceof \DateTime) { + $expire = $expire->getTimestamp() - time(); + } + $filename = $this->getCacheKey($name, true); if ($this->tag && !is_file($filename)) { $first = true; } @@ -148,7 +155,7 @@ class File extends Driver //数据压缩 $data = gzcompress($data, 3); } - $data = ""; + $data = "\n" . $data; $result = file_put_contents($filename, $data); if ($result) { isset($first) && $this->setTagItem($filename); @@ -169,11 +176,14 @@ class File extends Driver public function inc($name, $step = 1) { if ($this->has($name)) { - $value = $this->get($name) + $step; + $value = $this->get($name) + $step; + $expire = $this->expire; } else { - $value = $step; + $value = $step; + $expire = 0; } - return $this->set($name, $value, 0) ? $value : false; + + return $this->set($name, $value, $expire) ? $value : false; } /** @@ -186,11 +196,14 @@ class File extends Driver public function dec($name, $step = 1) { if ($this->has($name)) { - $value = $this->get($name) - $step; + $value = $this->get($name) - $step; + $expire = $this->expire; } else { - $value = $step; + $value = -$step; + $expire = 0; } - return $this->set($name, $value, 0) ? $value : false; + + return $this->set($name, $value, $expire) ? $value : false; } /** @@ -201,7 +214,11 @@ class File extends Driver */ public function rm($name) { - return $this->unlink($this->getCacheKey($name)); + $filename = $this->getCacheKey($name); + try { + return $this->unlink($filename); + } catch (\Exception $e) { + } } /** @@ -224,7 +241,10 @@ class File extends Driver $files = (array) glob($this->options['path'] . ($this->options['prefix'] ? $this->options['prefix'] . DS : '') . '*'); foreach ($files as $path) { if (is_dir($path)) { - array_map('unlink', glob($path . '/*.php')); + $matches = glob($path . '/*.php'); + if (is_array($matches)) { + array_map('unlink', $matches); + } rmdir($path); } else { unlink($path); diff --git a/thinkphp/library/think/cache/driver/Lite.php b/thinkphp/library/think/cache/driver/Lite.php index eeb96a19..8cbf08f9 100644 --- a/thinkphp/library/think/cache/driver/Lite.php +++ b/thinkphp/library/think/cache/driver/Lite.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -77,7 +77,7 @@ class Lite extends Driver if (is_file($filename)) { // 判断是否过期 $mtime = filemtime($filename); - if ($mtime < $_SERVER['REQUEST_TIME']) { + if ($mtime < time()) { // 清除已经过期的文件 unlink($filename); return $default; @@ -91,9 +91,9 @@ class Lite extends Driver /** * 写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param int $expire 有效时间 0为永久 + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer|\DateTime $expire 有效时间(秒) * @return bool */ public function set($name, $value, $expire = null) @@ -101,9 +101,11 @@ class Lite extends Driver if (is_null($expire)) { $expire = $this->options['expire']; } - // 模拟永久 - if (0 === $expire) { - $expire = 10 * 365 * 24 * 3600; + if ($expire instanceof \DateTime) { + $expire = $expire->getTimestamp(); + } else { + $expire = 0 === $expire ? 10 * 365 * 24 * 3600 : $expire; + $expire = time() + $expire; } $filename = $this->getCacheKey($name); if ($this->tag && !is_file($filename)) { @@ -113,7 +115,7 @@ class Lite extends Driver // 通过设置修改时间实现有效期 if ($ret) { isset($first) && $this->setTagItem($filename); - touch($filename, $_SERVER['REQUEST_TIME'] + $expire); + touch($filename, $expire); } return $ret; } @@ -147,7 +149,7 @@ class Lite extends Driver if ($this->has($name)) { $value = $this->get($name) - $step; } else { - $value = $step; + $value = -$step; } return $this->set($name, $value, 0) ? $value : false; } diff --git a/thinkphp/library/think/cache/driver/Memcache.php b/thinkphp/library/think/cache/driver/Memcache.php index e464f38c..58703ea3 100644 --- a/thinkphp/library/think/cache/driver/Memcache.php +++ b/thinkphp/library/think/cache/driver/Memcache.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -63,7 +63,7 @@ class Memcache extends Driver public function has($name) { $key = $this->getCacheKey($name); - return $this->handler->get($key) ? true : false; + return false !== $this->handler->get($key); } /** @@ -82,9 +82,9 @@ class Memcache extends Driver /** * 写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param integer $expire 有效时间(秒) + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer|\DateTime $expire 有效时间(秒) * @return bool */ public function set($name, $value, $expire = null) @@ -92,6 +92,9 @@ class Memcache extends Driver if (is_null($expire)) { $expire = $this->options['expire']; } + if ($expire instanceof \DateTime) { + $expire = $expire->getTimestamp() - time(); + } if ($this->tag && !$this->has($name)) { $first = true; } diff --git a/thinkphp/library/think/cache/driver/Memcached.php b/thinkphp/library/think/cache/driver/Memcached.php index 4a9d898c..5aab5a30 100644 --- a/thinkphp/library/think/cache/driver/Memcached.php +++ b/thinkphp/library/think/cache/driver/Memcached.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -93,9 +93,9 @@ class Memcached extends Driver /** * 写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param integer $expire 有效时间(秒) + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer|\DateTime $expire 有效时间(秒) * @return bool */ public function set($name, $value, $expire = null) @@ -103,6 +103,9 @@ class Memcached extends Driver if (is_null($expire)) { $expire = $this->options['expire']; } + if ($expire instanceof \DateTime) { + $expire = $expire->getTimestamp() - time(); + } if ($this->tag && !$this->has($name)) { $first = true; } diff --git a/thinkphp/library/think/cache/driver/Redis.php b/thinkphp/library/think/cache/driver/Redis.php index 4f618785..027b3ea2 100644 --- a/thinkphp/library/think/cache/driver/Redis.php +++ b/thinkphp/library/think/cache/driver/Redis.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -46,9 +46,12 @@ class Redis extends Driver if (!empty($options)) { $this->options = array_merge($this->options, $options); } - $func = $this->options['persistent'] ? 'pconnect' : 'connect'; $this->handler = new \Redis; - $this->handler->$func($this->options['host'], $this->options['port'], $this->options['timeout']); + if ($this->options['persistent']) { + $this->handler->pconnect($this->options['host'], $this->options['port'], $this->options['timeout'], 'persistent_id_' . $this->options['select']); + } else { + $this->handler->connect($this->options['host'], $this->options['port'], $this->options['timeout']); + } if ('' != $this->options['password']) { $this->handler->auth($this->options['password']); @@ -67,7 +70,7 @@ class Redis extends Driver */ public function has($name) { - return $this->handler->get($this->getCacheKey($name)) ? true : false; + return $this->handler->exists($this->getCacheKey($name)); } /** @@ -80,20 +83,25 @@ class Redis extends Driver public function get($name, $default = false) { $value = $this->handler->get($this->getCacheKey($name)); - if (is_null($value)) { + if (is_null($value) || false === $value) { return $default; } - $jsonData = json_decode($value, true); - // 检测是否为JSON数据 true 返回JSON解析数组, false返回源数据 byron sampson - return (null === $jsonData) ? $value : $jsonData; + + try { + $result = 0 === strpos($value, 'think_serialize:') ? unserialize(substr($value, 16)) : $value; + } catch (\Exception $e) { + $result = $default; + } + + return $result; } /** * 写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param integer $expire 有效时间(秒) + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer|\DateTime $expire 有效时间(秒) * @return boolean */ public function set($name, $value, $expire = null) @@ -101,13 +109,15 @@ class Redis extends Driver if (is_null($expire)) { $expire = $this->options['expire']; } + if ($expire instanceof \DateTime) { + $expire = $expire->getTimestamp() - time(); + } if ($this->tag && !$this->has($name)) { $first = true; } - $key = $this->getCacheKey($name); - //对数组/对象数据进行缓存处理,保证数据完整性 byron sampson - $value = (is_object($value) || is_array($value)) ? json_encode($value) : $value; - if (is_int($expire) && $expire) { + $key = $this->getCacheKey($name); + $value = is_scalar($value) ? $value : 'think_serialize:' . serialize($value); + if ($expire) { $result = $this->handler->setex($key, $expire, $value); } else { $result = $this->handler->set($key, $value); @@ -119,26 +129,28 @@ class Redis extends Driver /** * 自增缓存(针对数值缓存) * @access public - * @param string $name 缓存变量名 - * @param int $step 步长 + * @param string $name 缓存变量名 + * @param int $step 步长 * @return false|int */ public function inc($name, $step = 1) { $key = $this->getCacheKey($name); + return $this->handler->incrby($key, $step); } /** * 自减缓存(针对数值缓存) * @access public - * @param string $name 缓存变量名 - * @param int $step 步长 + * @param string $name 缓存变量名 + * @param int $step 步长 * @return false|int */ public function dec($name, $step = 1) { $key = $this->getCacheKey($name); + return $this->handler->decrby($key, $step); } diff --git a/thinkphp/library/think/cache/driver/Sqlite.php b/thinkphp/library/think/cache/driver/Sqlite.php index 305fd9e8..dc2ee055 100644 --- a/thinkphp/library/think/cache/driver/Sqlite.php +++ b/thinkphp/library/think/cache/driver/Sqlite.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -96,9 +96,9 @@ class Sqlite extends Driver /** * 写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param integer $expire 有效时间(秒) + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer|\DateTime $expire 有效时间(秒) * @return boolean */ public function set($name, $value, $expire = null) @@ -108,7 +108,11 @@ class Sqlite extends Driver if (is_null($expire)) { $expire = $this->options['expire']; } - $expire = (0 == $expire) ? 0 : ($_SERVER['REQUEST_TIME'] + $expire); //缓存有效期为0表示永久缓存 + if ($expire instanceof \DateTime) { + $expire = $expire->getTimestamp(); + } else { + $expire = (0 == $expire) ? 0 : (time() + $expire); //缓存有效期为0表示永久缓存 + } if (function_exists('gzcompress')) { //数据压缩 $value = gzcompress($value, 3); @@ -155,7 +159,7 @@ class Sqlite extends Driver if ($this->has($name)) { $value = $this->get($name) - $step; } else { - $value = $step; + $value = -$step; } return $this->set($name, $value, 0) ? $value : false; } diff --git a/thinkphp/library/think/cache/driver/Wincache.php b/thinkphp/library/think/cache/driver/Wincache.php index 3076fc14..03f8d357 100644 --- a/thinkphp/library/think/cache/driver/Wincache.php +++ b/thinkphp/library/think/cache/driver/Wincache.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -68,9 +68,9 @@ class Wincache extends Driver /** * 写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param integer $expire 有效时间(秒) + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer|\DateTime $expire 有效时间(秒) * @return boolean */ public function set($name, $value, $expire = null) @@ -78,6 +78,9 @@ class Wincache extends Driver if (is_null($expire)) { $expire = $this->options['expire']; } + if ($expire instanceof \DateTime) { + $expire = $expire->getTimestamp() - time(); + } $key = $this->getCacheKey($name); if ($this->tag && !$this->has($name)) { $first = true; diff --git a/thinkphp/library/think/cache/driver/Xcache.php b/thinkphp/library/think/cache/driver/Xcache.php index 2a0e07ad..4d94c033 100644 --- a/thinkphp/library/think/cache/driver/Xcache.php +++ b/thinkphp/library/think/cache/driver/Xcache.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -68,9 +68,9 @@ class Xcache extends Driver /** * 写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param integer $expire 有效时间(秒) + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer|\DateTime $expire 有效时间(秒) * @return boolean */ public function set($name, $value, $expire = null) @@ -78,6 +78,9 @@ class Xcache extends Driver if (is_null($expire)) { $expire = $this->options['expire']; } + if ($expire instanceof \DateTime) { + $expire = $expire->getTimestamp() - time(); + } if ($this->tag && !$this->has($name)) { $first = true; } diff --git a/thinkphp/library/think/config/driver/Ini.php b/thinkphp/library/think/config/driver/Ini.php index a223a578..bcd12b69 100644 --- a/thinkphp/library/think/config/driver/Ini.php +++ b/thinkphp/library/think/config/driver/Ini.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- diff --git a/thinkphp/library/think/config/driver/Json.php b/thinkphp/library/think/config/driver/Json.php index 557f75fe..479dcc89 100644 --- a/thinkphp/library/think/config/driver/Json.php +++ b/thinkphp/library/think/config/driver/Json.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- diff --git a/thinkphp/library/think/config/driver/Xml.php b/thinkphp/library/think/config/driver/Xml.php index b573a562..1158519f 100644 --- a/thinkphp/library/think/config/driver/Xml.php +++ b/thinkphp/library/think/config/driver/Xml.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- diff --git a/thinkphp/library/think/console/command/Clear.php b/thinkphp/library/think/console/command/Clear.php index 41019cea..1b5102ec 100644 --- a/thinkphp/library/think/console/command/Clear.php +++ b/thinkphp/library/think/console/command/Clear.php @@ -10,8 +10,10 @@ // +---------------------------------------------------------------------- namespace think\console\command; +use think\Cache; use think\console\Command; use think\console\Input; +use think\console\input\Argument; use think\console\input\Option; use think\console\Output; @@ -22,6 +24,7 @@ class Clear extends Command // 指令配置 $this ->setName('clear') + ->addArgument('type', Argument::OPTIONAL, 'type to clear', null) ->addOption('path', 'd', Option::VALUE_OPTIONAL, 'path to clear', null) ->setDescription('Clear runtime file'); } @@ -30,8 +33,14 @@ class Clear extends Command { $path = $input->getOption('path') ?: RUNTIME_PATH; - if (is_dir($path)) { - $this->clearPath($path); + $type = $input->getArgument('type'); + + if ($type == 'route') { + Cache::clear('route_check'); + } else { + if (is_dir($path)) { + $this->clearPath($path); + } } $output->writeln("Clear Successed"); diff --git a/thinkphp/library/think/console/command/optimize/Config.php b/thinkphp/library/think/console/command/optimize/Config.php index cadfe5ee..59c69a8e 100644 --- a/thinkphp/library/think/console/command/optimize/Config.php +++ b/thinkphp/library/think/console/command/optimize/Config.php @@ -30,7 +30,7 @@ class Config extends Command protected function execute(Input $input, Output $output) { - if ($input->hasArgument('module')) { + if ($input->getArgument('module')) { $module = $input->getArgument('module') . DS; } else { $module = ''; diff --git a/thinkphp/library/think/console/command/optimize/Route.php b/thinkphp/library/think/console/command/optimize/Route.php index 911e4c14..6da1d9a6 100644 --- a/thinkphp/library/think/console/command/optimize/Route.php +++ b/thinkphp/library/think/console/command/optimize/Route.php @@ -27,6 +27,11 @@ class Route extends Command protected function execute(Input $input, Output $output) { + + if (!is_dir(RUNTIME_PATH)) { + @mkdir(RUNTIME_PATH, 0755, true); + } + file_put_contents(RUNTIME_PATH . 'route.php', $this->buildRouteCache()); $output->writeln('Succeed!'); } diff --git a/thinkphp/library/think/console/command/optimize/Schema.php b/thinkphp/library/think/console/command/optimize/Schema.php index 27eb9dbb..33534240 100644 --- a/thinkphp/library/think/console/command/optimize/Schema.php +++ b/thinkphp/library/think/console/command/optimize/Schema.php @@ -44,7 +44,8 @@ class Schema extends Command if ($input->hasOption('module')) { $module = $input->getOption('module'); // 读取模型 - $list = scandir(APP_PATH . $module . DS . 'model'); + $path = APP_PATH . $module . DS . 'model'; + $list = is_dir($path) ? scandir($path) : []; $app = App::$namespace; foreach ($list as $file) { if (0 === strpos($file, '.')) { @@ -66,7 +67,8 @@ class Schema extends Command $tables = Db::connect($config)->getTables($dbName); } elseif (!\think\Config::get('app_multi_module')) { $app = App::$namespace; - $list = scandir(APP_PATH . 'model'); + $path = APP_PATH . 'model'; + $list = is_dir($path) ? scandir($path) : []; foreach ($list as $file) { if (0 === strpos($file, '.')) { continue; diff --git a/thinkphp/library/think/console/output/Descriptor.php b/thinkphp/library/think/console/output/Descriptor.php index 6d98d53c..23dc6481 100644 --- a/thinkphp/library/think/console/output/Descriptor.php +++ b/thinkphp/library/think/console/output/Descriptor.php @@ -69,7 +69,7 @@ class Descriptor * 描述参数 * @param InputArgument $argument * @param array $options - * @return string|mixed + * @return void */ protected function describeInputArgument(InputArgument $argument, array $options = []) { @@ -93,7 +93,7 @@ class Descriptor * 描述选项 * @param InputOption $option * @param array $options - * @return string|mixed + * @return void */ protected function describeInputOption(InputOption $option, array $options = []) { @@ -128,7 +128,7 @@ class Descriptor * 描述输入 * @param InputDefinition $definition * @param array $options - * @return string|mixed + * @return void */ protected function describeInputDefinition(InputDefinition $definition, array $options = []) { @@ -173,7 +173,7 @@ class Descriptor * 描述指令 * @param Command $command * @param array $options - * @return string|mixed + * @return void */ protected function describeCommand(Command $command, array $options = []) { @@ -208,7 +208,7 @@ class Descriptor * 描述控制台 * @param Console $console * @param array $options - * @return string|mixed + * @return void */ protected function describeConsole(Console $console, array $options = []) { diff --git a/thinkphp/library/think/controller/Rest.php b/thinkphp/library/think/controller/Rest.php index 8c5911df..43ab2f62 100644 --- a/thinkphp/library/think/controller/Rest.php +++ b/thinkphp/library/think/controller/Rest.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -43,7 +43,7 @@ abstract class Rest if ('' == $ext) { // 自动检测资源类型 $this->type = $request->type(); - } elseif (!preg_match('/\(' . $this->restTypeList . '\)$/i', $ext)) { + } elseif (!preg_match('/(' . $this->restTypeList . ')$/i', $ext)) { // 资源类型非法 则用默认资源类型访问 $this->type = $this->restDefaultType; } else { @@ -51,7 +51,7 @@ abstract class Rest } // 请求方式检测 $method = strtolower($request->method()); - if (false === stripos($this->restMethodList, $method)) { + if (!preg_match('/(' . $this->restMethodList . ')$/i', $method)) { // 请求方式非法 则用默认请求方法 $method = $this->restDefaultMethod; } diff --git a/thinkphp/library/think/db/Builder.php b/thinkphp/library/think/db/Builder.php index 23f88836..58b45aa8 100644 --- a/thinkphp/library/think/db/Builder.php +++ b/thinkphp/library/think/db/Builder.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -22,10 +22,10 @@ abstract class Builder protected $query; // 数据库表达式 - protected $exp = ['eq' => '=', 'neq' => '<>', 'gt' => '>', 'egt' => '>=', 'lt' => '<', 'elt' => '<=', 'notlike' => 'NOT LIKE', 'like' => 'LIKE', 'in' => 'IN', 'exp' => 'EXP', 'notin' => 'NOT IN', 'not in' => 'NOT IN', 'between' => 'BETWEEN', 'not between' => 'NOT BETWEEN', 'notbetween' => 'NOT BETWEEN', 'exists' => 'EXISTS', 'notexists' => 'NOT EXISTS', 'not exists' => 'NOT EXISTS', 'null' => 'NULL', 'notnull' => 'NOT NULL', 'not null' => 'NOT NULL', '> time' => '> TIME', '< time' => '< TIME', '>= time' => '>= TIME', '<= time' => '<= TIME', 'between time' => 'BETWEEN TIME', 'not between time' => 'NOT BETWEEN TIME', 'notbetween time' => 'NOT BETWEEN TIME']; + protected $exp = ['eq' => '=', 'neq' => '<>', 'gt' => '>', 'egt' => '>=', 'lt' => '<', 'elt' => '<=', 'notlike' => 'NOT LIKE', 'not like' => 'NOT LIKE', 'like' => 'LIKE', 'in' => 'IN', 'exp' => 'EXP', 'notin' => 'NOT IN', 'not in' => 'NOT IN', 'between' => 'BETWEEN', 'not between' => 'NOT BETWEEN', 'notbetween' => 'NOT BETWEEN', 'exists' => 'EXISTS', 'notexists' => 'NOT EXISTS', 'not exists' => 'NOT EXISTS', 'null' => 'NULL', 'notnull' => 'NOT NULL', 'not null' => 'NOT NULL', '> time' => '> TIME', '< time' => '< TIME', '>= time' => '>= TIME', '<= time' => '<= TIME', 'between time' => 'BETWEEN TIME', 'not between time' => 'NOT BETWEEN TIME', 'notbetween time' => 'NOT BETWEEN TIME']; // SQL表达式 - protected $selectSql = 'SELECT%DISTINCT% %FIELD% FROM %TABLE%%FORCE%%JOIN%%WHERE%%GROUP%%HAVING%%ORDER%%LIMIT% %UNION%%LOCK%%COMMENT%'; + protected $selectSql = 'SELECT%DISTINCT% %FIELD% FROM %TABLE%%FORCE%%JOIN%%WHERE%%GROUP%%HAVING%%UNION%%ORDER%%LIMIT%%LOCK%%COMMENT%'; protected $insertSql = '%INSERT% INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%'; protected $insertAllSql = '%INSERT% INTO %TABLE% (%FIELD%) %DATA% %COMMENT%'; protected $updateSql = 'UPDATE %TABLE% SET %SET% %JOIN% %WHERE% %ORDER%%LIMIT% %LOCK%%COMMENT%'; @@ -46,7 +46,7 @@ abstract class Builder /** * 获取当前的连接对象实例 * @access public - * @return void + * @return Connection */ public function getConnection() { @@ -56,7 +56,7 @@ abstract class Builder /** * 获取当前的Query对象实例 * @access public - * @return void + * @return Query */ public function getQuery() { @@ -80,6 +80,7 @@ abstract class Builder * @param array $data 数据 * @param array $options 查询参数 * @return array + * @throws Exception */ protected function parseData($data, $options) { @@ -97,8 +98,15 @@ abstract class Builder $result = []; foreach ($data as $key => $val) { - $item = $this->parseKey($key, $options); - if (is_object($val) && method_exists($val, '__toString')) { + if ('*' != $options['field'] && !in_array($key, $fields, true)) { + continue; + } + + $item = $this->parseKey($key, $options, true); + if ($val instanceof Expression) { + $result[$item] = $val->getValue(); + continue; + } elseif (is_object($val) && method_exists($val, '__toString')) { // 对象数据写入 $val = $val->__toString(); } @@ -108,16 +116,25 @@ abstract class Builder } } elseif (is_null($val)) { $result[$item] = 'NULL'; - } elseif (isset($val[0]) && 'exp' == $val[0]) { - $result[$item] = $val[1]; + } elseif (is_array($val) && !empty($val)) { + switch (strtolower($val[0])) { + case 'inc': + $result[$item] = $item . '+' . floatval($val[1]); + break; + case 'dec': + $result[$item] = $item . '-' . floatval($val[1]); + break; + case 'exp': + throw new Exception('not support data:[' . $val[0] . ']'); + } } elseif (is_scalar($val)) { // 过滤非标量数据 if (0 === strpos($val, ':') && $this->query->isBind(substr($val, 1))) { $result[$item] = $val; } else { $key = str_replace('.', '_', $key); - $this->query->bind('__data__' . $key, $val, isset($bind[$key]) ? $bind[$key] : PDO::PARAM_STR); - $result[$item] = ':__data__' . $key; + $this->query->bind('data__' . $key, $val, isset($bind[$key]) ? $bind[$key] : PDO::PARAM_STR); + $result[$item] = ':data__' . $key; } } } @@ -131,7 +148,7 @@ abstract class Builder * @param array $options * @return string */ - protected function parseKey($key, $options = []) + protected function parseKey($key, $options = [], $strict = false) { return $key; } @@ -172,8 +189,10 @@ abstract class Builder // 支持 'field1'=>'field2' 这样的字段别名定义 $array = []; foreach ($fields as $key => $field) { - if (!is_numeric($key)) { - $array[] = $this->parseKey($key, $options) . ' AS ' . $this->parseKey($field, $options); + if ($field instanceof Expression) { + $array[] = $field->getValue(); + } elseif (!is_numeric($key)) { + $array[] = $this->parseKey($key, $options) . ' AS ' . $this->parseKey($field, $options, true); } else { $array[] = $this->parseKey($field, $options); } @@ -195,11 +214,8 @@ abstract class Builder $item = []; foreach ((array) $tables as $key => $table) { if (!is_numeric($key)) { - if (strpos($key, '@think')) { - $key = strstr($key, '@think', true); - } $key = $this->parseSqlTable($key); - $item[] = $this->parseKey($key) . ' ' . $this->parseKey($table); + $item[] = $this->parseKey($key) . ' ' . (isset($options['alias'][$table]) ? $this->parseKey($options['alias'][$table]) : $this->parseKey($table)); } else { $table = $this->parseSqlTable($table); if (isset($options['alias'][$table])) { @@ -255,7 +271,9 @@ abstract class Builder foreach ($where as $key => $val) { $str = []; foreach ($val as $field => $value) { - if ($value instanceof \Closure) { + if ($value instanceof Expression) { + $str[] = ' ' . $key . ' ( ' . $value->getValue() . ' )'; + } elseif ($value instanceof \Closure) { // 使用闭包查询 $query = new Query($this->connection); call_user_func_array($value, [ & $query]); @@ -296,11 +314,11 @@ abstract class Builder protected function parseWhereItem($field, $val, $rule = '', $options = [], $binds = [], $bindName = null) { // 字段分析 - $key = $field ? $this->parseKey($field, $options) : ''; + $key = $field ? $this->parseKey($field, $options, true) : ''; // 查询规则和条件 if (!is_array($val)) { - $val = ['=', $val]; + $val = is_null($val) ? ['null', ''] : ['=', $val]; } list($exp, $value) = $val; @@ -329,12 +347,19 @@ abstract class Builder throw new Exception('where express error:' . $exp); } } - $bindName = $bindName ?: 'where_' . str_replace(['.', '-'], '_', $field); + $bindName = $bindName ?: 'where_' . $rule . '_' . str_replace(['.', '-'], '_', $field); if (preg_match('/\W/', $bindName)) { // 处理带非单词字符的字段名 $bindName = md5($bindName); } + if ($value instanceof Expression) { + + } elseif (is_object($value) && method_exists($value, '__toString')) { + // 对象数据写入 + $value = $value->__toString(); + } + $bindType = isset($binds[$field]) ? $binds[$field] : PDO::PARAM_STR; if (is_scalar($value) && array_key_exists($field, $binds) && !in_array($exp, ['EXP', 'NOT NULL', 'NULL', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN']) && strpos($exp, 'TIME') === false) { if (strpos($value, ':') !== 0 || !$this->query->isBind(substr($value, 1))) { @@ -367,7 +392,11 @@ abstract class Builder } } elseif ('EXP' == $exp) { // 表达式查询 - $whereStr .= '( ' . $key . ' ' . $value . ' )'; + if ($value instanceof Expression) { + $whereStr .= '( ' . $key . ' ' . $value->getValue() . ' )'; + } else { + throw new Exception('where express error:' . $exp); + } } elseif (in_array($exp, ['NOT NULL', 'NULL'])) { // NULL 查询 $whereStr .= $key . ' IS ' . $exp; @@ -380,11 +409,13 @@ abstract class Builder if (array_key_exists($field, $binds)) { $bind = []; $array = []; - foreach ($value as $k => $v) { - if ($this->query->isBind($bindName . '_in_' . $k)) { - $bindKey = $bindName . '_in_' . uniqid() . '_' . $k; + $i = 0; + foreach ($value as $v) { + $i++; + if ($this->query->isBind($bindName . '_in_' . $i)) { + $bindKey = $bindName . '_in_' . uniqid() . '_' . $i; } else { - $bindKey = $bindName . '_in_' . $k; + $bindKey = $bindName . '_in_' . $i; } $bind[$bindKey] = [$v, $bindType]; $array[] = ':' . $bindKey; @@ -483,6 +514,11 @@ abstract class Builder } } $bindName = $bindName ?: $key; + + if ($this->query->isBind($bindName)) { + $bindName .= '_' . str_replace('.', '_', uniqid('', true)); + } + $this->query->bind($bindName, $value, $bindType); return ':' . $bindName; } @@ -490,7 +526,7 @@ abstract class Builder /** * limit分析 * @access protected - * @param mixed $lmit + * @param mixed $limit * @return string */ protected function parseLimit($limit) @@ -513,7 +549,9 @@ abstract class Builder list($table, $type, $on) = $item; $condition = []; foreach ((array) $on as $val) { - if (strpos($val, '=')) { + if ($val instanceof Expression) { + $condition[] = $val->getValue(); + } elseif (strpos($val, '=')) { list($val1, $val2) = explode('=', $val, 2); $condition[] = $this->parseKey($val1, $options) . '=' . $this->parseKey($val2, $options); } else { @@ -537,24 +575,29 @@ abstract class Builder */ protected function parseOrder($order, $options = []) { - if (is_array($order)) { - $array = []; - foreach ($order as $key => $val) { + if (empty($order)) { + return ''; + } + + $array = []; + foreach ($order as $key => $val) { + if ($val instanceof Expression) { + $array[] = $val->getValue(); + } elseif ('[rand]' == $val) { + $array[] = $this->parseRand(); + } else { if (is_numeric($key)) { - if ('[rand]' == $val) { - $array[] = $this->parseRand(); - } elseif (false === strpos($val, '(')) { - $array[] = $this->parseKey($val, $options); - } else { - $array[] = $val; - } + list($key, $sort) = explode(' ', strpos($val, ' ') ? $val : $val . ' '); } else { - $sort = in_array(strtolower(trim($val)), ['asc', 'desc']) ? ' ' . $val : ''; - $array[] = $this->parseKey($key, $options) . ' ' . $sort; + $sort = $val; } + $sort = strtoupper($sort); + $sort = in_array($sort, ['ASC', 'DESC'], true) ? ' ' . $sort : ''; + $array[] = $this->parseKey($key, $options, true) . $sort; } - $order = implode(',', $array); } + $order = implode(',', $array); + return !empty($order) ? ' ORDER BY ' . $order : ''; } @@ -566,7 +609,7 @@ abstract class Builder */ protected function parseGroup($group) { - return !empty($group) ? ' GROUP BY ' . $group : ''; + return !empty($group) ? ' GROUP BY ' . $this->parseKey($group) : ''; } /** @@ -588,6 +631,9 @@ abstract class Builder */ protected function parseComment($comment) { + if (false !== strpos($comment, '*/')) { + $comment = strstr($comment, '*/', true); + } return !empty($comment) ? ' /* ' . $comment . ' */' : ''; } @@ -617,12 +663,12 @@ abstract class Builder unset($union['type']); foreach ($union as $u) { if ($u instanceof \Closure) { - $sql[] = $type . ' ' . $this->parseClosure($u, false); + $sql[] = $type . ' ' . $this->parseClosure($u); } elseif (is_string($u)) { - $sql[] = $type . ' ' . $this->parseSqlTable($u); + $sql[] = $type . ' ( ' . $this->parseSqlTable($u) . ' )'; } } - return implode(' ', $sql); + return ' ' . implode(' ', $sql); } /** @@ -637,22 +683,22 @@ abstract class Builder return ''; } - if (is_array($index)) { - $index = join(",", $index); - } - - return sprintf(" FORCE INDEX ( %s ) ", $index); + return sprintf(" FORCE INDEX ( %s ) ", is_array($index) ? implode(',', $index) : $index); } /** * 设置锁机制 * @access protected - * @param bool $locl + * @param bool|string $lock * @return string */ protected function parseLock($lock = false) { - return $lock ? ' FOR UPDATE ' : ''; + if (is_bool($lock)) { + return $lock ? ' FOR UPDATE ' : ''; + } elseif (is_string($lock)) { + return ' ' . trim($lock) . ' '; + } } /** @@ -721,8 +767,9 @@ abstract class Builder * @param array $options 表达式 * @param bool $replace 是否replace * @return string + * @throws Exception */ - public function insertAll($dataSet, $options, $replace = false) + public function insertAll($dataSet, $options = [], $replace = false) { // 获取合法的字段 if ('*' == $options['field']) { @@ -731,7 +778,7 @@ abstract class Builder $fields = $options['field']; } - foreach ($dataSet as &$data) { + foreach ($dataSet as $data) { foreach ($data as $key => $val) { if (!in_array($key, $fields, true)) { if ($options['strict']) { @@ -752,23 +799,29 @@ abstract class Builder } $value = array_values($data); $values[] = 'SELECT ' . implode(',', $value); + + if (!isset($insertFields)) { + $insertFields = array_keys($data); + } } - $fields = array_map([$this, 'parseKey'], array_keys(reset($dataSet))); - $sql = str_replace( + + foreach ($insertFields as $field) { + $fields[] = $this->parseKey($field, $options, true); + } + + return str_replace( ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'], [ $replace ? 'REPLACE' : 'INSERT', $this->parseTable($options['table'], $options), - implode(' , ', $fields), + implode(' , ', $insertFields), implode(' UNION ALL ', $values), $this->parseComment($options['comment']), ], $this->insertAllSql); - - return $sql; } /** - * 生成slectinsert SQL + * 生成select insert SQL * @access public * @param array $fields 数据 * @param string $table 数据表 @@ -789,7 +842,7 @@ abstract class Builder /** * 生成update SQL * @access public - * @param array $fields 数据 + * @param array $data 数据 * @param array $options 表达式 * @return string */ diff --git a/thinkphp/library/think/db/Connection.php b/thinkphp/library/think/db/Connection.php index fa773e51..578cc8f9 100644 --- a/thinkphp/library/think/db/Connection.php +++ b/thinkphp/library/think/db/Connection.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -90,6 +90,8 @@ abstract class Connection 'master_num' => 1, // 指定从服务器序号 'slave_no' => '', + // 模型写入后自动读取主服务器 + 'read_master' => false, // 是否严格检查字段是否存在 'fields_strict' => true, // 数据返回类型 @@ -338,8 +340,8 @@ abstract class Connection * @param bool $master 是否在主服务器读操作 * @param bool $pdo 是否返回PDO对象 * @return mixed - * @throws BindParamException * @throws PDOException + * @throws \Exception */ public function query($sql, $bind = [], $master = false, $pdo = false) { @@ -354,19 +356,14 @@ abstract class Connection $this->bind = $bind; } - // 释放前次的查询结果 - if (!empty($this->PDOStatement)) { - $this->free(); - } - Db::$queryTimes++; try { // 调试开始 $this->debug(true); + // 预处理 - if (empty($this->PDOStatement)) { - $this->PDOStatement = $this->linkID->prepare($sql); - } + $this->PDOStatement = $this->linkID->prepare($sql); + // 是否为存储过程调用 $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']); // 参数绑定 @@ -378,7 +375,7 @@ abstract class Connection // 执行查询 $this->PDOStatement->execute(); // 调试结束 - $this->debug(false); + $this->debug(false, '', $master); // 返回结果集 return $this->getResult($pdo, $procedure); } catch (\PDOException $e) { @@ -386,7 +383,12 @@ abstract class Connection return $this->close()->query($sql, $bind, $master, $pdo); } throw new PDOException($e, $this->config, $this->getLastsql()); - } catch (\ErrorException $e) { + } catch (\Throwable $e) { + if ($this->isBreak($e)) { + return $this->close()->query($sql, $bind, $master, $pdo); + } + throw $e; + } catch (\Exception $e) { if ($this->isBreak($e)) { return $this->close()->query($sql, $bind, $master, $pdo); } @@ -397,13 +399,14 @@ abstract class Connection /** * 执行语句 * @access public - * @param string $sql sql指令 - * @param array $bind 参数绑定 + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @param Query $query 查询对象 * @return int - * @throws BindParamException * @throws PDOException + * @throws \Exception */ - public function execute($sql, $bind = []) + public function execute($sql, $bind = [], Query $query = null) { $this->initConnect(true); if (!$this->linkID) { @@ -416,19 +419,14 @@ abstract class Connection $this->bind = $bind; } - //释放前次的查询结果 - if (!empty($this->PDOStatement) && $this->PDOStatement->queryString != $sql) { - $this->free(); - } - Db::$executeTimes++; try { // 调试开始 $this->debug(true); + // 预处理 - if (empty($this->PDOStatement)) { - $this->PDOStatement = $this->linkID->prepare($sql); - } + $this->PDOStatement = $this->linkID->prepare($sql); + // 是否为存储过程调用 $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']); // 参数绑定 @@ -440,18 +438,27 @@ abstract class Connection // 执行语句 $this->PDOStatement->execute(); // 调试结束 - $this->debug(false); + $this->debug(false, '', true); + + if ($query && !empty($this->config['deploy']) && !empty($this->config['read_master'])) { + $query->readMaster(); + } $this->numRows = $this->PDOStatement->rowCount(); return $this->numRows; } catch (\PDOException $e) { if ($this->isBreak($e)) { - return $this->close()->execute($sql, $bind); + return $this->close()->execute($sql, $bind, $query); } throw new PDOException($e, $this->config, $this->getLastsql()); - } catch (\ErrorException $e) { + } catch (\Throwable $e) { if ($this->isBreak($e)) { - return $this->close()->execute($sql, $bind); + return $this->close()->execute($sql, $bind, $query); + } + throw $e; + } catch (\Exception $e) { + if ($this->isBreak($e)) { + return $this->close()->execute($sql, $bind, $query); } throw $e; } @@ -466,6 +473,10 @@ abstract class Connection */ public function getRealSql($sql, array $bind = []) { + if (is_array($sql)) { + $sql = implode(';', $sql); + } + foreach ($bind as $key => $val) { $value = is_array($val) ? $val[0] : $val; $type = is_array($val) ? $val[1] : PDO::PARAM_STR; @@ -478,8 +489,8 @@ abstract class Connection $sql = is_numeric($key) ? substr_replace($sql, $value, strpos($sql, '?'), 1) : str_replace( - [':' . $key . ')', ':' . $key . ',', ':' . $key . ' '], - [$value . ')', $value . ',', $value . ' '], + [':' . $key . ')', ':' . $key . ',', ':' . $key . ' ', ':' . $key . PHP_EOL], + [$value . ')', $value . ',', $value . ' ', $value . PHP_EOL], $sql . ' '); } return rtrim($sql); @@ -552,7 +563,7 @@ abstract class Connection * @access protected * @param bool $pdo 是否返回PDOStatement * @param bool $procedure 是否存储过程 - * @return array + * @return PDOStatement|array */ protected function getResult($pdo = false, $procedure = false) { @@ -618,7 +629,8 @@ abstract class Connection /** * 启动事务 * @access public - * @return void + * @return bool|mixed + * @throws \Exception */ public function startTrans() { @@ -637,13 +649,15 @@ abstract class Connection ); } - } catch (\PDOException $e) { + } catch (\Exception $e) { if ($this->isBreak($e)) { + --$this->transTimes; return $this->close()->startTrans(); } throw $e; - } catch (\ErrorException $e) { + } catch (\Error $e) { if ($this->isBreak($e)) { + --$this->transTimes; return $this->close()->startTrans(); } throw $e; @@ -724,7 +738,7 @@ abstract class Connection * @param array $sqlArray SQL批处理指令 * @return boolean */ - public function batchQuery($sqlArray = []) + public function batchQuery($sqlArray = [], $bind = [], Query $query = null) { if (!is_array($sqlArray)) { return false; @@ -733,7 +747,7 @@ abstract class Connection $this->startTrans(); try { foreach ($sqlArray as $sql) { - $this->execute($sql); + $this->execute($sql, $bind, $query); } // 提交事务 $this->commit(); @@ -741,6 +755,7 @@ abstract class Connection $this->rollback(); throw $e; } + return true; } @@ -776,13 +791,15 @@ abstract class Connection $this->linkWrite = null; $this->linkRead = null; $this->links = []; + // 释放查询 + $this->free(); return $this; } /** * 是否断线 * @access protected - * @param \PDOException $e 异常对象 + * @param \PDOException|\Exception $e 异常对象 * @return bool */ protected function isBreak($e) @@ -802,6 +819,7 @@ abstract class Connection 'SSL connection has been closed unexpectedly', 'Error writing data to the connection', 'Resource deadlock avoided', + 'failed with errno', ]; $error = $e->getMessage(); @@ -882,9 +900,10 @@ abstract class Connection * @access protected * @param boolean $start 调试开始标记 true 开始 false 结束 * @param string $sql 执行的SQL语句 留空自动获取 + * @param boolean $master 主从标记 * @return void */ - protected function debug($start, $sql = '') + protected function debug($start, $sql = '', $master = false) { if (!empty($this->config['debug'])) { // 开启数据库调试模式 @@ -901,7 +920,7 @@ abstract class Connection $result = $this->getExplain($sql); } // SQL监听 - $this->trigger($sql, $runtime, $result); + $this->trigger($sql, $runtime, $result, $master); } } } @@ -923,19 +942,27 @@ abstract class Connection * @param string $sql SQL语句 * @param float $runtime SQL运行时间 * @param mixed $explain SQL分析 - * @return bool + * @param bool $master 主从标记 + * @return void */ - protected function trigger($sql, $runtime, $explain = []) + protected function trigger($sql, $runtime, $explain = [], $master = false) { if (!empty(self::$event)) { foreach (self::$event as $callback) { if (is_callable($callback)) { - call_user_func_array($callback, [$sql, $runtime, $explain]); + call_user_func_array($callback, [$sql, $runtime, $explain, $master]); } } } else { // 未注册监听则记录到日志中 - Log::record('[ SQL ] ' . $sql . ' [ RunTime:' . $runtime . 's ]', 'sql'); + if ($this->config['deploy']) { + // 分布式记录当前操作的主从 + $master = $master ? 'master|' : 'slave|'; + } else { + $master = ''; + } + + Log::record('[ SQL ] ' . $sql . ' [ ' . $master . 'RunTime:' . $runtime . 's ]', 'sql'); if (!empty($explain)) { Log::record('[ EXPLAIN : ' . var_export($explain, true) . ' ]', 'sql'); } diff --git a/thinkphp/library/think/db/Expression.php b/thinkphp/library/think/db/Expression.php new file mode 100644 index 00000000..f1b92abd --- /dev/null +++ b/thinkphp/library/think/db/Expression.php @@ -0,0 +1,48 @@ + +// +---------------------------------------------------------------------- + +namespace think\db; + +class Expression +{ + /** + * 查询表达式 + * + * @var string + */ + protected $value; + + /** + * 创建一个查询表达式 + * + * @param string $value + * @return void + */ + public function __construct($value) + { + $this->value = $value; + } + + /** + * 获取表达式 + * + * @return string + */ + public function getValue() + { + return $this->value; + } + + public function __toString() + { + return (string) $this->value; + } +} diff --git a/thinkphp/library/think/db/Query.php b/thinkphp/library/think/db/Query.php index a8722a1a..ac4adea1 100644 --- a/thinkphp/library/think/db/Query.php +++ b/thinkphp/library/think/db/Query.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -12,6 +12,7 @@ namespace think\db; use PDO; +use think\App; use think\Cache; use think\Collection; use think\Config; @@ -52,14 +53,16 @@ class Query protected static $info = []; // 回调事件 private static $event = []; + // 读取主库 + protected static $readMaster = []; /** * 构造函数 * @access public * @param Connection $connection 数据库对象实例 - * @param string $model 模型名 + * @param Model $model 模型对象 */ - public function __construct(Connection $connection = null, $model = '') + public function __construct(Connection $connection = null, $model = null) { $this->connection = $connection ?: Db::connect([], true); $this->prefix = $this->connection->getConfig('prefix'); @@ -89,6 +92,13 @@ class Query $name = Loader::parseName(substr($method, 10)); $where[$name] = $args[0]; return $this->where($where)->value($args[1]); + } elseif ($this->model && method_exists($this->model, 'scope' . $method)) { + // 动态调用命名范围 + $method = 'scope' . $method; + array_unshift($args, $this); + + call_user_func_array([$this->model, $method], $args); + return $this; } else { throw new Exception('method not exist:' . __CLASS__ . '->' . $method); } @@ -114,6 +124,7 @@ class Query { $this->connection = Db::connect($config); $this->setBuilder(); + $this->prefix = $this->connection->getConfig('prefix'); return $this; } @@ -129,15 +140,34 @@ class Query } /** - * 获取当前的模型对象名 + * 获取当前的模型对象实例 * @access public - * @return string + * @return Model|null */ public function getModel() { return $this->model; } + /** + * 设置后续从主库读取数据 + * @access public + * @param bool $allTable + * @return void + */ + public function readMaster($allTable = false) + { + if ($allTable) { + $table = '*'; + } else { + $table = isset($this->options['table']) ? $this->options['table'] : $this->getTable(); + } + + static::$readMaster[$table] = true; + + return $this; + } + /** * 获取当前的builder实例对象 * @access public @@ -236,7 +266,7 @@ class Query */ public function execute($sql, $bind = []) { - return $this->connection->execute($sql, $bind); + return $this->connection->execute($sql, $bind, $this); } /** @@ -310,9 +340,9 @@ class Query * @param array $sql SQL批处理指令 * @return boolean */ - public function batchQuery($sql = []) + public function batchQuery($sql = [], $bind = []) { - return $this->connection->batchQuery($sql); + return $this->connection->batchQuery($sql, $bind); } /** @@ -401,7 +431,7 @@ class Query if (empty($this->options['table'])) { $this->options['table'] = $this->getTable(); } - $key = is_string($cache['key']) ? $cache['key'] : md5($field . serialize($this->options) . serialize($this->bind)); + $key = is_string($cache['key']) ? $cache['key'] : md5($this->connection->getConfig('database') . '.' . $field . serialize($this->options) . serialize($this->bind)); $result = Cache::get($key); } if (false === $result) { @@ -413,11 +443,13 @@ class Query // 返回SQL语句 return $pdo; } + $result = $pdo->fetchColumn(); if ($force) { - $result = is_numeric($result) ? $result + 0 : $result; + $result = (float) $result; } - if (isset($cache)) { + + if (isset($cache) && false !== $result) { // 缓存数据 $this->cacheData($key, $result, $cache); } @@ -444,7 +476,7 @@ class Query if (empty($this->options['table'])) { $this->options['table'] = $this->getTable(); } - $guid = is_string($cache['key']) ? $cache['key'] : md5($field . serialize($this->options) . serialize($this->bind)); + $guid = is_string($cache['key']) ? $cache['key'] : md5($this->connection->getConfig('database') . '.' . $field . serialize($this->options) . serialize($this->bind)); $result = Cache::get($guid); } if (false === $result) { @@ -507,13 +539,43 @@ class Query public function count($field = '*') { if (isset($this->options['group'])) { + if (!preg_match('/^[\w\.\*]+$/', $field)) { + throw new Exception('not support data:' . $field); + } // 支持GROUP $options = $this->getOptions(); $subSql = $this->options($options)->field('count(' . $field . ')')->bind($this->bind)->buildSql(); - return $this->table([$subSql => '_group_count_'])->value('COUNT(*) AS tp_count', 0, true); + + $count = $this->table([$subSql => '_group_count_'])->value('COUNT(*) AS tp_count', 0, true); + } else { + $count = $this->aggregate('COUNT', $field, true); + } + + return is_string($count) ? $count : (int) $count; + + } + + /** + * 聚合查询 + * @access public + * @param string $aggregate 聚合方法 + * @param string $field 字段名 + * @param bool $force 强制转为数字类型 + * @return mixed + */ + public function aggregate($aggregate, $field, $force = false) + { + if (0 === stripos($field, 'DISTINCT ')) { + list($distinct, $field) = explode(' ', $field); + } + + if (!preg_match('/^[\w\.\+\-\*]+$/', $field)) { + throw new Exception('not support data:' . $field); } - return $this->value('COUNT(' . $field . ') AS tp_count', 0, true); + $result = $this->value($aggregate . '(' . (!empty($distinct) ? 'DISTINCT ' : '') . $field . ') AS tp_' . strtolower($aggregate), 0, $force); + + return $result; } /** @@ -524,29 +586,31 @@ class Query */ public function sum($field) { - return $this->value('SUM(' . $field . ') AS tp_sum', 0, true); + return $this->aggregate('SUM', $field, true); } /** * MIN查询 * @access public * @param string $field 字段名 + * @param bool $force 强制转为数字类型 * @return mixed */ - public function min($field) + public function min($field, $force = true) { - return $this->value('MIN(' . $field . ') AS tp_min', 0, true); + return $this->aggregate('MIN', $field, $force); } /** * MAX查询 * @access public * @param string $field 字段名 + * @param bool $force 强制转为数字类型 * @return mixed */ - public function max($field) + public function max($field, $force = true) { - return $this->value('MAX(' . $field . ') AS tp_max', 0, true); + return $this->aggregate('MAX', $field, $force); } /** @@ -557,7 +621,7 @@ class Query */ public function avg($field) { - return $this->value('AVG(' . $field . ') AS tp_avg', 0, true); + return $this->aggregate('AVG', $field, true); } /** @@ -604,7 +668,7 @@ class Query return true; } } - return $this->setField($field, ['exp', $field . '+' . $step]); + return $this->setField($field, ['inc', $step]); } /** @@ -632,8 +696,9 @@ class Query $this->options = []; return true; } + return $this->setField($field, ['inc', $step]); } - return $this->setField($field, ['exp', $field . '-' . $step]); + return $this->setField($field, ['dec', $step]); } /** @@ -701,7 +766,8 @@ class Query { // 传入的表名为数组 if (is_array($join)) { - list($table, $alias) = each($join); + $table = $join; + $alias = array_shift($join); } else { $join = trim($join); if (false !== strpos($join, '(')) { @@ -722,13 +788,9 @@ class Query $table = $this->getTable($table); } } - } - if (isset($alias)) { - if (isset($this->options['alias'][$table])) { - $table = $table . '@think' . uniqid(); + if (isset($alias) && $table != $alias) { + $table = [$table => $alias]; } - $table = [$table => $alias]; - $this->alias($table); } return $table; } @@ -766,8 +828,15 @@ class Query { if (empty($field)) { return $this; + } elseif ($field instanceof Expression) { + $this->options['field'][] = $field; + return $this; } + if (is_string($field)) { + if (preg_match('/[\<\'\"\(]/', $field)) { + return $this->fieldRaw($field); + } $field = array_map('trim', explode(',', $field)); } if (true === $field) { @@ -791,12 +860,30 @@ class Query } if (isset($this->options['field'])) { - $field = array_merge($this->options['field'], $field); + $field = array_merge((array) $this->options['field'], $field); } $this->options['field'] = array_unique($field); return $this; } + /** + * 表达式方式指定查询字段 + * @access public + * @param string $field 字段名 + * @param array $bind 参数绑定 + * @return $this + */ + public function fieldRaw($field, array $bind = []) + { + $this->options['field'][] = $this->raw($field); + + if ($bind) { + $this->bind($bind); + } + + return $this; + } + /** * 设置数据 * @access public @@ -825,7 +912,7 @@ class Query { $fields = is_string($field) ? explode(',', $field) : $field; foreach ($fields as $field) { - $this->data($field, ['exp', $field . '+' . $step]); + $this->data($field, ['inc', $step]); } return $this; } @@ -841,7 +928,7 @@ class Query { $fields = is_string($field) ? explode(',', $field) : $field; foreach ($fields as $field) { - $this->data($field, ['exp', $field . '-' . $step]); + $this->data($field, ['dec', $step]); } return $this; } @@ -855,25 +942,36 @@ class Query */ public function exp($field, $value) { - $this->data($field, ['exp', $value]); + $this->data($field, $this->raw($value)); return $this; } + /** + * 使用表达式设置数据 + * @access public + * @param mixed $value 表达式 + * @return Expression + */ + public function raw($value) + { + return new Expression($value); + } + /** * 指定JOIN查询字段 * @access public * @param string|array $table 数据表 * @param string|array $field 查询字段 - * @param string|array $on JOIN条件 + * @param mixed $on JOIN条件 * @param string $type JOIN类型 * @return $this */ public function view($join, $field = true, $on = null, $type = 'INNER') { $this->options['view'] = true; - if (is_array($join) && key($join) !== 0) { + if (is_array($join) && key($join) === 0) { foreach ($join as $key => $val) { - $this->view($key, $val[0], isset($val[1]) ? $val[1] : null, isset($val[2]) ? $val[2] : 'INNER'); + $this->view($val[0], $val[1], isset($val[2]) ? $val[2] : null, isset($val[3]) ? $val[3] : 'INNER'); } } else { $fields = []; @@ -972,6 +1070,37 @@ class Query return $this; } + /** + * 指定表达式查询条件 + * @access public + * @param string $where 查询条件 + * @param array $bind 参数绑定 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereRaw($where, $bind = [], $logic = 'AND') + { + $this->options['where'][$logic][] = $this->raw($where); + + if ($bind) { + $this->bind($bind); + } + + return $this; + } + + /** + * 指定表达式查询条件 OR + * @access public + * @param string $where 查询条件 + * @param array $bind 参数绑定 + * @return $this + */ + public function whereOrRaw($where, $bind = []) + { + return $this->whereRaw($where, $bind, 'OR'); + } + /** * 指定Null查询条件 * @access public @@ -981,7 +1110,7 @@ class Query */ public function whereNull($field, $logic = 'AND') { - $this->parseWhereExp($logic, $field, 'null', null); + $this->parseWhereExp($logic, $field, 'null', null, [], true); return $this; } @@ -994,7 +1123,7 @@ class Query */ public function whereNotNull($field, $logic = 'AND') { - $this->parseWhereExp($logic, $field, 'notnull', null); + $this->parseWhereExp($logic, $field, 'notnull', null, [], true); return $this; } @@ -1034,7 +1163,7 @@ class Query */ public function whereIn($field, $condition, $logic = 'AND') { - $this->parseWhereExp($logic, $field, 'in', $condition); + $this->parseWhereExp($logic, $field, 'in', $condition, [], true); return $this; } @@ -1048,7 +1177,7 @@ class Query */ public function whereNotIn($field, $condition, $logic = 'AND') { - $this->parseWhereExp($logic, $field, 'not in', $condition); + $this->parseWhereExp($logic, $field, 'not in', $condition, [], true); return $this; } @@ -1062,7 +1191,7 @@ class Query */ public function whereLike($field, $condition, $logic = 'AND') { - $this->parseWhereExp($logic, $field, 'like', $condition); + $this->parseWhereExp($logic, $field, 'like', $condition, [], true); return $this; } @@ -1076,7 +1205,7 @@ class Query */ public function whereNotLike($field, $condition, $logic = 'AND') { - $this->parseWhereExp($logic, $field, 'not like', $condition); + $this->parseWhereExp($logic, $field, 'not like', $condition, [], true); return $this; } @@ -1090,7 +1219,7 @@ class Query */ public function whereBetween($field, $condition, $logic = 'AND') { - $this->parseWhereExp($logic, $field, 'between', $condition); + $this->parseWhereExp($logic, $field, 'between', $condition, [], true); return $this; } @@ -1104,7 +1233,7 @@ class Query */ public function whereNotBetween($field, $condition, $logic = 'AND') { - $this->parseWhereExp($logic, $field, 'not between', $condition); + $this->parseWhereExp($logic, $field, 'not between', $condition, [], true); return $this; } @@ -1118,7 +1247,7 @@ class Query */ public function whereExp($field, $condition, $logic = 'AND') { - $this->parseWhereExp($logic, $field, 'exp', $condition); + $this->parseWhereExp($logic, $field, 'exp', $this->raw($condition), [], true); return $this; } @@ -1145,9 +1274,10 @@ class Query * @param mixed $op 查询表达式 * @param mixed $condition 查询条件 * @param array $param 查询参数 + * @param bool $strict 严格模式 * @return void */ - protected function parseWhereExp($logic, $field, $op, $condition, $param = []) + protected function parseWhereExp($logic, $field, $op, $condition, $param = [], $strict = false) { $logic = strtoupper($logic); if ($field instanceof \Closure) { @@ -1158,8 +1288,17 @@ class Query if (is_string($field) && !empty($this->options['via']) && !strpos($field, '.')) { $field = $this->options['via'] . '.' . $field; } - if (is_string($field) && preg_match('/[,=\>\<\'\"\(\s]/', $field)) { - $where[] = ['exp', $field]; + + if ($field instanceof Expression) { + return $this->whereRaw($field, is_array($op) ? $op : []); + } elseif ($strict) { + // 使用严格模式查询 + $where[$field] = [$op, $condition]; + + // 记录一个字段多次查询条件 + $this->options['multi'][$logic][$field][] = $where[$field]; + } elseif (is_string($field) && preg_match('/[,=\>\<\'\"\(\s]/', $field)) { + $where[] = ['exp', $this->raw($field)]; if (is_array($op)) { // 参数绑定 $this->bind($op); @@ -1180,23 +1319,28 @@ class Query $where[$field] = $param; } elseif (in_array(strtolower($op), ['null', 'notnull', 'not null'])) { // null查询 - $where[$field] = [$op, '']; + $where[$field] = [$op, '']; + $this->options['multi'][$logic][$field][] = $where[$field]; } elseif (is_null($condition)) { // 字段相等查询 $where[$field] = ['eq', $op]; - if ('AND' != $logic) { - $this->options['multi'][$logic][$field][] = $where[$field]; - } + + $this->options['multi'][$logic][$field][] = $where[$field]; } else { - $where[$field] = [$op, $condition, isset($param[2]) ? $param[2] : null]; - if ('exp' == strtolower($op) && isset($param[2]) && is_array($param[2])) { + if ('exp' == strtolower($op)) { + $where[$field] = ['exp', $this->raw($condition)]; // 参数绑定 - $this->bind($param[2]); + if (isset($param[2]) && is_array($param[2])) { + $this->bind($param[2]); + } + } else { + $where[$field] = [$op, $condition]; } // 记录一个字段多次查询条件 $this->options['multi'][$logic][$field][] = $where[$field]; } + if (!empty($where)) { if (!isset($this->options['where'][$logic])) { $this->options['where'][$logic] = []; @@ -1238,6 +1382,7 @@ class Query $logic = strtoupper($logic); if (isset($this->options['where'][$logic][$field])) { unset($this->options['where'][$logic][$field]); + unset($this->options['multi'][$logic][$field]); } return $this; } @@ -1413,49 +1558,78 @@ class Query */ public function order($field, $order = null) { - if (!empty($field)) { - if (is_string($field)) { - if (!empty($this->options['via'])) { - $field = $this->options['via'] . '.' . $field; - } - $field = empty($order) ? $field : [$field => $order]; - } elseif (!empty($this->options['via'])) { - foreach ($field as $key => $val) { - if (is_numeric($key)) { - $field[$key] = $this->options['via'] . '.' . $val; - } else { - $field[$this->options['via'] . '.' . $key] = $val; - unset($field[$key]); - } - } - } - if (!isset($this->options['order'])) { - $this->options['order'] = []; + if (empty($field)) { + return $this; + } elseif ($field instanceof Expression) { + $this->options['order'][] = $field; + return $this; + } + + if (is_string($field)) { + if (!empty($this->options['via'])) { + $field = $this->options['via'] . '.' . $field; } - if (is_array($field)) { - $this->options['order'] = array_merge($this->options['order'], $field); + if (strpos($field, ',')) { + $field = array_map('trim', explode(',', $field)); } else { - $this->options['order'][] = $field; + $field = empty($order) ? $field : [$field => $order]; } + } elseif (!empty($this->options['via'])) { + foreach ($field as $key => $val) { + if (is_numeric($key)) { + $field[$key] = $this->options['via'] . '.' . $val; + } else { + $field[$this->options['via'] . '.' . $key] = $val; + unset($field[$key]); + } + } + } + if (!isset($this->options['order'])) { + $this->options['order'] = []; + } + if (is_array($field)) { + $this->options['order'] = array_merge($this->options['order'], $field); + } else { + $this->options['order'][] = $field; } + + return $this; + } + + /** + * 表达式方式指定Field排序 + * @access public + * @param string $field 排序字段 + * @param array $bind 参数绑定 + * @return $this + */ + public function orderRaw($field, array $bind = []) + { + $this->options['order'][] = $this->raw($field); + + if ($bind) { + $this->bind($bind); + } + return $this; } /** * 查询缓存 * @access public - * @param mixed $key 缓存key - * @param integer $expire 缓存有效期 - * @param string $tag 缓存标签 + * @param mixed $key 缓存key + * @param integer|\DateTime $expire 缓存有效期 + * @param string $tag 缓存标签 * @return $this */ public function cache($key = true, $expire = null, $tag = null) { // 增加快捷调用方式 cache(10) 等同于 cache(true, 10) - if (is_numeric($key) && is_null($expire)) { + if ($key instanceof \DateTime || (is_numeric($key) && is_null($expire))) { $expire = $key; $key = true; } + if (false !== $key) { $this->options['cache'] = ['key' => $key, 'expire' => $expire, 'tag' => $tag]; } @@ -1489,7 +1663,7 @@ class Query /** * 指定查询lock * @access public - * @param boolean $lock 是否lock + * @param bool|string $lock 是否lock * @return $this */ public function lock($lock = false) @@ -1521,7 +1695,12 @@ class Query { if (is_array($alias)) { foreach ($alias as $key => $val) { - $this->options['alias'][$key] = $val; + if (false !== strpos($key, '__')) { + $table = $this->parseSqlTable($key); + } else { + $table = $key; + } + $this->options['alias'][$table] = $val; } } else { if (isset($this->options['table'])) { @@ -1649,46 +1828,49 @@ class Query * 查询日期或者时间 * @access public * @param string $field 日期字段名 - * @param string $op 比较运算符或者表达式 + * @param string|array $op 比较运算符或者表达式 * @param string|array $range 比较范围 * @return $this */ public function whereTime($field, $op, $range = null) { if (is_null($range)) { - // 使用日期表达式 - $date = getdate(); - switch (strtolower($op)) { - case 'today': - case 'd': - $range = ['today', 'tomorrow']; - break; - case 'week': - case 'w': - $range = 'this week 00:00:00'; - break; - case 'month': - case 'm': - $range = mktime(0, 0, 0, $date['mon'], 1, $date['year']); - break; - case 'year': - case 'y': - $range = mktime(0, 0, 0, 1, 1, $date['year']); - break; - case 'yesterday': - $range = ['yesterday', 'today']; - break; - case 'last week': - $range = ['last week 00:00:00', 'this week 00:00:00']; - break; - case 'last month': - $range = [date('y-m-01', strtotime('-1 month')), mktime(0, 0, 0, $date['mon'], 1, $date['year'])]; - break; - case 'last year': - $range = [mktime(0, 0, 0, 1, 1, $date['year'] - 1), mktime(0, 0, 0, 1, 1, $date['year'])]; - break; - default: - $range = $op; + if (is_array($op)) { + $range = $op; + } else { + // 使用日期表达式 + switch (strtolower($op)) { + case 'today': + case 'd': + $range = ['today', 'tomorrow']; + break; + case 'week': + case 'w': + $range = ['this week 00:00:00', 'next week 00:00:00']; + break; + case 'month': + case 'm': + $range = ['first Day of this month 00:00:00', 'first Day of next month 00:00:00']; + break; + case 'year': + case 'y': + $range = ['this year 1/1', 'next year 1/1']; + break; + case 'yesterday': + $range = ['yesterday', 'today']; + break; + case 'last week': + $range = ['last week 00:00:00', 'this week 00:00:00']; + break; + case 'last month': + $range = ['first Day of last month 00:00:00', 'first Day of this month 00:00:00']; + break; + case 'last year': + $range = ['last year 1/1', 'this year 1/1']; + break; + default: + $range = $op; + } } $op = is_array($range) ? 'between' : '>'; } @@ -1733,7 +1915,7 @@ class Query $schema = $guid; } // 读取缓存 - if (is_file(RUNTIME_PATH . 'schema/' . $schema . '.php')) { + if (!App::$debug && is_file(RUNTIME_PATH . 'schema/' . $schema . '.php')) { $info = include RUNTIME_PATH . 'schema/' . $schema . '.php'; } else { $info = $this->connection->getFields($guid); @@ -1808,7 +1990,9 @@ class Query */ protected function getFieldBindType($type) { - if (preg_match('/(int|double|float|decimal|real|numeric|serial|bit)/is', $type)) { + if (0 === strpos($type, 'set') || 0 === strpos($type, 'enum')) { + $bind = PDO::PARAM_STR; + } elseif (preg_match('/(int|double|float|decimal|real|numeric|serial|bit)/is', $type)) { $bind = PDO::PARAM_INT; } elseif (preg_match('/bool/is', $type)) { $bind = PDO::PARAM_BOOL; @@ -1890,11 +2074,10 @@ class Query $with = explode(',', $with); } - $first = true; - $currentModel = $this->model; + $first = true; /** @var Model $class */ - $class = new $currentModel; + $class = $this->model; foreach ($with as $key => $relation) { $subRelation = ''; $closure = false; @@ -1947,14 +2130,23 @@ class Query $this->field('*'); } foreach ($relations as $key => $relation) { - $closure = false; + $closure = $name = null; if ($relation instanceof \Closure) { $closure = $relation; $relation = $key; + } elseif (!is_int($key)) { + $name = $relation; + $relation = $key; } $relation = Loader::parseName($relation, 1, false); - $count = '(' . (new $this->model)->$relation()->getRelationCountQuery($closure) . ')'; - $this->field([$count => Loader::parseName($relation) . '_count']); + + $count = '(' . $this->model->$relation()->getRelationCountQuery($closure, $name) . ')'; + + if (empty($name)) { + $name = Loader::parseName($relation) . '_count'; + } + + $this->field([$count => $name]); } } return $this; @@ -2080,7 +2272,7 @@ class Query } // 执行操作 - $result = $this->execute($sql, $bind); + $result = 0 === $sql ? 0 : $this->execute($sql, $bind, $this); if ($result) { $sequence = $sequence ?: (isset($options['sequence']) ? $options['sequence'] : null); $lastInsId = $this->getLastInsID($sequence); @@ -2116,27 +2308,40 @@ class Query /** * 批量插入记录 * @access public - * @param mixed $dataSet 数据集 - * @param boolean $replace 是否replace + * @param mixed $dataSet 数据集 + * @param boolean $replace 是否replace + * @param integer $limit 每次写入数据限制 * @return integer|string */ - public function insertAll(array $dataSet, $replace = false) + public function insertAll(array $dataSet, $replace = false, $limit = null) { // 分析查询表达式 $options = $this->parseExpress(); if (!is_array(reset($dataSet))) { return false; } + // 生成SQL语句 - $sql = $this->builder->insertAll($dataSet, $options, $replace); + if (is_null($limit)) { + $sql = $this->builder->insertAll($dataSet, $options, $replace); + } else { + $array = array_chunk($dataSet, $limit, true); + foreach ($array as $item) { + $sql[] = $this->builder->insertAll($item, $options, $replace); + } + } + // 获取参数绑定 $bind = $this->getBind(); if ($options['fetch_sql']) { // 获取实际执行的SQL语句 return $this->connection->getRealSql($sql, $bind); + } elseif (is_array($sql)) { + // 执行操作 + return $this->batchQuery($sql, $bind, $this); } else { // 执行操作 - return $this->execute($sql, $bind); + return $this->execute($sql, $bind, $this); } } @@ -2162,7 +2367,7 @@ class Query return $this->connection->getRealSql($sql, $bind); } else { // 执行操作 - return $this->execute($sql, $bind); + return $this->execute($sql, $bind, $this); } } @@ -2229,7 +2434,7 @@ class Query Cache::clear($options['cache']['tag']); } // 执行操作 - $result = '' == $sql ? 0 : $this->execute($sql, $bind); + $result = '' == $sql ? 0 : $this->execute($sql, $bind, $this); if ($result) { if (is_string($pk) && isset($where[$pk])) { $data[$pk] = $where[$pk]; @@ -2298,10 +2503,10 @@ class Query // 判断查询缓存 $cache = $options['cache']; unset($options['cache']); - $key = is_string($cache['key']) ? $cache['key'] : md5(serialize($options) . serialize($this->bind)); + $key = is_string($cache['key']) ? $cache['key'] : md5($this->connection->getConfig('database') . '.' . serialize($options) . serialize($this->bind)); $resultSet = Cache::get($key); } - if (!$resultSet) { + if (false === $resultSet) { // 生成查询SQL $sql = $this->builder->select($options); // 获取参数绑定 @@ -2323,7 +2528,7 @@ class Query } } - if (isset($cache) && $resultSet) { + if (isset($cache) && false !== $resultSet) { // 缓存数据集 $this->cacheData($key, $resultSet, $cache); } @@ -2332,11 +2537,10 @@ class Query // 数据列表读取后的处理 if (!empty($this->model)) { // 生成模型对象 - $modelName = $this->model; if (count($resultSet) > 0) { foreach ($resultSet as $key => $result) { - /** @var Model $result */ - $model = new $modelName($result); + /** @var Model $model */ + $model = $this->model->newInstance($result); $model->isUpdate(true); // 关联查询 @@ -2356,7 +2560,7 @@ class Query // 模型数据集转换 $resultSet = $model->toCollection($resultSet); } else { - $resultSet = (new $modelName)->toCollection($resultSet); + $resultSet = $this->model->toCollection($resultSet); } } elseif ('collection' == $this->connection->getConfig('resultset_type')) { // 返回Collection对象 @@ -2391,18 +2595,25 @@ class Query * @param mixed $value 缓存数据 * @param array $options 缓存参数 * @param array $bind 绑定参数 + * @return string */ protected function getCacheKey($value, $options, $bind = []) { if (is_scalar($value)) { $data = $value; - } elseif (is_array($value) && 'eq' == strtolower($value[0])) { + } elseif (is_array($value) && is_string($value[0]) && 'eq' == strtolower($value[0])) { $data = $value[1]; } + $prefix = $this->connection->getConfig('database') . '.'; + if (isset($data)) { - return 'think:' . (is_array($options['table']) ? key($options['table']) : $options['table']) . '|' . $data; - } else { - return md5(serialize($options) . serialize($bind)); + return 'think:' . $prefix . (is_array($options['table']) ? key($options['table']) : $options['table']) . '|' . $data; + } + + try { + return md5($prefix . serialize($options) . serialize($bind)); + } catch (\Exception $e) { + throw new Exception('closure not support cache(true)'); } } @@ -2439,11 +2650,11 @@ class Query // 判断查询缓存 $cache = $options['cache']; if (true === $cache['key'] && !is_null($data) && !is_array($data)) { - $key = 'think:' . (is_array($options['table']) ? key($options['table']) : $options['table']) . '|' . $data; + $key = 'think:' . $this->connection->getConfig('database') . '.' . (is_array($options['table']) ? key($options['table']) : $options['table']) . '|' . $data; } elseif (is_string($cache['key'])) { $key = $cache['key']; } elseif (!isset($key)) { - $key = md5(serialize($options) . serialize($this->bind)); + $key = md5($this->connection->getConfig('database') . '.' . serialize($options) . serialize($this->bind)); } $result = Cache::get($key); } @@ -2491,8 +2702,7 @@ class Query if (!empty($result)) { if (!empty($this->model)) { // 返回模型对象 - $model = $this->model; - $result = new $model($result); + $result = $this->model->newInstance($result); $result->isUpdate(true, isset($options['where']['AND']) ? $options['where']['AND'] : null); // 关联查询 if (!empty($options['relation'])) { @@ -2523,7 +2733,8 @@ class Query protected function throwNotFound($options = []) { if (!empty($this->model)) { - throw new ModelNotFoundException('model data Not Found:' . $this->model, $this->model, $options); + $class = get_class($this->model); + throw new ModelNotFoundException('model data Not Found:' . $class, $class, $options); } else { $table = is_array($options['table']) ? key($options['table']) : $options['table']; throw new DataNotFoundException('table data not Found:' . $table, $table, $options); @@ -2564,44 +2775,61 @@ class Query * @param integer $count 每次处理的数据数量 * @param callable $callback 处理回调方法 * @param string $column 分批处理的字段名 + * @param string $order 排序规则 * @return boolean + * @throws \LogicException */ - public function chunk($count, $callback, $column = null) + public function chunk($count, $callback, $column = null, $order = 'asc') { $options = $this->getOptions(); - if (isset($options['table'])) { - $table = is_array($options['table']) ? key($options['table']) : $options['table']; - } else { - $table = ''; + if (empty($options['table'])) { + $options['table'] = $this->getTable(); } - $column = $column ?: $this->getPk($table); - $bind = $this->bind; - $resultSet = $this->limit($count)->order($column, 'asc')->select(); - if (strpos($column, '.')) { - list($alias, $key) = explode('.', $column); - } else { - $key = $column; + $column = $column ?: $this->getPk($options); + + if (isset($options['order'])) { + if (App::$debug) { + throw new \LogicException('chunk not support call order'); + } + unset($options['order']); } - if ($resultSet instanceof Collection) { - $resultSet = $resultSet->all(); + $bind = $this->bind; + if (is_array($column)) { + $times = 1; + $query = $this->options($options)->page($times, $count); + } else { + if (strpos($column, '.')) { + list($alias, $key) = explode('.', $column); + } else { + $key = $column; + } + $query = $this->options($options)->limit($count); } + $resultSet = $query->order($column, $order)->select(); + + while (count($resultSet) > 0) { + if ($resultSet instanceof Collection) { + $resultSet = $resultSet->all(); + } - while (!empty($resultSet)) { if (false === call_user_func($callback, $resultSet)) { return false; } - $end = end($resultSet); - $lastId = is_array($end) ? $end[$key] : $end->$key; - $resultSet = $this->options($options) - ->limit($count) - ->bind($bind) - ->where($column, '>', $lastId) - ->order($column, 'asc') - ->select(); - if ($resultSet instanceof Collection) { - $resultSet = $resultSet->all(); + + if (is_array($column)) { + $times++; + $query = $this->options($options)->page($times, $count); + } else { + $end = end($resultSet); + $lastId = is_array($end) ? $end[$key] : $end->getData($key); + $query = $this->options($options) + ->limit($count) + ->where($column, 'asc' == strtolower($order) ? '>' : '<', $lastId); } + + $resultSet = $query->bind($bind)->order($column, $order)->select(); } + return true; } @@ -2678,7 +2906,7 @@ class Query Cache::clear($options['cache']['tag']); } // 执行操作 - $result = $this->execute($sql, $bind); + $result = $this->execute($sql, $bind, $this); if ($result) { if (!is_array($data) && is_string($pk) && isset($key) && strpos($key, '|')) { list($a, $val) = explode('|', $key); @@ -2763,6 +2991,10 @@ class Query } } + if (isset(static::$readMaster['*']) || (is_string($options['table']) && isset(static::$readMaster[$options['table']]))) { + $options['master'] = true; + } + foreach (['join', 'union', 'group', 'having', 'limit', 'order', 'force', 'comment'] as $name) { if (!isset($options[$name])) { $options[$name] = ''; diff --git a/thinkphp/library/think/db/builder/Mysql.php b/thinkphp/library/think/db/builder/Mysql.php index 5bc9d03b..be2af714 100644 --- a/thinkphp/library/think/db/builder/Mysql.php +++ b/thinkphp/library/think/db/builder/Mysql.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -12,28 +12,93 @@ namespace think\db\builder; use think\db\Builder; +use think\Exception; /** * mysql数据库驱动 */ class Mysql extends Builder { - protected $updateSql = 'UPDATE %TABLE% %JOIN% SET %SET% %WHERE% %ORDER%%LIMIT% %LOCK%%COMMENT%'; + + protected $insertAllSql = '%INSERT% INTO %TABLE% (%FIELD%) VALUES %DATA% %COMMENT%'; + protected $updateSql = 'UPDATE %TABLE% %JOIN% SET %SET% %WHERE% %ORDER%%LIMIT% %LOCK%%COMMENT%'; + + /** + * 生成insertall SQL + * @access public + * @param array $dataSet 数据集 + * @param array $options 表达式 + * @param bool $replace 是否replace + * @return string + * @throws Exception + */ + public function insertAll($dataSet, $options = [], $replace = false) + { + // 获取合法的字段 + if ('*' == $options['field']) { + $fields = array_keys($this->query->getFieldsType($options['table'])); + } else { + $fields = $options['field']; + } + + foreach ($dataSet as $data) { + foreach ($data as $key => $val) { + if (!in_array($key, $fields, true)) { + if ($options['strict']) { + throw new Exception('fields not exists:[' . $key . ']'); + } + unset($data[$key]); + } elseif (is_null($val)) { + $data[$key] = 'NULL'; + } elseif (is_scalar($val)) { + $data[$key] = $this->parseValue($val, $key); + } elseif (is_object($val) && method_exists($val, '__toString')) { + // 对象数据写入 + $data[$key] = $val->__toString(); + } else { + // 过滤掉非标量数据 + unset($data[$key]); + } + } + $value = array_values($data); + $values[] = '( ' . implode(',', $value) . ' )'; + + if (!isset($insertFields)) { + $insertFields = array_map([$this, 'parseKey'], array_keys($data)); + } + } + + return str_replace( + ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'], + [ + $replace ? 'REPLACE' : 'INSERT', + $this->parseTable($options['table'], $options), + implode(' , ', $insertFields), + implode(' , ', $values), + $this->parseComment($options['comment']), + ], $this->insertAllSql); + } /** * 字段和表名处理 * @access protected - * @param string $key + * @param mixed $key * @param array $options * @return string */ - protected function parseKey($key, $options = []) + protected function parseKey($key, $options = [], $strict = false) { + if (is_numeric($key)) { + return $key; + } elseif ($key instanceof Expression) { + return $key->getValue(); + } + $key = trim($key); if (strpos($key, '$.') && false === strpos($key, '(')) { // JSON字段支持 list($field, $name) = explode('$.', $key); - $key = 'json_extract(' . $field . ', \'$.' . $name . '\')'; + return 'json_extract(' . $field . ', \'$.' . $name . '\')'; } elseif (strpos($key, '.') && !preg_match('/[,\'\"\(\)`\s]/', $key)) { list($table, $key) = explode('.', $key, 2); if ('__TABLE__' == $table) { @@ -43,7 +108,11 @@ class Mysql extends Builder $table = $options['alias'][$table]; } } - if (!preg_match('/[,\'\"\*\(\)`.\s]/', $key)) { + + if ($strict && !preg_match('/^[\w\.\*]+$/', $key)) { + throw new Exception('not support data:' . $key); + } + if ('*' != $key && ($strict || !preg_match('/[,\'\"\*\(\)`.\s]/', $key))) { $key = '`' . $key . '`'; } if (isset($table)) { diff --git a/thinkphp/library/think/db/builder/Pgsql.php b/thinkphp/library/think/db/builder/Pgsql.php index 5be0468a..acc22896 100644 --- a/thinkphp/library/think/db/builder/Pgsql.php +++ b/thinkphp/library/think/db/builder/Pgsql.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -18,6 +18,8 @@ use think\db\Builder; */ class Pgsql extends Builder { + protected $insertSql = 'INSERT INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%'; + protected $insertAllSql = 'INSERT INTO %TABLE% (%FIELD%) %DATA% %COMMENT%'; /** * limit分析 @@ -42,12 +44,18 @@ class Pgsql extends Builder /** * 字段和表名处理 * @access protected - * @param string $key + * @param mixed $key * @param array $options * @return string */ - protected function parseKey($key, $options = []) + protected function parseKey($key, $options = [], $strict = false) { + if (is_numeric($key)) { + return $key; + } elseif ($key instanceof Expression) { + return $key->getValue(); + } + $key = trim($key); if (strpos($key, '$.') && false === strpos($key, '(')) { // JSON字段支持 diff --git a/thinkphp/library/think/db/builder/Sqlite.php b/thinkphp/library/think/db/builder/Sqlite.php index 55d3abc4..c727f04b 100644 --- a/thinkphp/library/think/db/builder/Sqlite.php +++ b/thinkphp/library/think/db/builder/Sqlite.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -22,6 +22,7 @@ class Sqlite extends Builder /** * limit * @access public + * @param string $limit * @return string */ public function parseLimit($limit) @@ -51,12 +52,18 @@ class Sqlite extends Builder /** * 字段和表名处理 * @access protected - * @param string $key + * @param mixed $key * @param array $options * @return string */ - protected function parseKey($key, $options = []) + protected function parseKey($key, $options = [], $strict = false) { + if (is_numeric($key)) { + return $key; + } elseif ($key instanceof Expression) { + return $key->getValue(); + } + $key = trim($key); if (strpos($key, '.')) { list($table, $key) = explode('.', $key, 2); diff --git a/thinkphp/library/think/db/builder/Sqlsrv.php b/thinkphp/library/think/db/builder/Sqlsrv.php index 52629e73..dc425d9e 100644 --- a/thinkphp/library/think/db/builder/Sqlsrv.php +++ b/thinkphp/library/think/db/builder/Sqlsrv.php @@ -12,6 +12,7 @@ namespace think\db\builder; use think\db\Builder; +use think\db\Expression; /** * Sqlsrv数据库驱动 @@ -22,6 +23,8 @@ class Sqlsrv extends Builder protected $selectInsertSql = 'SELECT %DISTINCT% %FIELD% FROM %TABLE%%JOIN%%WHERE%%GROUP%%HAVING%'; protected $updateSql = 'UPDATE %TABLE% SET %SET% FROM %TABLE% %JOIN% %WHERE% %LIMIT% %LOCK%%COMMENT%'; protected $deleteSql = 'DELETE FROM %TABLE% %USING% FROM %TABLE% %JOIN% %WHERE% %LIMIT% %LOCK%%COMMENT%'; + protected $insertSql = 'INSERT INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%'; + protected $insertAllSql = 'INSERT INTO %TABLE% (%FIELD%) %DATA% %COMMENT%'; /** * order分析 @@ -32,23 +35,29 @@ class Sqlsrv extends Builder */ protected function parseOrder($order, $options = []) { - if (is_array($order)) { - $array = []; - foreach ($order as $key => $val) { - if (is_numeric($key)) { - if (false === strpos($val, '(')) { - $array[] = $this->parseKey($val, $options); - } elseif ('[rand]' == $val) { - $array[] = $this->parseRand(); - } + if (empty($order)) { + return ' ORDER BY rand()'; + } + + $array = []; + foreach ($order as $key => $val) { + if ($val instanceof Expression) { + $array[] = $val->getValue(); + } elseif (is_numeric($key)) { + if (false === strpos($val, '(')) { + $array[] = $this->parseKey($val, $options); + } elseif ('[rand]' == $val) { + $array[] = $this->parseRand(); } else { - $sort = in_array(strtolower(trim($val)), ['asc', 'desc']) ? ' ' . $val : ''; - $array[] = $this->parseKey($key, $options) . ' ' . $sort; + $array[] = $val; } + } else { + $sort = in_array(strtolower(trim($val)), ['asc', 'desc'], true) ? ' ' . $val : ''; + $array[] = $this->parseKey($key, $options, true) . ' ' . $sort; } - $order = implode(',', $array); } - return !empty($order) ? ' ORDER BY ' . $order : ' ORDER BY rand()'; + + return ' ORDER BY ' . implode(',', $array); } /** @@ -64,12 +73,17 @@ class Sqlsrv extends Builder /** * 字段和表名处理 * @access protected - * @param string $key + * @param mixed $key * @param array $options * @return string */ - protected function parseKey($key, $options = []) + protected function parseKey($key, $options = [], $strict = false) { + if (is_numeric($key)) { + return $key; + } elseif ($key instanceof Expression) { + return $key->getValue(); + } $key = trim($key); if (strpos($key, '.') && !preg_match('/[,\'\"\(\)\[\s]/', $key)) { list($table, $key) = explode('.', $key, 2); @@ -80,7 +94,11 @@ class Sqlsrv extends Builder $table = $options['alias'][$table]; } } - if (!is_numeric($key) && !preg_match('/[,\'\"\*\(\)\[.\s]/', $key)) { + + if ($strict && !preg_match('/^[\w\.\*]+$/', $key)) { + throw new Exception('not support data:' . $key); + } + if ('*' != $key && ($strict || !preg_match('/[,\'\"\*\(\)\[.\s]/', $key))) { $key = '[' . $key . ']'; } if (isset($table)) { diff --git a/thinkphp/library/think/db/connector/Mysql.php b/thinkphp/library/think/db/connector/Mysql.php index 536125fd..be1a85ce 100644 --- a/thinkphp/library/think/db/connector/Mysql.php +++ b/thinkphp/library/think/db/connector/Mysql.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -31,12 +31,15 @@ class Mysql extends Connection */ protected function parseDsn($config) { - $dsn = 'mysql:dbname=' . $config['database'] . ';host=' . $config['hostname']; - if (!empty($config['hostport'])) { - $dsn .= ';port=' . $config['hostport']; - } elseif (!empty($config['socket'])) { - $dsn .= ';unix_socket=' . $config['socket']; + if (!empty($config['socket'])) { + $dsn = 'mysql:unix_socket=' . $config['socket']; + } elseif (!empty($config['hostport'])) { + $dsn = 'mysql:host=' . $config['hostname'] . ';port=' . $config['hostport']; + } else { + $dsn = 'mysql:host=' . $config['hostname']; } + $dsn .= ';dbname=' . $config['database']; + if (!empty($config['charset'])) { $dsn .= ';charset=' . $config['charset']; } diff --git a/thinkphp/library/think/db/connector/Pgsql.php b/thinkphp/library/think/db/connector/Pgsql.php index 761fbac4..bbcf5768 100644 --- a/thinkphp/library/think/db/connector/Pgsql.php +++ b/thinkphp/library/think/db/connector/Pgsql.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- diff --git a/thinkphp/library/think/db/connector/Sqlite.php b/thinkphp/library/think/db/connector/Sqlite.php index fd7f02b0..c4e3a724 100644 --- a/thinkphp/library/think/db/connector/Sqlite.php +++ b/thinkphp/library/think/db/connector/Sqlite.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- diff --git a/thinkphp/library/think/db/connector/Sqlsrv.php b/thinkphp/library/think/db/connector/Sqlsrv.php index 08ad45e2..35c66005 100644 --- a/thinkphp/library/think/db/connector/Sqlsrv.php +++ b/thinkphp/library/think/db/connector/Sqlsrv.php @@ -50,7 +50,10 @@ class Sqlsrv extends Connection public function getFields($tableName) { list($tableName) = explode(' ', $tableName); - $sql = "SELECT column_name, data_type, column_default, is_nullable + $tableNames = explode('.', $tableName); + $tableName = isset($tableNames[1]) ? $tableNames[1] : $tableNames[0]; + + $sql = "SELECT column_name, data_type, column_default, is_nullable FROM information_schema.tables AS t JOIN information_schema.columns AS c ON t.table_catalog = c.table_catalog diff --git a/thinkphp/library/think/db/exception/BindParamException.php b/thinkphp/library/think/db/exception/BindParamException.php index d0e2387b..4ed19546 100644 --- a/thinkphp/library/think/db/exception/BindParamException.php +++ b/thinkphp/library/think/db/exception/BindParamException.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- diff --git a/thinkphp/library/think/db/exception/DataNotFoundException.php b/thinkphp/library/think/db/exception/DataNotFoundException.php index e399b063..f2542ac6 100644 --- a/thinkphp/library/think/db/exception/DataNotFoundException.php +++ b/thinkphp/library/think/db/exception/DataNotFoundException.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- diff --git a/thinkphp/library/think/db/exception/ModelNotFoundException.php b/thinkphp/library/think/db/exception/ModelNotFoundException.php index 2180ab07..6e5f930c 100644 --- a/thinkphp/library/think/db/exception/ModelNotFoundException.php +++ b/thinkphp/library/think/db/exception/ModelNotFoundException.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- diff --git a/thinkphp/library/think/debug/Console.php b/thinkphp/library/think/debug/Console.php index 8a232c8c..c17911b2 100644 --- a/thinkphp/library/think/debug/Console.php +++ b/thinkphp/library/think/debug/Console.php @@ -137,7 +137,7 @@ JS; } break; case '错误': - $msg = str_replace("\n", '\n', $m); + $msg = str_replace("\n", '\n', json_encode($m)); $style = 'color:#F4006B;font-size:14px;'; $line[] = "console.error(\"%c{$msg}\", \"{$style}\");"; break; diff --git a/thinkphp/library/think/debug/Html.php b/thinkphp/library/think/debug/Html.php index f8651aa9..b6be7adb 100644 --- a/thinkphp/library/think/debug/Html.php +++ b/thinkphp/library/think/debug/Html.php @@ -53,7 +53,7 @@ class Html return false; } // 获取基本信息 - $runtime = number_format(microtime(true) - THINK_START_TIME, 10); + $runtime = number_format(microtime(true) - THINK_START_TIME, 10, '.', ''); $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞'; $mem = number_format((memory_get_usage() - THINK_START_MEM) / 1024, 2); diff --git a/thinkphp/library/think/exception/DbException.php b/thinkphp/library/think/exception/DbException.php index 656d6913..0ae80ad1 100644 --- a/thinkphp/library/think/exception/DbException.php +++ b/thinkphp/library/think/exception/DbException.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -36,6 +36,7 @@ class DbException extends Exception 'Error SQL' => $sql, ]); + unset($config['username'], $config['password']); $this->setData('Database Config', $config); } diff --git a/thinkphp/library/think/exception/ErrorException.php b/thinkphp/library/think/exception/ErrorException.php index e3f18375..b3a9a30a 100644 --- a/thinkphp/library/think/exception/ErrorException.php +++ b/thinkphp/library/think/exception/ErrorException.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- diff --git a/thinkphp/library/think/exception/Handle.php b/thinkphp/library/think/exception/Handle.php index 8b6e6c40..f523db09 100644 --- a/thinkphp/library/think/exception/Handle.php +++ b/thinkphp/library/think/exception/Handle.php @@ -21,11 +21,16 @@ use think\Response; class Handle { - + protected $render; protected $ignoreReport = [ '\\think\\exception\\HttpException', ]; + public function setRender($render) + { + $this->render = $render; + } + /** * Report or log an exception. * @@ -52,6 +57,10 @@ class Handle $log = "[{$data['code']}]{$data['message']}"; } + if (Config::get('record_trace')) { + $log .= "\r\n" . $exception->getTraceAsString(); + } + Log::record($log, 'error'); } } @@ -74,6 +83,13 @@ class Handle */ public function render(Exception $e) { + if ($this->render && $this->render instanceof \Closure) { + $result = call_user_func_array($this->render, [$e]); + if ($result) { + return $result; + } + } + if ($e instanceof HttpException) { return $this->renderHttpException($e); } else { diff --git a/thinkphp/library/think/exception/PDOException.php b/thinkphp/library/think/exception/PDOException.php index ebd53dff..044f82a0 100644 --- a/thinkphp/library/think/exception/PDOException.php +++ b/thinkphp/library/think/exception/PDOException.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- diff --git a/thinkphp/library/think/exception/RouteNotFoundException.php b/thinkphp/library/think/exception/RouteNotFoundException.php index f85a3c4b..d22e3a63 100644 --- a/thinkphp/library/think/exception/RouteNotFoundException.php +++ b/thinkphp/library/think/exception/RouteNotFoundException.php @@ -16,7 +16,7 @@ class RouteNotFoundException extends HttpException public function __construct() { - parent::__construct(404); + parent::__construct(404, 'Route Not Found'); } } diff --git a/thinkphp/library/think/log/driver/File.php b/thinkphp/library/think/log/driver/File.php index d82a5243..f2296cfa 100644 --- a/thinkphp/library/think/log/driver/File.php +++ b/thinkphp/library/think/log/driver/File.php @@ -12,6 +12,7 @@ namespace think\log\driver; use think\App; +use think\Request; /** * 本地化调试输出到文件 @@ -20,13 +21,14 @@ class File { protected $config = [ 'time_format' => ' c ', + 'single' => false, 'file_size' => 2097152, 'path' => LOG_PATH, 'apart_level' => [], + 'max_files' => 0, + 'json' => false, ]; - protected $writed = []; - // 实例化并传入参数 public function __construct($config = []) { @@ -38,82 +40,231 @@ class File /** * 日志写入接口 * @access public - * @param array $log 日志信息 + * @param array $log 日志信息 + * @param bool $append 是否追加请求信息 * @return bool */ - public function save(array $log = []) + public function save(array $log = [], $append = false) { - $cli = IS_CLI ? '_cli' : ''; - $destination = $this->config['path'] . date('Ym') . DS . date('d') . $cli . '.log'; + $destination = $this->getMasterLogFile(); $path = dirname($destination); !is_dir($path) && mkdir($path, 0755, true); - $info = ''; + $info = []; foreach ($log as $type => $val) { - $level = ''; + foreach ($val as $msg) { if (!is_string($msg)) { $msg = var_export($msg, true); } - $level .= '[ ' . $type . ' ] ' . $msg . "\r\n"; + + $info[$type][] = $this->config['json'] ? $msg : '[ ' . $type . ' ] ' . $msg; } - if (in_array($type, $this->config['apart_level'])) { + + if (!$this->config['json'] && (true === $this->config['apart_level'] || in_array($type, $this->config['apart_level']))) { // 独立记录的日志级别 - $filename = $path . DS . date('d') . '_' . $type . $cli . '.log'; - $this->write($level, $filename, true); - } else { - $info .= $level; + $filename = $this->getApartLevelFile($path, $type); + + $this->write($info[$type], $filename, true, $append); + unset($info[$type]); } } + if ($info) { - return $this->write($info, $destination); + return $this->write($info, $destination, false, $append); } + return true; } - protected function write($message, $destination, $apart = false) + /** + * 获取主日志文件名 + * @access public + * @return string + */ + protected function getMasterLogFile() + { + if ($this->config['single']) { + $name = is_string($this->config['single']) ? $this->config['single'] : 'single'; + + $destination = $this->config['path'] . $name . '.log'; + } else { + $cli = PHP_SAPI == 'cli' ? '_cli' : ''; + + if ($this->config['max_files']) { + $filename = date('Ymd') . $cli . '.log'; + $files = glob($this->config['path'] . '*.log'); + + try { + if (count($files) > $this->config['max_files']) { + unlink($files[0]); + } + } catch (\Exception $e) { + } + } else { + $filename = date('Ym') . DIRECTORY_SEPARATOR . date('d') . $cli . '.log'; + } + + $destination = $this->config['path'] . $filename; + } + + return $destination; + } + + /** + * 获取独立日志文件名 + * @access public + * @param string $path 日志目录 + * @param string $type 日志类型 + * @return string + */ + protected function getApartLevelFile($path, $type) + { + $cli = PHP_SAPI == 'cli' ? '_cli' : ''; + + if ($this->config['single']) { + $name = is_string($this->config['single']) ? $this->config['single'] : 'single'; + + $name .= '_' . $type; + } elseif ($this->config['max_files']) { + $name = date('Ymd') . '_' . $type . $cli; + } else { + $name = date('d') . '_' . $type . $cli; + } + + return $path . DIRECTORY_SEPARATOR . $name . '.log'; + } + + /** + * 日志写入 + * @access protected + * @param array $message 日志信息 + * @param string $destination 日志文件 + * @param bool $apart 是否独立文件写入 + * @param bool $append 是否追加请求信息 + * @return bool + */ + protected function write($message, $destination, $apart = false, $append = false) + { + // 检测日志文件大小,超过配置大小则备份日志文件重新生成 + $this->checkLogSize($destination); + + // 日志信息封装 + $info['timestamp'] = date($this->config['time_format']); + + foreach ($message as $type => $msg) { + $info[$type] = is_array($msg) ? implode("\r\n", $msg) : $msg; + } + + if (PHP_SAPI == 'cli') { + $message = $this->parseCliLog($info); + } else { + // 添加调试日志 + $this->getDebugLog($info, $append, $apart); + + $message = $this->parseLog($info); + } + + return error_log($message, 3, $destination); + } + + /** + * 检查日志文件大小并自动生成备份文件 + * @access protected + * @param string $destination 日志文件 + * @return void + */ + protected function checkLogSize($destination) { - //检测日志文件大小,超过配置大小则备份日志文件重新生成 if (is_file($destination) && floor($this->config['file_size']) <= filesize($destination)) { - rename($destination, dirname($destination) . DS . time() . '-' . basename($destination)); - $this->writed[$destination] = false; + try { + rename($destination, dirname($destination) . DIRECTORY_SEPARATOR . time() . '-' . basename($destination)); + } catch (\Exception $e) { + } } + } - if (empty($this->writed[$destination]) && !IS_CLI) { - if (App::$debug && !$apart) { + /** + * CLI日志解析 + * @access protected + * @param array $info 日志信息 + * @return string + */ + protected function parseCliLog($info) + { + if ($this->config['json']) { + $message = json_encode($info, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\r\n"; + } else { + $now = $info['timestamp']; + unset($info['timestamp']); + + $message = implode("\r\n", $info); + + $message = "[{$now}]" . $message . "\r\n"; + } + + return $message; + } + + /** + * 解析日志 + * @access protected + * @param array $info 日志信息 + * @return string + */ + protected function parseLog($info) + { + $request = Request::instance(); + $requestInfo = [ + 'ip' => $request->ip(), + 'method' => $request->method(), + 'host' => $request->host(), + 'uri' => $request->url(), + ]; + + if ($this->config['json']) { + $info = $requestInfo + $info; + return json_encode($info, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\r\n"; + } + + array_unshift($info, "---------------------------------------------------------------\r\n[{$info['timestamp']}] {$requestInfo['ip']} {$requestInfo['method']} {$requestInfo['host']}{$requestInfo['uri']}"); + unset($info['timestamp']); + + return implode("\r\n", $info) . "\r\n"; + } + + protected function getDebugLog(&$info, $append, $apart) + { + if (App::$debug && $append) { + + if ($this->config['json']) { // 获取基本信息 - if (isset($_SERVER['HTTP_HOST'])) { - $current_uri = $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; - } else { - $current_uri = "cmd:" . implode(' ', $_SERVER['argv']); - } + $runtime = round(microtime(true) - THINK_START_TIME, 10); + $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞'; - $runtime = round(microtime(true) - THINK_START_TIME, 10); - $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞'; - $time_str = ' [运行时间:' . number_format($runtime, 6) . 's][吞吐率:' . $reqs . 'req/s]'; $memory_use = number_format((memory_get_usage() - THINK_START_MEM) / 1024, 2); + + $info = [ + 'runtime' => number_format($runtime, 6) . 's', + 'reqs' => $reqs . 'req/s', + 'memory' => $memory_use . 'kb', + 'file' => count(get_included_files()), + ] + $info; + + } elseif (!$apart) { + // 增加额外的调试信息 + $runtime = round(microtime(true) - THINK_START_TIME, 10); + $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞'; + + $memory_use = number_format((memory_get_usage() - THINK_START_MEM) / 1024, 2); + + $time_str = '[运行时间:' . number_format($runtime, 6) . 's] [吞吐率:' . $reqs . 'req/s]'; $memory_str = ' [内存消耗:' . $memory_use . 'kb]'; $file_load = ' [文件加载:' . count(get_included_files()) . ']'; - $message = '[ info ] ' . $current_uri . $time_str . $memory_str . $file_load . "\r\n" . $message; + array_unshift($info, $time_str . $memory_str . $file_load); } - $now = date($this->config['time_format']); - $server = isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : '0.0.0.0'; - $remote = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0'; - $method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'CLI'; - $uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''; - $message = "---------------------------------------------------------------\r\n[{$now}] {$server} {$remote} {$method} {$uri}\r\n" . $message; - - $this->writed[$destination] = true; } - - if (IS_CLI) { - $now = date($this->config['time_format']); - $message = "[{$now}]" . $message; - } - - return error_log($message, 3, $destination); } - } diff --git a/thinkphp/library/think/log/driver/Socket.php b/thinkphp/library/think/log/driver/Socket.php index d30bba30..4f62915b 100644 --- a/thinkphp/library/think/log/driver/Socket.php +++ b/thinkphp/library/think/log/driver/Socket.php @@ -60,7 +60,7 @@ class Socket * @param array $log 日志信息 * @return bool */ - public function save(array $log = []) + public function save(array $log = [], $append = false) { if (!$this->check()) { return false; diff --git a/thinkphp/library/think/model/Collection.php b/thinkphp/library/think/model/Collection.php index 4e4bb4dc..0406533c 100644 --- a/thinkphp/library/think/model/Collection.php +++ b/thinkphp/library/think/model/Collection.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -16,20 +16,6 @@ use think\Model; class Collection extends BaseCollection { - /** - * 返回数组中指定的一列 - * @param string $column_key - * @param string|null $index_key - * @return array - */ - public function column($column_key, $index_key = null) - { - if (function_exists('array_column')) { - return array_column($this->toArray(), $column_key, $index_key); - } - return parent::column($column_key, $index_key); - } - /** * 延迟预载入关联查询 * @access public @@ -85,7 +71,7 @@ class Collection extends BaseCollection { $this->each(function ($model) use ($append, $override) { /** @var Model $model */ - $model->append($append, $override); + $model && $model->append($append, $override); }); return $this; } diff --git a/thinkphp/library/think/model/Merge.php b/thinkphp/library/think/model/Merge.php index d944979e..4a9da81e 100644 --- a/thinkphp/library/think/model/Merge.php +++ b/thinkphp/library/think/model/Merge.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- diff --git a/thinkphp/library/think/model/Pivot.php b/thinkphp/library/think/model/Pivot.php index 06c6b181..13525cdd 100644 --- a/thinkphp/library/think/model/Pivot.php +++ b/thinkphp/library/think/model/Pivot.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -24,11 +24,11 @@ class Pivot extends Model /** * 架构函数 * @access public - * @param Model $parent 上级模型 * @param array|object $data 数据 + * @param Model $parent 上级模型 * @param string $table 中间数据表名 */ - public function __construct(Model $parent, $data = [], $table = '') + public function __construct($data = [], Model $parent = null, $table = '') { $this->parent = $parent; @@ -37,8 +37,6 @@ class Pivot extends Model } parent::__construct($data); - - $this->class = $this->name; } } diff --git a/thinkphp/library/think/model/Relation.php b/thinkphp/library/think/model/Relation.php index 4869e526..25fe88db 100644 --- a/thinkphp/library/think/model/Relation.php +++ b/thinkphp/library/think/model/Relation.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -35,6 +35,8 @@ abstract class Relation protected $localKey; // 基础查询 protected $baseQuery; + // 是否为自关联 + protected $selfRelation; /** * 获取关联的所属模型 @@ -47,13 +49,13 @@ abstract class Relation } /** - * 获取当前的关联模型类 + * 获取当前的关联模型对象实例 * @access public - * @return string + * @return Model */ public function getModel() { - return $this->model; + return $this->query->getModel(); } /** @@ -66,6 +68,28 @@ abstract class Relation return $this->query; } + /** + * 设置当前关联为自关联 + * @access public + * @param bool $self 是否自关联 + * @return $this + */ + public function selfRelation($self = true) + { + $this->selfRelation = $self; + return $this; + } + + /** + * 当前关联是否为自关联 + * @access public + * @return bool + */ + public function isSelfRelation() + { + return $this->selfRelation; + } + /** * 封装关联数据集 * @access public @@ -108,7 +132,8 @@ abstract class Relation * @access protected * @return void */ - abstract protected function baseQuery(); + protected function baseQuery() + {} public function __call($method, $args) { diff --git a/thinkphp/library/think/model/relation/BelongsTo.php b/thinkphp/library/think/model/relation/BelongsTo.php index 905ea944..c1cbab97 100644 --- a/thinkphp/library/think/model/relation/BelongsTo.php +++ b/thinkphp/library/think/model/relation/BelongsTo.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -11,6 +11,7 @@ namespace think\model\relation; +use think\db\Query; use think\Loader; use think\Model; @@ -51,6 +52,7 @@ class BelongsTo extends OneToOne call_user_func_array($closure, [ & $this->query]); } $relationModel = $this->query + ->removeWhereField($this->localKey) ->where($this->localKey, $this->parent->$foreignKey) ->relation($subRelation) ->find(); @@ -68,7 +70,6 @@ class BelongsTo extends OneToOne * @param string $operator 比较操作符 * @param integer $count 个数 * @param string $id 关联表的统计字段 - * @param string $joinType JOIN类型 * @return Query */ public function has($operator = '>=', $count = 1, $id = '*') @@ -79,14 +80,16 @@ class BelongsTo extends OneToOne /** * 根据关联条件查询当前模型 * @access public - * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 * @return Query */ - public function hasWhere($where = []) + public function hasWhere($where = [], $fields = null) { $table = $this->query->getTable(); $model = basename(str_replace('\\', '/', get_class($this->parent))); $relation = basename(str_replace('\\', '/', $this->model)); + if (is_array($where)) { foreach ($where as $key => $val) { if (false === strpos($key, '.')) { @@ -95,9 +98,11 @@ class BelongsTo extends OneToOne } } } + $fields = $this->getRelationQueryFields($fields, $model); + return $this->parent->db()->alias($model) - ->field($model . '.*') - ->join($table . ' ' . $relation, $model . '.' . $this->foreignKey . '=' . $relation . '.' . $this->localKey, $this->joinType) + ->field($fields) + ->join([$table => $relation], $model . '.' . $this->foreignKey . '=' . $relation . '.' . $this->localKey, $this->joinType) ->where($where); } @@ -124,7 +129,8 @@ class BelongsTo extends OneToOne } if (!empty($range)) { - $data = $this->eagerlyWhere($this, [ + $this->query->removeWhereField($localKey); + $data = $this->eagerlyWhere($this->query, [ $localKey => [ 'in', $range, @@ -143,12 +149,13 @@ class BelongsTo extends OneToOne $relationModel->isUpdate(true); } - if ($relationModel && !empty($this->bindAttr)) { + if (!empty($this->bindAttr)) { // 绑定关联属性 $this->bindAttr($relationModel, $result, $this->bindAttr); + } else { + // 设置关联属性 + $result->setRelation($attr, $relationModel); } - // 设置关联属性 - $result->setRelation($attr, $relationModel); } } } @@ -166,7 +173,8 @@ class BelongsTo extends OneToOne { $localKey = $this->localKey; $foreignKey = $this->foreignKey; - $data = $this->eagerlyWhere($this, [$localKey => $result->$foreignKey], $localKey, $relation, $subRelation, $closure); + $this->query->removeWhereField($localKey); + $data = $this->eagerlyWhere($this->query, [$localKey => $result->$foreignKey], $localKey, $relation, $subRelation, $closure); // 关联模型 if (!isset($data[$result->$foreignKey])) { $relationModel = null; @@ -175,12 +183,13 @@ class BelongsTo extends OneToOne $relationModel->setParent(clone $result); $relationModel->isUpdate(true); } - if ($relationModel && !empty($this->bindAttr)) { + if (!empty($this->bindAttr)) { // 绑定关联属性 $this->bindAttr($relationModel, $result, $this->bindAttr); + } else { + // 设置关联属性 + $result->setRelation(Loader::parseName($relation), $relationModel); } - // 设置关联属性 - $result->setRelation(Loader::parseName($relation), $relationModel); } /** @@ -214,4 +223,21 @@ class BelongsTo extends OneToOne return $this->parent->setRelation($this->relation, null); } + + /** + * 执行基础查询(仅执行一次) + * @access protected + * @return void + */ + protected function baseQuery() + { + if (empty($this->baseQuery)) { + if (isset($this->parent->{$this->foreignKey})) { + // 关联查询带入关联条件 + $this->query->where($this->localKey, '=', $this->parent->{$this->foreignKey}); + } + + $this->baseQuery = true; + } + } } diff --git a/thinkphp/library/think/model/relation/BelongsToMany.php b/thinkphp/library/think/model/relation/BelongsToMany.php index 71ab4266..a41c45ce 100644 --- a/thinkphp/library/think/model/relation/BelongsToMany.php +++ b/thinkphp/library/think/model/relation/BelongsToMany.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -12,6 +12,7 @@ namespace think\model\relation; use think\Collection; +use think\Db; use think\db\Query; use think\Exception; use think\Loader; @@ -28,6 +29,8 @@ class BelongsToMany extends Relation protected $pivotName; // 中间表模型对象 protected $pivot; + // 中间表数据名称 + protected $pivotDataName = 'pivot'; /** * 构造函数 @@ -52,6 +55,10 @@ class BelongsToMany extends Relation } $this->query = (new $model)->db(); $this->pivot = $this->newPivot(); + + if ('think\model\Pivot' == get_class($this->pivot)) { + $this->pivot->name($this->middle); + } } /** @@ -66,14 +73,46 @@ class BelongsToMany extends Relation } /** - * 实例化中间表模型 + * 设置中间表数据名称 + * @access public + * @param string $name + * @return $this + */ + public function pivotDataName($name) + { + $this->pivotDataName = $name; + return $this; + } + + /** + * 获取中间表更新条件 * @param $data - * @return mixed + * @return array */ - protected function newPivot($data = []) + protected function getUpdateWhere($data) { - $pivot = $this->pivotName ?: '\\think\\model\\Pivot'; - return new $pivot($this->parent, $data, $this->middle); + return [ + $this->localKey => $data[$this->localKey], + $this->foreignKey => $data[$this->foreignKey], + ]; + } + + /** + * 实例化中间表模型 + * @param array $data + * @param bool $isUpdate + * @return Pivot + * @throws Exception + */ + protected function newPivot($data = [], $isUpdate = false) + { + $class = $this->pivotName ?: '\\think\\model\\Pivot'; + $pivot = new $class($data, $this->parent, $this->middle); + if ($pivot instanceof Pivot) { + return $isUpdate ? $pivot->isUpdate(true, $this->getUpdateWhere($data)) : $pivot; + } else { + throw new Exception('pivot model must extends: \think\model\Pivot'); + } } /** @@ -93,7 +132,7 @@ class BelongsToMany extends Relation } } } - $model->setRelation('pivot', $this->newPivot($pivot)); + $model->setRelation($this->pivotDataName, $this->newPivot($pivot, true)); } } @@ -105,9 +144,8 @@ class BelongsToMany extends Relation { $foreignKey = $this->foreignKey; $localKey = $this->localKey; - $middle = $this->middle; + $pk = $this->parent->getPk(); // 关联查询 - $pk = $this->parent->getPk(); $condition['pivot.' . $localKey] = $this->parent->$pk; return $this->belongsToManyQuery($foreignKey, $localKey, $condition); } @@ -207,11 +245,12 @@ class BelongsToMany extends Relation /** * 根据关联条件查询当前模型 * @access public - * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 * @return Query * @throws Exception */ - public function hasWhere($where = []) + public function hasWhere($where = [], $fields = null) { throw new Exception('relation not support: hasWhere'); } @@ -321,14 +360,22 @@ class BelongsToMany extends Relation * 获取关联统计子查询 * @access public * @param \Closure $closure 闭包 + * @param string $name 统计数据别名 * @return string */ - public function getRelationCountQuery($closure) + public function getRelationCountQuery($closure, &$name = null) { + if ($closure) { + $return = call_user_func_array($closure, [ & $this->query]); + if ($return && is_string($return)) { + $name = $return; + } + } + return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [ 'pivot.' . $this->localKey => [ 'exp', - '=' . $this->parent->getTable() . '.' . $this->parent->getPk(), + Db::raw('=' . $this->parent->getTable() . '.' . $this->parent->getPk()), ], ])->fetchSql()->count(); } @@ -359,7 +406,7 @@ class BelongsToMany extends Relation } } } - $set->setRelation('pivot', $this->newPivot($pivot)); + $set->setRelation($this->pivotDataName, $this->newPivot($pivot, true)); $data[$pivot[$this->localKey]][] = $set; } return $data; @@ -385,7 +432,7 @@ class BelongsToMany extends Relation if (empty($this->baseQuery)) { $relationFk = $this->query->getPk(); - $query->join($table . ' pivot', 'pivot.' . $foreignKey . '=' . $tableName . '.' . $relationFk) + $query->join([$table => 'pivot'], 'pivot.' . $foreignKey . '=' . $tableName . '.' . $relationFk) ->where($condition); } return $query; @@ -462,7 +509,7 @@ class BelongsToMany extends Relation foreach ($ids as $id) { $pivot[$this->foreignKey] = $id; $this->pivot->insert($pivot, true); - $result[] = $this->newPivot($pivot); + $result[] = $this->newPivot($pivot, true); } if (count($result) == 1) { // 返回中间表模型对象 @@ -474,6 +521,29 @@ class BelongsToMany extends Relation } } + /** + * 判断是否存在关联数据 + * @access public + * @param mixed $data 数据 可以使用关联模型对象 或者 关联对象的主键 + * @return Pivot + * @throws Exception + */ + public function attached($data) + { + if ($data instanceof Model) { + $relationFk = $data->getPk(); + $id = $data->$relationFk; + } else { + $id = $data; + } + + $pk = $this->parent->getPk(); + + $pivot = $this->pivot->where($this->localKey, $this->parent->$pk)->where($this->foreignKey, $id)->find(); + + return $pivot ?: false; + } + /** * 解除关联的一个中间表数据 * @access public @@ -566,7 +636,7 @@ class BelongsToMany extends Relation if (empty($this->baseQuery) && $this->parent->getData()) { $pk = $this->parent->getPk(); $table = $this->pivot->getTable(); - $this->query->join($table . ' pivot', 'pivot.' . $this->foreignKey . '=' . $this->query->getTable() . '.' . $this->query->getPk())->where('pivot.' . $this->localKey, $this->parent->$pk); + $this->query->join([$table => 'pivot'], 'pivot.' . $this->foreignKey . '=' . $this->query->getTable() . '.' . $this->query->getPk())->where('pivot.' . $this->localKey, $this->parent->$pk); $this->baseQuery = true; } } diff --git a/thinkphp/library/think/model/relation/HasMany.php b/thinkphp/library/think/model/relation/HasMany.php index 9624fc67..ebab051a 100644 --- a/thinkphp/library/think/model/relation/HasMany.php +++ b/thinkphp/library/think/model/relation/HasMany.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -24,7 +24,7 @@ class HasMany extends Relation * @param Model $parent 上级模型对象 * @param string $model 模型名 * @param string $foreignKey 关联外键 - * @param string $localKey 关联主键 + * @param string $localKey 当前模型主键 */ public function __construct(Model $parent, $model, $foreignKey, $localKey) { @@ -77,7 +77,7 @@ class HasMany extends Relation } if (!empty($range)) { - $data = $this->eagerlyOneToMany($this, [ + $data = $this->eagerlyOneToMany($this->query, [ $this->foreignKey => [ 'in', $range, @@ -114,7 +114,7 @@ class HasMany extends Relation $localKey = $this->localKey; if (isset($result->$localKey)) { - $data = $this->eagerlyOneToMany($this, [$this->foreignKey => $result->$localKey], $relation, $subRelation, $closure); + $data = $this->eagerlyOneToMany($this->query, [$this->foreignKey => $result->$localKey], $relation, $subRelation, $closure); // 关联数据封装 if (!isset($data[$result->$localKey])) { $data[$result->$localKey] = []; @@ -143,7 +143,7 @@ class HasMany extends Relation if ($closure) { call_user_func_array($closure, [ & $this->query]); } - $count = $this->query->where([$this->foreignKey => $result->$localKey])->count(); + $count = $this->query->where($this->foreignKey, $result->$localKey)->count(); } return $count; } @@ -152,20 +152,19 @@ class HasMany extends Relation * 创建关联统计子查询 * @access public * @param \Closure $closure 闭包 + * @param string $name 统计数据别名 * @return string */ - public function getRelationCountQuery($closure) + public function getRelationCountQuery($closure, &$name = null) { if ($closure) { - call_user_func_array($closure, [ & $this->query]); + $return = call_user_func_array($closure, [ & $this->query]); + if ($return && is_string($return)) { + $name = $return; + } } - - return $this->query->where([ - $this->foreignKey => [ - 'exp', - '=' . $this->parent->getTable() . '.' . $this->parent->getPk(), - ], - ])->fetchSql()->count(); + $localKey = $this->localKey ?: $this->parent->getPk(); + return $this->query->whereExp($this->foreignKey, '=' . $this->parent->getTable() . '.' . $localKey)->fetchSql()->count(); } /** @@ -185,7 +184,7 @@ class HasMany extends Relation if ($closure) { call_user_func_array($closure, [ & $model]); } - $list = $model->where($where)->with($subRelation)->select(); + $list = $model->removeWhereField($foreignKey)->where($where)->with($subRelation)->select(); // 组装模型数据 $data = []; @@ -206,12 +205,31 @@ class HasMany extends Relation if ($data instanceof Model) { $data = $data->getData(); } + // 保存关联表数据 - $model = new $this->model; $data[$this->foreignKey] = $this->parent->{$this->localKey}; + + $model = new $this->model(); return $model->save($data) ? $model : false; } + /** + * 创建关联对象实例 + * @param array $data + * @return Model + */ + public function make($data = []) + { + if ($data instanceof Model) { + $data = $data->getData(); + } + + // 保存关联表数据 + $data[$this->foreignKey] = $this->parent->{$this->localKey}; + + return new $this->model($data); + } + /** * 批量保存当前关联数据对象 * @access public @@ -238,24 +256,31 @@ class HasMany extends Relation */ public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') { - $table = $this->query->getTable(); - return $this->parent->db()->alias('a') - ->join($table . ' b', 'a.' . $this->localKey . '=b.' . $this->foreignKey, $joinType) - ->group('b.' . $this->foreignKey) + $table = $this->query->getTable(); + $model = basename(str_replace('\\', '/', get_class($this->parent))); + $relation = basename(str_replace('\\', '/', $this->model)); + + return $this->parent->db() + ->alias($model) + ->field($model . '.*') + ->join([$table => $relation], $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey, $joinType) + ->group($relation . '.' . $this->foreignKey) ->having('count(' . $id . ')' . $operator . $count); } /** * 根据关联条件查询当前模型 * @access public - * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 * @return Query */ - public function hasWhere($where = []) + public function hasWhere($where = [], $fields = null) { $table = $this->query->getTable(); $model = basename(str_replace('\\', '/', get_class($this->parent))); $relation = basename(str_replace('\\', '/', $this->model)); + if (is_array($where)) { foreach ($where as $key => $val) { if (false === strpos($key, '.')) { @@ -264,9 +289,13 @@ class HasMany extends Relation } } } + + $fields = $this->getRelationQueryFields($fields, $model); + return $this->parent->db()->alias($model) - ->field($model . '.*') - ->join($table . ' ' . $relation, $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey) + ->field($fields) + ->group($model . '.' . $this->localKey) + ->join([$table => $relation], $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey) ->where($where); } diff --git a/thinkphp/library/think/model/relation/HasManyThrough.php b/thinkphp/library/think/model/relation/HasManyThrough.php index 1573fc65..3a9a5482 100644 --- a/thinkphp/library/think/model/relation/HasManyThrough.php +++ b/thinkphp/library/think/model/relation/HasManyThrough.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -77,10 +77,11 @@ class HasManyThrough extends Relation /** * 根据关联条件查询当前模型 * @access public - * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 * @return Query */ - public function hasWhere($where = []) + public function hasWhere($where = [], $fields = null) { throw new Exception('relation not support: hasWhere'); } @@ -92,10 +93,9 @@ class HasManyThrough extends Relation * @param string $relation 当前关联名 * @param string $subRelation 子关联名 * @param \Closure $closure 闭包 - * @param string $class 数据集对象名 为空表示数组 * @return void */ - public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure, $class) + public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure) {} /** @@ -105,10 +105,9 @@ class HasManyThrough extends Relation * @param string $relation 当前关联名 * @param string $subRelation 子关联名 * @param \Closure $closure 闭包 - * @param string $class 数据集对象名 为空表示数组 * @return void */ - public function eagerlyResult(&$result, $relation, $subRelation, $closure, $class) + public function eagerlyResult(&$result, $relation, $subRelation, $closure) {} /** @@ -121,6 +120,18 @@ class HasManyThrough extends Relation public function relationCount($result, $closure) {} + /** + * 创建关联统计子查询 + * @access public + * @param \Closure $closure 闭包 + * @param string $name 统计数据别名 + * @return string + */ + public function getRelationCountQuery($closure, &$name = null) + { + throw new Exception('relation not support: withCount'); + } + /** * 执行基础查询(进执行一次) * @access protected diff --git a/thinkphp/library/think/model/relation/HasOne.php b/thinkphp/library/think/model/relation/HasOne.php index c500c02a..db74e4a6 100644 --- a/thinkphp/library/think/model/relation/HasOne.php +++ b/thinkphp/library/think/model/relation/HasOne.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -23,7 +23,7 @@ class HasOne extends OneToOne * @param Model $parent 上级模型对象 * @param string $model 模型名 * @param string $foreignKey 关联外键 - * @param string $localKey 关联主键 + * @param string $localKey 当前模型主键 * @param string $joinType JOIN类型 */ public function __construct(Model $parent, $model, $foreignKey, $localKey, $joinType = 'INNER') @@ -50,7 +50,11 @@ class HasOne extends OneToOne call_user_func_array($closure, [ & $this->query]); } // 判断关联类型执行查询 - $relationModel = $this->query->where($this->foreignKey, $this->parent->$localKey)->relation($subRelation)->find(); + $relationModel = $this->query + ->removeWhereField($this->foreignKey) + ->where($this->foreignKey, $this->parent->$localKey) + ->relation($subRelation) + ->find(); if ($relationModel) { $relationModel->setParent(clone $this->parent); @@ -67,25 +71,30 @@ class HasOne extends OneToOne public function has() { $table = $this->query->getTable(); + $model = basename(str_replace('\\', '/', get_class($this->parent))); + $relation = basename(str_replace('\\', '/', $this->model)); $localKey = $this->localKey; $foreignKey = $this->foreignKey; - return $this->parent->db()->alias('a') - ->whereExists(function ($query) use ($table, $localKey, $foreignKey) { - $query->table([$table => 'b'])->field('b.' . $foreignKey)->whereExp('a.' . $localKey, '=b.' . $foreignKey); + return $this->parent->db() + ->alias($model) + ->whereExists(function ($query) use ($table, $model, $relation, $localKey, $foreignKey) { + $query->table([$table => $relation])->field($relation . '.' . $foreignKey)->whereExp($model . '.' . $localKey, '=' . $relation . '.' . $foreignKey); }); } /** * 根据关联条件查询当前模型 * @access public - * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 * @return Query */ - public function hasWhere($where = []) + public function hasWhere($where = [], $fields = null) { $table = $this->query->getTable(); $model = basename(str_replace('\\', '/', get_class($this->parent))); $relation = basename(str_replace('\\', '/', $this->model)); + if (is_array($where)) { foreach ($where as $key => $val) { if (false === strpos($key, '.')) { @@ -94,9 +103,11 @@ class HasOne extends OneToOne } } } + $fields = $this->getRelationQueryFields($fields, $model); + return $this->parent->db()->alias($model) - ->field($model . '.*') - ->join($table . ' ' . $relation, $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey, $this->joinType) + ->field($fields) + ->join([$table => $relation], $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey, $this->joinType) ->where($where); } @@ -123,7 +134,8 @@ class HasOne extends OneToOne } if (!empty($range)) { - $data = $this->eagerlyWhere($this, [ + $this->query->removeWhereField($foreignKey); + $data = $this->eagerlyWhere($this->query, [ $foreignKey => [ 'in', $range, @@ -140,13 +152,14 @@ class HasOne extends OneToOne $relationModel = $data[$result->$localKey]; $relationModel->setParent(clone $result); $relationModel->isUpdate(true); - if (!empty($this->bindAttr)) { - // 绑定关联属性 - $this->bindAttr($relationModel, $result, $this->bindAttr); - } } - // 设置关联属性 - $result->setRelation($attr, $relationModel); + if (!empty($this->bindAttr)) { + // 绑定关联属性 + $this->bindAttr($relationModel, $result, $this->bindAttr); + } else { + // 设置关联属性 + $result->setRelation($attr, $relationModel); + } } } } @@ -164,7 +177,8 @@ class HasOne extends OneToOne { $localKey = $this->localKey; $foreignKey = $this->foreignKey; - $data = $this->eagerlyWhere($this, [$foreignKey => $result->$localKey], $foreignKey, $relation, $subRelation, $closure); + $this->query->removeWhereField($foreignKey); + $data = $this->eagerlyWhere($this->query, [$foreignKey => $result->$localKey], $foreignKey, $relation, $subRelation, $closure); // 关联模型 if (!isset($data[$result->$localKey])) { @@ -173,13 +187,29 @@ class HasOne extends OneToOne $relationModel = $data[$result->$localKey]; $relationModel->setParent(clone $result); $relationModel->isUpdate(true); - if (!empty($this->bindAttr)) { - // 绑定关联属性 - $this->bindAttr($relationModel, $result, $this->bindAttr); - } } - - $result->setRelation(Loader::parseName($relation), $relationModel); + if (!empty($this->bindAttr)) { + // 绑定关联属性 + $this->bindAttr($relationModel, $result, $this->bindAttr); + } else { + $result->setRelation(Loader::parseName($relation), $relationModel); + } } + /** + * 执行基础查询(仅执行一次) + * @access protected + * @return void + */ + protected function baseQuery() + { + if (empty($this->baseQuery)) { + if (isset($this->parent->{$this->localKey})) { + // 关联查询带入关联条件 + $this->query->where($this->foreignKey, '=', $this->parent->{$this->localKey}); + } + + $this->baseQuery = true; + } + } } diff --git a/thinkphp/library/think/model/relation/MorphMany.php b/thinkphp/library/think/model/relation/MorphMany.php index 87c2d66c..2755d575 100644 --- a/thinkphp/library/think/model/relation/MorphMany.php +++ b/thinkphp/library/think/model/relation/MorphMany.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -11,6 +11,7 @@ namespace think\model\relation; +use think\Db; use think\db\Query; use think\Exception; use think\Loader; @@ -82,10 +83,11 @@ class MorphMany extends Relation /** * 根据关联条件查询当前模型 * @access public - * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 * @return Query */ - public function hasWhere($where = []) + public function hasWhere($where = [], $fields = null) { throw new Exception('relation not support: hasWhere'); } @@ -186,21 +188,25 @@ class MorphMany extends Relation } /** - * 获取关联统计子查询 + * 创建关联统计子查询 * @access public * @param \Closure $closure 闭包 + * @param string $name 统计数据别名 * @return string */ - public function getRelationCountQuery($closure) + public function getRelationCountQuery($closure, &$name = null) { if ($closure) { - call_user_func_array($closure, [ & $this->query]); + $return = call_user_func_array($closure, [ & $this->query]); + if ($return && is_string($return)) { + $name = $return; + } } return $this->query->where([ $this->morphKey => [ 'exp', - '=' . $this->parent->getTable() . '.' . $this->parent->getPk(), + Db::raw('=' . $this->parent->getTable() . '.' . $this->parent->getPk()), ], $this->morphType => $this->type, ])->fetchSql()->count(); @@ -242,13 +248,36 @@ class MorphMany extends Relation if ($data instanceof Model) { $data = $data->getData(); } + + // 保存关联表数据 + $pk = $this->parent->getPk(); + + $data[$this->morphKey] = $this->parent->$pk; + $data[$this->morphType] = $this->type; + + $model = new $this->model(); + + return $model->save() ? $model : false; + } + + /** + * 创建关联对象实例 + * @param array $data + * @return Model + */ + public function make($data = []) + { + if ($data instanceof Model) { + $data = $data->getData(); + } + // 保存关联表数据 $pk = $this->parent->getPk(); - $model = new $this->model; $data[$this->morphKey] = $this->parent->$pk; $data[$this->morphType] = $this->type; - return $model->save($data) ? $model : false; + + return new $this->model($data); } /** diff --git a/thinkphp/library/think/model/relation/MorphOne.php b/thinkphp/library/think/model/relation/MorphOne.php index 4dfd1c0f..5ec71724 100644 --- a/thinkphp/library/think/model/relation/MorphOne.php +++ b/thinkphp/library/think/model/relation/MorphOne.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -81,10 +81,11 @@ class MorphOne extends Relation /** * 根据关联条件查询当前模型 * @access public - * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 * @return Query */ - public function hasWhere($where = []) + public function hasWhere($where = [], $fields = null) { throw new Exception('relation not support: hasWhere'); } @@ -197,6 +198,28 @@ class MorphOne extends Relation * @return Model|false */ public function save($data) + { + if ($data instanceof Model) { + $data = $data->getData(); + } + + // 保存关联表数据 + $pk = $this->parent->getPk(); + + $data[$this->morphKey] = $this->parent->$pk; + $data[$this->morphType] = $this->type; + + $model = new $this->model(); + + return $model->save() ? $model : false; + } + + /** + * 创建关联对象实例 + * @param array $data + * @return Model + */ + public function make($data = []) { if ($data instanceof Model) { $data = $data->getData(); @@ -204,10 +227,10 @@ class MorphOne extends Relation // 保存关联表数据 $pk = $this->parent->getPk(); - $model = new $this->model; $data[$this->morphKey] = $this->parent->$pk; $data[$this->morphType] = $this->type; - return $model->save($data) ? $model : false; + + return new $this->model($data); } /** @@ -226,4 +249,15 @@ class MorphOne extends Relation } } + /** + * 创建关联统计子查询 + * @access public + * @param \Closure $closure 闭包 + * @param string $name 统计数据别名 + * @return string + */ + public function getRelationCountQuery($closure, &$name = null) + { + throw new Exception('relation not support: withCount'); + } } diff --git a/thinkphp/library/think/model/relation/MorphTo.php b/thinkphp/library/think/model/relation/MorphTo.php index df104a90..7d452651 100644 --- a/thinkphp/library/think/model/relation/MorphTo.php +++ b/thinkphp/library/think/model/relation/MorphTo.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -43,6 +43,18 @@ class MorphTo extends Relation $this->relation = $relation; } + /** + * 获取当前的关联模型类的实例 + * @access public + * @return Model + */ + public function getModel() + { + $morphType = $this->morphType; + $model = $this->parseModel($this->parent->$morphType); + return (new $model); + } + /** * 延迟获取关联数据 * @param string $subRelation 子关联名 @@ -82,17 +94,18 @@ class MorphTo extends Relation /** * 根据关联条件查询当前模型 * @access public - * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 * @return Query */ - public function hasWhere($where = []) + public function hasWhere($where = [], $fields = null) { throw new Exception('relation not support: hasWhere'); } /** * 解析模型的完整命名空间 - * @access public + * @access protected * @param string $model 模型名(或者完整类名) * @return string */ @@ -238,17 +251,18 @@ class MorphTo extends Relation /** * 添加关联数据 * @access public - * @param Model $model 关联模型对象 + * @param Model $model 关联模型对象 + * @param string $type 多态类型 * @return Model */ - public function associate($model) + public function associate($model, $type = '') { $morphKey = $this->morphKey; $morphType = $this->morphType; $pk = $model->getPk(); $this->parent->setAttr($morphKey, $model->$pk); - $this->parent->setAttr($morphType, get_class($model)); + $this->parent->setAttr($morphType, $type ?: get_class($model)); $this->parent->save(); return $this->parent->setRelation($this->relation, $model); @@ -272,10 +286,14 @@ class MorphTo extends Relation } /** - * 执行基础查询(进执行一次) - * @access protected - * @return void + * 创建关联统计子查询 + * @access public + * @param \Closure $closure 闭包 + * @param string $name 统计数据别名 + * @return string */ - protected function baseQuery() - {} + public function getRelationCountQuery($closure, &$name = null) + { + throw new Exception('relation not support: withCount'); + } } diff --git a/thinkphp/library/think/model/relation/OneToOne.php b/thinkphp/library/think/model/relation/OneToOne.php index ff066ed3..353ce21b 100644 --- a/thinkphp/library/think/model/relation/OneToOne.php +++ b/thinkphp/library/think/model/relation/OneToOne.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -57,18 +57,18 @@ abstract class OneToOne extends Relation */ public function eagerly(Query $query, $relation, $subRelation, $closure, $first) { - $name = Loader::parseName(basename(str_replace('\\', '/', $query->getModel()))); - $alias = $name; + $name = Loader::parseName(basename(str_replace('\\', '/', get_class($query->getModel())))); + if ($first) { $table = $query->getTable(); - $query->table([$table => $alias]); + $query->table([$table => $name]); if ($query->getOptions('field')) { $field = $query->getOptions('field'); $query->removeOption('field'); } else { $field = true; } - $query->field($field, false, $table, $alias); + $query->field($field, false, $table, $name); $field = null; } @@ -78,9 +78,9 @@ abstract class OneToOne extends Relation $query->via($joinAlias); if ($this instanceof BelongsTo) { - $query->join($joinTable . ' ' . $joinAlias, $alias . '.' . $this->foreignKey . '=' . $joinAlias . '.' . $this->localKey, $this->joinType); + $query->join([$joinTable => $joinAlias], $name . '.' . $this->foreignKey . '=' . $joinAlias . '.' . $this->localKey, $this->joinType); } else { - $query->join($joinTable . ' ' . $joinAlias, $alias . '.' . $this->localKey . '=' . $joinAlias . '.' . $this->foreignKey, $this->joinType); + $query->join([$joinTable => $joinAlias], $name . '.' . $this->localKey . '=' . $joinAlias . '.' . $this->foreignKey, $this->joinType); } if ($closure) { @@ -214,6 +214,16 @@ abstract class OneToOne extends Relation return $this; } + /** + * 获取绑定属性 + * @access public + * @return array + */ + public function getBindAttr() + { + return $this->bindAttr; + } + /** * 关联统计 * @access public @@ -276,7 +286,7 @@ abstract class OneToOne extends Relation if (isset($result->$key)) { throw new Exception('bind attr has exists:' . $key); } else { - $result->setAttr($key, $model->$attr); + $result->setAttr($key, $model ? $model->$attr : null); } } } @@ -294,6 +304,8 @@ abstract class OneToOne extends Relation */ protected function eagerlyWhere($model, $where, $key, $relation, $subRelation = '', $closure = false) { + $this->baseQuery = true; + // 预载入关联查询 支持嵌套预载入 if ($closure) { call_user_func_array($closure, [ & $model]); @@ -312,10 +324,14 @@ abstract class OneToOne extends Relation } /** - * 执行基础查询(进执行一次) - * @access protected - * @return void + * 创建关联统计子查询 + * @access public + * @param \Closure $closure 闭包 + * @param string $name 统计数据别名 + * @return string */ - protected function baseQuery() - {} + public function getRelationCountQuery($closure, &$name = null) + { + throw new Exception('relation not support: withCount'); + } } diff --git a/thinkphp/library/think/paginator/driver/Bootstrap.php b/thinkphp/library/think/paginator/driver/Bootstrap.php index 58fa9436..c5ac60db 100644 --- a/thinkphp/library/think/paginator/driver/Bootstrap.php +++ b/thinkphp/library/think/paginator/driver/Bootstrap.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- diff --git a/thinkphp/library/think/response/Json.php b/thinkphp/library/think/response/Json.php index 538fc5a0..c906bfc1 100644 --- a/thinkphp/library/think/response/Json.php +++ b/thinkphp/library/think/response/Json.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- diff --git a/thinkphp/library/think/response/Jsonp.php b/thinkphp/library/think/response/Jsonp.php index de8fb304..404bacbe 100644 --- a/thinkphp/library/think/response/Jsonp.php +++ b/thinkphp/library/think/response/Jsonp.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- diff --git a/thinkphp/library/think/response/Redirect.php b/thinkphp/library/think/response/Redirect.php index f45b0e1c..91694cb2 100644 --- a/thinkphp/library/think/response/Redirect.php +++ b/thinkphp/library/think/response/Redirect.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -67,7 +67,11 @@ class Redirect extends Response */ public function getTargetUrl() { - return strpos($this->data, '://') ? $this->data : Url::build($this->data, $this->params); + if (strpos($this->data, '://') || (0 === strpos($this->data, '/') && empty($this->params))) { + return $this->data; + } else { + return Url::build($this->data, $this->params); + } } public function params($params = []) diff --git a/thinkphp/library/think/response/View.php b/thinkphp/library/think/response/View.php index de75515a..48f944a7 100644 --- a/thinkphp/library/think/response/View.php +++ b/thinkphp/library/think/response/View.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- diff --git a/thinkphp/library/think/response/Xml.php b/thinkphp/library/think/response/Xml.php index ca12e295..3bdc052a 100644 --- a/thinkphp/library/think/response/Xml.php +++ b/thinkphp/library/think/response/Xml.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -11,6 +11,8 @@ namespace think\response; +use think\Collection; +use think\Model; use think\Response; class Xml extends Response @@ -81,6 +83,11 @@ class Xml extends Response protected function dataToXml($data, $item, $id) { $xml = $attr = ''; + + if ($data instanceof Collection || $data instanceof Model) { + $data = $data->toArray(); + } + foreach ($data as $key => $val) { if (is_numeric($key)) { $id && $attr = " {$id}=\"{$key}\""; diff --git a/thinkphp/library/think/session/driver/Memcache.php b/thinkphp/library/think/session/driver/Memcache.php index 0c02e23e..877d7bd7 100644 --- a/thinkphp/library/think/session/driver/Memcache.php +++ b/thinkphp/library/think/session/driver/Memcache.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- diff --git a/thinkphp/library/think/session/driver/Memcached.php b/thinkphp/library/think/session/driver/Memcached.php index bcaf8a1b..2994a07c 100644 --- a/thinkphp/library/think/session/driver/Memcached.php +++ b/thinkphp/library/think/session/driver/Memcached.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- diff --git a/thinkphp/library/think/session/driver/Redis.php b/thinkphp/library/think/session/driver/Redis.php index a4c2b54a..8d05126b 100644 --- a/thinkphp/library/think/session/driver/Redis.php +++ b/thinkphp/library/think/session/driver/Redis.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- diff --git a/thinkphp/library/think/template/TagLib.php b/thinkphp/library/think/template/TagLib.php index d343bed0..c5b72f91 100644 --- a/thinkphp/library/think/template/TagLib.php +++ b/thinkphp/library/think/template/TagLib.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -264,8 +264,8 @@ class TagLib if (!empty($this->tags[$name]['expression'])) { static $_taglibs; if (!isset($_taglibs[$name])) { - $_taglibs[$name][0] = strlen(ltrim($this->tpl->config('taglib_begin'), '\\') . $name); - $_taglibs[$name][1] = strlen(ltrim($this->tpl->config('taglib_end'), '\\')); + $_taglibs[$name][0] = strlen($this->tpl->config('taglib_begin_origin') . $name); + $_taglibs[$name][1] = strlen($this->tpl->config('taglib_end_origin')); } $result['expression'] = substr($str, $_taglibs[$name][0], -$_taglibs[$name][1]); // 清除自闭合标签尾部/ diff --git a/thinkphp/library/think/template/driver/File.php b/thinkphp/library/think/template/driver/File.php index b27e7265..a9a86bf2 100644 --- a/thinkphp/library/think/template/driver/File.php +++ b/thinkphp/library/think/template/driver/File.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -15,6 +15,8 @@ use think\Exception; class File { + protected $cacheFile; + /** * 写入编译缓存 * @param string $cacheFile 缓存的文件名 @@ -42,12 +44,13 @@ class File */ public function read($cacheFile, $vars = []) { + $this->cacheFile = $cacheFile; if (!empty($vars) && is_array($vars)) { // 模板阵列变量分解成为独立变量 extract($vars, EXTR_OVERWRITE); } //载入模版缓存文件 - include $cacheFile; + include $this->cacheFile; } /** diff --git a/thinkphp/library/think/template/taglib/Cx.php b/thinkphp/library/think/template/taglib/Cx.php index af7a54c8..31e0698d 100644 --- a/thinkphp/library/think/template/taglib/Cx.php +++ b/thinkphp/library/think/template/taglib/Cx.php @@ -278,7 +278,7 @@ class Cx extends Taglib */ public function tagCase($tag, $content) { - $value = !empty($tag['expression']) ? $tag['expression'] : $tag['value']; + $value = isset($tag['expression']) ? $tag['expression'] : $tag['value']; $flag = substr($value, 0, 1); if ('$' == $flag || ':' == $flag) { $value = $this->autoBuildVar($value); diff --git a/thinkphp/library/think/view/driver/Php.php b/thinkphp/library/think/view/driver/Php.php index 468d3611..f594a438 100644 --- a/thinkphp/library/think/view/driver/Php.php +++ b/thinkphp/library/think/view/driver/Php.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -29,7 +29,11 @@ class Php 'view_suffix' => 'php', // 模板文件名分隔符 'view_depr' => DS, + // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 + 'auto_rule' => 1, ]; + protected $template; + protected $content; public function __construct($config = []) { @@ -68,16 +72,12 @@ class Php if (!is_file($template)) { throw new TemplateNotFoundException('template not exists:' . $template, $template); } + $this->template = $template; // 记录视图信息 App::$debug && Log::record('[ VIEW ] ' . $template . ' [ ' . var_export(array_keys($data), true) . ' ]', 'info'); - if (isset($data['template'])) { - $__template__ = $template; - extract($data, EXTR_OVERWRITE); - include $__template__; - } else { - extract($data, EXTR_OVERWRITE); - include $template; - } + + extract($data, EXTR_OVERWRITE); + include $this->template; } /** @@ -89,14 +89,10 @@ class Php */ public function display($content, $data = []) { - if (isset($data['content'])) { - $__content__ = $content; - extract($data, EXTR_OVERWRITE); - eval('?>' . $__content__); - } else { - extract($data, EXTR_OVERWRITE); - eval('?>' . $content); - } + $this->content = $content; + + extract($data, EXTR_OVERWRITE); + eval('?>' . $this->content); } /** @@ -132,7 +128,7 @@ class Php if ($controller) { if ('' == $template) { // 如果模板文件名为空 按照默认规则定位 - $template = str_replace('.', DS, $controller) . $depr . $request->action(); + $template = str_replace('.', DS, $controller) . $depr . (1 == $this->config['auto_rule'] ? Loader::parseName($request->action(true)) : $request->action()); } elseif (false === strpos($template, $depr)) { $template = str_replace('.', DS, $controller) . $depr . $template; } diff --git a/thinkphp/library/think/view/driver/Think.php b/thinkphp/library/think/view/driver/Think.php index 39a14ca3..a314ad60 100644 --- a/thinkphp/library/think/view/driver/Think.php +++ b/thinkphp/library/think/view/driver/Think.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -34,6 +34,8 @@ class Think 'view_depr' => DS, // 是否开启模板编译缓存,设为false则每次都会重新编译 'tpl_cache' => true, + // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 + 'auto_rule' => 1, ]; public function __construct($config = []) @@ -127,7 +129,7 @@ class Think if ($controller) { if ('' == $template) { // 如果模板文件名为空 按照默认规则定位 - $template = str_replace('.', DS, $controller) . $depr . $request->action(); + $template = str_replace('.', DS, $controller) . $depr . (1 == $this->config['auto_rule'] ? Loader::parseName($request->action(true)) : $request->action()); } elseif (false === strpos($template, $depr)) { $template = str_replace('.', DS, $controller) . $depr . $template; } diff --git a/thinkphp/library/traits/controller/Jump.php b/thinkphp/library/traits/controller/Jump.php index 36f2f94d..6a572246 100644 --- a/thinkphp/library/traits/controller/Jump.php +++ b/thinkphp/library/traits/controller/Jump.php @@ -1,5 +1,4 @@ server('HTTP_REFERER'))) { + $url = Request::instance()->server('HTTP_REFERER'); + } elseif ('' !== $url && !strpos($url, '://') && 0 !== strpos($url, '/')) { + $url = Url::build($url); } + + $type = $this->getResponseType(); $result = [ - 'code' => $code, + 'code' => 1, 'msg' => $msg, 'data' => $data, 'url' => $url, 'wait' => $wait, ]; - $type = $this->getResponseType(); if ('html' == strtolower($type)) { - $result = ViewTemplate::instance(Config::get('template'), Config::get('view_replace_str')) + $template = Config::get('template'); + $view = Config::get('view_replace_str'); + + $result = ViewTemplate::instance($template, $view) ->fetch(Config::get('dispatch_success_tmpl'), $result); } + $response = Response::create($result, $type)->header($header); + throw new HttpResponseException($response); } /** * 操作错误跳转的快捷方法 * @access protected - * @param mixed $msg 提示信息 - * @param string $url 跳转的URL地址 - * @param mixed $data 返回的数据 - * @param integer $wait 跳转等待时间 - * @param array $header 发送的Header信息 + * @param mixed $msg 提示信息 + * @param string $url 跳转的 URL 地址 + * @param mixed $data 返回的数据 + * @param int $wait 跳转等待时间 + * @param array $header 发送的 Header 信息 * @return void + * @throws HttpResponseException */ - protected function error($msg = '',$code=404, $options=[],$url = null, $data = '', $wait = 3, array $header = []) + protected function error($msg = '', $url = null, $data = '', $wait = 3, array $header = []) { - $code = 0; - if (is_numeric($msg)) { - $code = $msg; - $msg = ''; - } if (is_null($url)) { $url = Request::instance()->isAjax() ? '' : 'javascript:history.back(-1);'; - } elseif ('' !== $url) { - $url = (strpos($url, '://') || 0 === strpos($url, '/')) ? $url : Url::build($url); + } elseif ('' !== $url && !strpos($url, '://') && 0 !== strpos($url, '/')) { + $url = Url::build($url); } + + $type = $this->getResponseType(); $result = [ - 'code' => $code, + 'code' => 0, 'msg' => $msg, 'data' => $data, 'url' => $url, 'wait' => $wait, - 'options' => $options, ]; - $type = $this->getResponseType(); if ('html' == strtolower($type)) { - $result = ViewTemplate::instance(Config::get('template'), Config::get('view_replace_str')) + $template = Config::get('template'); + $view = Config::get('view_replace_str'); + + $result = ViewTemplate::instance($template, $view) ->fetch(Config::get('dispatch_error_tmpl'), $result); } + $response = Response::create($result, $type)->header($header); + throw new HttpResponseException($response); } /** - * 返回封装后的API数据到客户端 + * 返回封装后的 API 数据到客户端 * @access protected - * @param mixed $data 要返回的数据 - * @param integer $code 返回的code - * @param mixed $msg 提示信息 - * @param string $type 返回数据格式 - * @param array $header 发送的Header信息 + * @param mixed $data 要返回的数据 + * @param int $code 返回的 code + * @param mixed $msg 提示信息 + * @param string $type 返回数据格式 + * @param array $header 发送的 Header 信息 * @return void + * @throws HttpResponseException */ protected function result($data, $code = 0, $msg = '', $type = '', array $header = []) { $result = [ 'code' => $code, 'msg' => $msg, - 'time' => $_SERVER['REQUEST_TIME'], + 'time' => Request::instance()->server('REQUEST_TIME'), 'data' => $data, ]; $type = $type ?: $this->getResponseType(); $response = Response::create($result, $type)->header($header); + throw new HttpResponseException($response); } /** - * URL重定向 + * URL 重定向 * @access protected - * @param string $url 跳转的URL表达式 - * @param array|integer $params 其它URL参数 - * @param integer $code http code - * @param array $with 隐式传参 + * @param string $url 跳转的 URL 表达式 + * @param array|int $params 其它 URL 参数 + * @param int $code http code + * @param array $with 隐式传参 * @return void + * @throws HttpResponseException */ protected function redirect($url, $params = [], $code = 302, $with = []) { - $response = new Redirect($url); if (is_integer($params)) { $code = $params; $params = []; } + + $response = new Redirect($url); $response->code($code)->params($params)->with($with); + throw new HttpResponseException($response); } /** - * 获取当前的response 输出类型 + * 获取当前的 response 输出类型 * @access protected * @return string */ protected function getResponseType() { - $isAjax = Request::instance()->isAjax(); - return $isAjax ? Config::get('default_ajax_return') : Config::get('default_return_type'); + return Request::instance()->isAjax() + ? Config::get('default_ajax_return') + : Config::get('default_return_type'); } } diff --git a/thinkphp/library/traits/model/SoftDelete.php b/thinkphp/library/traits/model/SoftDelete.php index be586342..70f31ba2 100644 --- a/thinkphp/library/traits/model/SoftDelete.php +++ b/thinkphp/library/traits/model/SoftDelete.php @@ -2,11 +2,15 @@ namespace traits\model; +use think\Collection; use think\db\Query; +use think\Model; +/** + * @mixin \Think\Model + */ trait SoftDelete { - /** * 判断当前实例是否被软删除 * @access public @@ -15,22 +19,21 @@ trait SoftDelete public function trashed() { $field = $this->getDeleteTimeField(); - if (!empty($this->data[$field])) { + + if ($field && !empty($this->data[$field])) { return true; } return false; } /** - * 查询软删除数据 + * 查询包含软删除的数据 * @access public * @return Query */ public static function withTrashed() { - $model = new static(); - $field = $model->getDeleteTimeField(true); - return $model->getQuery(); + return (new static )->getQuery(); } /** @@ -42,14 +45,18 @@ trait SoftDelete { $model = new static(); $field = $model->getDeleteTimeField(true); - return $model->getQuery() - ->useSoftDelete($field, ['not null', '']); + + if ($field) { + return $model->getQuery()->useSoftDelete($field, ['not null', '']); + } else { + return $model->getQuery(); + } } /** * 删除当前的记录 * @access public - * @param bool $force 是否强制删除 + * @param bool $force 是否强制删除 * @return integer */ public function delete($force = false) @@ -57,48 +64,71 @@ trait SoftDelete if (false === $this->trigger('before_delete', $this)) { return false; } + $name = $this->getDeleteTimeField(); - if (!$force) { + if ($name && !$force) { // 软删除 $this->data[$name] = $this->autoWriteTimestamp($name); $result = $this->isUpdate()->save(); } else { - $result = $this->getQuery()->delete($this->data); + // 强制删除当前模型数据 + $result = $this->getQuery()->where($this->getWhere())->delete(); + } + + // 关联删除 + if (!empty($this->relationWrite)) { + foreach ($this->relationWrite as $key => $name) { + $name = is_numeric($key) ? $name : $key; + $result = $this->getRelation($name); + if ($result instanceof Model) { + $result->delete(); + } elseif ($result instanceof Collection || is_array($result)) { + foreach ($result as $model) { + $model->delete(); + } + } + } } $this->trigger('after_delete', $this); + + // 清空原始数据 + $this->origin = []; + return $result; } /** * 删除记录 * @access public - * @param mixed $data 主键列表 支持闭包查询条件 + * @param mixed $data 主键列表(支持闭包查询条件) * @param bool $force 是否强制删除 * @return integer 成功删除的记录数 */ public static function destroy($data, $force = false) { + if (is_null($data)) { + return 0; + } + // 包含软删除数据 - $query = self::withTrashed(); + $query = (new static())->db(false); if (is_array($data) && key($data) !== 0) { $query->where($data); $data = null; } elseif ($data instanceof \Closure) { call_user_func_array($data, [ & $query]); $data = null; - } elseif (is_null($data)) { - return 0; } - $resultSet = $query->select($data); - $count = 0; - if ($resultSet) { + $count = 0; + if ($resultSet = $query->select($data)) { foreach ($resultSet as $data) { $result = $data->delete($force); $count += $result; } } + return $count; } @@ -110,46 +140,61 @@ trait SoftDelete */ public function restore($where = []) { - $name = $this->getDeleteTimeField(); if (empty($where)) { $pk = $this->getPk(); $where[$pk] = $this->getData($pk); } - // 恢复删除 - return $this->getQuery() - ->useSoftDelete($name, ['not null', '']) - ->where($where) - ->update([$name => null]); + + $name = $this->getDeleteTimeField(); + + if ($name) { + // 恢复删除 + return $this->getQuery() + ->useSoftDelete($name, ['not null', '']) + ->where($where) + ->update([$name => null]); + } else { + return 0; + } } /** * 查询默认不包含软删除数据 * @access protected * @param Query $query 查询对象 - * @return void + * @return Query */ protected function base($query) { $field = $this->getDeleteTimeField(true); - $query->useSoftDelete($field); + return $field ? $query->useSoftDelete($field) : $query; } /** * 获取软删除字段 * @access public - * @param bool $read 是否查询操作 写操作的时候会自动去掉表别名 + * @param bool $read 是否查询操作(写操作的时候会自动去掉表别名) * @return string */ protected function getDeleteTimeField($read = false) { - $field = property_exists($this, 'deleteTime') && isset($this->deleteTime) ? $this->deleteTime : 'delete_time'; + $field = property_exists($this, 'deleteTime') && isset($this->deleteTime) ? + $this->deleteTime : + 'delete_time'; + + if (false === $field) { + return false; + } + if (!strpos($field, '.')) { $field = '__TABLE__.' . $field; } + if (!$read && strpos($field, '.')) { $array = explode('.', $field); $field = array_pop($array); } + return $field; } } diff --git a/thinkphp/library/traits/think/Instance.php b/thinkphp/library/traits/think/Instance.php index ba45ddd8..428c8fde 100644 --- a/thinkphp/library/traits/think/Instance.php +++ b/thinkphp/library/traits/think/Instance.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -15,31 +15,40 @@ use think\Exception; trait Instance { + /** + * @var null|static 实例对象 + */ protected static $instance = null; /** - * @param array $options + * 获取示例 + * @param array $options 实例配置 * @return static */ public static function instance($options = []) { - if (is_null(self::$instance)) { - self::$instance = new self($options); - } + if (is_null(self::$instance)) self::$instance = new self($options); + return self::$instance; } - // 静态调用 - public static function __callStatic($method, $params) + /** + * 静态调用 + * @param string $method 调用方法 + * @param array $params 调用参数 + * @return mixed + * @throws Exception + */ + public static function __callStatic($method, array $params) { - if (is_null(self::$instance)) { - self::$instance = new self(); - } + if (is_null(self::$instance)) self::$instance = new self(); + $call = substr($method, 1); - if (0 === strpos($method, '_') && is_callable([self::$instance, $call])) { - return call_user_func_array([self::$instance, $call], $params); - } else { + + if (0 !== strpos($method, '_') || !is_callable([self::$instance, $call])) { throw new Exception("method not exists:" . $method); } + + return call_user_func_array([self::$instance, $call], $params); } } diff --git a/thinkphp/start.php b/thinkphp/start.php index 8af62ef3..adb1bc65 100644 --- a/thinkphp/start.php +++ b/thinkphp/start.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -12,7 +12,8 @@ namespace think; // ThinkPHP 引导文件 -// 加载基础文件 +// 1. 加载基础文件 require __DIR__ . '/base.php'; -// 执行应用 + +// 2. 执行应用 App::run()->send(); diff --git a/thinkphp/tests/.gitignore b/thinkphp/tests/.gitignore deleted file mode 100644 index a0306ecc..00000000 --- a/thinkphp/tests/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/runtime/ -/application/common.php -/application/demo/ -/application/runtime/ \ No newline at end of file diff --git a/thinkphp/tests/README.md b/thinkphp/tests/README.md deleted file mode 100644 index 0c1d0869..00000000 --- a/thinkphp/tests/README.md +++ /dev/null @@ -1,132 +0,0 @@ -## 测试目录结构 - -测试文件主要在 tests 文件下面,主要有以下几个文件夹 - -- conf 测试环境配置文件。 -- script 测试环境配置脚本。 -- thinkphp 测试用例和相关文件,与项目文件夹结构一致。 -- mock.php 测试入口文件。 - -## 主要测试流程 - -thinkphp5 的测试的主要流程是跟 thinkphp 的系统流程是相似的,大体的流程为: - -1. 引用 mock.php 文件加载框架 - -2. 根据文件目录,添加测试文件 - -3. 执行单元测试,输出结果 - -## 测试举例 - -例如测试 thinkphp 里的 apc 缓存,将分为以下几个过程: - -1. 创建 apcTest.php 文件 - -该文件应与 apc.php 目录路径 `thinkphp/library/think/cache/driver` 一致,命名空间与目录所在一致,并引用 `PHPUnit_Framework_TestCase`。 - - ```php - markTestSkipped('apc扩展不可用!'); - }; - ``` - - - 编写测试用例 - - *具体写法参照 [PHPUnit 官方文档](https://phpunit.de/manual/4.8/zh_cn/index.html)* - - ```php - public function testGet() - { - App::run(); - $this->assertInstanceOf( - '\think\cache\driver\Apc', - Cache::connect(['type' => 'apc', 'expire' => 1]) - ); - $this->assertTrue(Cache::set('key', 'value')); - $this->assertEquals('value', Cache::get('key')); - $this->assertTrue(Cache::rm('key')); - $this->assertFalse(Cache::get('key')); - $this->assertTrue(Cache::clear('key')); - Config::reset(); - } - ``` - -3. 执行单元测试命令 - - 在项目根目录执行 - - ```bash - $ phpunit - ``` - - 若想看到所有结果,请添加-v参数 - - ```bash - $ phpunit -v - ``` - -4. 输出结果 - -## 相关文档 - -[各个部分单元测试说明](http://www.kancloud.cn/brother_simon/tp5_test/96971 "各部分单元测试说明") - -## 大家一起来 - -单元测试的内容会跟框架同步,测试内容方方面面,是一个相对复杂的模块,同时也是一个值得重视的部分。希望大家能够多多提出意见,多多参与。如果你有任何问题或想法,可以随时提 issue,我们期待着收到听大家的质疑和讨论。 - -## 任务进度 - -单元测试任务进度,请大家认领模块 - -|模块|认领人|进度| -|---|---|---| -|Base||| -|App|Haotong Lin|√| -|Build|刘志淳|| -|Config|Haotong Lin|√| -|Cache||| -|Controller|Haotong Lin|√| -|Cookie|Haotong Lin|√| -|Db||| -|Debug|大漠|√| -|Error|大漠|| -|Exception|Haotong Lin|√| -|Hook|流年|√| -|Input|Haotong Lin|√| -|Lang|流年|√| -|Loader|流年|| -|Log||| -|Model||| -|Response|大漠|√| -|Route|流年|| -|Session|大漠|√| -|Template|oldrind|| -|Url|流年|| -|View|mahuan|| diff --git a/thinkphp/tests/application/config.php b/thinkphp/tests/application/config.php deleted file mode 100644 index d94a8641..00000000 --- a/thinkphp/tests/application/config.php +++ /dev/null @@ -1,33 +0,0 @@ - -// +---------------------------------------------------------------------- -// $Id$ - -return [ - 'url_route_on' => true, - 'log' => [ - 'type' => 'file', // 支持 socket trace file - ], - 'view' => [ - // 模板引擎 - 'engine_type' => 'think', - // 模板引擎配置 - 'engine_config' => [ - // 模板路径 - 'view_path' => '', - // 模板后缀 - 'view_suffix' => '.html', - // 模板文件名分隔符 - 'view_depr' => DS, - ], - // 输出字符串替换 - 'parse_str' => [], - ], -]; diff --git a/thinkphp/tests/application/database.php b/thinkphp/tests/application/database.php deleted file mode 100644 index 24434efb..00000000 --- a/thinkphp/tests/application/database.php +++ /dev/null @@ -1,44 +0,0 @@ - -// +---------------------------------------------------------------------- -// $Id$ - -return [ - // 数据库类型 - 'type' => 'mysql', - // 数据库连接DSN配置 - 'dsn' => '', - // 服务器地址 - 'hostname' => '127.0.0.1', - // 数据库名 - 'database' => '', - // 数据库用户名 - 'username' => 'root', - // 数据库密码 - 'password' => '', - // 数据库连接端口 - 'hostport' => '', - // 数据库连接参数 - 'params' => [], - // 数据库编码默认采用utf8 - 'charset' => 'utf8', - // 数据库表前缀 - 'prefix' => '', - // 数据库调试模式 - 'debug' => true, - // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) - 'deploy' => 0, - // 数据库读写是否分离 主从式有效 - 'rw_separate' => false, - // 读写分离后 主服务器数量 - 'master_num' => 1, - // 指定从服务器序号 - 'slave_no' => '', -]; diff --git a/thinkphp/tests/application/index/controller/Index.php b/thinkphp/tests/application/index/controller/Index.php deleted file mode 100644 index 803fe952..00000000 --- a/thinkphp/tests/application/index/controller/Index.php +++ /dev/null @@ -1,10 +0,0 @@ -*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }
'; - } -} diff --git a/thinkphp/tests/application/route.php b/thinkphp/tests/application/route.php deleted file mode 100644 index 45bcf79d..00000000 --- a/thinkphp/tests/application/route.php +++ /dev/null @@ -1,22 +0,0 @@ - -// +---------------------------------------------------------------------- -// $Id$ - -return [ - '__pattern__' => [ - 'name' => '\w+', - ], - '[hello]' => [ - ':id' => ['index/hello', ['method' => 'get'], ['id' => '\d+']], - ':name' => ['index/hello', ['method' => 'post']], - ], - -]; diff --git a/thinkphp/tests/conf/apcu.ini b/thinkphp/tests/conf/apcu.ini deleted file mode 100644 index 9c2abed2..00000000 --- a/thinkphp/tests/conf/apcu.ini +++ /dev/null @@ -1,3 +0,0 @@ -extension=apcu.so - -apc.enable_cli=1 diff --git a/thinkphp/tests/conf/apcu_bc.ini b/thinkphp/tests/conf/apcu_bc.ini deleted file mode 100644 index a7f6182d..00000000 --- a/thinkphp/tests/conf/apcu_bc.ini +++ /dev/null @@ -1,4 +0,0 @@ -extension=apcu.so -extension=apc.so - -apc.enable_cli=1 diff --git a/thinkphp/tests/conf/memcached.ini b/thinkphp/tests/conf/memcached.ini deleted file mode 100644 index 7d09664e..00000000 --- a/thinkphp/tests/conf/memcached.ini +++ /dev/null @@ -1 +0,0 @@ -extension=memcached.so diff --git a/thinkphp/tests/conf/redis.ini b/thinkphp/tests/conf/redis.ini deleted file mode 100644 index 6aecae48..00000000 --- a/thinkphp/tests/conf/redis.ini +++ /dev/null @@ -1 +0,0 @@ -extension=redis.so diff --git a/thinkphp/tests/conf/timezone.ini b/thinkphp/tests/conf/timezone.ini deleted file mode 100644 index ced3e0d7..00000000 --- a/thinkphp/tests/conf/timezone.ini +++ /dev/null @@ -1 +0,0 @@ -date.timezone = UTC \ No newline at end of file diff --git a/thinkphp/tests/extensions/5.4/apcu.so b/thinkphp/tests/extensions/5.4/apcu.so deleted file mode 100644 index 42a24a66..00000000 Binary files a/thinkphp/tests/extensions/5.4/apcu.so and /dev/null differ diff --git a/thinkphp/tests/extensions/5.5/apcu.so b/thinkphp/tests/extensions/5.5/apcu.so deleted file mode 100644 index 26e5228a..00000000 Binary files a/thinkphp/tests/extensions/5.5/apcu.so and /dev/null differ diff --git a/thinkphp/tests/extensions/5.6/apcu.so b/thinkphp/tests/extensions/5.6/apcu.so deleted file mode 100644 index fe952969..00000000 Binary files a/thinkphp/tests/extensions/5.6/apcu.so and /dev/null differ diff --git a/thinkphp/tests/extensions/7.0/apc.so b/thinkphp/tests/extensions/7.0/apc.so deleted file mode 100644 index 7948350d..00000000 Binary files a/thinkphp/tests/extensions/7.0/apc.so and /dev/null differ diff --git a/thinkphp/tests/extensions/7.0/apcu.so b/thinkphp/tests/extensions/7.0/apcu.so deleted file mode 100644 index c2f0ad14..00000000 Binary files a/thinkphp/tests/extensions/7.0/apcu.so and /dev/null differ diff --git a/thinkphp/tests/extensions/7.0/memcached.so b/thinkphp/tests/extensions/7.0/memcached.so deleted file mode 100644 index 544ba6d5..00000000 Binary files a/thinkphp/tests/extensions/7.0/memcached.so and /dev/null differ diff --git a/thinkphp/tests/extensions/7.0/redis.so b/thinkphp/tests/extensions/7.0/redis.so deleted file mode 100644 index e3aea8fc..00000000 Binary files a/thinkphp/tests/extensions/7.0/redis.so and /dev/null differ diff --git a/thinkphp/tests/mock.php b/thinkphp/tests/mock.php deleted file mode 100644 index 0c29e596..00000000 --- a/thinkphp/tests/mock.php +++ /dev/null @@ -1,20 +0,0 @@ - -// +---------------------------------------------------------------------- - -// 测试入口文件 -$_SERVER['REQUEST_METHOD'] = 'GET'; -// 定义项目测试基础路径 -define('TEST_PATH', __DIR__ . '/'); -// 定义项目路径 -define('APP_PATH', __DIR__ . '/application/'); -// 加载框架基础文件 -require __DIR__ . '/../base.php'; -\think\Loader::addNamespace('tests', TEST_PATH); diff --git a/thinkphp/tests/script/install.sh b/thinkphp/tests/script/install.sh deleted file mode 100644 index 0adee7ed..00000000 --- a/thinkphp/tests/script/install.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -if [ $(phpenv version-name) != "hhvm" ]; then - cp tests/extensions/$(phpenv version-name)/*.so $(php-config --extension-dir) - - if [ $(phpenv version-name) = "7.0" ]; then - phpenv config-add tests/conf/apcu_bc.ini - else - phpenv config-add tests/conf/apcu.ini - fi - - phpenv config-add tests/conf/memcached.ini - phpenv config-add tests/conf/redis.ini - - phpenv config-add tests/conf/timezone.ini -fi - -composer install --no-interaction --ignore-platform-reqs diff --git a/thinkphp/tests/thinkphp/baseTest.php b/thinkphp/tests/thinkphp/baseTest.php deleted file mode 100644 index 18f1d3dd..00000000 --- a/thinkphp/tests/thinkphp/baseTest.php +++ /dev/null @@ -1,39 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * 保证运行环境正常 - */ -class baseTest extends \PHPUnit_Framework_TestCase -{ - public function testConstants() - { - $this->assertNotEmpty(THINK_START_TIME); - $this->assertNotEmpty(THINK_START_MEM); - $this->assertNotEmpty(THINK_VERSION); - $this->assertNotEmpty(DS); - $this->assertNotEmpty(THINK_PATH); - $this->assertNotEmpty(LIB_PATH); - $this->assertNotEmpty(EXTEND_PATH); - $this->assertNotEmpty(CORE_PATH); - $this->assertNotEmpty(TRAIT_PATH); - $this->assertNotEmpty(APP_PATH); - $this->assertNotEmpty(RUNTIME_PATH); - $this->assertNotEmpty(LOG_PATH); - $this->assertNotEmpty(CACHE_PATH); - $this->assertNotEmpty(TEMP_PATH); - $this->assertNotEmpty(VENDOR_PATH); - $this->assertNotEmpty(EXT); - $this->assertNotEmpty(ENV_PREFIX); - $this->assertTrue(!is_null(IS_WIN)); - $this->assertTrue(!is_null(IS_CLI)); - } -} diff --git a/thinkphp/tests/thinkphp/library/think/appTest.php b/thinkphp/tests/thinkphp/library/think/appTest.php deleted file mode 100644 index 016bbf9c..00000000 --- a/thinkphp/tests/thinkphp/library/think/appTest.php +++ /dev/null @@ -1,90 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * app类测试 - * @author Haotong Lin - */ - -namespace tests\thinkphp\library\think; - -use think\App; -use think\Config; -use think\Request; - -function func_trim($value) -{ - return trim($value); -} - -function func_strpos($haystack, $needle) -{ - return strpos($haystack, $needle); -} - -class AppInvokeMethodTestClass -{ - public static function staticRun($string) - { - return $string; - } - - public function run($string) - { - return $string; - } -} - -class appTest extends \PHPUnit_Framework_TestCase -{ - public function testRun() - { - $response = App::run(Request::create("http://www.example.com")); - - $expectOutputString = '

:)

ThinkPHP V5
十年磨一剑 - 为API开发设计的高性能框架

[ V5.0 版本由 七牛云 独家赞助发布 ]
'; - - $this->assertEquals($expectOutputString, $response->getContent()); - $this->assertEquals(200, $response->getCode()); - - $this->assertEquals(true, function_exists('lang')); - $this->assertEquals(true, function_exists('config')); - $this->assertEquals(true, function_exists('input')); - - $this->assertEquals(Config::get('default_timezone'), date_default_timezone_get()); - - } - - // function调度 - public function testInvokeFunction() - { - $args1 = ['a b c ']; - $this->assertEquals( - trim($args1[0]), - App::invokeFunction('tests\thinkphp\library\think\func_trim', $args1) - ); - - $args2 = ['abcdefg', 'g']; - $this->assertEquals( - strpos($args2[0], $args2[1]), - App::invokeFunction('tests\thinkphp\library\think\func_strpos', $args2) - ); - } - - // 类method调度 - public function testInvokeMethod() - { - $result = App::invokeMethod(['tests\thinkphp\library\think\AppInvokeMethodTestClass', 'run'], ['thinkphp']); - $this->assertEquals('thinkphp', $result); - - $result = App::invokeMethod('tests\thinkphp\library\think\AppInvokeMethodTestClass::staticRun', ['thinkphp']); - $this->assertEquals('thinkphp', $result); - } -} diff --git a/thinkphp/tests/thinkphp/library/think/behavior/One.php b/thinkphp/tests/thinkphp/library/think/behavior/One.php deleted file mode 100644 index 1ec6e66e..00000000 --- a/thinkphp/tests/thinkphp/library/think/behavior/One.php +++ /dev/null @@ -1,15 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * build测试 - * @author 刘志淳 - */ - -namespace tests\thinkphp\library\think; - -use think\Build; - -class buildTest extends \PHPUnit_Framework_TestCase -{ - public function testRun() - { - $build = [ - // Test run directory - '__dir__' => ['runtime/cache', 'runtime/log', 'runtime/temp', 'runtime/template'], - '__file__' => ['common.php'], - - // Test generation module - 'demo' => [ - '__file__' => ['common.php'], - '__dir__' => ['behavior', 'controller', 'model', 'view', 'service'], - 'controller' => ['Index', 'Test', 'UserType'], - 'model' => ['User', 'UserType'], - 'service' => ['User', 'UserType'], - 'view' => ['index/index'], - ], - ]; - Build::run($build); - - $this->buildFileExists($build); - } - - protected function buildFileExists($build) - { - foreach ($build as $module => $list) { - if ('__dir__' == $module || '__file__' == $module) { - foreach ($list as $file) { - $this->assertFileExists(APP_PATH . $file); - } - } else { - foreach ($list as $path => $moduleList) { - if ('__file__' == $path || '__dir__' == $path) { - foreach ($moduleList as $file) { - $this->assertFileExists(APP_PATH . $module . '/' . $file); - } - } else { - foreach ($moduleList as $file) { - if ('view' == $path) { - $file_name = APP_PATH . $module . '/' . $path . '/' . $file . '.html'; - } else { - $file_name = APP_PATH . $module . '/' . $path . '/' . $file . EXT; - } - $this->assertFileExists($file_name); - } - } - } - $this->assertFileExists(APP_PATH . ($module ? $module . DS : '') . 'config.php'); - } - } - } -} diff --git a/thinkphp/tests/thinkphp/library/think/cache/driver/cacheTestCase.php b/thinkphp/tests/thinkphp/library/think/cache/driver/cacheTestCase.php deleted file mode 100644 index 5b2f2a3e..00000000 --- a/thinkphp/tests/thinkphp/library/think/cache/driver/cacheTestCase.php +++ /dev/null @@ -1,207 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * 缓存抽象类,提供一些测试 - * @author simon - */ - -namespace tests\thinkphp\library\think\cache\driver; - -use think\Cache; - -abstract class cacheTestCase extends \PHPUnit_Framework_TestCase -{ - - /** - * 获取缓存句柄,子类必须有 - * @access protected - */ - abstract protected function getCacheInstance(); - - /** - * tearDown函数 - */ - protected function tearDown() - { - } - - /** - * 设定一组测试值,包括测试字符串、整数、数组和对象 - * @return mixed - * @access public - */ - public function prepare() - { - $cache = $this->getCacheInstance(); - $cache->clear(); - $cache->set('string_test', 'string_test'); - $cache->set('number_test', 11); - $cache->set('array_test', ['array_test' => 'array_test']); - return $cache; - } - - /** - * 测试缓存设置,包括测试字符串、整数、数组和对象 - * @return mixed - * @access public - */ - public function testSet() - { - $cache = $this->getCacheInstance(); - $this->assertTrue($cache->set('string_test', 'string_test')); - $this->assertTrue($cache->set('number_test', 11)); - $this->assertTrue($cache->set('array_test', ['array_test' => 'array_test'])); - } - - /** - * 测试缓存自增 - * @return mixed - * @access public - */ - public function testInc() - { - $cache = $this->getCacheInstance(); - $this->assertEquals(14, $cache->inc('number_test', 3)); - } - - /** - * 测试缓存自减 - * @return mixed - * @access public - */ - public function testDec() - { - $cache = $this->getCacheInstance(); - $this->assertEquals(8, $cache->dec('number_test', 6)); - } - - /** - * 测试缓存读取,包括测试字符串、整数、数组和对象 - * @return mixed - * @access public - */ - public function testGet() - { - $cache = $this->prepare(); - $this->assertEquals('string_test', $cache->get('string_test')); - $this->assertEquals(11, $cache->get('number_test')); - $array = $cache->get('array_test'); - $this->assertArrayHasKey('array_test', $array); - $this->assertEquals('array_test', $array['array_test']); - - $result = $cache->set('no_expire', 1, 0); - $this->assertTrue($result); - } - - /** - * 测试缓存存在情况,包括测试字符串、整数、数组和对象 - * @return mixed - * @access public - */ - public function testExists() - { - $cache = $this->prepare(); - $this->assertNotEmpty($cache->has('string_test')); - $this->assertNotEmpty($cache->has('number_test')); - $this->assertFalse($cache->has('not_exists')); - } - - /** - * 测试缓存不存在情况,包括测试字符串、整数、数组和对象 - * @return mixed - * @access public - */ - public function testGetNonExistent() - { - $cache = $this->getCacheInstance(); - $this->assertFalse($cache->get('non_existent_key', false)); - } - - /** - * 测试特殊值缓存,包括测试字符串、整数、数组和对象 - * @return mixed - * @access public - */ - public function testStoreSpecialValues() - { - $cache = $this->getCacheInstance(); - $cache->set('null_value', null); - //清空缓存后,返回null而不是false - $this->assertTrue(is_null($cache->get('null_value'))); - } - - /** - * 缓存过期测试 - * @return mixed - * @access public - */ - public function testExpire() - { - $cache = $this->getCacheInstance(); - $this->assertTrue($cache->set('expire_test', 'expire_test', 1)); - usleep(600000); - $this->assertEquals('expire_test', $cache->get('expire_test')); - usleep(800000); - $this->assertFalse($cache->get('expire_test')); - } - - /** - * 删除缓存测试 - * @return mixed - * @access public - */ - public function testDelete() - { - $cache = $this->prepare(); - $this->assertNotNull($cache->rm('number_test')); - $this->assertFalse($cache->get('number_test')); - } - - /** - * 获取并删除缓存测试 - * @return mixed - * @access public - */ - public function testPull() - { - $cache = $this->prepare(); - $this->assertEquals(11, $cache->pull('number_test')); - $this->assertFalse($cache->get('number_test')); - } - - /** - * 清空缓存测试 - * @return mixed - * @access public - */ - public function testClear() - { - $cache = $this->prepare(); - $this->assertTrue($cache->clear()); - $this->assertFalse($cache->get('number_test')); - } - - public function testStaticCall() - { - $this->assertTrue(Cache::set('a', 1)); - $this->assertEquals(1, Cache::get('a')); - $this->assertEquals(2, Cache::inc('a')); - $this->assertEquals(4, Cache::inc('a', 2)); - $this->assertEquals(4, Cache::get('a')); - $this->assertEquals(3, Cache::dec('a')); - $this->assertEquals(1, Cache::dec('a', 2)); - $this->assertEquals(1, Cache::get('a')); - $this->assertNotNull(Cache::rm('a')); - $this->assertNotNull(Cache::clear()); - } - -} diff --git a/thinkphp/tests/thinkphp/library/think/cache/driver/fileTest.php b/thinkphp/tests/thinkphp/library/think/cache/driver/fileTest.php deleted file mode 100644 index eb2099ce..00000000 --- a/thinkphp/tests/thinkphp/library/think/cache/driver/fileTest.php +++ /dev/null @@ -1,46 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * File缓存驱动测试 - * @author 刘志淳 - */ - -namespace tests\thinkphp\library\think\cache\driver; - -class fileTest extends cacheTestCase -{ - private $_cacheInstance = null; - - /** - * 基境缓存类型 - */ - protected function setUp() - { - \think\Cache::connect(['type' => 'File', 'path' => CACHE_PATH]); - } - - /** - * @return FileCache - */ - protected function getCacheInstance() - { - if (null === $this->_cacheInstance) { - $this->_cacheInstance = new \think\cache\driver\File(); - } - return $this->_cacheInstance; - } - - // skip testExpire - public function testExpire() - { - } -} diff --git a/thinkphp/tests/thinkphp/library/think/cache/driver/liteTest.php b/thinkphp/tests/thinkphp/library/think/cache/driver/liteTest.php deleted file mode 100644 index 19cbb9ef..00000000 --- a/thinkphp/tests/thinkphp/library/think/cache/driver/liteTest.php +++ /dev/null @@ -1,69 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * Lite缓存驱动测试 - * @author 刘志淳 - */ - -namespace tests\thinkphp\library\think\cache\driver; - -use think\Cache; - -class liteTest extends \PHPUnit_Framework_TestCase -{ - protected function getCacheInstance() - { - return Cache::connect(['type' => 'Lite', 'path' => CACHE_PATH]); - } - - /** - * 测试缓存读取 - * @return mixed - * @access public - */ - public function testGet() - { - $cache = $this->getCacheInstance(); - $this->assertFalse($cache->get('test')); - } - - /** - * 测试缓存设置 - * @return mixed - * @access public - */ - public function testSet() - { - $cache = $this->getCacheInstance(); - $this->assertNotEmpty($cache->set('test', 'test')); - } - - /** - * 删除缓存测试 - * @return mixed - * @access public - */ - public function testRm() - { - $cache = $this->getCacheInstance(); - $this->assertTrue($cache->rm('test')); - } - - /** - * 清空缓存测试 - * @return mixed - * @access public - */ - public function testClear() - { - } -} diff --git a/thinkphp/tests/thinkphp/library/think/cache/driver/memcacheTest.php b/thinkphp/tests/thinkphp/library/think/cache/driver/memcacheTest.php deleted file mode 100644 index 8975fa20..00000000 --- a/thinkphp/tests/thinkphp/library/think/cache/driver/memcacheTest.php +++ /dev/null @@ -1,49 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * Memcache缓存驱动测试 - * @author 刘志淳 - */ - -namespace tests\thinkphp\library\think\cache\driver; - -class memcacheTest extends cacheTestCase -{ - private $_cacheInstance = null; - - /** - * 基境缓存类型 - */ - protected function setUp() - { - if (!extension_loaded('memcache')) { - $this->markTestSkipped("Memcache没有安装,已跳过测试!"); - } - \think\Cache::connect(['type' => 'memcache', 'expire' => 2]); - } - - /** - * @return ApcCache - */ - protected function getCacheInstance() - { - if (null === $this->_cacheInstance) { - $this->_cacheInstance = new \think\cache\driver\Memcache(['length' => 3]); - } - return $this->_cacheInstance; - } - - // skip testExpire - public function testExpire() - { - } -} diff --git a/thinkphp/tests/thinkphp/library/think/cache/driver/memcachedTest.php b/thinkphp/tests/thinkphp/library/think/cache/driver/memcachedTest.php deleted file mode 100644 index d528dd22..00000000 --- a/thinkphp/tests/thinkphp/library/think/cache/driver/memcachedTest.php +++ /dev/null @@ -1,72 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * Memcached缓存驱动测试 - * @author 7IN0SAN9 - */ - -namespace tests\thinkphp\library\think\cache\driver; - -class memcachedTest extends cacheTestCase -{ - private $_cacheInstance = null; - /** - * 基境缓存类型 - */ - protected function setUp() - { - if (!extension_loaded("memcached") && !extension_loaded('memcache')) { - $this->markTestSkipped("Memcached或Memcache没有安装,已跳过测试!"); - } - \think\Cache::connect(array('type' => 'memcached', 'expire' => 2)); - } - /** - * @return ApcCache - */ - protected function getCacheInstance() - { - if (null === $this->_cacheInstance) { - $this->_cacheInstance = new \think\cache\driver\Memcached(['length' => 3]); - } - return $this->_cacheInstance; - } - /** - * 缓存过期测试《提出来测试,因为目前看通不过缓存过期测试,所以还需研究》 - * @return mixed - * @access public - */ - public function testExpire() - { - } - - public function testStaticCall() - { - } - - /** - * 测试缓存自增 - * @return mixed - * @access public - */ - public function testInc() - { - } - - /** - * 测试缓存自减 - * @return mixed - * @access public - */ - public function testDec() - { - } -} diff --git a/thinkphp/tests/thinkphp/library/think/cache/driver/redisTest.php b/thinkphp/tests/thinkphp/library/think/cache/driver/redisTest.php deleted file mode 100644 index 839e5165..00000000 --- a/thinkphp/tests/thinkphp/library/think/cache/driver/redisTest.php +++ /dev/null @@ -1,66 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * Redis缓存驱动测试 - * @author 7IN0SAN9 - */ - -namespace tests\thinkphp\library\think\cache\driver; - -class redisTest extends cacheTestCase -{ - private $_cacheInstance = null; - - protected function setUp() - { - if (!extension_loaded("redis")) { - $this->markTestSkipped("Redis没有安装,已跳过测试!"); - } - \think\Cache::connect(array('type' => 'redis', 'expire' => 2)); - } - - protected function getCacheInstance() - { - if (null === $this->_cacheInstance) { - $this->_cacheInstance = new \think\cache\driver\Redis(['length' => 3]); - } - return $this->_cacheInstance; - } - - public function testGet() - { - $cache = $this->prepare(); - $this->assertEquals('string_test', $cache->get('string_test')); - $this->assertEquals(11, $cache->get('number_test')); - $result = $cache->get('array_test'); - $this->assertEquals('array_test', $result['array_test']); - } - - public function testStoreSpecialValues() - { - $redis = new \think\cache\driver\Redis(['length' => 3]); - $redis->set('key', 'value'); - $redis->get('key'); - - $redis->handler()->setnx('key', 'value'); - $value = $redis->handler()->get('key'); - $this->assertEquals('value', $value); - - $redis->handler()->hset('hash', 'key', 'value'); - $value = $redis->handler()->hget('hash', 'key'); - $this->assertEquals('value', $value); - } - - public function testExpire() - { - } -} diff --git a/thinkphp/tests/thinkphp/library/think/config/driver/fixtures/config.ini b/thinkphp/tests/thinkphp/library/think/config/driver/fixtures/config.ini deleted file mode 100644 index 2a17cc0d..00000000 --- a/thinkphp/tests/thinkphp/library/think/config/driver/fixtures/config.ini +++ /dev/null @@ -1 +0,0 @@ -inifile=1 \ No newline at end of file diff --git a/thinkphp/tests/thinkphp/library/think/config/driver/fixtures/config.json b/thinkphp/tests/thinkphp/library/think/config/driver/fixtures/config.json deleted file mode 100644 index 716ed064..00000000 --- a/thinkphp/tests/thinkphp/library/think/config/driver/fixtures/config.json +++ /dev/null @@ -1 +0,0 @@ -{"jsonstring":1} \ No newline at end of file diff --git a/thinkphp/tests/thinkphp/library/think/config/driver/fixtures/config.xml b/thinkphp/tests/thinkphp/library/think/config/driver/fixtures/config.xml deleted file mode 100644 index 3c3266a1..00000000 --- a/thinkphp/tests/thinkphp/library/think/config/driver/fixtures/config.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - 1 - - \ No newline at end of file diff --git a/thinkphp/tests/thinkphp/library/think/config/driver/iniTest.php b/thinkphp/tests/thinkphp/library/think/config/driver/iniTest.php deleted file mode 100644 index 2575157c..00000000 --- a/thinkphp/tests/thinkphp/library/think/config/driver/iniTest.php +++ /dev/null @@ -1,33 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * Ini配置测试 - * @author 7IN0SAN9 - */ - -namespace tests\thinkphp\library\think\config\driver; - -use think\config; - -class iniTest extends \PHPUnit_Framework_TestCase -{ - public function testParse() - { - Config::parse('inistring=1', 'ini'); - $this->assertEquals(1, Config::get('inistring')); - Config::reset(); - Config::parse(__DIR__ . '/fixtures/config.ini'); - $this->assertTrue(Config::has('inifile')); - $this->assertEquals(1, Config::get('inifile')); - Config::reset(); - } -} diff --git a/thinkphp/tests/thinkphp/library/think/config/driver/jsonTest.php b/thinkphp/tests/thinkphp/library/think/config/driver/jsonTest.php deleted file mode 100644 index 9d438d40..00000000 --- a/thinkphp/tests/thinkphp/library/think/config/driver/jsonTest.php +++ /dev/null @@ -1,33 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * Xml配置测试 - * @author 7IN0SAN9 - */ - -namespace tests\thinkphp\library\think\config\driver; - -use think\config; - -class jsonTest extends \PHPUnit_Framework_TestCase -{ - public function testParse() - { - Config::parse('{"jsonstring":1}', 'json'); - $this->assertEquals(1, Config::get('jsonstring')); - Config::reset(); - Config::parse(__DIR__ . '/fixtures/config.json'); - $this->assertTrue(Config::has('jsonstring')); - $this->assertEquals(1, Config::get('jsonstring')); - Config::reset(); - } -} diff --git a/thinkphp/tests/thinkphp/library/think/config/driver/xmlTest.php b/thinkphp/tests/thinkphp/library/think/config/driver/xmlTest.php deleted file mode 100644 index 9ab1d257..00000000 --- a/thinkphp/tests/thinkphp/library/think/config/driver/xmlTest.php +++ /dev/null @@ -1,33 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * Xml配置测试 - * @author 7IN0SAN9 - */ - -namespace tests\thinkphp\library\think\config\driver; - -use think\config; - -class xmlTest extends \PHPUnit_Framework_TestCase -{ - public function testParse() - { - Config::parse('1', 'xml'); - $this->assertEquals(1, Config::get('xmlstring')); - Config::reset(); - Config::parse(__DIR__ . '/fixtures/config.xml'); - $this->assertTrue(Config::has('xmlfile.istrue')); - $this->assertEquals(1, Config::get('xmlfile.istrue')); - Config::reset(); - } -} diff --git a/thinkphp/tests/thinkphp/library/think/configTest.php b/thinkphp/tests/thinkphp/library/think/configTest.php deleted file mode 100644 index 166f0f8a..00000000 --- a/thinkphp/tests/thinkphp/library/think/configTest.php +++ /dev/null @@ -1,169 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * 配置测试 - * @author Haotong Lin - */ - -namespace tests\thinkphp\library\think; - -use ReflectionClass; -use think\Config; - -class configTest extends \PHPUnit_Framework_TestCase -{ - public function testRange() - { - $reflectedClass = new ReflectionClass('\think\Config'); - $reflectedPropertyRange = $reflectedClass->getProperty('range'); - $reflectedPropertyRange->setAccessible(true); - $reflectedPropertyConfig = $reflectedClass->getProperty('config'); - $reflectedPropertyConfig->setAccessible(true); - // test default range - $this->assertEquals('_sys_', $reflectedPropertyRange->getValue()); - $config = $reflectedPropertyConfig->getValue(); - $this->assertTrue(is_array($config)); - // test range initialization - Config::range('_test_'); - $this->assertEquals('_test_', $reflectedPropertyRange->getValue()); - $config = $reflectedPropertyConfig->getValue(); - $this->assertEquals([], $config['_test_']); - } - - // public function testParse() - // { - // see \think\config\driver\...Test.php - // } - - public function testLoad() - { - $file = APP_PATH . 'config' . EXT; - $config = array_change_key_case(include $file); - $name = '_name_'; - $range = '_test_'; - - $reflectedClass = new ReflectionClass('\think\Config'); - $reflectedPropertyConfig = $reflectedClass->getProperty('config'); - $reflectedPropertyConfig->setAccessible(true); - $reflectedPropertyConfig->setValue([]); - - $this->assertEquals($config, Config::load($file, $name, $range)); - $this->assertNotEquals(null, Config::load($file, $name, $range)); - } - - public function testHas() - { - $range = '_test_'; - $this->assertFalse(Config::has('abcd', $range)); - $reflectedClass = new ReflectionClass('\think\Config'); - $reflectedPropertyConfig = $reflectedClass->getProperty('config'); - $reflectedPropertyConfig->setAccessible(true); - - // if (!strpos($name, '.')): - $reflectedPropertyConfig->setValue([ - $range => ['abcd' => 'value'], - ]); - $this->assertTrue(Config::has('abcd', $range)); - - // else ... - $this->assertFalse(Config::has('abcd.efg', $range)); - - $reflectedPropertyConfig->setValue([ - $range => ['abcd' => ['efg' => 'value']], - ]); - $this->assertTrue(Config::has('abcd.efg', $range)); - } - - public function testGet() - { - $range = '_test_'; - $reflectedClass = new ReflectionClass('\think\Config'); - $reflectedPropertyConfig = $reflectedClass->getProperty('config'); - $reflectedPropertyConfig->setAccessible(true); - // test all configurations - $reflectedPropertyConfig->setValue([$range => []]); - $this->assertEquals([], Config::get(null, $range)); - $this->assertEquals(null, Config::get(null, 'does_not_exist')); - $value = 'value'; - // test getting configuration - $reflectedPropertyConfig->setValue([$range => ['abcd' => 'efg']]); - $this->assertEquals('efg', Config::get('abcd', $range)); - $this->assertEquals(null, Config::get('does_not_exist', $range)); - $this->assertEquals(null, Config::get('abcd', 'does_not_exist')); - // test getting configuration with dot syntax - $reflectedPropertyConfig->setValue([$range => [ - 'one' => ['two' => $value], - ]]); - $this->assertEquals($value, Config::get('one.two', $range)); - $this->assertEquals(null, Config::get('one.does_not_exist', $range)); - $this->assertEquals(null, Config::get('one.two', 'does_not_exist')); - } - - public function testSet() - { - $range = '_test_'; - $reflectedClass = new ReflectionClass('\think\Config'); - $reflectedPropertyConfig = $reflectedClass->getProperty('config'); - $reflectedPropertyConfig->setAccessible(true); - $reflectedPropertyConfig->setValue([]); - // if (is_string($name)): - // without dot syntax - $name = 'name'; - $value = 'value'; - Config::set($name, $value, $range); - $config = $reflectedPropertyConfig->getValue(); - $this->assertEquals($value, $config[$range][$name]); - // with dot syntax - $name = 'one.two'; - $value = 'dot value'; - Config::set($name, $value, $range); - $config = $reflectedPropertyConfig->getValue(); - $this->assertEquals($value, $config[$range]['one']['two']); - // if (is_array($name)): - // see testLoad() - // ... - // test getting all configurations...? - // return self::$config[$range]; ?? - $value = ['all' => 'configuration']; - $reflectedPropertyConfig->setValue([$range => $value]); - $this->assertEquals($value, Config::set(null, null, $range)); - $this->assertNotEquals(null, Config::set(null, null, $range)); - } - - public function testReset() - { - $range = '_test_'; - $reflectedClass = new ReflectionClass('\think\Config'); - $reflectedPropertyConfig = $reflectedClass->getProperty('config'); - $reflectedPropertyConfig->setAccessible(true); - $reflectedPropertyConfig->setValue([$range => ['abcd' => 'efg']]); - - // clear all configurations - Config::reset(true); - $config = $reflectedPropertyConfig->getValue(); - $this->assertEquals([], $config); - // clear the configuration in range of parameter. - $reflectedPropertyConfig->setValue([ - $range => [ - 'abcd' => 'efg', - 'hijk' => 'lmn', - ], - 'a' => 'b', - ]); - Config::reset($range); - $config = $reflectedPropertyConfig->getValue(); - $this->assertEquals([ - $range => [], - 'a' => 'b', - ], $config); - } -} diff --git a/thinkphp/tests/thinkphp/library/think/controller/.gitignore b/thinkphp/tests/thinkphp/library/think/controller/.gitignore deleted file mode 100644 index c96a04f0..00000000 --- a/thinkphp/tests/thinkphp/library/think/controller/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/thinkphp/tests/thinkphp/library/think/controllerTest.php b/thinkphp/tests/thinkphp/library/think/controllerTest.php deleted file mode 100644 index fba8d6c5..00000000 --- a/thinkphp/tests/thinkphp/library/think/controllerTest.php +++ /dev/null @@ -1,194 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * 控制器测试 - * @author Haotong Lin - */ - -namespace tests\thinkphp\library\think; - -use ReflectionClass; -use think\Controller; -use think\Request; -use think\View; - -require_once CORE_PATH . '../../helper.php'; - -class Foo extends Controller -{ - public $test = 'test'; - - public function _initialize() - { - $this->test = 'abcd'; - } - - public function assignTest() - { - $this->assign('abcd', 'dcba'); - $this->assign(['key1' => 'value1', 'key2' => 'value2']); - } - - public function fetchTest() - { - $template = dirname(__FILE__) . '/display.html'; - return $this->fetch($template, ['name' => 'ThinkPHP']); - } - - public function displayTest() - { - $template = dirname(__FILE__) . '/display.html'; - return $this->display($template, ['name' => 'ThinkPHP']); - } - public function test() - { - $data = [ - 'username' => 'username', - 'nickname' => 'nickname', - 'password' => '123456', - 'repassword' => '123456', - 'email' => 'abc@abc.com', - 'sex' => '0', - 'age' => '20', - 'code' => '1234', - ]; - - $validate = [ - ['username', 'length:5,15', '用户名长度为5到15个字符'], - ['nickname', 'require', '请填昵称'], - ['password', '[\w-]{6,15}', '密码长度为6到15个字符'], - ['repassword', 'confirm:password', '两次密码不一到致'], - ['email', 'filter:validate_email', '邮箱格式错误'], - ['sex', 'in:0,1', '性别只能为为男或女'], - ['age', 'between:1,80', '年龄只能在10-80之间'], - ]; - return $this->validate($data, $validate); - } -} - -class Bar extends Controller -{ - public $test = 1; - - public $beforeActionList = ['action1', 'action2']; - - public function action1() - { - $this->test += 2; - return 'action1'; - } - - public function action2() - { - $this->test += 4; - return 'action2'; - } -} - -class Baz extends Controller -{ - public $test = 1; - - public $beforeActionList = [ - 'action1' => ['only' => 'index'], - 'action2' => ['except' => 'index'], - 'action3' => ['only' => 'abcd'], - 'action4' => ['except' => 'abcd'], - ]; - - public function action1() - { - $this->test += 2; - return 'action1'; - } - - public function action2() - { - $this->test += 4; - return 'action2'; - } - - public function action3() - { - $this->test += 8; - return 'action2'; - } - - public function action4() - { - $this->test += 16; - return 'action2'; - } -} - -class controllerTest extends \PHPUnit_Framework_TestCase -{ - public function testInitialize() - { - $foo = new Foo(Request::instance()); - $this->assertEquals('abcd', $foo->test); - } - - public function testBeforeAction() - { - $obj = new Bar(Request::instance()); - $this->assertEquals(7, $obj->test); - - $obj = new Baz(Request::instance()); - $this->assertEquals(19, $obj->test); - } - - private function getView($controller) - { - $view = new View(); - $rc = new ReflectionClass(get_class($controller)); - $property = $rc->getProperty('view'); - $property->setAccessible(true); - $property->setValue($controller, $view); - return $view; - } - - public function testFetch() - { - $controller = new Foo(Request::instance()); - $view = $this->getView($controller); - $template = dirname(__FILE__) . '/display.html'; - $viewFetch = $view->fetch($template, ['name' => 'ThinkPHP']); - $this->assertEquals($controller->fetchTest(), $viewFetch); - } - - public function testDisplay() - { - $controller = new Foo; - $view = $this->getView($controller); - $template = dirname(__FILE__) . '/display.html'; - $viewFetch = $view->display($template, ['name' => 'ThinkPHP']); - - $this->assertEquals($controller->displayTest(), $viewFetch); - } - - public function testAssign() - { - $controller = new Foo(Request::instance()); - $view = $this->getView($controller); - $controller->assignTest(); - $expect = ['abcd' => 'dcba', 'key1' => 'value1', 'key2' => 'value2']; - $this->assertAttributeEquals($expect, 'data', $view); - } - - public function testValidate() - { - $controller = new Foo(Request::instance()); - $result = $controller->test(); - $this->assertTrue($result); - } -} diff --git a/thinkphp/tests/thinkphp/library/think/cookieTest.php b/thinkphp/tests/thinkphp/library/think/cookieTest.php deleted file mode 100644 index 24cb153e..00000000 --- a/thinkphp/tests/thinkphp/library/think/cookieTest.php +++ /dev/null @@ -1,150 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * Cookie测试 - * @author Haotong Lin - */ - -namespace tests\thinkphp\library\think; - -use ReflectionClass; -use think\Cookie; - -class cookieTest extends \PHPUnit_Framework_TestCase -{ - protected $ref; - - protected $default = [ - // cookie 名称前缀 - 'prefix' => '', - // cookie 保存时间 - 'expire' => 0, - // cookie 保存路径 - 'path' => '/', - // cookie 有效域名 - 'domain' => '', - // cookie 启用安全传输 - 'secure' => false, - // httponly设置 - 'httponly' => '', - // 是否使用 setcookie - 'setcookie' => false, - ]; - - protected function setUp() - { - $reflectedClass = new ReflectionClass('\think\Cookie'); - $reflectedPropertyConfig = $reflectedClass->getProperty('config'); - $reflectedPropertyConfig->setAccessible(true); - $reflectedPropertyConfig->setValue($this->default); - $this->ref = $reflectedPropertyConfig; - } - - public function testInit() - { - $config = [ - // cookie 名称前缀 - 'prefix' => 'think_', - // cookie 保存时间 - 'expire' => 0, - // cookie 保存路径 - 'path' => '/path/to/test/', - // cookie 有效域名 - 'domain' => '.thinkphp.cn', - // cookie 启用安全传输 - 'secure' => true, - // httponly设置 - 'httponly' => '1', - ]; - Cookie::init($config); - - $this->assertEquals( - array_merge($this->default, array_change_key_case($config)), - $this->ref->getValue() - ); - } - - public function testPrefix() - { - $this->assertEquals($this->default['prefix'], Cookie::prefix()); - - $prefix = '_test_'; - $this->assertNotEquals($prefix, Cookie::prefix()); - Cookie::prefix($prefix); - - $config = $this->ref->getValue(); - $this->assertEquals($prefix, $config['prefix']); - } - - public function testSet() - { - $value = 'value'; - - $name = 'name1'; - Cookie::set($name, $value, 10); - $this->assertEquals($value, $_COOKIE[$this->default['prefix'] . $name]); - - $name = 'name2'; - Cookie::set($name, $value, null); - $this->assertEquals($value, $_COOKIE[$this->default['prefix'] . $name]); - - $name = 'name3'; - Cookie::set($name, $value, 'expire=100&prefix=pre_'); - $this->assertEquals($value, $_COOKIE['pre_' . $name]); - - $name = 'name4'; - $value = ['_test_中文_']; - Cookie::set($name, $value); - $this->assertEquals('think:' . json_encode([urlencode('_test_中文_')]), $_COOKIE[$name]); - } - - public function testGet() - { - $_COOKIE = [ - 'a' => 'b', - 'pre_abc' => 'c', - 'd' => 'think:' . json_encode([urlencode('_test_中文_')]), - ]; - $this->assertEquals('b', Cookie::get('a')); - $this->assertEquals(null, Cookie::get('does_not_exist')); - $this->assertEquals('c', Cookie::get('abc', 'pre_')); - $this->assertEquals(['_test_中文_'], Cookie::get('d')); - } - - public function testDelete() - { - $_COOKIE = [ - 'a' => 'b', - 'pre_abc' => 'c', - ]; - $this->assertEquals('b', Cookie::get('a')); - Cookie::delete('a'); - $this->assertEquals(null, Cookie::get('a')); - - $this->assertEquals('c', Cookie::get('abc', 'pre_')); - Cookie::delete('abc', 'pre_'); - $this->assertEquals(null, Cookie::get('abc', 'pre_')); - } - - public function testClear() - { - $_COOKIE = []; - $this->assertEquals(null, Cookie::clear()); - - $_COOKIE = [ - 'a' => 'b', - 'pre_abc' => 'c', - ]; - Cookie::clear('pre_'); - $this->assertEquals(['a' => 'b'], $_COOKIE); - } -} diff --git a/thinkphp/tests/thinkphp/library/think/db/driver/.gitignore b/thinkphp/tests/thinkphp/library/think/db/driver/.gitignore deleted file mode 100644 index c96a04f0..00000000 --- a/thinkphp/tests/thinkphp/library/think/db/driver/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/thinkphp/tests/thinkphp/library/think/dbTest.php b/thinkphp/tests/thinkphp/library/think/dbTest.php deleted file mode 100644 index 5724ee21..00000000 --- a/thinkphp/tests/thinkphp/library/think/dbTest.php +++ /dev/null @@ -1,352 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * Db类测试 - * @author: 刘志淳 - */ - -namespace tests\thinkphp\library\think; - -use think\Db; - -class dbTest extends \PHPUnit_Framework_TestCase -{ - // 获取测试数据库配置 - private function getConfig() - { - return [ - // 数据库类型 - 'type' => 'mysql', - // 服务器地址 - 'hostname' => '127.0.0.1', - // 数据库名 - 'database' => 'test', - // 用户名 - 'username' => 'root', - // 密码 - 'password' => '', - // 端口 - 'hostport' => '', - // 连接dsn - 'dsn' => '', - // 数据库连接参数 - 'params' => [], - // 数据库编码默认采用utf8 - 'charset' => 'utf8', - // 数据库表前缀 - 'prefix' => 'tp_', - // 数据库调试模式 - 'debug' => true, - // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) - 'deploy' => 0, - // 数据库读写是否分离 主从式有效 - 'rw_separate' => false, - // 读写分离后 主服务器数量 - 'master_num' => 1, - // 指定从服务器序号 - 'slave_no' => '', - // 是否严格检查字段是否存在 - 'fields_strict' => true, - // 数据集返回类型 array 数组 collection Collection对象 - 'resultset_type' => 'array', - // 是否自动写入时间戳字段 - 'auto_timestamp' => false, - // 是否需要进行SQL性能分析 - 'sql_explain' => false, - ]; - } - - // 获取创建数据库 SQL - private function getCreateTableSql() - { - $sql[] = <<getConfig(); - $result = Db::connect($config)->execute('show databases'); - $this->assertNotEmpty($result); - } - - public function testExecute() - { - $config = $this->getConfig(); - $sql = $this->getCreateTableSql(); - foreach ($sql as $one) { - Db::connect($config)->execute($one); - } - $tableNum = Db::connect($config)->execute("show tables;"); - $this->assertEquals(4, $tableNum); - } - - public function testQuery() - { - $config = $this->getConfig(); - $sql = $this->getCreateTableSql(); - Db::connect($config)->batchQuery($sql); - - $tableQueryResult = Db::connect($config)->query("show tables;"); - - $this->assertTrue(is_array($tableQueryResult)); - - $tableNum = count($tableQueryResult); - $this->assertEquals(4, $tableNum); - } - - public function testBatchQuery() - { - $config = $this->getConfig(); - $sql = $this->getCreateTableSql(); - Db::connect($config)->batchQuery($sql); - - $tableNum = Db::connect($config)->execute("show tables;"); - $this->assertEquals(4, $tableNum); - } - - public function testTable() - { - $config = $this->getConfig(); - $tableName = 'tp_user'; - $result = Db::connect($config)->table($tableName); - $this->assertEquals($tableName, $result->getOptions()['table']); - } - - public function testName() - { - $config = $this->getConfig(); - $tableName = 'user'; - $result = Db::connect($config)->name($tableName); - $this->assertEquals($config['prefix'] . $tableName, $result->getTable()); - } - - public function testInsert() - { - $config = $this->getConfig(); - $data = [ - 'username' => 'chunice', - 'password' => md5('chunice'), - 'status' => 1, - 'create_time' => time(), - ]; - $result = Db::connect($config)->name('user')->insert($data); - $this->assertEquals(1, $result); - } - - public function testUpdate() - { - $config = $this->getConfig(); - $data = [ - 'username' => 'chunice_update', - 'password' => md5('chunice'), - 'status' => 1, - 'create_time' => time(), - ]; - $result = Db::connect($config)->name('user')->where('username', 'chunice')->update($data); - $this->assertEquals(1, $result); - } - - public function testFind() - { - $config = $this->getConfig(); - $mustFind = Db::connect($config)->name('user')->where('username', 'chunice_update')->find(); - $this->assertNotEmpty($mustFind); - $mustNotFind = Db::connect($config)->name('user')->where('username', 'chunice')->find(); - $this->assertEmpty($mustNotFind); - } - - public function testInsertAll() - { - $config = $this->getConfig(); - - $data = [ - ['username' => 'foo', 'password' => md5('foo'), 'status' => 1, 'create_time' => time()], - ['username' => 'bar', 'password' => md5('bar'), 'status' => 1, 'create_time' => time()], - ]; - - $insertNum = Db::connect($config)->name('user')->insertAll($data); - $this->assertEquals(count($data), $insertNum); - } - - public function testSelect() - { - $config = $this->getConfig(); - $mustFound = Db::connect($config)->name('user')->where('status', 1)->select(); - $this->assertNotEmpty($mustFound); - $mustNotFound = Db::connect($config)->name('user')->where('status', 0)->select(); - $this->assertEmpty($mustNotFound); - } - - public function testValue() - { - $config = $this->getConfig(); - $username = Db::connect($config)->name('user')->where('id', 1)->value('username'); - $this->assertEquals('chunice_update', $username); - $usernameNull = Db::connect($config)->name('user')->where('id', 0)->value('username'); - $this->assertEmpty($usernameNull); - } - - public function testColumn() - { - $config = $this->getConfig(); - $username = Db::connect($config)->name('user')->where('status', 1)->column('username'); - $this->assertNotEmpty($username); - $usernameNull = Db::connect($config)->name('user')->where('status', 0)->column('username'); - $this->assertEmpty($usernameNull); - - } - - public function testInsertGetId() - { - $config = $this->getConfig(); - $id = Db::connect($config)->name('user')->order('id', 'desc')->value('id'); - - $data = [ - 'username' => uniqid(), - 'password' => md5('chunice'), - 'status' => 1, - 'create_time' => time(), - ]; - $lastId = Db::connect($config)->name('user')->insertGetId($data); - $this->assertEquals($id + 1, $lastId); - - } - - public function testGetLastInsId() - { - $config = $this->getConfig(); - $data = [ - 'username' => uniqid(), - 'password' => md5('chunice'), - 'status' => 1, - 'create_time' => time(), - ]; - $lastId = Db::connect($config)->name('user')->insertGetId($data); - - $lastInsId = Db::connect($config)->name('user')->getLastInsID(); - $this->assertEquals($lastId, $lastInsId); - } - - public function testSetField() - { - $config = $this->getConfig(); - - $setFieldNum = Db::connect($config)->name('user')->where('id', 1)->setField('username', 'chunice_setField'); - $this->assertEquals(1, $setFieldNum); - - $setFieldNum = Db::connect($config)->name('user')->where('id', 1)->setField('username', 'chunice_setField'); - $this->assertEquals(0, $setFieldNum); - } - - public function testSetInc() - { - $config = $this->getConfig(); - $originCreateTime = Db::connect($config)->name('user')->where('id', 1)->value('create_time'); - Db::connect($config)->name('user')->where('id', 1)->setInc('create_time'); - $newCreateTime = Db::connect($config)->name('user')->where('id', 1)->value('create_time'); - $this->assertEquals($originCreateTime + 1, $newCreateTime); - - } - - public function testSetDec() - { - $config = $this->getConfig(); - $originCreateTime = Db::connect($config)->name('user')->where('id', 1)->value('create_time'); - Db::connect($config)->name('user')->where('id', 1)->setDec('create_time'); - $newCreateTime = Db::connect($config)->name('user')->where('id', 1)->value('create_time'); - $this->assertEquals($originCreateTime - 1, $newCreateTime); - } - - public function testDelete() - { - $config = $this->getConfig(); - Db::connect($config)->name('user')->where('id', 1)->delete(); - $result = Db::connect($config)->name('user')->where('id', 1)->find(); - $this->assertEmpty($result); - } - - public function testChunk() - { - // todo 暂未想到测试方法 - } - - public function testCache() - { - $config = $this->getConfig(); - $result = Db::connect($config)->name('user')->where('id', 1)->cache('key', 60)->find(); - $cache = \think\Cache::get('key'); - $this->assertEquals($result, $cache); - - $updateCache = Db::connect($config)->name('user')->cache('key')->find(1); - $this->assertEquals($cache, $updateCache); - } - -} diff --git a/thinkphp/tests/thinkphp/library/think/debugTest.php b/thinkphp/tests/thinkphp/library/think/debugTest.php deleted file mode 100644 index fc2fdd47..00000000 --- a/thinkphp/tests/thinkphp/library/think/debugTest.php +++ /dev/null @@ -1,179 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * Debug测试 - * @author 大漠 - */ - -namespace tests\thinkphp\library\think; - -use think\Debug; - -class debugTest extends \PHPUnit_Framework_TestCase -{ - - /** - * - * @var Debug - */ - protected $object; - - /** - * Sets up the fixture, for example, opens a network connection. - * This method is called before a test is executed. - */ - protected function setUp() - { - $this->object = new Debug(); - } - - /** - * Tears down the fixture, for example, closes a network connection. - * This method is called after a test is executed. - */ - protected function tearDown() - {} - - /** - * @covers think\Debug::remark - * @todo Implement testRemark(). - */ - public function testRemark() - { - $name = "testremarkkey"; - Debug::remark($name); - } - - /** - * @covers think\Debug::getRangeTime - * @todo Implement testGetRangeTime(). - */ - public function testGetRangeTime() - { - $start = "testGetRangeTimeStart"; - $end = "testGetRangeTimeEnd"; - Debug::remark($start); - usleep(20000); - // \think\Debug::remark($end); - - $time = Debug::getRangeTime($start, $end); - $this->assertLessThan(0.03, $time); - //$this->assertEquals(0.03, ceil($time)); - } - - /** - * @covers think\Debug::getUseTime - * @todo Implement testGetUseTime(). - */ - public function testGetUseTime() - { - $time = Debug::getUseTime(); - $this->assertLessThan(20, $time); - } - - /** - * @covers think\Debug::getThroughputRate - * @todo Implement testGetThroughputRate(). - */ - public function testGetThroughputRate() - { - usleep(100000); - $throughputRate = Debug::getThroughputRate(); - $this->assertLessThan(10, $throughputRate); - } - - /** - * @covers think\Debug::getRangeMem - * @todo Implement testGetRangeMem(). - */ - public function testGetRangeMem() - { - $start = "testGetRangeMemStart"; - $end = "testGetRangeMemEnd"; - Debug::remark($start); - $str = ""; - for ($i = 0; $i < 10000; $i++) { - $str .= "mem"; - } - - $rangeMem = Debug::getRangeMem($start, $end); - - $this->assertLessThan(33, explode(" ", $rangeMem)[0]); - } - - /** - * @covers think\Debug::getUseMem - * @todo Implement testGetUseMem(). - */ - public function testGetUseMem() - { - $useMem = Debug::getUseMem(); - - $this->assertLessThan(35, explode(" ", $useMem)[0]); - } - - /** - * @covers think\Debug::getMemPeak - * @todo Implement testGetMemPeak(). - */ - public function testGetMemPeak() - { - $start = "testGetMemPeakStart"; - $end = "testGetMemPeakEnd"; - Debug::remark($start); - $str = ""; - for ($i = 0; $i < 100000; $i++) { - $str .= "mem"; - } - $memPeak = Debug::getMemPeak($start, $end); - $this->assertLessThan(500, explode(" ", $memPeak)[0]); - } - - /** - * @covers think\Debug::getFile - * @todo Implement testGetFile(). - */ - public function testGetFile() - { - $count = Debug::getFile(); - - $this->assertEquals(count(get_included_files()), $count); - - $info = Debug::getFile(true); - $this->assertEquals(count(get_included_files()), count($info)); - - $this->assertContains("KB", $info[0]); - } - - /** - * @covers think\Debug::dump - * @todo Implement testDump(). - */ - public function testDump() - { - if (strstr(PHP_VERSION, 'hhvm')) { - return; - } - - $var = []; - $var["key"] = "val"; - $output = Debug::dump($var, false, $label = "label"); - $array = explode("array", json_encode($output)); - if (IS_WIN) { - $this->assertEquals("(1) {\\n [\\\"key\\\"] => string(3) \\\"val\\\"\\n}\\n\\r\\n\"", end($array)); - } elseif (strstr(PHP_OS, 'Darwin')) { - $this->assertEquals("(1) {\\n [\\\"key\\\"] => string(3) \\\"val\\\"\\n}\\n\\n\"", end($array)); - } else { - $this->assertEquals("(1) {\\n 'key' =>\\n string(3) \\\"val\\\"\\n}\\n\\n\"", end($array)); - } - } -} diff --git a/thinkphp/tests/thinkphp/library/think/display.html b/thinkphp/tests/thinkphp/library/think/display.html deleted file mode 100644 index e99d3025..00000000 --- a/thinkphp/tests/thinkphp/library/think/display.html +++ /dev/null @@ -1 +0,0 @@ -{$name??'default'} \ No newline at end of file diff --git a/thinkphp/tests/thinkphp/library/think/exceptionTest.php b/thinkphp/tests/thinkphp/library/think/exceptionTest.php deleted file mode 100644 index 66957ce6..00000000 --- a/thinkphp/tests/thinkphp/library/think/exceptionTest.php +++ /dev/null @@ -1,52 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * exception类测试 - * @author Haotong Lin - */ - -namespace tests\thinkphp\library\think; - -use ReflectionMethod; -use think\Exception as ThinkException; -use think\exception\HttpException; - -class MyException extends ThinkException -{ - -} - -class exceptionTest extends \PHPUnit_Framework_TestCase -{ - public function testGetHttpStatus() - { - try { - throw new HttpException(404, "Error Processing Request"); - } catch (HttpException $e) { - $this->assertEquals(404, $e->getStatusCode()); - } - } - - public function testDebugData() - { - $data = ['a' => 'b', 'c' => 'd']; - try { - $e = new MyException("Error Processing Request", 1); - $method = new ReflectionMethod($e, 'setData'); - $method->setAccessible(true); - $method->invokeArgs($e, ['test', $data]); - throw $e; - } catch (MyException $e) { - $this->assertEquals(['test' => $data], $e->getData()); - } - } -} diff --git a/thinkphp/tests/thinkphp/library/think/extend.html b/thinkphp/tests/thinkphp/library/think/extend.html deleted file mode 100644 index 922192dc..00000000 --- a/thinkphp/tests/thinkphp/library/think/extend.html +++ /dev/null @@ -1,2 +0,0 @@ -{extend name="extend2" /} -{block name="head"}header{/block} \ No newline at end of file diff --git a/thinkphp/tests/thinkphp/library/think/extend2.html b/thinkphp/tests/thinkphp/library/think/extend2.html deleted file mode 100644 index eb22e1da..00000000 --- a/thinkphp/tests/thinkphp/library/think/extend2.html +++ /dev/null @@ -1,17 +0,0 @@ -{layout name="layout2" replace="[__REPLACE__]" /} -{block name="head"}{/block} -
- {include file="include" name="info" value="$info.value" /} -{block name="main"} -{block name="side"} - side -{/block} -{block name="mainbody"} - -{/block} -{/block} -{literal} - {$name} -{/literal} - -
\ No newline at end of file diff --git a/thinkphp/tests/thinkphp/library/think/hookTest.php b/thinkphp/tests/thinkphp/library/think/hookTest.php deleted file mode 100644 index 930ecf53..00000000 --- a/thinkphp/tests/thinkphp/library/think/hookTest.php +++ /dev/null @@ -1,67 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * Hook类测试 - * @author liu21st - */ - -namespace tests\thinkphp\library\think; - -use think\Hook; - -class hookTest extends \PHPUnit_Framework_TestCase -{ - - public function testRun() - { - Hook::add('my_pos', '\tests\thinkphp\library\think\behavior\One'); - Hook::add('my_pos', ['\tests\thinkphp\library\think\behavior\Two']); - Hook::add('my_pos', '\tests\thinkphp\library\think\behavior\Three', true); - $data['id'] = 0; - $data['name'] = 'thinkphp'; - Hook::listen('my_pos', $data); - $this->assertEquals(2, $data['id']); - $this->assertEquals('thinkphp', $data['name']); - $this->assertEquals([ - '\tests\thinkphp\library\think\behavior\Three', - '\tests\thinkphp\library\think\behavior\One', - '\tests\thinkphp\library\think\behavior\Two'], - Hook::get('my_pos')); - } - - public function testImport() - { - Hook::import(['my_pos' => [ - '\tests\thinkphp\library\think\behavior\One', - '\tests\thinkphp\library\think\behavior\Three'], - ]); - Hook::import(['my_pos' => ['\tests\thinkphp\library\think\behavior\Two']], false); - Hook::import(['my_pos' => ['\tests\thinkphp\library\think\behavior\Three', '_overlay' => true]]); - $data['id'] = 0; - $data['name'] = 'thinkphp'; - Hook::listen('my_pos', $data); - $this->assertEquals(3, $data['id']); - - } - - public function testExec() - { - $data['id'] = 0; - $data['name'] = 'thinkphp'; - $this->assertEquals(true, Hook::exec('\tests\thinkphp\library\think\behavior\One')); - $this->assertEquals(false, Hook::exec('\tests\thinkphp\library\think\behavior\One', 'test', $data)); - $this->assertEquals('test', $data['name']); - $this->assertEquals('Closure', Hook::exec(function (&$data) {$data['name'] = 'Closure';return 'Closure';})); - - } - -} diff --git a/thinkphp/tests/thinkphp/library/think/include.html b/thinkphp/tests/thinkphp/library/think/include.html deleted file mode 100644 index e01ae9ef..00000000 --- a/thinkphp/tests/thinkphp/library/think/include.html +++ /dev/null @@ -1,2 +0,0 @@ - -{include file="include2" /} \ No newline at end of file diff --git a/thinkphp/tests/thinkphp/library/think/include2.html b/thinkphp/tests/thinkphp/library/think/include2.html deleted file mode 100644 index 24bdacb3..00000000 --- a/thinkphp/tests/thinkphp/library/think/include2.html +++ /dev/null @@ -1 +0,0 @@ -{$info.value}: \ No newline at end of file diff --git a/thinkphp/tests/thinkphp/library/think/lang/lang.php b/thinkphp/tests/thinkphp/library/think/lang/lang.php deleted file mode 100644 index 96880b15..00000000 --- a/thinkphp/tests/thinkphp/library/think/lang/lang.php +++ /dev/null @@ -1,4 +0,0 @@ -'加载', -]; diff --git a/thinkphp/tests/thinkphp/library/think/langTest.php b/thinkphp/tests/thinkphp/library/think/langTest.php deleted file mode 100644 index 360ee43d..00000000 --- a/thinkphp/tests/thinkphp/library/think/langTest.php +++ /dev/null @@ -1,76 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * Lang测试 - * @author liu21st - */ - -namespace tests\thinkphp\library\think; - -use think\Config; -use think\Lang; - -class langTest extends \PHPUnit_Framework_TestCase -{ - - public function testSetAndGet() - { - Lang::set('hello,%s', '欢迎,%s'); - $this->assertEquals('欢迎,ThinkPHP', Lang::get('hello,%s', ['ThinkPHP'])); - Lang::set('hello,%s', '歡迎,%s', 'zh-tw'); - $this->assertEquals('歡迎,ThinkPHP', Lang::get('hello,%s', ['ThinkPHP'], 'zh-tw')); - Lang::set(['hello' => '欢迎', 'use' => '使用']); - $this->assertEquals('欢迎', Lang::get('hello')); - $this->assertEquals('欢迎', Lang::get('HELLO')); - $this->assertEquals('使用', Lang::get('use')); - - Lang::set('hello,{:name}', '欢迎,{:name}'); - $this->assertEquals('欢迎,liu21st', Lang::get('hello,{:name}', ['name' => 'liu21st'])); - } - - public function testLoad() - { - Lang::load(__DIR__ . DS . 'lang' . DS . 'lang.php'); - $this->assertEquals('加载', Lang::get('load')); - Lang::load(__DIR__ . DS . 'lang' . DS . 'lang.php', 'test'); - $this->assertEquals('加载', Lang::get('load', [], 'test')); - } - - public function testDetect() - { - - Config::set('lang_list', ['zh-cn', 'zh-tw']); - Lang::set('hello', '欢迎', 'zh-cn'); - Lang::set('hello', '歡迎', 'zh-tw'); - - Config::set('lang_detect_var', 'lang'); - Config::set('lang_cookie_var', 'think_cookie'); - - $_GET['lang'] = 'zh-tw'; - Lang::detect(); - $this->assertEquals('歡迎', Lang::get('hello')); - - $_GET['lang'] = 'zh-cn'; - Lang::detect(); - $this->assertEquals('欢迎', Lang::get('hello')); - - } - - public function testRange() - { - $this->assertEquals('zh-cn', Lang::range()); - Lang::set('hello', '欢迎', 'test'); - Lang::range('test'); - $this->assertEquals('test', Lang::range()); - $this->assertEquals('欢迎', Lang::get('hello')); - } -} diff --git a/thinkphp/tests/thinkphp/library/think/layout.html b/thinkphp/tests/thinkphp/library/think/layout.html deleted file mode 100644 index be8d4a07..00000000 --- a/thinkphp/tests/thinkphp/library/think/layout.html +++ /dev/null @@ -1,2 +0,0 @@ -
{__CONTENT__} -
\ No newline at end of file diff --git a/thinkphp/tests/thinkphp/library/think/layout2.html b/thinkphp/tests/thinkphp/library/think/layout2.html deleted file mode 100644 index a5b75729..00000000 --- a/thinkphp/tests/thinkphp/library/think/layout2.html +++ /dev/null @@ -1,2 +0,0 @@ - \ No newline at end of file diff --git a/thinkphp/tests/thinkphp/library/think/loader/test/Hello.php b/thinkphp/tests/thinkphp/library/think/loader/test/Hello.php deleted file mode 100644 index c1414561..00000000 --- a/thinkphp/tests/thinkphp/library/think/loader/test/Hello.php +++ /dev/null @@ -1,7 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * Loader测试 - * @author liu21st - */ - -namespace tests\thinkphp\library\think; - -use think\Loader; - -class loaderTest extends \PHPUnit_Framework_TestCase -{ - - public function testAutoload() - { - $this->assertEquals(false, Loader::autoload('\think\Url')); - $this->assertEquals(false, Loader::autoload('think\Test')); - $this->assertEquals(false, Loader::autoload('my\HelloTest')); - } - - public function testAddClassMap() - { - Loader::addClassMap('my\hello\Test', __DIR__ . DS . 'loader' . DS . 'Test.php'); - } - - public function testAddNamespace() - { - Loader::addNamespace('top', __DIR__ . DS . 'loader' . DS); - $this->assertEquals(true, Loader::autoload('top\test\Hello')); - } - - public function testAddNamespaceAlias() - { - Loader::addNamespaceAlias('top', 'top\test'); - Loader::addNamespaceAlias(['top' => 'top\test', 'app' => 'app\index']); - //$this->assertEquals(true, Loader::autoload('top\Hello')); - } - - public function testTable() - { - Loader::db('mysql://root@127.0.0.1/test#utf8'); - } - - public function testImport() - { - $this->assertEquals(false, Loader::import('think.log.driver.MyTest')); - } - - public function testParseName() - { - $this->assertEquals('HelloTest', Loader::parseName('hello_test', 1)); - $this->assertEquals('hello_test', Loader::parseName('HelloTest', 0)); - } - - public function testParseClass() - { - $this->assertEquals('app\index\controller\User', Loader::parseClass('index', 'controller', 'user')); - $this->assertEquals('app\index\controller\user\Type', Loader::parseClass('index', 'controller', 'user.type')); - $this->assertEquals('app\admin\model\UserType', Loader::parseClass('admin', 'model', 'user_type')); - } -} diff --git a/thinkphp/tests/thinkphp/library/think/log/driver/fileTest.php b/thinkphp/tests/thinkphp/library/think/log/driver/fileTest.php deleted file mode 100644 index ad6dd224..00000000 --- a/thinkphp/tests/thinkphp/library/think/log/driver/fileTest.php +++ /dev/null @@ -1,34 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * Test File Log - */ -namespace tests\thinkphp\library\think\log\driver; - -use think\Log; - -class fileTest extends \PHPUnit_Framework_TestCase -{ - protected function setUp() - { - Log::init(['type' => 'file']); - } - - public function testRecord() - { - $record_msg = 'record'; - Log::record($record_msg, 'notice'); - $logs = Log::getLog(); - - $this->assertEquals([], $logs); - } -} diff --git a/thinkphp/tests/thinkphp/library/think/logTest.php b/thinkphp/tests/thinkphp/library/think/logTest.php deleted file mode 100644 index e786b173..00000000 --- a/thinkphp/tests/thinkphp/library/think/logTest.php +++ /dev/null @@ -1,39 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * Log测试 - * @author liu21st - */ -namespace tests\thinkphp\library\think; - -use think\Log; - -class logTest extends \PHPUnit_Framework_TestCase -{ - - public function testSave() - { - Log::init(['type' => 'test']); - Log::clear(); - Log::record('test'); - Log::record([1, 2, 3]); - $this->assertTrue(Log::save()); - } - - public function testWrite() - { - Log::init(['type' => 'test']); - Log::clear(); - $this->assertTrue(Log::write('hello', 'info')); - $this->assertTrue(Log::write([1, 2, 3], 'log')); - } -} diff --git a/thinkphp/tests/thinkphp/library/think/model/.gitignore b/thinkphp/tests/thinkphp/library/think/model/.gitignore deleted file mode 100644 index c96a04f0..00000000 --- a/thinkphp/tests/thinkphp/library/think/model/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/thinkphp/tests/thinkphp/library/think/paginateTest.php b/thinkphp/tests/thinkphp/library/think/paginateTest.php deleted file mode 100644 index 8cd45507..00000000 --- a/thinkphp/tests/thinkphp/library/think/paginateTest.php +++ /dev/null @@ -1,40 +0,0 @@ - -// +---------------------------------------------------------------------- - -namespace tests\thinkphp\library\think; - -use think\paginator\driver\Bootstrap; - -class paginateTest extends \PHPUnit_Framework_TestCase -{ - public function testPaginatorInfo() - { - $p = Bootstrap::make($array = ['item3', 'item4'], 2, 2, 4); - - $this->assertEquals(4, $p->total()); - - $this->assertEquals(2, $p->listRows()); - - $this->assertEquals(2, $p->currentPage()); - - $p2 = Bootstrap::make($array2 = ['item3', 'item4'], 2, 2, 2); - $this->assertEquals(1, $p2->currentPage()); - } - - public function testPaginatorRender() - { - $p = Bootstrap::make($array = ['item3', 'item4'], 2, 2, 100); - $render = ''; - - $this->assertEquals($render, $p->render()); - } - -} diff --git a/thinkphp/tests/thinkphp/library/think/requestTest.php b/thinkphp/tests/thinkphp/library/think/requestTest.php deleted file mode 100644 index f86546e6..00000000 --- a/thinkphp/tests/thinkphp/library/think/requestTest.php +++ /dev/null @@ -1,187 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * Db类测试 - */ - -namespace tests\thinkphp\library\think; - -use think\Config; -use think\Request; - -class requestTest extends \PHPUnit_Framework_TestCase -{ - protected $request; - - public function setUp() - { - //$request = Request::create('http://www.domain.com/index/index/hello/?name=thinkphp'); - - } - - public function testCreate() - { - $request = Request::create('http://www.thinkphp.cn/index/index/hello.html?name=thinkphp'); - $this->assertEquals('http://www.thinkphp.cn', $request->domain()); - $this->assertEquals('/index/index/hello.html?name=thinkphp', $request->url()); - $this->assertEquals('/index/index/hello.html', $request->baseurl()); - $this->assertEquals('index/index/hello.html', $request->pathinfo()); - $this->assertEquals('index/index/hello', $request->path()); - $this->assertEquals('html', $request->ext()); - $this->assertEquals('name=thinkphp', $request->query()); - $this->assertEquals('www.thinkphp.cn', $request->host()); - $this->assertEquals(80, $request->port()); - $this->assertEquals($_SERVER['REQUEST_TIME'], $request->time()); - $this->assertEquals($_SERVER['REQUEST_TIME_FLOAT'], $request->time(true)); - $this->assertEquals('GET', $request->method()); - $this->assertEquals(['name' => 'thinkphp'], $request->param()); - $this->assertFalse($request->isSsl()); - $this->assertEquals('http', $request->scheme()); - } - - public function testDomain() - { - $request = Request::instance(); - $request->domain('http://thinkphp.cn'); - $this->assertEquals('http://thinkphp.cn', $request->domain()); - } - - public function testUrl() - { - $request = Request::instance(); - $request->url('/index.php/index/hello?name=thinkphp'); - $this->assertEquals('/index.php/index/hello?name=thinkphp', $request->url()); - $this->assertEquals('http://thinkphp.cn/index.php/index/hello?name=thinkphp', $request->url(true)); - } - - public function testBaseUrl() - { - $request = Request::instance(); - $request->baseurl('/index.php/index/hello'); - $this->assertEquals('/index.php/index/hello', $request->baseurl()); - $this->assertEquals('http://thinkphp.cn/index.php/index/hello', $request->baseurl(true)); - } - - public function testbaseFile() - { - $request = Request::instance(); - $request->basefile('/index.php'); - $this->assertEquals('/index.php', $request->basefile()); - $this->assertEquals('http://thinkphp.cn/index.php', $request->basefile(true)); - } - - public function testroot() - { - $request = Request::instance(); - $request->root('/index.php'); - $this->assertEquals('/index.php', $request->root()); - $this->assertEquals('http://thinkphp.cn/index.php', $request->root(true)); - } - - public function testType() - { - $request = Request::instance(); - $request->server(['HTTP_ACCEPT' => 'application/json']); - - $this->assertEquals('json', $request->type()); - $request->mimeType('test', 'application/test'); - $request->mimeType(['test' => 'application/test']); - $request->server(['HTTP_ACCEPT' => 'application/test']); - - $this->assertEquals('test', $request->type()); - } - - public function testmethod() - { - $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'DELETE'; - - $request = Request::create('', ''); - $this->assertEquals('DELETE', $request->method()); - $this->assertEquals('GET', $request->method(true)); - - Config::set('var_method', '_method'); - $_POST['_method'] = 'POST'; - $request = Request::create('', ''); - $this->assertEquals('POST', $request->method()); - $this->assertEquals('GET', $request->method(true)); - $this->assertTrue($request->isPost()); - $this->assertFalse($request->isGet()); - $this->assertFalse($request->isPut()); - $this->assertFalse($request->isDelete()); - $this->assertFalse($request->isHead()); - $this->assertFalse($request->isPatch()); - $this->assertFalse($request->isOptions()); - } - - public function testCli() - { - $request = Request::instance(); - $this->assertTrue($request->isCli()); - } - - public function testVar() - { - Config::set('app_multi_module', true); - $request = Request::create(''); - $request->route(['name' => 'thinkphp', 'id' => 6]); - $request->get(['id' => 10]); - $request->post(['id' => 8]); - $request->put(['id' => 7]); - $request->request(['test' => 'value']); - $this->assertEquals(['name' => 'thinkphp', 'id' => 6], $request->route()); - //$this->assertEquals(['id' => 10], $request->get()); - $this->assertEquals('thinkphp', $request->route('name')); - $this->assertEquals('default', $request->route('test', 'default')); - $this->assertEquals(10, $request->get('id')); - $this->assertEquals(0, $request->get('ids', 0)); - $this->assertEquals(8, $request->post('id')); - $this->assertEquals(7, $request->put('id')); - $this->assertEquals('value', $request->request('test')); - $this->assertEquals('thinkphp', $request->param('name')); - $this->assertEquals(6, $request->param('id')); - $this->assertFalse($request->has('user_id')); - $this->assertTrue($request->has('test', 'request')); - $this->assertEquals(['id' => 6], $request->only('id')); - $this->assertEquals(['name' => 'thinkphp', 'lang' => 'zh-cn'], $request->except('id')); - $this->assertEquals('THINKPHP', $request->param('name', '', 'strtoupper')); - } - - public function testIsAjax() - { - $request = Request::create(''); - $_SERVER['HTTP_X_REQUESTED_WITH'] = 'xmlhttprequest'; - $this->assertTrue($request->isAjax()); - } - - public function testIsPjax() - { - $request = Request::create(''); - $_SERVER['HTTP_X_PJAX'] = true; - $this->assertTrue($request->isPjax()); - } - - public function testIsMobile() - { - $request = Request::create(''); - $_SERVER['HTTP_VIA'] = 'wap'; - $this->assertTrue($request->isMobile()); - } - - public function testBind() - { - $request = Request::create(''); - $request->user = 'User1'; - $request->bind(['user' => 'User2']); - $this->assertEquals('User2', $request->user); - } - -} diff --git a/thinkphp/tests/thinkphp/library/think/responseTest.php b/thinkphp/tests/thinkphp/library/think/responseTest.php deleted file mode 100644 index 62d15747..00000000 --- a/thinkphp/tests/thinkphp/library/think/responseTest.php +++ /dev/null @@ -1,95 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * Response测试 - * @author 大漠 - */ - -namespace tests\thinkphp\library\think; - -use think\Config; -use think\Request; -use think\Response; - -class responseTest extends \PHPUnit_Framework_TestCase -{ - - /** - * - * @var \think\Response - */ - protected $object; - - protected $default_return_type; - - protected $default_ajax_return; - - /** - * Sets up the fixture, for example, opens a network connection. - * This method is called before a test is executed. - */ - protected function setUp() - { - // 1. - // restore_error_handler(); - // Warning: Cannot modify header information - headers already sent by (output started at PHPUnit\Util\Printer.php:173) - // more see in https://www.analysisandsolutions.com/blog/html/writing-phpunit-tests-for-wordpress-plugins-wp-redirect-and-continuing-after-php-errors.htm - - // 2. - // the Symfony used the HeaderMock.php - - // 3. - // not run the eclipse will held, and travis-ci.org Searching for coverage reports - // **> Python coverage not found - // **> No coverage report found. - // add the - // /** - // * @runInSeparateProcess - // */ - if (!$this->default_return_type) { - $this->default_return_type = Config::get('default_return_type'); - } - if (!$this->default_ajax_return) { - $this->default_ajax_return = Config::get('default_ajax_return'); - } - } - - /** - * Tears down the fixture, for example, closes a network connection. - * This method is called after a test is executed. - */ - protected function tearDown() - { - Config::set('default_ajax_return', $this->default_ajax_return); - Config::set('default_return_type', $this->default_return_type); - } - - /** - * @covers think\Response::send - * @todo Implement testSend(). - */ - public function testSend() - { - $dataArr = []; - $dataArr["key"] = "value"; - - $response = Response::create($dataArr, 'json'); - $result = $response->getContent(); - $this->assertEquals('{"key":"value"}', $result); - $request = Request::instance(); - $request->get(['callback' => 'callback']); - $response = Response::create($dataArr, 'jsonp'); - $result = $response->getContent(); - $this->assertEquals('callback({"key":"value"});', $result); - } - -} diff --git a/thinkphp/tests/thinkphp/library/think/routeTest.php b/thinkphp/tests/thinkphp/library/think/routeTest.php deleted file mode 100644 index 31e1b861..00000000 --- a/thinkphp/tests/thinkphp/library/think/routeTest.php +++ /dev/null @@ -1,287 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * Route测试 - * @author liu21st - */ - -namespace tests\thinkphp\library\think; - -use think\Config; -use think\Request; -use think\Route; - -class routeTest extends \PHPUnit_Framework_TestCase -{ - - protected function setUp() - { - Config::set('app_multi_module', true); - } - - public function testRegister() - { - $request = Request::instance(); - Route::get('hello/:name', 'index/hello'); - Route::get(['hello/:name' => 'index/hello']); - Route::post('hello/:name', 'index/post'); - Route::put('hello/:name', 'index/put'); - Route::delete('hello/:name', 'index/delete'); - Route::patch('hello/:name', 'index/patch'); - Route::any('user/:id', 'index/user'); - $result = Route::check($request, 'hello/thinkphp'); - $this->assertEquals([null, 'index', 'hello'], $result['module']); - $this->assertEquals(['hello' => true, 'user/:id' => true, 'hello/:name' => ['rule' => 'hello/:name', 'route' => 'index/hello', 'var' => ['name' => 1], 'option' => [], 'pattern' => []]], Route::rules('GET')); - Route::rule('type1/:name', 'index/type', 'PUT|POST'); - Route::rule(['type2/:name' => 'index/type1']); - Route::rule([['type3/:name', 'index/type2', ['method' => 'POST']]]); - Route::rule(['name', 'type4/:name'], 'index/type4'); - } - - public function testImport() - { - $rule = [ - '__domain__' => ['subdomain2.thinkphp.cn' => 'blog1'], - '__alias__' => ['blog1' => 'blog1'], - '__rest__' => ['res' => ['index/blog']], - 'bbb' => ['index/blog1', ['method' => 'POST']], - 'ddd' => '', - ['hello1/:ddd', 'index/hello1', ['method' => 'POST']], - ]; - Route::import($rule); - } - - public function testResource() - { - $request = Request::instance(); - Route::resource('res', 'index/blog'); - Route::resource(['res' => ['index/blog']]); - $result = Route::check($request, 'res'); - $this->assertEquals(['index', 'blog', 'index'], $result['module']); - $result = Route::check($request, 'res/create'); - $this->assertEquals(['index', 'blog', 'create'], $result['module']); - $result = Route::check($request, 'res/8'); - $this->assertEquals(['index', 'blog', 'read'], $result['module']); - $result = Route::check($request, 'res/8/edit'); - $this->assertEquals(['index', 'blog', 'edit'], $result['module']); - - Route::resource('blog.comment', 'index/comment'); - $result = Route::check($request, 'blog/8/comment/10'); - $this->assertEquals(['index', 'comment', 'read'], $result['module']); - $result = Route::check($request, 'blog/8/comment/10/edit'); - $this->assertEquals(['index', 'comment', 'edit'], $result['module']); - - } - - public function testRest() - { - $request = Request::instance(); - Route::rest('read', ['GET', '/:id', 'look']); - Route::rest('create', ['GET', '/create', 'add']); - Route::rest(['read' => ['GET', '/:id', 'look'], 'create' => ['GET', '/create', 'add']]); - Route::resource('res', 'index/blog'); - $result = Route::check($request, 'res/create'); - $this->assertEquals(['index', 'blog', 'add'], $result['module']); - $result = Route::check($request, 'res/8'); - $this->assertEquals(['index', 'blog', 'look'], $result['module']); - - } - - public function testMixVar() - { - $request = Request::instance(); - Route::get('hello-', 'index/hello', [], ['name' => '\w+']); - $result = Route::check($request, 'hello-thinkphp'); - $this->assertEquals([null, 'index', 'hello'], $result['module']); - Route::get('hello-', 'index/hello', [], ['name' => '\w+', 'id' => '\d+']); - $result = Route::check($request, 'hello-thinkphp2016'); - $this->assertEquals([null, 'index', 'hello'], $result['module']); - Route::get('hello-/[:id]', 'index/hello', [], ['name' => '\w+', 'id' => '\d+']); - $result = Route::check($request, 'hello-thinkphp/2016'); - $this->assertEquals([null, 'index', 'hello'], $result['module']); - } - - public function testParseUrl() - { - $result = Route::parseUrl('hello'); - $this->assertEquals(['hello', null, null], $result['module']); - $result = Route::parseUrl('index/hello'); - $this->assertEquals(['index', 'hello', null], $result['module']); - $result = Route::parseUrl('index/hello?name=thinkphp'); - $this->assertEquals(['index', 'hello', null], $result['module']); - $result = Route::parseUrl('index/user/hello'); - $this->assertEquals(['index', 'user', 'hello'], $result['module']); - $result = Route::parseUrl('index/user/hello/name/thinkphp'); - $this->assertEquals(['index', 'user', 'hello'], $result['module']); - $result = Route::parseUrl('index-index-hello', '-'); - $this->assertEquals(['index', 'index', 'hello'], $result['module']); - } - - public function testCheckRoute() - { - Route::get('hello/:name', 'index/hello'); - Route::get('blog/:id', 'blog/read', [], ['id' => '\d+']); - $request = Request::instance(); - $this->assertEquals(false, Route::check($request, 'test/thinkphp')); - $this->assertEquals(false, Route::check($request, 'blog/thinkphp')); - $result = Route::check($request, 'blog/5'); - $this->assertEquals([null, 'blog', 'read'], $result['module']); - $result = Route::check($request, 'hello/thinkphp/abc/test'); - $this->assertEquals([null, 'index', 'hello'], $result['module']); - } - - public function testCheckRouteGroup() - { - $request = Request::instance(); - Route::pattern(['id' => '\d+']); - Route::pattern('name', '\w{6,25}'); - Route::group('group', [':id' => 'index/hello', ':name' => 'index/say']); - $this->assertEquals(false, Route::check($request, 'empty/think')); - $result = Route::check($request, 'group/think'); - $this->assertEquals(false, $result['module']); - $result = Route::check($request, 'group/10'); - $this->assertEquals([null, 'index', 'hello'], $result['module']); - $result = Route::check($request, 'group/thinkphp'); - $this->assertEquals([null, 'index', 'say'], $result['module']); - Route::group('group2', function () { - Route::group('group3', [':id' => 'index/hello', ':name' => 'index/say']); - Route::rule(':name', 'index/hello'); - Route::auto('index'); - }); - $result = Route::check($request, 'group2/thinkphp'); - $this->assertEquals([null, 'index', 'hello'], $result['module']); - $result = Route::check($request, 'group2/think'); - $this->assertEquals(['index', 'group2', 'think'], $result['module']); - $result = Route::check($request, 'group2/group3/thinkphp'); - $this->assertEquals([null, 'index', 'say'], $result['module']); - Route::group('group4', function () { - Route::group('group3', [':id' => 'index/hello', ':name' => 'index/say']); - Route::rule(':name', 'index/hello'); - Route::miss('index/__miss__'); - }); - $result = Route::check($request, 'group4/thinkphp'); - $this->assertEquals([null, 'index', 'hello'], $result['module']); - $result = Route::check($request, 'group4/think'); - $this->assertEquals([null, 'index', '__miss__'], $result['module']); - - Route::group(['prefix' => 'prefix/'], function () { - Route::rule('hello4/:name', 'hello'); - }); - Route::group(['prefix' => 'prefix/'], [ - 'hello4/:name' => 'hello', - ]); - $result = Route::check($request, 'hello4/thinkphp'); - $this->assertEquals([null, 'prefix', 'hello'], $result['module']); - Route::group('group5', [ - [':name', 'hello', ['method' => 'GET|POST']], - ':id' => 'hello', - ], ['prefix' => 'prefix/']); - $result = Route::check($request, 'group5/thinkphp'); - $this->assertEquals([null, 'prefix', 'hello'], $result['module']); - } - - public function testControllerRoute() - { - $request = Request::instance(); - Route::controller('controller', 'index/Blog'); - $result = Route::check($request, 'controller/info'); - $this->assertEquals(['index', 'Blog', 'getinfo'], $result['module']); - Route::setMethodPrefix('GET', 'read'); - Route::setMethodPrefix(['get' => 'read']); - Route::controller('controller', 'index/Blog'); - $result = Route::check($request, 'controller/phone'); - $this->assertEquals(['index', 'Blog', 'readphone'], $result['module']); - } - - public function testAliasRoute() - { - $request = Request::instance(); - Route::alias('alias', 'index/Alias'); - $result = Route::check($request, 'alias/info'); - $this->assertEquals('index/Alias/info', $result['module']); - } - - public function testRouteToModule() - { - $request = Request::instance(); - Route::get('hello/:name', 'index/hello'); - Route::get('blog/:id', 'blog/read', [], ['id' => '\d+']); - $this->assertEquals(false, Route::check($request, 'test/thinkphp')); - $this->assertEquals(false, Route::check($request, 'blog/thinkphp')); - $result = Route::check($request, 'hello/thinkphp'); - $this->assertEquals([null, 'index', 'hello'], $result['module']); - $result = Route::check($request, 'blog/5'); - $this->assertEquals([null, 'blog', 'read'], $result['module']); - } - - public function testRouteToController() - { - $request = Request::instance(); - Route::get('say/:name', '@index/hello'); - $this->assertEquals(['type' => 'controller', 'controller' => 'index/hello', 'var' => []], Route::check($request, 'say/thinkphp')); - } - - public function testRouteToMethod() - { - $request = Request::instance(); - Route::get('user/:name', '\app\index\service\User::get', [], ['name' => '\w+']); - Route::get('info/:name', '\app\index\model\Info@getInfo', [], ['name' => '\w+']); - $this->assertEquals(['type' => 'method', 'method' => '\app\index\service\User::get', 'var' => []], Route::check($request, 'user/thinkphp')); - $this->assertEquals(['type' => 'method', 'method' => ['\app\index\model\Info', 'getInfo'], 'var' => []], Route::check($request, 'info/thinkphp')); - } - - public function testRouteToRedirect() - { - $request = Request::instance(); - Route::get('art/:id', '/article/read/id/:id', [], ['id' => '\d+']); - $this->assertEquals(['type' => 'redirect', 'url' => '/article/read/id/8', 'status' => 301], Route::check($request, 'art/8')); - } - - public function testBind() - { - $request = Request::instance(); - Route::bind('index/blog'); - Route::get('blog/:id', 'index/blog/read'); - $result = Route::check($request, 'blog/10'); - $this->assertEquals(['index', 'blog', 'read'], $result['module']); - $result = Route::parseUrl('test'); - $this->assertEquals(['index', 'blog', 'test'], $result['module']); - - Route::bind('\app\index\controller', 'namespace'); - $this->assertEquals(['type' => 'method', 'method' => ['\app\index\controller\Blog', 'read'], 'var' => []], Route::check($request, 'blog/read')); - - Route::bind('\app\index\controller\Blog', 'class'); - $this->assertEquals(['type' => 'method', 'method' => ['\app\index\controller\Blog', 'read'], 'var' => []], Route::check($request, 'read')); - } - - public function testDomain() - { - $request = Request::create('http://subdomain.thinkphp.cn'); - Route::domain('subdomain.thinkphp.cn', 'sub?abc=test&status=1'); - $rules = Route::rules('GET'); - Route::checkDomain($request, $rules); - $this->assertEquals('sub', Route::getbind('module')); - $this->assertEquals('test', $_GET['abc']); - $this->assertEquals(1, $_GET['status']); - - Route::domain('subdomain.thinkphp.cn', '\app\index\controller'); - $rules = Route::rules('GET'); - Route::checkDomain($request, $rules); - $this->assertEquals('\app\index\controller', Route::getbind('namespace')); - - Route::domain(['subdomain.thinkphp.cn' => '@\app\index\controller\blog']); - $rules = Route::rules('GET'); - Route::checkDomain($request, $rules); - $this->assertEquals('\app\index\controller\blog', Route::getbind('class')); - - } -} diff --git a/thinkphp/tests/thinkphp/library/think/session/.gitignore b/thinkphp/tests/thinkphp/library/think/session/.gitignore deleted file mode 100644 index c96a04f0..00000000 --- a/thinkphp/tests/thinkphp/library/think/session/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/thinkphp/tests/thinkphp/library/think/sessionTest.php b/thinkphp/tests/thinkphp/library/think/sessionTest.php deleted file mode 100644 index 5fd95f10..00000000 --- a/thinkphp/tests/thinkphp/library/think/sessionTest.php +++ /dev/null @@ -1,319 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * Session测试 - * @author 大漠 - */ - -namespace tests\thinkphp\library\think; - -use think\Session; - -class sessionTest extends \PHPUnit_Framework_TestCase -{ - - /** - * - * @var \think\Session - */ - protected $object; - - /** - * Sets up the fixture, for example, opens a network connection. - * This method is called before a test is executed. - */ - protected function setUp() - { - // $this->object = new Session (); - // register_shutdown_function ( function () { - // } ); // 此功能无法取消,需要回调函数配合。 - set_exception_handler(function () {}); - set_error_handler(function () {}); - } - - /** - * Tears down the fixture, for example, closes a network connection. - * This method is called after a test is executed. - */ - protected function tearDown() - { - register_shutdown_function('think\Error::appShutdown'); - set_error_handler('think\Error::appError'); - set_exception_handler('think\Error::appException'); - } - - /** - * @covers think\Session::prefix - * - * @todo Implement testPrefix(). - */ - public function testPrefix() - { - Session::prefix(null); - Session::prefix('think_'); - - $this->assertEquals('think_', Session::prefix()); - } - - /** - * @covers think\Session::init - * - * @todo Implement testInit(). - */ - public function testInit() - { - Session::prefix(null); - $config = [ - // cookie 名称前缀 - 'prefix' => 'think_', - // cookie 保存时间 - 'expire' => 60, - // cookie 保存路径 - 'path' => '/path/to/test/session/', - // cookie 有效域名 - 'domain' => '.thinkphp.cn', - 'var_session_id' => 'sessionidtest', - 'id' => 'sess_8fhgkjuakhatbeg2fa14lo84q1', - 'name' => 'session_name', - 'use_trans_sid' => '1', - 'use_cookies' => '1', - 'cache_limiter' => '60', - 'cache_expire' => '60', - 'type' => '', // memcache - 'namespace' => '\\think\\session\\driver\\', // ? - 'auto_start' => '1', - ]; - - $_REQUEST[$config['var_session_id']] = $config['id']; - Session::init($config); - - // 开始断言 - $this->assertEquals($config['prefix'], Session::prefix()); - $this->assertEquals($config['id'], $_REQUEST[$config['var_session_id']]); - $this->assertEquals($config['name'], session_name()); - - $this->assertEquals($config['path'], session_save_path()); - $this->assertEquals($config['use_cookies'], ini_get('session.use_cookies')); - $this->assertEquals($config['domain'], ini_get('session.cookie_domain')); - $this->assertEquals($config['expire'], ini_get('session.gc_maxlifetime')); - $this->assertEquals($config['expire'], ini_get('session.cookie_lifetime')); - - $this->assertEquals($config['cache_limiter'], session_cache_limiter($config['cache_limiter'])); - $this->assertEquals($config['cache_expire'], session_cache_expire($config['cache_expire'])); - - // 检测分支 - $_REQUEST[$config['var_session_id']] = null; - session_write_close(); - session_destroy(); - - Session::init($config); - - // 测试auto_start - // PHP_SESSION_DISABLED - // PHP_SESSION_NONE - // PHP_SESSION_ACTIVE - // session_status() - if (strstr(PHP_VERSION, 'hhvm')) { - $this->assertEquals('', ini_get('session.auto_start')); - } else { - $this->assertEquals(0, ini_get('session.auto_start')); - } - - $this->assertEquals($config['use_trans_sid'], ini_get('session.use_trans_sid')); - - Session::init($config); - $this->assertEquals($config['id'], session_id()); - } - - /** - * 单独重现异常 - * @expectedException \think\Exception - */ - public function testException() - { - $config = [ - // cookie 名称前缀 - 'prefix' => 'think_', - // cookie 保存时间 - 'expire' => 0, - // cookie 保存路径 - 'path' => '/path/to/test/session/', - // cookie 有效域名 - 'domain' => '.thinkphp.cn', - 'var_session_id' => 'sessionidtest', - 'id' => 'sess_8fhgkjuakhatbeg2fa14lo84q1', - 'name' => 'session_name', - 'use_trans_sid' => '1', - 'use_cookies' => '1', - 'cache_limiter' => '60', - 'cache_expire' => '60', - 'type' => '\\think\\session\\driver\\Memcache', // - 'auto_start' => '1', - ]; - - // 测试session驱动是否存在 - // @expectedException 异常类名 - $this->setExpectedException('\think\exception\ClassNotFoundException', 'error session handler'); - - Session::init($config); - } - - /** - * @covers think\Session::set - * - * @todo Implement testSet(). - */ - public function testSet() - { - Session::prefix(null); - Session::set('sessionname', 'sessionvalue'); - $this->assertEquals('sessionvalue', $_SESSION['sessionname']); - - Session::set('sessionnamearr.subname', 'sessionvalue'); - $this->assertEquals('sessionvalue', $_SESSION['sessionnamearr']['subname']); - - Session::set('sessionnameper', 'sessionvalue', 'think_'); - $this->assertEquals('sessionvalue', $_SESSION['think_']['sessionnameper']); - - Session::set('sessionnamearrper.subname', 'sessionvalue', 'think_'); - $this->assertEquals('sessionvalue', $_SESSION['think_']['sessionnamearrper']['subname']); - } - - /** - * @covers think\Session::get - * - * @todo Implement testGet(). - */ - public function testGet() - { - Session::prefix(null); - - Session::set('sessionnameget', 'sessionvalue'); - $this->assertEquals(Session::get('sessionnameget'), $_SESSION['sessionnameget']); - - Session::set('sessionnamegetarr.subname', 'sessionvalue'); - $this->assertEquals(Session::get('sessionnamegetarr.subname'), $_SESSION['sessionnamegetarr']['subname']); - - Session::set('sessionnamegetarrperall', 'sessionvalue', 'think_'); - $this->assertEquals(Session::get('', 'think_')['sessionnamegetarrperall'], $_SESSION['think_']['sessionnamegetarrperall']); - - Session::set('sessionnamegetper', 'sessionvalue', 'think_'); - $this->assertEquals(Session::get('sessionnamegetper', 'think_'), $_SESSION['think_']['sessionnamegetper']); - - Session::set('sessionnamegetarrper.subname', 'sessionvalue', 'think_'); - $this->assertEquals(Session::get('sessionnamegetarrper.subname', 'think_'), $_SESSION['think_']['sessionnamegetarrper']['subname']); - } - - public function testPull() - { - Session::prefix(null); - Session::set('sessionnamedel', 'sessionvalue'); - $this->assertEquals('sessionvalue', Session::pull('sessionnameget')); - $this->assertNull(Session::get('sessionnameget')); - } - - /** - * @covers think\Session::delete - * - * @todo Implement testDelete(). - */ - public function testDelete() - { - Session::prefix(null); - Session::set('sessionnamedel', 'sessionvalue'); - Session::delete('sessionnamedel'); - $this->assertEmpty($_SESSION['sessionnamedel']); - - Session::set('sessionnamedelarr.subname', 'sessionvalue'); - Session::delete('sessionnamedelarr.subname'); - $this->assertEmpty($_SESSION['sessionnamedelarr']['subname']); - - Session::set('sessionnamedelper', 'sessionvalue', 'think_'); - Session::delete('sessionnamedelper', 'think_'); - $this->assertEmpty($_SESSION['think_']['sessionnamedelper']); - - Session::set('sessionnamedelperarr.subname', 'sessionvalue', 'think_'); - Session::delete('sessionnamedelperarr.subname', 'think_'); - $this->assertEmpty($_SESSION['think_']['sessionnamedelperarr']['subname']); - } - - /** - * @covers think\Session::clear - * - * @todo Implement testClear(). - */ - public function testClear() - { - Session::prefix(null); - - Session::set('sessionnameclsper', 'sessionvalue1', 'think_'); - Session::clear('think_'); - $this->assertNull($_SESSION['think_']); - - Session::set('sessionnameclsper', 'sessionvalue1', 'think_'); - Session::clear(); - $this->assertEmpty($_SESSION); - } - - /** - * @covers think\Session::has - * - * @todo Implement testHas(). - */ - public function testHas() - { - Session::prefix(null); - Session::set('sessionnamehas', 'sessionvalue'); - $this->assertTrue(Session::has('sessionnamehas')); - - Session::set('sessionnamehasarr.subname', 'sessionvalue'); - $this->assertTrue(Session::has('sessionnamehasarr.subname')); - - Session::set('sessionnamehasper', 'sessionvalue', 'think_'); - $this->assertTrue(Session::has('sessionnamehasper', 'think_')); - - Session::set('sessionnamehasarrper.subname', 'sessionvalue', 'think_'); - $this->assertTrue(Session::has('sessionnamehasarrper.subname', 'think_')); - } - - /** - * @covers think\Session::pause - * - * @todo Implement testPause(). - */ - public function testPause() - { - Session::pause(); - } - - /** - * @covers think\Session::start - * - * @todo Implement testStart(). - */ - public function testStart() - { - Session::start(); - } - - /** - * @covers think\Session::destroy - * - * @todo Implement testDestroy(). - */ - public function testDestroy() - { - Session::set('sessionnamedestroy', 'sessionvalue'); - Session::destroy(); - $this->assertEmpty($_SESSION['sessionnamedestroy']); - } -} diff --git a/thinkphp/tests/thinkphp/library/think/template/driver/.gitignore b/thinkphp/tests/thinkphp/library/think/template/driver/.gitignore deleted file mode 100644 index c96a04f0..00000000 --- a/thinkphp/tests/thinkphp/library/think/template/driver/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/thinkphp/tests/thinkphp/library/think/template/taglib/cxTest.php b/thinkphp/tests/thinkphp/library/think/template/taglib/cxTest.php deleted file mode 100644 index 8aee392f..00000000 --- a/thinkphp/tests/thinkphp/library/think/template/taglib/cxTest.php +++ /dev/null @@ -1,575 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * 模板测试 - * @author Haotong Lin - */ - -namespace tests\thinkphp\library\think\tempplate\taglib; - -use think\Template; -use think\template\taglib\Cx; - -class cxTest extends \PHPUnit_Framework_TestCase -{ - public function testPhp() - { - $template = new template(); - $cx = new Cx($template); - - $content = << -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - } - - public function testVolist() - { - $template = new template(); - $cx = new Cx($template); - - $content = <<\$vo): \$mod = (\$key % 2 );++\$key;?> - - -EOF; - $cx->parseTag($content); - $this->assertEquals($data, $content); - - $content = <<display($content, ['list' => [1, 2, 3, 4, 5]]); - $this->expectOutputString('234'); - } - - public function testForeach() - { - $template = new template(); - $cx = new Cx($template); - - $content = <<\$val} - -{/foreach} -EOF; - $data = <<\$val): ?> - - -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - - $content = <<\$val): ?> - - -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - - $content = <<display($content); - $this->expectOutputString('234'); - } - - public function testIf() - { - $template = new template(); - $cx = new Cx($template); - - $content = << -one - -two - -default - -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - } - - public function testSwitch() - { - $template = new template(); - $cx = new Cx($template); - - $content = << - -a - - -b - - -d - - -default - -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - } - - public function testCompare() - { - $template = new template(); - $cx = new Cx($template); - - $content = << -default - -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - - $content = << -default - -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - - $content = << -default - -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - - $content = << -default - -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - - $content = << '0'): ?> -default - -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - - $content = <<= '0'): ?> -default - -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - - $content = << -default - -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - - $content = << -default - -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - - $content = << -default - -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - - $content = << -default - -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - - } - - public function testRange() - { - $template = new template(); - $cx = new Cx($template); - - $content = << -default - -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - - $content = << -default - -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - - $content = <<display($content); - $this->expectOutputString('yesno'); - } - - public function testPresent() - { - $template = new template(); - $cx = new Cx($template); - - $content = << -default - -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - - $content = << -default - -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - } - - public function testEmpty() - { - $template = new template(); - $cx = new Cx($template); - - $content = <<isEmpty())): ?> -default - -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - - $content = <<isEmpty()))): ?> -default - -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - } - - public function testDefined() - { - $template = new template(); - $cx = new Cx($template); - - $content = << -default - -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - - $content = << -default - -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - } - - public function testImport() - { - $template = new template(); - $cx = new Cx($template); - - $content = << -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - - $content = << -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - - $content = << -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - } - - public function testAssign() - { - $template = new template(); - $cx = new Cx($template); - - $content = << -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - - $content = << -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - } - - public function testDefine() - { - $template = new template(); - $cx = new Cx($template); - - $content = << -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - - $content = << -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - } - - public function testFor() - { - $template = new template(); - - $content = <<display($content); - $this->expectOutputString('123456789'); - } - public function testUrl() - { - $template = new template(); - $content = <<display($content); - $this->expectOutputString(\think\Url::build('Index/index')); - } - - public function testFunction() - { - $template = new template(); - $data = [ - 'list' => ['language' => 'php', 'version' => ['5.4', '5.5']], - 'a' => '[', - 'b' => ']', - ]; - - $content = <<\$val} -{if is_array(\$val)} -{~\$func(\$val)} -{else} -{if !is_numeric(\$key)} -{\$key.':'.\$val.','} -{else} -{\$a.\$val.\$b} -{/if} -{/if} -{/foreach} -{/function} -EOF; - $template->display($content, $data); - $this->expectOutputString("language:php,[5.4][5.5]"); - } -} diff --git a/thinkphp/tests/thinkphp/library/think/templateTest.php b/thinkphp/tests/thinkphp/library/think/templateTest.php deleted file mode 100644 index 27e49dbb..00000000 --- a/thinkphp/tests/thinkphp/library/think/templateTest.php +++ /dev/null @@ -1,414 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * 模板测试 - * @author oldrind - */ - -namespace tests\thinkphp\library\think; - -use think\Template; - -class templateTest extends \PHPUnit_Framework_TestCase -{ - public function testVar() - { - $template = new Template(); - - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = <<parse($content); - $this->assertEquals($data, $content); - - $content = <<parse($content); - $this->assertEquals($data, $content); - - } - - public function testVarFunction() - { - $template = new Template(); - - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << - -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - } - - public function testVarIdentify() - { - $config['tpl_begin'] = '<#'; - $config['tpl_end'] = '#>'; - $config['tpl_var_identify'] = ''; - $template = new Template($config); - - $content = << -EOF; - $data = <<a)) ? (is_array(\$info)?\$info['a']:\$info->a) : 'test'; ?> -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << -EOF; - $data = <<a)) echo 'test'; ?> -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << -EOF; - $data = <<a)==(is_array(\$info)?\$info['b']:\$info->b)) echo 'test'; ?> -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << -EOF; - $data = <<a) ?: 'test')?'yes':'no'; ?> -EOF; - $template->parse($content); - $this->assertEquals($data, $content); - - $template2 = new Template(); - $template2->tpl_var_identify = 'obj'; - $content = <<b)?'yes':'no'; ?> -EOF; - $template2->parse($content); - $this->assertEquals($data, $content); - } - - public function testThinkVar() - { - $config['tpl_begin'] = '{'; - $config['tpl_end'] = '}'; - $template = new Template($config); - - $_SERVER['SERVER_NAME'] = 'server_name'; - $_GET['action'] = 'action'; - $_POST['action'] = 'action'; - $_COOKIE['name'] = 'name'; - $_SESSION['action'] = ['name' => 'name']; - define('SITE_NAME', 'site_name'); - - $content = << -{\$Think.GET.action}
-{\$Think.POST.action}
-{\$Think.COOKIE.action}
-{\$Think.COOKIE.action.name}
-{\$Think.SESSION.action}
-{\$Think.SESSION.action.name}
-{\$Think.ENV.OS}
-{\$Think.REQUEST.action}
-{\$Think.CONST.SITE_NAME}
-{\$Think.LANG.action}
-{\$Think.CONFIG.action.name}
-{\$Think.NOW}
-{\$Think.VERSION}
-{\$Think.LDELIM}
-{\$Think.RDELIM}
-{\$Think.SITE_NAME}
-{\$Think.SITE.URL} -EOF; - $data = <<server('SERVER_NAME'); ?>
-get('action'); ?>
-post('action'); ?>
-
-
-
-
-env('OS'); ?>
-request('action'); ?>
-
-
-
-
-
-
-
-
- -EOF; - $template->parse($content); - $this->assertEquals($data, $content); - } - - public function testFetch() - { - $template = new Template(); - $template->assign('name', 'name'); - $config = [ - 'strip_space' => true, - 'view_path' => dirname(__FILE__) . DS, - 'cache_id' => '__CACHE_ID__', - 'display_cache' => true, - ]; - $data = ['name' => 'value']; - $template->layout('layout')->fetch('display', $data, $config); - $this->expectOutputString('value'); - } - - public function testDisplay() - { - $config['view_path'] = dirname(__FILE__) . DS; - $config['view_suffix'] = '.html'; - $config['layout_on'] = true; - $config['layout_name'] = 'layout'; - $template = new Template($config); - $files = ['extend' => 'extend', 'include' => 'include']; - $template->assign('files', $files); - $template->assign('user', ['name' => 'name', 'account' => 100]); - $template->assign('message', 'message'); - $template->assign('info', ['value' => 'value']); - - $content = << -header -
- -value: - -main - - - side - - -value: - message{\$message} - - - mainbody - - - - {\$name} - - php code
- -EOF; - $template->display($content); - $this->expectOutputString($content2); -// $template->parse($content); - // var_dump($content); - } - - public function testVarAssign() - { - $template = new Template(); - $template->assign('name', 'value'); - $value = $template->get('name'); - $this->assertEquals('value', $value); - } - - public function testVarGet() - { - $template = new Template(); - $data = ['a' => 'a', 'b' => 'b']; - $template->assign($data); - $this->assertEquals($data, $template->get()); - } - - public function testIsCache() - { - $template = new Template(['cache_id' => '__CACHE_ID__', 'display_cache' => true]); - $this->assertTrue($template->isCache('__CACHE_ID__')); - $template->display_cache = false; - $this->assertTrue(!$template->isCache('__CACHE_ID__')); - } -} diff --git a/thinkphp/tests/thinkphp/library/think/urlTest.php b/thinkphp/tests/thinkphp/library/think/urlTest.php deleted file mode 100644 index 1cb64084..00000000 --- a/thinkphp/tests/thinkphp/library/think/urlTest.php +++ /dev/null @@ -1,127 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * Url测试 - * @author liu21st - */ - -namespace tests\thinkphp\library\think; - -use think\Config; -use think\Route; -use think\Url; - -class urlTest extends \PHPUnit_Framework_TestCase -{ - - public function setUp() - { - Route::rules(['get' => [], - 'post' => [], - 'put' => [], - 'delete' => [], - 'patch' => [], - 'head' => [], - 'options' => [], - '*' => [], - 'alias' => [], - 'domain' => [], - 'pattern' => [], - 'name' => []]); - Route::name([]); - } - - public function testBuildModule() - { - - Route::get('blog/:name', 'index/blog'); - Route::get('blog/:id', 'index/blog'); - Config::set('pathinfo_depr', '/'); - Config::set('url_html_suffix', ''); - - $this->assertEquals('/blog/thinkphp', Url::build('index/blog?name=thinkphp')); - $this->assertEquals('/blog/thinkphp.html', Url::build('index/blog', 'name=thinkphp', 'html')); - $this->assertEquals('/blog/10', Url::build('index/blog?id=10')); - $this->assertEquals('/blog/10.html', Url::build('index/blog', 'id=10', 'html')); - - Route::get('item-', 'blog/item', [], ['name' => '\w+', 'id' => '\d+']); - $this->assertEquals('/item-thinkphp', Url::build('blog/item?name=thinkphp')); - $this->assertEquals('/item-thinkphp2016', Url::build('blog/item?name=thinkphp&id=2016')); - } - - public function testBuildController() - { - Config::set('url_html_suffix', ''); - Route::get('blog/:id', '@index/blog/read'); - $this->assertEquals('/blog/10.html', Url::build('@index/blog/read', 'id=10', 'html')); - - Route::get('foo/bar', '@foo/bar/index'); - $this->assertEquals('/foo/bar', Url::build('@foo/bar/index')); - - Route::get('foo/bar/baz', '@foo/bar.BAZ/index'); - $this->assertEquals('/foo/bar/baz', Url::build('@foo/bar.BAZ/index')); - } - - public function testBuildMethod() - { - Route::get('blog/:id', '\app\index\controller\blog@read'); - $this->assertEquals('/blog/10.html', Url::build('\app\index\controller\blog@read', 'id=10', 'html')); - } - - public function testBuildRoute() - { - Route::get('blog/:id', 'index/blog'); - Config::set('url_html_suffix', 'shtml'); - $this->assertNotEquals('/blog/10.html', Url::build('/blog/10')); - $this->assertEquals('/blog/10.shtml', Url::build('/blog/10')); - } - - public function testBuildNameRoute() - { - Route::get(['name', 'blog/:id'], 'index/blog'); - $this->assertEquals([['blog/:id', ['id' => 1], null, null]], Route::name('name')); - Config::set('url_html_suffix', 'shtml'); - $this->assertEquals('/blog/10.shtml', Url::build('name?id=10')); - } - - public function testBuildAnchor() - { - Route::get('blog/:id', 'index/blog'); - Config::set('url_html_suffix', 'shtml'); - $this->assertEquals('/blog/10.shtml#detail', Url::build('index/blog#detail', 'id=10')); - - Config::set('url_common_param', true); - $this->assertEquals('/blog/10.shtml?foo=bar#detail', Url::build('index/blog#detail', "id=10&foo=bar")); - } - - public function testBuildDomain() - { - Config::set('url_domain_deploy', true); - Route::domain('subdomain.thinkphp.cn', 'admin'); - $this->assertEquals('http://subdomain.thinkphp.cn/blog/10.shtml', Url::build('/blog/10')); - Route::domain('subdomain.thinkphp.cn', [ - 'hello/:name' => 'index/hello', - ]); - $this->assertEquals('http://subdomain.thinkphp.cn/hello/thinkphp.shtml', Url::build('index/hello?name=thinkphp')); - } - - public function testRoot() - { - Config::set('url_domain_deploy', false); - Config::set('url_common_param', false); - Url::root('/index.php'); - Route::get('blog/:id', 'index/blog/read'); - Config::set('url_html_suffix', 'shtml'); - $this->assertEquals('/index.php/blog/10/name/thinkphp.shtml', Url::build('index/blog/read?id=10&name=thinkphp')); - - } -} diff --git a/thinkphp/tests/thinkphp/library/think/validateTest.php b/thinkphp/tests/thinkphp/library/think/validateTest.php deleted file mode 100644 index 081a6f2b..00000000 --- a/thinkphp/tests/thinkphp/library/think/validateTest.php +++ /dev/null @@ -1,200 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * Validate类测试 - */ - -namespace tests\thinkphp\library\think; - -use think\File; -use think\Validate; - -class validateTest extends \PHPUnit_Framework_TestCase -{ - - public function testCheck() - { - $rule = [ - 'name' => 'require|max:25', - 'age' => 'number|between:1,120', - 'email' => 'email', - ]; - $msg = [ - 'name.require' => '名称必须', - 'name.max' => '名称最多不能超过25个字符', - 'age.number' => '年龄必须是数字', - 'age.between' => '年龄只能在1-120之间', - 'email' => '邮箱格式错误', - ]; - $data = [ - 'name' => 'thinkphp', - 'age' => 10, - 'email' => 'thinkphp@qq.com', - ]; - $validate = new Validate($rule, $msg); - $result = $validate->check($data); - $this->assertEquals(true, $result); - } - - public function testRule() - { - $rule = [ - 'name' => 'require|method:get|alphaNum|max:25|expire:2016-1-1,2026-1-1', - 'account' => 'requireIf:name,thinkphp|alphaDash|min:4|length:4,30', - 'age' => 'number|between:1,120', - 'email' => 'requireWith:name|email', - 'host' => 'activeUrl|activeUrl:A', - 'url' => 'url', - 'ip' => 'ip|ip:ipv4', - 'score' => 'float|gt:60|notBetween:90,100|notIn:70,80|lt:100|elt:100|egt:60', - 'status' => 'integer|in:0,1,2', - 'begin_time' => 'after:2016-3-18', - 'end_time' => 'before:2016-10-01', - 'info' => 'require|array|length:4|max:5|min:2', - 'info.name' => 'require|length:8|alpha|same:thinkphp', - 'value' => 'same:100|different:status', - 'bool' => 'boolean', - 'title' => 'chsAlpha', - 'city' => 'chs', - 'nickname' => 'chsDash', - 'aliasname' => 'chsAlphaNum', - 'file' => 'file|fileSize:20480', - 'image' => 'image|fileMime:image/png|image:80,80,png', - 'test' => 'test', - ]; - $data = [ - 'name' => 'thinkphp', - 'account' => 'liuchen', - 'age' => 10, - 'email' => 'thinkphp@qq.com', - 'host' => 'thinkphp.cn', - 'url' => 'http://thinkphp.cn/topic', - 'ip' => '114.34.54.5', - 'score' => '89.15', - 'status' => 1, - 'begin_time' => '2016-3-20', - 'end_time' => '2016-5-1', - 'info' => [1, 2, 3, 'name' => 'thinkphp'], - 'zip' => '200000', - 'date' => '16-3-8', - 'ok' => 'yes', - 'value' => 100, - 'bool' => true, - 'title' => '流年ThinkPHP', - 'city' => '上海', - 'nickname' => '流年ThinkPHP_2016', - 'aliasname' => '流年Think2016', - 'file' => new File(THINK_PATH . 'base.php'), - 'image' => new File(THINK_PATH . 'logo.png'), - 'test' => 'test', - ]; - $validate = new Validate($rule); - $validate->extend('test', function ($value) {return 'test' == $value ? true : false;}); - $validate->rule('zip', '/^\d{6}$/'); - $validate->rule([ - 'ok' => 'require|accepted', - 'date' => 'date|dateFormat:y-m-d', - ]); - $result = $validate->batch()->check($data); - $this->assertEquals(true, $result); - } - - public function testMsg() - { - $validate = new Validate(); - $validate->message('name.require', '名称必须'); - $validate->message([ - 'name.require' => '名称必须', - 'name.max' => '名称最多不能超过25个字符', - 'age.number' => '年龄必须是数字', - 'age.between' => '年龄只能在1-120之间', - 'email' => '邮箱格式错误', - ]); - } - - public function testMake() - { - $rule = [ - 'name' => 'require|max:25', - 'age' => 'number|between:1,120', - 'email' => 'email', - ]; - $msg = [ - 'name.require' => '名称必须', - 'name.max' => '名称最多不能超过25个字符', - 'age.number' => '年龄必须是数字', - 'age.between' => '年龄只能在1-120之间', - 'email' => '邮箱格式错误', - ]; - $validate = Validate::make($rule, $msg); - } - - public function testExtend() - { - $validate = new Validate(['name' => 'check:1']); - $validate->extend('check', function ($value, $rule) {return $rule == $value ? true : false;}); - $validate->extend(['check' => function ($value, $rule) {return $rule == $value ? true : false;}]); - $data = ['name' => 1]; - $result = $validate->check($data); - $this->assertEquals(true, $result); - } - - public function testScene() - { - $rule = [ - 'name' => 'require|max:25', - 'age' => 'number|between:1,120', - 'email' => 'email', - ]; - $msg = [ - 'name.require' => '名称必须', - 'name.max' => '名称最多不能超过25个字符', - 'age.number' => '年龄必须是数字', - 'age.between' => '年龄只能在1-120之间', - 'email' => '邮箱格式错误', - ]; - $data = [ - 'name' => 'thinkphp', - 'age' => 10, - 'email' => 'thinkphp@qq.com', - ]; - $validate = new Validate($rule); - $validate->scene(['edit' => ['name', 'age']]); - $validate->scene('edit', ['name', 'age']); - $validate->scene('edit'); - $result = $validate->check($data); - $this->assertEquals(true, $result); - } - - public function testSetTypeMsg() - { - $rule = [ - 'name|名称' => 'require|max:25', - 'age' => 'number|between:1,120', - 'email' => 'email', - ['sex', 'in:1,2', '性别错误'], - ]; - $data = [ - 'name' => '', - 'age' => 10, - 'email' => 'thinkphp@qq.com', - 'sex' => '3', - ]; - $validate = new Validate($rule); - $validate->setTypeMsg('require', ':attribute必须'); - $validate->setTypeMsg(['require' => ':attribute必须']); - $result = $validate->batch()->check($data); - $this->assertFalse($result); - $this->assertEquals(['name' => '名称必须', 'sex' => '性别错误'], $validate->getError()); - } - -} diff --git a/thinkphp/tests/thinkphp/library/think/view/driver/.gitignore b/thinkphp/tests/thinkphp/library/think/view/driver/.gitignore deleted file mode 100644 index c96a04f0..00000000 --- a/thinkphp/tests/thinkphp/library/think/view/driver/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/thinkphp/tests/thinkphp/library/think/view/theme/index/template.html b/thinkphp/tests/thinkphp/library/think/view/theme/index/template.html deleted file mode 100644 index 5f4548be..00000000 --- a/thinkphp/tests/thinkphp/library/think/view/theme/index/template.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - Document - - - - - diff --git a/thinkphp/tests/thinkphp/library/think/viewTest.php b/thinkphp/tests/thinkphp/library/think/viewTest.php deleted file mode 100644 index 5bb7de16..00000000 --- a/thinkphp/tests/thinkphp/library/think/viewTest.php +++ /dev/null @@ -1,76 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * view测试 - * @author mahuan - */ - -namespace tests\thinkphp\library\think; - -class viewTest extends \PHPUnit_Framework_TestCase -{ - - /** - * 句柄测试 - * @return mixed - * @access public - */ - public function testGetInstance() - { - \think\Cookie::get('a'); - $view_instance = \think\View::instance(); - $this->assertInstanceOf('\think\view', $view_instance, 'instance方法返回错误'); - } - - /** - * 测试变量赋值 - * @return mixed - * @access public - */ - public function testAssign() - { - $view_instance = \think\View::instance(); - $view_instance->key = 'value'; - $this->assertTrue(isset($view_instance->key)); - $this->assertEquals('value', $view_instance->key); - $data = $view_instance->assign(array('key' => 'value')); - $data = $view_instance->assign('key2', 'value2'); - //测试私有属性 - $expect_data = array('key' => 'value', 'key2' => 'value2'); - $this->assertAttributeEquals($expect_data, 'data', $view_instance); - } - - /** - * 测试引擎设置 - * @return mixed - * @access public - */ - public function testEngine() - { - $view_instance = \think\View::instance(); - $data = $view_instance->engine('php'); - $data = $view_instance->engine(['type' => 'php', 'view_path' => '', 'view_suffix' => '.php', 'view_depr' => DS]); - $php_engine = new \think\view\driver\Php(['view_path' => '', 'view_suffix' => '.php', 'view_depr' => DS]); - $this->assertAttributeEquals($php_engine, 'engine', $view_instance); - //测试模板引擎驱动 - $data = $view_instance->engine(['type' => 'think', 'view_path' => '', 'view_suffix' => '.html', 'view_depr' => DS]); - $think_engine = new \think\view\driver\Think(['view_path' => '', 'view_suffix' => '.html', 'view_depr' => DS]); - $this->assertAttributeEquals($think_engine, 'engine', $view_instance); - } - - public function testReplace() - { - $view_instance = \think\View::instance(); - $view_instance->replace('string', 'replace')->display('string'); - } - -} diff --git a/thinkphp/tests/thinkphp/library/traits/controller/.gitignore b/thinkphp/tests/thinkphp/library/traits/controller/.gitignore deleted file mode 100644 index c96a04f0..00000000 --- a/thinkphp/tests/thinkphp/library/traits/controller/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/thinkphp/tests/thinkphp/library/traits/model/.gitignore b/thinkphp/tests/thinkphp/library/traits/model/.gitignore deleted file mode 100644 index c96a04f0..00000000 --- a/thinkphp/tests/thinkphp/library/traits/model/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/thinkphp/tpl/dispatch_jump.tpl b/thinkphp/tpl/dispatch_jump.tpl index 18ee01bd..583376bb 100644 --- a/thinkphp/tpl/dispatch_jump.tpl +++ b/thinkphp/tpl/dispatch_jump.tpl @@ -2,6 +2,7 @@ + 跳转提示